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.
This commit is contained in:
claude@clouddev1
2026-05-25 16:31:41 +00:00
parent 76d60591bf
commit e52e90c45b
16 changed files with 597 additions and 19 deletions
+66
View File
@@ -477,6 +477,15 @@ enum Request {
source: Option<String>,
reply: oneshot::Sender<Result<(), DbError>>,
},
/// Advanced-mode SQL `DROP TABLE [IF EXISTS]` (ADR-0035 §4, 4c).
/// Executes through `do_drop_table`; `if_exists` turns an absent
/// table into a no-op (`DropOutcome::Skipped`, no snapshot).
SqlDropTable {
name: String,
if_exists: bool,
source: Option<String>,
reply: oneshot::Sender<Result<DropOutcome, DbError>>,
},
AddColumn {
table: String,
column: ColumnSpec,
@@ -865,6 +874,26 @@ impl Database {
recv.await.map_err(|_| DbError::WorkerGone)?
}
/// Advanced-mode SQL `DROP TABLE [IF EXISTS]` (ADR-0035 §4, 4c).
/// Returns whether the table was dropped or skipped (the `IF EXISTS`
/// no-op on an absent table).
pub async fn sql_drop_table(
&self,
name: String,
if_exists: bool,
source: Option<String>,
) -> Result<DropOutcome, DbError> {
let (reply, recv) = oneshot::channel();
self.send(Request::SqlDropTable {
name,
if_exists,
source,
reply,
})
.await?;
recv.await.map_err(|_| DbError::WorkerGone)?
}
pub async fn add_column(
&self,
table: String,
@@ -1765,6 +1794,31 @@ fn handle_request(
do_drop_table(conn, persistence, source.as_deref(), &name)
});
}
Request::SqlDropTable {
name,
if_exists,
source,
reply,
} => {
// `IF EXISTS` on an absent table is a no-op: reply `Skipped`
// and take **no** snapshot (nothing to undo). The submitted
// line is still journalled — like the `CREATE TABLE IF NOT
// EXISTS` skip and other no-ops (ADR-0034). ADR-0035 §4.
if if_exists && !user_table_exists(conn, &name).unwrap_or(false) {
let result = (|| {
if let (Some(p), Some(text)) = (persistence, source.as_deref()) {
p.append_history(text).map_err(DbError::from_persistence)?;
}
Ok(DropOutcome::Skipped)
})();
let _ = reply.send(result);
} else {
snapshot_then(snap, batch, conn, source.as_deref(), reply, || {
do_drop_table(conn, persistence, source.as_deref(), &name)
.map(|()| DropOutcome::Dropped)
});
}
}
Request::AddColumn {
table,
column,
@@ -2633,6 +2687,18 @@ pub enum CreateOutcome {
Skipped(TableDescription),
}
/// The result of an advanced-mode SQL `DROP TABLE` (ADR-0035 §4, 4c).
///
/// Either the table was dropped, or `IF EXISTS` matched no table and
/// the statement was a no-op that drives the "doesn't exist — skipped"
/// note. Carries no payload — the runtime renders the note from the
/// command's table name.
#[derive(Debug)]
pub enum DropOutcome {
Dropped,
Skipped,
}
#[allow(clippy::too_many_arguments)]
fn do_create_table(
conn: &Connection,