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
+12 -3
View File
@@ -514,8 +514,11 @@ fn render_items_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rec
lines.push(Line::from(Span::styled(name.as_str(), style)));
if let Some(indexes) = app.schema_cache.table_indexes.get(name) {
for index in indexes {
// Mark a UNIQUE index so the panel distinguishes it from
// a performance-only index (ADR-0035 §4d).
let unique = if index.unique { " [unique]" } else { "" };
lines.push(Line::from(Span::styled(
format!(" {index}"),
format!(" {}{unique}", index.name),
Style::default().fg(theme.muted),
)));
}
@@ -1280,17 +1283,23 @@ mod tests {
#[test]
fn items_panel_nests_indexes_under_their_table() {
// S2 (ADR-0025): the items panel renders each table
// with its index names indented beneath it.
// with its index names indented beneath it. A UNIQUE index is
// marked `[unique]` (ADR-0035 §4d).
use crate::completion::IndexEntry;
let mut app = App::new();
app.tables = vec!["Customers".to_string(), "Orders".to_string()];
app.schema_cache.table_indexes.insert(
"Customers".to_string(),
vec!["idx_email".to_string()],
vec![
IndexEntry { name: "idx_email".to_string(), unique: false },
IndexEntry { name: "uidx_login".to_string(), unique: true },
],
);
let theme = Theme::dark();
let out = render_to_string(&mut app, &theme, 80, 24);
assert!(out.contains("Customers"), "table listed:\n{out}");
assert!(out.contains("Orders"), "table listed:\n{out}");
assert!(out.contains("idx_email"), "index nested in panel:\n{out}");
assert!(out.contains("uidx_login [unique]"), "unique index marked:\n{out}");
}
}