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
+81 -1
View File
@@ -235,6 +235,7 @@ fn format_expectation(e: &crate::dsl::walker::outcome::Expectation) -> String {
IdentSource::Tables => "table name".to_string(),
IdentSource::Columns => "column name".to_string(),
IdentSource::Relationships => "relationship name".to_string(),
IdentSource::Indexes => "index name".to_string(),
IdentSource::Types => "type".to_string(),
IdentSource::NewName | IdentSource::Free => "identifier".to_string(),
},
@@ -316,7 +317,7 @@ mod tests {
use super::*;
use crate::dsl::action::ReferentialAction;
use crate::dsl::command::{
ChangeColumnMode, ColumnSpec, RelationshipSelector, RowFilter,
ChangeColumnMode, ColumnSpec, IndexSelector, RelationshipSelector, RowFilter,
};
use crate::dsl::types::Type;
use crate::dsl::value::Value;
@@ -471,6 +472,7 @@ mod tests {
Command::DropColumn {
table: "Customers".to_string(),
column: "Email".to_string(),
cascade: false,
}
);
}
@@ -482,6 +484,7 @@ mod tests {
Command::DropColumn {
table: "Customers".to_string(),
column: "Email".to_string(),
cascade: false,
}
);
assert_eq!(
@@ -489,6 +492,7 @@ mod tests {
Command::DropColumn {
table: "Customers".to_string(),
column: "Email".to_string(),
cascade: false,
}
);
assert_eq!(
@@ -496,6 +500,7 @@ mod tests {
Command::DropColumn {
table: "Customers".to_string(),
column: "Email".to_string(),
cascade: false,
}
);
}
@@ -1156,6 +1161,81 @@ mod tests {
);
}
// --- add index / drop index (ADR-0025) ---
#[test]
fn add_index_named() {
assert_eq!(
ok("add index as idx_email on Customers (Email)"),
Command::AddIndex {
name: Some("idx_email".to_string()),
table: "Customers".to_string(),
columns: vec!["Email".to_string()],
}
);
}
#[test]
fn add_index_unnamed() {
assert_eq!(
ok("add index on Customers (Email)"),
Command::AddIndex {
name: None,
table: "Customers".to_string(),
columns: vec!["Email".to_string()],
}
);
}
#[test]
fn add_index_composite_columns() {
assert_eq!(
ok("add index on Orders (CustId, Date)"),
Command::AddIndex {
name: None,
table: "Orders".to_string(),
columns: vec!["CustId".to_string(), "Date".to_string()],
}
);
}
#[test]
fn drop_index_by_name() {
assert_eq!(
ok("drop index idx_email"),
Command::DropIndex {
selector: IndexSelector::Named {
name: "idx_email".to_string(),
},
}
);
}
#[test]
fn drop_index_by_columns() {
assert_eq!(
ok("drop index on Customers (Email)"),
Command::DropIndex {
selector: IndexSelector::Columns {
table: "Customers".to_string(),
columns: vec!["Email".to_string()],
},
}
);
}
#[test]
fn drop_column_cascade_flag() {
assert_eq!(
ok("drop column Customers: Email --cascade"),
Command::DropColumn {
table: "Customers".to_string(),
column: "Email".to_string(),
cascade: true,
}
);
}
#[test]
fn identifier_allows_underscores_and_digits_after_start() {
assert_eq!(