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:
@@ -120,6 +120,11 @@ pub struct SchemaSnapshot {
|
||||
pub created_at: String,
|
||||
pub tables: Vec<TableSchema>,
|
||||
pub relationships: Vec<RelationshipSchema>,
|
||||
/// Indexes across all tables (ADR-0025). Carried as a flat
|
||||
/// list mirroring `relationships`; each entry names its
|
||||
/// table. Empty for project files written before indexes
|
||||
/// existed — the YAML field is optional on read.
|
||||
pub indexes: Vec<IndexSchema>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -142,6 +147,15 @@ pub struct ColumnSchema {
|
||||
pub unique: bool,
|
||||
}
|
||||
|
||||
/// One index as recorded in `project.yaml` (ADR-0025).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexSchema {
|
||||
pub name: String,
|
||||
pub table: String,
|
||||
/// The indexed columns, in index order.
|
||||
pub columns: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RelationshipSchema {
|
||||
pub name: String,
|
||||
@@ -342,6 +356,7 @@ mod tests {
|
||||
created_at: "2026-05-07T14:30:12Z".to_string(),
|
||||
tables: vec![],
|
||||
relationships: vec![],
|
||||
indexes: vec![],
|
||||
};
|
||||
p.write_schema(&schema).unwrap();
|
||||
let body = fs::read_to_string(dir.path().join(PROJECT_YAML)).unwrap();
|
||||
|
||||
+59
-1
@@ -23,7 +23,7 @@ use serde::Deserialize;
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::types::Type;
|
||||
|
||||
use super::{ColumnSchema, RelationshipSchema, SchemaSnapshot, TableSchema};
|
||||
use super::{ColumnSchema, IndexSchema, RelationshipSchema, SchemaSnapshot, TableSchema};
|
||||
|
||||
/// Serialize a `SchemaSnapshot` to a `project.yaml` body.
|
||||
#[must_use]
|
||||
@@ -51,9 +51,31 @@ pub(super) fn serialize_schema(schema: &SchemaSnapshot) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
if schema.indexes.is_empty() {
|
||||
let _ = writeln!(out, "indexes: []");
|
||||
} else {
|
||||
let _ = writeln!(out, "indexes:");
|
||||
for index in &schema.indexes {
|
||||
write_index(&mut out, index);
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn write_index(out: &mut String, index: &IndexSchema) {
|
||||
let _ = writeln!(out, " - name: {}", quote_if_needed(&index.name));
|
||||
let _ = writeln!(out, " table: {}", quote_if_needed(&index.table));
|
||||
write!(out, " columns: [").unwrap();
|
||||
for (i, col) in index.columns.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str("e_if_needed(col));
|
||||
}
|
||||
let _ = writeln!(out, "]");
|
||||
}
|
||||
|
||||
fn write_table(out: &mut String, table: &TableSchema) {
|
||||
let _ = writeln!(out, " - name: {}", quote_if_needed(&table.name));
|
||||
write!(out, " primary_key: [").unwrap();
|
||||
@@ -215,10 +237,20 @@ pub(crate) fn parse_schema(body: &str) -> Result<SchemaSnapshot, YamlError> {
|
||||
on_update,
|
||||
});
|
||||
}
|
||||
let indexes: Vec<IndexSchema> = raw
|
||||
.indexes
|
||||
.into_iter()
|
||||
.map(|i| IndexSchema {
|
||||
name: i.name,
|
||||
table: i.table,
|
||||
columns: i.columns,
|
||||
})
|
||||
.collect();
|
||||
Ok(SchemaSnapshot {
|
||||
created_at: raw.project.created_at,
|
||||
tables,
|
||||
relationships,
|
||||
indexes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -279,6 +311,10 @@ struct RawProject {
|
||||
tables: Vec<RawTable>,
|
||||
#[serde(default)]
|
||||
relationships: Vec<RawRelationship>,
|
||||
/// Optional: project files written before ADR-0025 carry no
|
||||
/// `indexes:` field and default to an empty list.
|
||||
#[serde(default)]
|
||||
indexes: Vec<RawIndex>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -320,6 +356,13 @@ struct RawEndpoint {
|
||||
column: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawIndex {
|
||||
name: String,
|
||||
table: String,
|
||||
columns: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -355,6 +398,11 @@ mod tests {
|
||||
on_delete: ReferentialAction::Cascade,
|
||||
on_update: ReferentialAction::NoAction,
|
||||
}],
|
||||
indexes: vec![IndexSchema {
|
||||
name: "Orders_CustId_idx".to_string(),
|
||||
table: "Orders".to_string(),
|
||||
columns: vec!["CustId".to_string()],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +422,9 @@ mod tests {
|
||||
assert!(body.contains("child: { table: Orders, column: CustId }"));
|
||||
assert!(body.contains("on_delete: cascade"));
|
||||
assert!(body.contains("on_update: no_action"));
|
||||
assert!(body.contains("- name: Orders_CustId_idx"));
|
||||
assert!(body.contains("table: Orders"));
|
||||
assert!(body.contains("columns: [CustId]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -382,9 +433,11 @@ mod tests {
|
||||
created_at: "2026-05-07T14:30:12Z".to_string(),
|
||||
tables: vec![],
|
||||
relationships: vec![],
|
||||
indexes: vec![],
|
||||
});
|
||||
assert!(body.contains("tables: []"));
|
||||
assert!(body.contains("relationships: []"));
|
||||
assert!(body.contains("indexes: []"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -401,6 +454,7 @@ mod tests {
|
||||
}],
|
||||
}],
|
||||
relationships: vec![],
|
||||
indexes: vec![],
|
||||
});
|
||||
assert!(body.contains("- name: \"true\""));
|
||||
assert!(body.contains("{ name: \"yes\", type: bool }"));
|
||||
@@ -432,6 +486,9 @@ relationships: []
|
||||
let parsed = parse_schema(body).expect("parse minimal");
|
||||
assert_eq!(parsed.tables.len(), 0);
|
||||
assert_eq!(parsed.relationships.len(), 0);
|
||||
// A project file with no `indexes:` field (written
|
||||
// before ADR-0025) parses with an empty index list.
|
||||
assert_eq!(parsed.indexes.len(), 0);
|
||||
assert_eq!(parsed.created_at, "2026-05-07T14:30:12Z");
|
||||
}
|
||||
|
||||
@@ -496,6 +553,7 @@ relationships:
|
||||
],
|
||||
}],
|
||||
relationships: vec![],
|
||||
indexes: vec![],
|
||||
});
|
||||
assert!(body.contains("primary_key: [a, b]"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user