Skip to content

feat(ddl): DEFAULT clause, DROP TABLE/INDEX, ALTER TABLE#86

Merged
joaoh82 merged 4 commits into
mainfrom
feat/alter-drop-tables
May 4, 2026
Merged

feat(ddl): DEFAULT clause, DROP TABLE/INDEX, ALTER TABLE#86
joaoh82 merged 4 commits into
mainfrom
feat/alter-drop-tables

Conversation

@joaoh82

@joaoh82 joaoh82 commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

Brings DDL surface to SQLite parity for the schema-modification statements that were previously listed under "Not yet supported". Closes SQLR-2.

  • DEFAULT <literal> on CREATE TABLE columns — landed first as a prerequisite so ALTER TABLE ADD COLUMN ... NOT NULL has a value to backfill with. Literal expressions only (integer/real/text/bool/null + unary +/-); non-literal expressions like CURRENT_TIMESTAMP are rejected at CREATE time.
  • DROP TABLE [IF EXISTS] <name> and DROP INDEX [IF EXISTS] <name> — single target per statement. DROP INDEX refuses auto-indexes (sqlrite_autoindex_*), matching SQLite.
  • ALTER TABLE [IF EXISTS] <t> <op> — one operation per statement, four sub-operations: RENAME TO, RENAME COLUMN, ADD COLUMN, DROP COLUMN. ADD COLUMN allows NOT NULL only with DEFAULT (matches SQLite). DROP COLUMN refuses the PK column and the only-column case.

Persistence is full-rebuild-on-save: dropped objects are simply absent from the next sqlrite_master write. Pages previously occupied by them become orphans on disk (no free-list yet — file size doesn't shrink until a future VACUUM).

Test plan

  • cargo build --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --all-targets
  • cargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs — 421 passing (380 baseline + 41 new), 1 ignored
  • cargo fmt --all -- --check
  • cargo clippy --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --all-targets — warning count unchanged from baseline (41)
  • End-to-end REPL smoke: CREATE w/ DEFAULT → INSERT → ALTER RENAME COLUMN → ALTER ADD COLUMN NOT NULL DEFAULT → ALTER RENAME TO → reopen → DROP TABLE → reopen confirms catalog cleared
  • Round-trip-through-save-and-reopen tests for every new sub-operation
  • Read-only enforcement tests for both DROP variants and all four ALTER ops
  • Transaction-rollback tests for ADD COLUMN and DROP TABLE inside BEGIN

Known limitations (documented in docs/supported-sql.md)

  • DEFAULT accepts literals only — no CURRENT_TIMESTAMP / function calls / column refs
  • DROP TABLE / DROP INDEX leave orphan pages on disk (no free-list)
  • ADD COLUMN rejects PRIMARY KEY / UNIQUE (would need backfill + uniqueness against existing rows)
  • One operation per ALTER TABLE; one target per DROP

🤖 Generated with Claude Code

joaoh82 and others added 4 commits May 4, 2026 07:32
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>
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>
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>
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>
@joaoh82 joaoh82 merged commit 00d1e4e into main May 4, 2026
16 checks passed
@joaoh82 joaoh82 mentioned this pull request May 4, 2026
4 tasks
joaoh82 added a commit that referenced this pull request May 4, 2026
Bumps every product manifest from 0.2.0 → 0.3.0 (engine, sqlrite-ask,
sqlrite-ffi, sqlrite-mcp, four SDK crates, desktop, Tauri config) and
the three inter-workspace dependency pins that `bump-version.sh`
currently misses (engine→sqlrite-ask, sqlrite-mcp→engine, wasm
SDK→sqlrite-ask). Refreshes Cargo.lock.

Headline change: PR #86 (SQLR-2) added DEFAULT clause on CREATE TABLE
columns, DROP TABLE / DROP INDEX with IF EXISTS, and ALTER TABLE with
the four SQLite-style sub-operations (RENAME TO, RENAME COLUMN, ADD
COLUMN, DROP COLUMN).

Note: the release-pr.yml workflow's bump step failed with
`failed to select a version for the requirement \`sqlrite-ask = "^0.2"\``
because bump-version.sh doesn't update inter-workspace path-dep
version specs. Filing a follow-up to fix the script; for this release
the three pins were patched manually.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant