feat: ADR-0035 4a.3 — table-level / multi-column CHECK
Add table-level CHECK (e.g. `CREATE TABLE t (a int, b int, CHECK (a < b))`) to advanced-mode SQL CREATE TABLE. Since SQLite exposes no PRAGMA for CHECK constraints, a table-level CHECK cannot be read back from the engine and becomes the source of truth in a new internal metadata table `__rdbms_playground_table_checks (table_name, seq, check_expr)`. - Grammar: new TABLE_CHECK element in ELEMENT_CHOICES. - Builder: distinguishes a table-level CHECK from a column-level one by element position (no column-def open in the element), using depth-aware boundary tracking so a length-arg comma (`numeric(10,2)`) or a table-PRIMARY KEY's inner comma is not mistaken for an element separator. - Worker: do_create_table emits the CHECK clauses and writes the metadata rows in its transaction; schema_to_ddl emits them identically on rebuild; read_schema / read_schema_snapshot read them from the metadata table; do_drop_table clears them. - Persistence: TableSchema.check_constraints round-trips through project.yaml (#[serde(default)], optional on read), mirroring unique_constraints. - Composite UNIQUE deliberately stays PRAGMA-detected (engine-reportable, unlike CHECK) — user-confirmed. DA/runda round added cross-cutting tests and a forward-looking doc fix: - table CHECK survives a rebuild triggered by `add column`, and a later rebuild_from_text (the ADR-0013 rebuild primitive uses a raw DROP, so the metadata rows keyed on the final name are preserved); - dropping a column a table CHECK references fails cleanly (rollback, table intact); detection is 4e, friendly wording is H1; - dropping a table clears its CHECK metadata (no orphan rows on re-create); - amended ADR §6 so 4h's RENAME also updates the new metadata table. 20 Tier-3 + 9 grammar/builder + 2 YAML tests. Docs: ADR-0035 Status/§13/§6, README index, requirements.md Q1. Help/usage skeleton + describe display of table-level constraints deferred to 4i (symmetric with 4a.2). Tests: 1769 passing, 0 failing, 1 ignored. Clippy clean.
This commit is contained in:
+38
-5
@@ -1320,7 +1320,20 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
let mut columns: Vec<ColumnSpec> = Vec::new();
|
||||
let mut primary_key: Vec<String> = Vec::new();
|
||||
let mut unique_constraints: Vec<Vec<String>> = Vec::new();
|
||||
let mut check_constraints: Vec<String> = Vec::new();
|
||||
let mut pending_name: Option<String> = None;
|
||||
// Distinguish a table-level `CHECK (…)` from a column-level one
|
||||
// (ADR-0035 §4a.3): both are spelled `check (`, and `Word` matches
|
||||
// carry no role, so position is the only signal. `column_open` is
|
||||
// `true` while a column definition is accepting constraints in the
|
||||
// current element; a `check` seen while it is `false` is table-level.
|
||||
// `depth` tracks the parens that reach this loop (the outer column
|
||||
// list, type length-args `(10, 2)`, and table-`PRIMARY KEY (a, b)` —
|
||||
// the `check`/`default`/table-`unique` arms consume their own parens
|
||||
// internally, so they never perturb it). An element separator is a
|
||||
// comma at the column-list interior, `depth == 1`.
|
||||
let mut column_open = false;
|
||||
let mut depth = 0usize;
|
||||
let mut items = path.items.iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
match &item.kind {
|
||||
@@ -1336,6 +1349,7 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
})?;
|
||||
let col_name = pending_name.take().ok_or_else(sql_col_type_without_name)?;
|
||||
columns.push(ColumnSpec::new(col_name, ty));
|
||||
column_open = true;
|
||||
}
|
||||
// `double precision` — the two-word alias maps to `real`.
|
||||
// The grammar guarantees `precision` follows `double`.
|
||||
@@ -1348,6 +1362,7 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
}
|
||||
let col_name = pending_name.take().ok_or_else(sql_col_type_without_name)?;
|
||||
columns.push(ColumnSpec::new(col_name, Type::Real));
|
||||
column_open = true;
|
||||
}
|
||||
// A table-level `PRIMARY KEY (col, …)` column reference.
|
||||
MatchedKind::Ident { role: "pk_column", .. } => {
|
||||
@@ -1432,14 +1447,31 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
}
|
||||
}
|
||||
// `check ( <expr> )` — capture the inner expression text
|
||||
// (without the wrapping parens) by matching paren depth.
|
||||
// (without the wrapping parens) by matching paren depth, then
|
||||
// route by element position: a CHECK inside an open column
|
||||
// definition is column-level (4a.2); one seen at element
|
||||
// start (no column open) is a table-level CHECK (4a.3).
|
||||
MatchedKind::Word("check") => {
|
||||
if let Some((s, e)) = capture_parenthesised_span(&mut items)
|
||||
&& let Some(last) = columns.last_mut()
|
||||
{
|
||||
last.check_sql = Some(source[s..e].trim().to_string());
|
||||
if let Some((s, e)) = capture_parenthesised_span(&mut items) {
|
||||
let text = source[s..e].trim().to_string();
|
||||
if column_open {
|
||||
if let Some(last) = columns.last_mut() {
|
||||
last.check_sql = Some(text);
|
||||
}
|
||||
} else {
|
||||
check_constraints.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Track paren depth for element-boundary detection. The
|
||||
// column/`check`/`default`/table-`unique` arms consume their
|
||||
// own parens, so the only parens reaching here are the outer
|
||||
// column list, type length-args, and table-`PRIMARY KEY (…)`.
|
||||
MatchedKind::Punct('(') => depth += 1,
|
||||
MatchedKind::Punct(')') => depth = depth.saturating_sub(1),
|
||||
// A comma at the column-list interior ends the current
|
||||
// element — the next element starts fresh (no column open).
|
||||
MatchedKind::Punct(',') if depth == 1 => column_open = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1461,6 +1493,7 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
columns,
|
||||
primary_key,
|
||||
unique_constraints,
|
||||
check_constraints,
|
||||
if_not_exists,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user