Files
rdbms-playground/docs/plans/20260525-adr-0035-sql-ddl-4c.md
T
claude@clouddev1 e52e90c45b feat: ADR-0035 4c — DROP TABLE [IF EXISTS]
Add advanced-mode SQL `DROP TABLE [IF EXISTS] <name>` -> SqlDropTable,
executing through the existing do_drop_table (cascade / inbound-
relationship refusal / metadata cleanup) — full parity with the simple
`drop table`. The only new behaviour is `IF EXISTS` as a
no-op-with-note: a new DropOutcome::Skipped mirroring
CreateOutcome::Skipped (journalled, no snapshot), rendered via a new
ddl.drop_skipped_absent note + DslDropSkipped event.

- Grammar: SQL_DROP_TABLE node (entry `drop`, shape `table [if exists]
  <name> [;]`), registered Advanced. SQL-first dispatch: `drop table T`
  -> SqlDropTable in advanced; `drop column`/`relationship`/`index`/
  `constraint` fall back to the simple `drop` node (and still execute).
- Worker: Request::SqlDropTable + db.sql_drop_table; the if-exists-and-
  absent arm journals + replies Skipped without a snapshot, else
  snapshot_then(do_drop_table) -> Dropped.
- Completion: advanced `drop ` now surfaces the SQL `table` (the
  shared-entry-word behaviour from `create`); test split into simple
  (full DSL list) + advanced (SQL surface).

Known shared-entry-word completion unevenness (advanced `drop ` offers
only `table`; partial `drop rel` returns an empty list) deferred to 4i
(merge candidate sets for shared entry words) along with a flagged user
request to visually distinguish simple- vs advanced-mode completions in
the hint UI — tracked in ADR §13 4i (d)/(e), the 4c plan, and the
completion test. The DSL drops still parse + execute via fallback.

10 new tests (parse/builder + Tier-3: drop existing + one-undo-step +
restore, IF EXISTS skip + journal, plain-absent error, inbound refusal).
Docs: ADR-0035 Status/§13, README, requirements.md Q1.

Tests: 1805 passing, 0 failing, 1 ignored. Clippy clean.
2026-05-25 16:31:41 +00:00

8.2 KiB
Raw Blame History

Plan: ADR-0035 Phase 4, sub-phase 4c — DROP TABLE [IF EXISTS]

Add advanced-mode SQL DROP TABLE [IF EXISTS] <name>SqlDropTable. Reuses the existing do_drop_table (cascade/inbound-refusal + metadata cleanup), so it has full parity with the simple drop table; the only new behaviour is IF EXISTS as a no-op-that-succeeds-with-a- note, mirroring 4a's CreateOutcome::Skipped with a DropOutcome (ADR-0035 §4/§13). Small slice.

1. Baseline

  • Tests: 1795 passing, 0 failing, 1 ignored; clippy clean. Branch main, last commit 76d6059 (4b). 4c starts here.

2. Decisions (from ADR §4/§13 + the 4a precedent — not re-litigated)

  1. IF EXISTS → no-op-with-note (universal cross-vendor idiom, already in scope per the 4a IF [NOT] EXISTS decision): dropping an absent table succeeds with a note instead of the plain "no such table" error. Mirrors CreateOutcome::Skipped via a new DropOutcome { Dropped, Skipped }. The skip is journalled (no snapshot), exactly as the create-skip is.
  2. Table only. SQL DROP TABLE drops a table; DROP INDEX is 4c's sibling 4d (SqlDropIndex); column/constraint drops are ALTER TABLE (4e+). drop column/drop relationship/drop index keep falling back to the simple drop node.
  3. Dispatch (ADR-0033 Amendment 1). drop is a shared entry word; the SQL node is tried first in advanced mode. drop table T parses as SqlDropTable in advanced mode (equivalent execution to the simple DropTable — both call do_drop_table); the simple form is the only one in simple mode.
  4. Cascade / inbound-relationship refusal parity — reuse do_drop_table unchanged (it already refuses dropping a table with inbound relationships and cleans __rdbms_* metadata, incl. the 4a.3 table_checks rows). One undo step (snapshot_then).

3. Phase 1 — Requirements checklist (4c)

  • DROP TABLE <name> parses in advanced mode → SqlDropTable.
  • DROP TABLE IF EXISTS <name> sets the if_exists flag.
  • Dropping an existing table removes it + its metadata (parity with simple drop table); one undo step; undo restores it.
  • IF EXISTS on an absent table → success with a note, no error, no snapshot; the line is journalled.
  • Plain DROP TABLE on an absent table → the existing "no such table" error (unchanged).
  • Dropping a table with inbound relationships is refused (existing do_drop_table semantics), in advanced mode too.
  • drop column / drop relationship / drop index still parse as the simple forms in advanced mode (fallback).
  • Engine-neutral note wording.

Testing

  • Tier 1 (builder): drop table <name>SqlDropTable{if_exists: false}; drop table if exists <name>if_exists: true; the simple drop column/drop relationship forms still parse (fallback).
  • Tier 3 (tests/sql_drop_table.rs): drop existing → gone + one undo step + undo restores; IF EXISTS absent → skipped + note + journalled; plain absent → error; inbound-relationship refusal.
  • Catalog lockstep + vocab audit for the new note key.

4. Architecture & design

4.1 Grammar (src/dsl/grammar/ddl.rs)

  • IF_EXISTS_OPT = Optional(Seq[Word("if"), Word("exists")]).
  • SQL_DROP_TABLE_SHAPE = Seq[Word("table"), IF_EXISTS_OPT, TABLE_NAME_EXISTING, Optional(Punct(';'))] (mirrors the simple DROP_TABLE shape + the optional IF EXISTS + trailing ;).
  • pub static SQL_DROP_TABLE: CommandNode { entry: "drop", shape, ast_builder: build_sql_drop_table, help_id: "ddl.sql_drop_table", usage_ids: ["parse.usage.sql_drop_table"] }.
  • build_sql_drop_table: name = require_ident("table_name"), if_exists = path.contains_word("if") (the if only appears in the IF EXISTS prefix; mirror the create builder's if_not_exists).

4.2 Command (src/dsl/command.rs)

SqlDropTable { name: String, if_exists: bool }. Verb label "drop table"; target_table()name (add to the existing match arms alongside DropTable).

4.3 Worker (src/db.rs)

  • DropOutcome { Dropped, Skipped } (the drop peer of CreateOutcome; Skipped needs no payload — the runtime renders the note from the command's name).
  • Request::SqlDropTable { name, if_exists, source, reply: oneshot<Result<DropOutcome>> } + db.sql_drop_table method.
  • Dispatch arm: if if_exists && !user_table_exists(name) → journal the line (no snapshot) and reply Skipped (mirror the create-skip arm); else snapshot_then(… do_drop_table(name) … → Dropped).
  • do_drop_table unchanged.

4.4 Runtime + event + app (src/runtime.rs, src/event.rs, src/app.rs)

  • Command::SqlDropTabledb.sql_drop_table → map DroppedCommandOutcome::Schema(None) (same as simple drop), Skipped → a new CommandOutcome::SchemaDropSkipped.
  • CommandOutcome::SchemaDropSkippedAppEvent::DslDropSkipped { command }; the app handler notes ddl.drop_skipped_absent with command.target_table() (mirror DslCreateSkipped). No structure to render (the table is gone / never existed).

4.5 Friendly catalog

  • New key ddl.drop_skipped_absent: "table '{name}' doesn't exist — skipped (no changes made)" + a keys.rs ("ddl.drop_skipped_absent", &["name"]) entry. Engine-neutral. Help/usage skeleton body for ddl.sql_drop_table + parse.usage.sql_drop_table (a fresh node — its keys must exist; unlike the deferred refresh of the create skeleton, these are new keys the node references, so they land now).

5. Out of 4c scope

  • DROP INDEX (4d → SqlDropIndex); ALTER TABLE (4e4h).
  • Shared-entry-word completion (deferred to 4i, user-confirmed 2026-05-25). Adding the SQL drop node makes advanced-mode drop completion surface only table; a partial DSL subcommand keyword (drop rel) returns an empty list (mid-word dead end). The DSL drops still parse + execute via fallback — only the completion hint is affected. This is the pre-existing shared-entry-word model (same as create/insert/update/delete), exposed here because drop has five distinct DSL subcommands with no SQL equivalent. 4i merges the candidate sets for shared entry words; the user also wants to discuss visually distinguishing simple- vs advanced-mode completions in the hint UI (likely by colour) before/with that work. Tracked in ADR-0035 §13 4i (d)/(e) and the advanced-mode completion test in src/completion.rs.

6. Devil's Advocate review of this plan

  • Reuse vs fork? do_drop_table is the single executor; the SQL path differs only at the IF EXISTS branch (mirrors the create-skip). ✓
  • Dispatch fallback? A builder test asserts drop column/drop relationship still parse simple in advanced mode. ✓
  • One undo step + restore? snapshot_then wrap + a dedicated undo test (drop → undo → table back, with its data/metadata). ✓
  • No-op journalled, not snapshotted? Mirrors the create-skip arm exactly; a journalling test. ✓
  • Engine-neutral? The note is a catalog key under the vocab audit; the plain-absent error is the existing do_drop_table path (unchanged, already engine-neutral). ✓

7. Implementation sequence (test-first)

  1. Command + grammar + builder — Tier-1 builder tests (parse + if_exists + simple fallback) → red → add SqlDropTable, the grammar node + shape + builder, REGISTRY entry, thread Request/method/ dispatch/runtime (drop execution can land here too) → green.
  2. Worker no-op-with-note — Tier-3 (tests/sql_drop_table.rs): drop existing, IF EXISTS skip + journal, plain-absent error, inbound refusal, one undo step + restore → red → add DropOutcome + the dispatch arm + the SchemaDropSkipped/DslDropSkipped/app-note path → green.
  3. Catalog — add the note + help/usage keys + bodies; lockstep + vocab audit → green.
  4. Full sweepcargo test (no regression from 1795) + clippy.
  5. Docs — ADR-0035 Status/§13 4c; README; requirements.md Q1. Propose commit; wait for approval.

8. Exit gate

  • All §3 items satisfied; four tiers green, zero skips; no regression from the 1795 baseline; written DA pass; clippy clean.