Skip to content

Commit 00d1e4e

Browse files
joaoh82claude
andauthored
feat(ddl): DEFAULT clause, DROP TABLE/INDEX, ALTER TABLE (#86)
* feat(ddl): DEFAULT clause for CREATE TABLE columns Honour `DEFAULT <literal>` on column definitions. Literal expressions only — text, integer, real, boolean, NULL, and unary +/- on numerics. Function calls and other non-literal expressions are rejected at CREATE TABLE time so users see the limit upfront. Default fires only when the column is omitted from INSERT (matches SQLite — explicit NULL is preserved as NULL). Persists through save and reopen via `table_to_create_sql` emitting the DEFAULT clause and `parse_create_sql` propagating it back into Column. Refactors `CreateQuery::new`'s per-column body into a free `parse_one_column` helper so ALTER TABLE ADD COLUMN can reuse the same column-shape parsing in a follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ddl): DROP TABLE and DROP INDEX Mirror SQLite's DROP semantics on the in-memory engine: - DROP TABLE [IF EXISTS] <name>; — single target, rejects the reserved catalog name `sqlrite_master`. The dropped table's secondary/HNSW/FTS indexes ride along with the Table struct. - DROP INDEX [IF EXISTS] <name>; — single target. Walks every table looking across all three index families. Refuses to drop auto-indexes (`sqlrite_autoindex_*` from PK / UNIQUE columns) — same rule SQLite enforces. The full-rebuild-on-save pager naturally cascades drops: the next `save_database` call regenerates `sqlrite_master` from current state and simply doesn't write rows for the dropped objects. Pages previously occupied become orphans on disk (no free-list yet — file size doesn't shrink until a future VACUUM lands). Replaces the existing `process_command_unsupported_statement_test` which used DROP TABLE as the canary; switched to CREATE VIEW since DROP TABLE now executes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ddl): ALTER TABLE — RENAME / ADD COLUMN / DROP COLUMN Implement the four SQLite-style ALTER TABLE sub-operations, one per statement: - ALTER TABLE [IF EXISTS] <t> RENAME TO <new>; - ALTER TABLE [IF EXISTS] <t> RENAME COLUMN <old> TO <new>; - ALTER TABLE [IF EXISTS] <t> ADD COLUMN <coldef>; - ALTER TABLE [IF EXISTS] <t> DROP COLUMN <col>; RENAME TO updates `tb_name`, every secondary index's `table_name`, and any auto-index whose name embedded the old table name. RENAME COLUMN re-keys row storage, fixes up the `primary_key` pointer if the renamed column was the PK, and updates dependent indexes (secondary / HNSW / FTS) including auto-index names. ADD COLUMN reuses the new `parse_one_column` helper from the parser refactor in the DEFAULT-clause commit. Allows NOT NULL only when a DEFAULT is supplied (matches SQLite — without DEFAULT there's no backfill source for existing rows). Rejects PRIMARY KEY / UNIQUE on the added column; both would need backfill+uniqueness against existing rows. With a DEFAULT, every existing rowid gets backfilled in place so the new column reads consistently from the start. DROP COLUMN refuses the PK column and refuses to leave the table columnless; cascades through every dependent index family. Docs: rewrote the schema-modification section of supported-sql.md to enumerate the new surface (ALTER TABLE, DROP TABLE, DROP INDEX), the DEFAULT clause expansion to CREATE TABLE, and the residual not-yet-supported gaps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ddl): JSON validation on DEFAULT + DROP TABLE rollback test Two findings from review of feat/alter-drop-tables: - JSON DEFAULT validation gap: ALTER TABLE ADD COLUMN ... JSON DEFAULT '<garbage>' would silently backfill every existing row with invalid JSON because insert_row's per-row validation is bypassed during the add_column backfill path. Tighten eval_literal_default to parse JSON literals at CREATE TABLE / ALTER TABLE time so the check fires before any rows are written. - Add a transaction-rollback test for DROP TABLE to lock down the Database::deep_clone snapshot path. The other ALTER ops are covered via the snapshot semantics implicitly, but DROP TABLE is the most destructive new statement and warrants explicit coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 80664b8 commit 00d1e4e

6 files changed

Lines changed: 1784 additions & 118 deletions

File tree

docs/supported-sql.md

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ If you're looking for _how_ to use SQLRite (REPL flow, meta-commands, history, e
88

99
| Statement | Supported today |
1010
|---|---|
11-
| [`CREATE TABLE`](#create-table) | Columns with `PRIMARY KEY` / `UNIQUE` / `NOT NULL`; typed columns; auto-indexes on constrained columns |
11+
| [`CREATE TABLE`](#create-table) | Columns with `PRIMARY KEY` / `UNIQUE` / `NOT NULL` / `DEFAULT <literal>`; typed columns; auto-indexes on constrained columns |
1212
| [`CREATE [UNIQUE] INDEX`](#create-index) | Single-column named indexes, `IF NOT EXISTS`, persisted as cell-based B-Trees |
13-
| [`INSERT INTO`](#insert-into) | Auto-ROWID, UNIQUE/PK enforcement, clean type errors, NULL padding |
13+
| [`INSERT INTO`](#insert-into) | Auto-ROWID, UNIQUE/PK enforcement, clean type errors, NULL/DEFAULT padding |
1414
| [`SELECT`](#select) | `*` or column list, `WHERE`, single-column `ORDER BY`, `LIMIT`; index probing on `col = literal` |
1515
| [`UPDATE`](#update) | Multi-column `SET`, `WHERE`, arithmetic RHS, type + UNIQUE enforcement |
1616
| [`DELETE`](#delete) | `WHERE` predicate or whole-table |
17+
| [`ALTER TABLE`](#alter-table) | `RENAME TO`, `RENAME COLUMN`, `ADD COLUMN`, `DROP COLUMN` (one operation per statement) |
18+
| [`DROP TABLE`](#drop-table) / [`DROP INDEX`](#drop-index) | `IF EXISTS`; single target; auto-indexes refused for `DROP INDEX` |
1719
| [`BEGIN`](#transactions) / [`COMMIT`](#transactions) / [`ROLLBACK`](#transactions) | Snapshot-based; single-level; WAL-backed commit; auto-rollback on COMMIT disk failure |
1820

1921
Statements the parser accepts (because sqlparser understands them in the SQLite dialect) but SQLRite doesn't execute yet return `SQL Statement not supported yet`. The [Not yet supported](#not-yet-supported) section below enumerates the common ones.
@@ -41,12 +43,12 @@ CREATE TABLE <name> (<col> <type> [column_constraint]* [, ...]);
4143

4244
- `PRIMARY KEY` — one column per table; the column **must** be `INTEGER` and gets auto-ROWID behavior (omitted on INSERT → auto-assigned). Auto-creates an index named `sqlrite_autoindex_<table>_<column>`.
4345
- `UNIQUE` — enforced at INSERT/UPDATE time. Auto-creates an index with the same naming scheme.
44-
- `NOT NULL` — rejects NULL at INSERT/UPDATE. Omitted columns on INSERT are NULL by default, so a `NOT NULL` without an INSERT-time value is an error.
46+
- `NOT NULL` — rejects NULL at INSERT/UPDATE. Omitted columns on INSERT are NULL by default (or pick up the column's `DEFAULT`, if any), so a `NOT NULL` without an INSERT-time value or DEFAULT is an error.
47+
- `DEFAULT <literal>` — value substituted when the column is omitted from an INSERT. Accepts integer / real / text / boolean / NULL literals (and unary `+` / `-` on numerics). Function-call defaults like `CURRENT_TIMESTAMP` and other non-literal expressions are rejected at CREATE TABLE time. Explicit `INSERT ... VALUES (..., NULL, ...)` is preserved as NULL — the default only fires for omitted columns (matches SQLite).
4548

4649
### What's **not** enforced at CREATE TABLE time
4750

4851
- **Table-level constraints** (`PRIMARY KEY (col1, col2)`, `FOREIGN KEY`, `CHECK`, `UNIQUE (col1, col2)`) are parsed but ignored.
49-
- **`DEFAULT` values** are parsed but ignored.
5052
- **Multi-column `PRIMARY KEY`** — only single-column PKs work; a composite PK is accepted by the parser but treated as no PK.
5153

5254
### Errors returned
@@ -208,6 +210,72 @@ DELETE FROM <table> [WHERE <expr>];
208210

209211
---
210212

213+
## `ALTER TABLE`
214+
215+
```sql
216+
ALTER TABLE [IF EXISTS] <table> RENAME TO <new_table>;
217+
ALTER TABLE [IF EXISTS] <table> RENAME COLUMN <old_col> TO <new_col>;
218+
ALTER TABLE [IF EXISTS] <table> ADD COLUMN <col_def>;
219+
ALTER TABLE [IF EXISTS] <table> DROP COLUMN <col>;
220+
```
221+
222+
One operation per statement (SQLite-style). `ALTER TABLE foo RENAME TO bar, ADD COLUMN x ...` is rejected — issue separate statements instead.
223+
224+
### `RENAME TO`
225+
226+
- Reserved-name rejection: cannot rename to `sqlrite_master`.
227+
- Errors if the target name is already a table.
228+
- Auto-indexes whose names embed the old table name (`sqlrite_autoindex_<old>_<col>`) are renamed in lockstep so the schema catalog stays consistent. Explicit indexes carry their user-given name unchanged.
229+
230+
### `RENAME COLUMN`
231+
232+
- Errors if the old column doesn't exist or the new name already exists in the table.
233+
- Re-keys the row storage and updates every dependent index (auto + explicit, secondary / HNSW / FTS) — including auto-index name regeneration.
234+
- Renaming the PRIMARY KEY column is allowed; the table's `primary_key` pointer follows the new name.
235+
236+
### `ADD COLUMN`
237+
238+
- The column definition reuses the same parser that handles CREATE TABLE columns: same types, same `NOT NULL` / `DEFAULT` semantics.
239+
- **Rejected:** `PRIMARY KEY` and `UNIQUE` constraints on the added column. Both would require backfilling the column under uniqueness constraints against existing rows; that path will land alongside multi-column UNIQUE.
240+
- **`NOT NULL` on a non-empty table requires `DEFAULT`.** Without one there's no value to backfill existing rowids with. Same rule SQLite applies.
241+
- With a `DEFAULT`, every existing rowid is backfilled with the default value at ADD COLUMN time. Without a `DEFAULT`, existing rowids read as NULL for the new column.
242+
243+
### `DROP COLUMN`
244+
245+
- **Rejected:** dropping the PRIMARY KEY column.
246+
- **Rejected:** dropping the only remaining column (degenerate table).
247+
- Cascades to every dependent index (auto + explicit, secondary / HNSW / FTS) on the dropped column.
248+
- `CASCADE` / `RESTRICT` modifiers are accepted by the parser and ignored — SQLite has no real distinction here either.
249+
250+
---
251+
252+
## `DROP TABLE`
253+
254+
```sql
255+
DROP TABLE [IF EXISTS] <table>;
256+
```
257+
258+
- Single target per statement. `DROP TABLE a, b, c;` is parsed but rejected with a NotImplemented error.
259+
- Reserved-name rejection: `DROP TABLE sqlrite_master` errors with the same message `CREATE TABLE` uses.
260+
- All indexes attached to the table (auto, explicit, HNSW, FTS) disappear with the table — they live inside the `Table` struct and ride along.
261+
- Without `IF EXISTS`, dropping a table that doesn't exist errors. With it, that's a benign 0-tables-dropped no-op.
262+
- **Disk pages are orphaned, not freed.** SQLRite has no free-list yet — the file size doesn't shrink until a future `VACUUM`. The behavior is safe (orphan pages are unreachable from `sqlrite_master` after reopen) but means a write-heavy schema churn won't reclaim space until VACUUM lands.
263+
264+
---
265+
266+
## `DROP INDEX`
267+
268+
```sql
269+
DROP INDEX [IF EXISTS] <index_name>;
270+
```
271+
272+
- Single target per statement.
273+
- Walks every table searching for an index with the given name across the secondary B-Tree, HNSW, and FTS index families.
274+
- **Refuses to drop auto-indexes.** `sqlrite_autoindex_*` names are constraint-bound to the column they index — the only way to remove them is to drop the underlying column or table. Same rule SQLite enforces for its `sqlite_autoindex_*` indexes.
275+
- `IF EXISTS` makes a missing index a benign no-op.
276+
277+
---
278+
211279
## Expressions
212280

213281
Expressions work inside `WHERE` (both in `SELECT`, `UPDATE`, `DELETE`) and on the right-hand side of `UPDATE`'s `SET`.
@@ -405,11 +473,12 @@ For context when you hit `NotImplemented`. See [Roadmap](roadmap.md) for when th
405473
- Built-in functions (`LENGTH`, `UPPER`, `LOWER`, `COALESCE`, `IFNULL`, date/time, `printf`, …)
406474

407475
### DDL
408-
- `ALTER TABLE` (add column, rename column, rename table)
409-
- `DROP TABLE`, `DROP INDEX`
476+
- `ALTER TABLE` extras: multi-operation (`ALTER TABLE foo RENAME TO bar, ADD COLUMN x ...`), `ALTER COLUMN ... SET / DROP DEFAULT`, `ALTER COLUMN ... TYPE`
477+
- `ADD COLUMN` constraint extras: `PRIMARY KEY` and `UNIQUE` on the added column (would need backfill + uniqueness against existing rows)
478+
- `DROP TABLE` / `DROP INDEX` extras: multi-target (`DROP TABLE a, b, c;`)
410479
- `CREATE VIEW`, `CREATE TRIGGER`
411480
- Table-level constraints (composite PK, composite UNIQUE, `FOREIGN KEY`, `CHECK`)
412-
- Column defaults (`DEFAULT <value>`)
481+
- Non-literal `DEFAULT` expressions (`CURRENT_TIMESTAMP`, function calls, column references)
413482
- Composite / multi-column indexes
414483

415484
### Transactions

0 commit comments

Comments
 (0)