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

159 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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::SqlDropTable` → `db.sql_drop_table` → map `Dropped` →
`CommandOutcome::Schema(None)` (same as simple drop), `Skipped` → a new
`CommandOutcome::SchemaDropSkipped`.
- `CommandOutcome::SchemaDropSkipped` → `AppEvent::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 sweep** — `cargo 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.