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
+35 -5
View File
@@ -30,7 +30,7 @@ use crate::app::App;
use crate::cli::Args;
use crate::db::{
AddColumnResult, ChangeColumnTypeResult, DataResult, Database, DbError, DeleteResult,
InsertResult, TableDescription, UpdateResult,
DropColumnResult, InsertResult, TableDescription, UpdateResult,
};
use crate::dsl::Command;
use crate::event::AppEvent;
@@ -863,6 +863,9 @@ async fn build_schema_cache(database: &Database) -> crate::completion::SchemaCac
if let Ok(rels) = database.list_names_for(IdentSource::Relationships).await {
cache.relationships = rels;
}
if let Ok(indexes) = database.list_names_for(IdentSource::Indexes).await {
cache.indexes = indexes;
}
// Phase D (ADR-0024 §Phase D): per-table column metadata
// with user-facing types. The walker's
// `DynamicSubgrammar(column_value_list)` reads this to
@@ -872,6 +875,11 @@ async fn build_schema_cache(database: &Database) -> crate::completion::SchemaCac
// walker falls back to the schemaless value-literal list.
for name in cache.tables.clone() {
if let Ok(desc) = database.describe_table(name.clone(), None).await {
// Per-table index names for the items panel (S2,
// ADR-0025). Captured before `desc.columns` is
// consumed below.
let index_names: Vec<String> =
desc.indexes.iter().map(|i| i.name.clone()).collect();
let cols: Vec<TableColumn> = desc
.columns
.into_iter()
@@ -882,7 +890,8 @@ async fn build_schema_cache(database: &Database) -> crate::completion::SchemaCac
})
})
.collect();
cache.table_columns.insert(name, cols);
cache.table_columns.insert(name.clone(), cols);
cache.table_indexes.insert(name, index_names);
}
}
cache
@@ -1039,6 +1048,10 @@ fn spawn_dsl_dispatch(
command: command.clone(),
result,
},
Ok(CommandOutcome::DropColumn(result)) => AppEvent::DslDropColumnSucceeded {
command: command.clone(),
result,
},
Err(DbError::PersistenceFatal {
operation,
path,
@@ -1367,6 +1380,7 @@ enum CommandOutcome {
Delete(DeleteResult),
ChangeColumn(ChangeColumnTypeResult),
AddColumn(AddColumnResult),
DropColumn(DropColumnResult),
}
/// Spawn a task that reads a script file and dispatches each
@@ -1576,10 +1590,14 @@ async fn execute_command_typed(
.add_column(table, column, ty, src)
.await
.map(CommandOutcome::AddColumn),
Command::DropColumn { table, column } => database
.drop_column(table, column, src)
Command::DropColumn {
table,
column,
cascade,
} => database
.drop_column(table, column, cascade, src)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
.map(CommandOutcome::DropColumn),
Command::RenameColumn { table, old, new } => database
.rename_column(table, old, new, src)
.await
@@ -1620,6 +1638,18 @@ async fn execute_command_typed(
.drop_relationship(selector, src)
.await
.map(CommandOutcome::Schema),
Command::AddIndex {
name,
table,
columns,
} => database
.add_index(name, table, columns, src)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
Command::DropIndex { selector } => database
.drop_index(selector, src)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
Command::ShowTable { name } => database
.describe_table(name, src)
.await