Indexes: add index / drop index, persistence, display (ADR-0025)

Implement ADR-0025 — indexes as a DSL DDL feature.

- Grammar: `add index [as <name>] on <T> (<cols>)`, `drop index
  <name>` / `drop index on <T> (<cols>)`, plus a `--cascade`
  flag on `drop column`.
- db.rs: index operations over the engine's native index
  catalog (no metadata table). The rebuild-table primitive now
  captures and recreates indexes, so `change column` and the
  relationship operations no longer silently drop them.
- `drop column` refuses an indexed column unless `--cascade`,
  which drops the covering indexes and reports each.
- Persistence: additive `indexes:` list in `project.yaml`
  (version unchanged); round-trips through rebuild/export/import.
- Display: an `Indexes:` section in the structure view and a
  nested tables/indexes items panel (S2).

Reconciles requirements.md (C3 index portion, S2 satisfied)
and CLAUDE.md. 1038 tests passing (+31), clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-16 00:15:55 +00:00
parent 41043d686b
commit 0dc159fd7e
35 changed files with 2155 additions and 73 deletions
+30 -11
View File
@@ -40,11 +40,15 @@ pub struct SchemaCache {
pub tables: Vec<String>,
pub columns: Vec<String>,
pub relationships: Vec<String>,
pub indexes: Vec<String>,
/// Per-table column metadata with user-facing types
/// (ADR-0024 §Phase D). Keyed by table name; lookup is
/// case-insensitive in `columns_for_table` so the walker
/// can resolve `Customers` regardless of how it was typed.
pub table_columns: std::collections::HashMap<String, Vec<TableColumn>>,
/// Per-table user index names (ADR-0025). Keyed by table
/// name; drives the nested tables/indexes items panel (S2).
pub table_indexes: std::collections::HashMap<String, Vec<String>>,
}
/// One column's user-facing type info, scoped to a table
@@ -65,6 +69,7 @@ impl SchemaCache {
IdentSource::Tables => &self.tables,
IdentSource::Columns => &self.columns,
IdentSource::Relationships => &self.relationships,
IdentSource::Indexes => &self.indexes,
IdentSource::NewName | IdentSource::Types | IdentSource::Free => &[],
}
}
@@ -816,16 +821,23 @@ mod tests {
}
#[test]
fn multi_candidate_position_offers_column_and_one_to_n() {
fn multi_candidate_position_offers_add_subcommands() {
// After `add ` the parser expects `column` (for
// `add column ...`) and `1` (the opener for
// `add column ...`), `index` (for `add index ...`,
// ADR-0025), and `1` (the opener for
// `add 1:n relationship ...`). The completion engine
// surfaces both: `column` straight from the keyword
// expected-set, and `1:n` as a composite literal
// candidate so the user can Tab through to the
// relationship form without knowing the surface syntax.
// sections keyword candidates (`column`, `index`)
// ahead of the `1:n` composite literal, so the literal
// sorts last even though `add 1:n` is declared second.
let cs = cands("add ", 4);
assert_eq!(cs, vec!["column".to_string(), "1:n".to_string()]);
assert_eq!(
cs,
vec![
"column".to_string(),
"index".to_string(),
"1:n".to_string(),
]
);
}
#[test]
@@ -1039,7 +1051,10 @@ mod tests {
}
#[test]
fn drop_offers_three_alternatives_alphabetised() {
fn drop_offers_all_four_subcommands() {
// `drop` branches: column / relationship / table / index
// (ADR-0025). Candidates follow grammar declaration
// order, so `index` — added last — appears last.
let cs = cands("drop ", 5);
assert_eq!(
cs,
@@ -1047,6 +1062,7 @@ mod tests {
"column".to_string(),
"relationship".to_string(),
"table".to_string(),
"index".to_string(),
],
);
}
@@ -1593,13 +1609,16 @@ mod tests {
c.sort_by(|a, b| a.text.cmp(&b.text));
c
}
// `add ` exposes `column` and `1:n` — alphabetic ranker
// flips them.
// `add ` exposes `column`, `1:n` and `index` — the
// alphabetic ranker reorders them.
let cache = SchemaCache::default();
let comp = candidates_at_cursor_with("add ", 4, &cache, alphabetic_ranker)
.expect("some completion");
let texts: Vec<String> = comp.candidates.into_iter().map(|c| c.text).collect();
assert_eq!(texts, vec!["1:n".to_string(), "column".to_string()]);
assert_eq!(
texts,
vec!["1:n".to_string(), "column".to_string(), "index".to_string()]
);
}
#[test]