feat: ADR-0035 4d — CREATE [UNIQUE] INDEX / DROP INDEX

Advanced-mode SQL CREATE [UNIQUE] INDEX [IF NOT EXISTS] [<name>] ON
<T> (cols) -> SqlCreateIndex and DROP INDEX [IF EXISTS] <name> ->
SqlDropIndex, both reusing the ADR-0025 executors (do_add_index /
do_drop_index), like 4c reused do_drop_table.

- CREATE UNIQUE INDEX admitted in advanced mode (ADR-0025 Amendment 1):
  ADR-0025 deferred UNIQUE indexes for the simple-mode DSL, but advanced
  mode trusts the user like SQL does. Adds an additive IndexSchema.unique
  flag (project.yaml, serde-default, version stays 1); rebuild re-emits
  CREATE UNIQUE INDEX; the redundant-set guard keys on (columns, unique).
  Simple-mode `add unique index` stays deferred.
- IF [NOT] EXISTS on both forms reuses the 4c no-op-with-note skip
  (journalled, not snapshotted) via CreateIndexOutcome / DropIndexOutcome.
- Unnamed CREATE INDEX auto-named (ADR-0025 convention); the [UNIQUE]
  prefix is a concrete-keyword Choice and the optional name an on-led-first
  selector (the drop-index selector precedent) — trap-safe.
- create/drop each gain a second advanced node; the existing all-candidates
  dispatch handles it (locked by parse tests).
- Unique indexes marked [unique] in the structure view and items panel.
- do_add_index refuses internal __rdbms_* tables as "no such table",
  closing a latent exposure on both the simple `add index` and the new
  SQL CREATE INDEX surfaces (ADR-0025 Amendment 1).

Docs: ADR-0035 status + §13 4d + 4i; ADR-0025 Amendment 1; ADR README;
requirements.md Q1/C3. Plan: docs/plans/20260525-adr-0035-sql-ddl-4d.md.

Tests: 1834 passing / 0 failing / 0 skipped / 1 ignored; clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-25 18:41:02 +00:00
parent 44248fb8bb
commit 701217d29f
22 changed files with 1865 additions and 48 deletions
+27
View File
@@ -469,6 +469,24 @@ impl App {
));
Vec::new()
}
AppEvent::DslDropIndexSkipped { command } => {
// No-op (DROP INDEX IF EXISTS on an absent index,
// ADR-0035 §4d): just the skip note. `target_table()`
// returns the index name for `SqlDropIndex`.
self.note_system(crate::t!(
"ddl.drop_index_skipped_absent",
name = command.target_table()
));
Vec::new()
}
AppEvent::DslCreateIndexSkipped { command: _, name } => {
// No-op (CREATE INDEX IF NOT EXISTS on an existing index
// name, ADR-0035 §4d): the skip note carries the resolved
// index name (the unnamed form's auto-name isn't on the
// command). No structure shown.
self.note_system(crate::t!("ddl.create_index_skipped_exists", name = name));
Vec::new()
}
AppEvent::DslDataSucceeded { command, data } => {
self.handle_dsl_query_success(&command, &data);
Vec::new()
@@ -1603,6 +1621,12 @@ impl App {
RelationshipSelector::Named { .. } => (Operation::DropRelationship, None, None),
},
C::AddIndex { table, .. } => (Operation::AddIndex, Some(table.as_str()), None),
// SQL `CREATE [UNIQUE] INDEX` shares the add-index operation
// (it reuses `do_add_index`); route engine/validation errors
// through it with the parsed table.
C::SqlCreateIndex { table, .. } => {
(Operation::AddIndex, Some(table.as_str()), None)
}
C::AddConstraint { table, column, .. } => (
Operation::AddConstraint,
Some(table.as_str()),
@@ -1619,6 +1643,9 @@ impl App {
}
IndexSelector::Named { .. } => (Operation::DropIndex, None, None),
},
// The SQL `DROP INDEX` is name-only (the table is resolved by
// the executor), like the named DSL drop.
C::SqlDropIndex { .. } => (Operation::DropIndex, None, None),
C::Insert { table, .. } => (Operation::Insert, Some(table.as_str()), None),
C::Update { table, .. } => (Operation::Update, Some(table.as_str()), None),
C::Delete { table, .. } => (Operation::Delete, Some(table.as_str()), None),