feat: ADR-0035 4i(a,b) — CREATE TABLE help/usage + describe table constraints; Phase 4 complete
(b) describe shows table-level constraints: TableDescription gains unique_constraints + check_constraints (populated by do_describe_table from read_schema), rendered in a new "Table constraints:" section — composite UNIQUE and table-level CHECK (named + unnamed). The per-column Constraints column already covered single-column NOT NULL/UNIQUE/PK/CHECK. (a) CREATE TABLE help/usage skeleton refreshed for the column DEFAULT/ CHECK/REFERENCES, table-level composite UNIQUE, table CHECK, and table-level FOREIGN KEY forms (4a.2/4a.3/4b) — engine-neutral, vocab-audit clean. With 4i's (c)/(d)/(e) already shipped, this completes sub-phase 4i — the verification sweep — and therefore ADR-0035 Phase 4 (4a–4i). ADR-0035 Status, §13 4i, the ADR index, and requirements.md Q1 updated to "Phase 4 complete". Tests: render_structure table-level-constraints unit test + e2e_describe_shows_table_level_constraints. Full suite 1917 passing / 0 failing / 1 ignored; clippy clean.
This commit is contained in:
@@ -152,6 +152,24 @@ pub fn render_structure(desc: &TableDescription) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Table-level constraints (ADR-0035 §4i b): composite `UNIQUE (a, b)`
|
||||
// and table `CHECK (…)` constraints. Single-column UNIQUE / NOT NULL /
|
||||
// PK / column-level CHECK already show in the per-column "Constraints"
|
||||
// column above; this section is the table-level constraints that span
|
||||
// columns or stand alone. A named CHECK shows its name.
|
||||
if !desc.unique_constraints.is_empty() || !desc.check_constraints.is_empty() {
|
||||
out.push("Table constraints:".to_string());
|
||||
for cols in &desc.unique_constraints {
|
||||
out.push(format!(" unique ({})", cols.join(", ")));
|
||||
}
|
||||
for chk in &desc.check_constraints {
|
||||
match &chk.name {
|
||||
Some(name) => out.push(format!(" check {name} ({})", chk.expr)),
|
||||
None => out.push(format!(" check ({})", chk.expr)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
@@ -730,6 +748,8 @@ mod tests {
|
||||
outbound_relationships: Vec::new(),
|
||||
inbound_relationships: Vec::new(),
|
||||
indexes: Vec::new(),
|
||||
unique_constraints: Vec::new(),
|
||||
check_constraints: Vec::new(),
|
||||
};
|
||||
assert_snapshot!(render_structure(&desc).join("\n"));
|
||||
}
|
||||
@@ -749,6 +769,8 @@ mod tests {
|
||||
on_update: ReferentialAction::NoAction,
|
||||
}],
|
||||
indexes: Vec::new(),
|
||||
unique_constraints: Vec::new(),
|
||||
check_constraints: Vec::new(),
|
||||
};
|
||||
let out = render_structure(&desc).join("\n");
|
||||
assert!(
|
||||
@@ -774,6 +796,8 @@ mod tests {
|
||||
outbound_relationships: Vec::new(),
|
||||
inbound_relationships: Vec::new(),
|
||||
indexes: Vec::new(),
|
||||
unique_constraints: Vec::new(),
|
||||
check_constraints: Vec::new(),
|
||||
};
|
||||
let out = render_structure(&desc).join("\n");
|
||||
// PK appears for id, NOT NULL for name, blank for nick.
|
||||
@@ -796,6 +820,8 @@ mod tests {
|
||||
columns: vec!["Email".to_string()],
|
||||
unique: false,
|
||||
}],
|
||||
unique_constraints: Vec::new(),
|
||||
check_constraints: Vec::new(),
|
||||
};
|
||||
let out = render_structure(&desc).join("\n");
|
||||
assert!(out.contains("Indexes:"), "got:\n{out}");
|
||||
@@ -821,11 +847,43 @@ mod tests {
|
||||
columns: vec!["Email".to_string()],
|
||||
unique: true,
|
||||
}],
|
||||
unique_constraints: Vec::new(),
|
||||
check_constraints: Vec::new(),
|
||||
};
|
||||
let out = render_structure(&desc).join("\n");
|
||||
assert!(out.contains("uidx_email (Email) [unique]"), "got:\n{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_structure_shows_table_level_constraints() {
|
||||
// ADR-0035 §4i (b): composite UNIQUE and table-level CHECK
|
||||
// (named + unnamed) render in a "Table constraints:" section,
|
||||
// distinct from the per-column "Constraints" column.
|
||||
let desc = TableDescription {
|
||||
name: "T".to_string(),
|
||||
columns: vec![
|
||||
col("a", Type::Int, true, false),
|
||||
col("b", Type::Int, false, false),
|
||||
],
|
||||
outbound_relationships: Vec::new(),
|
||||
inbound_relationships: Vec::new(),
|
||||
indexes: Vec::new(),
|
||||
unique_constraints: vec![vec!["a".to_string(), "b".to_string()]],
|
||||
check_constraints: vec![
|
||||
crate::persistence::TableCheck { name: None, expr: "a < b".to_string() },
|
||||
crate::persistence::TableCheck {
|
||||
name: Some("a_lt_b".to_string()),
|
||||
expr: "a <> b".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let out = render_structure(&desc).join("\n");
|
||||
assert!(out.contains("Table constraints:"), "got:\n{out}");
|
||||
assert!(out.contains("unique (a, b)"), "got:\n{out}");
|
||||
assert!(out.contains("check (a < b)"), "unnamed check; got:\n{out}");
|
||||
assert!(out.contains("check a_lt_b (a <> b)"), "named check shows its name; got:\n{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_structure_omits_indexes_section_when_none() {
|
||||
let desc = TableDescription {
|
||||
@@ -834,6 +892,8 @@ mod tests {
|
||||
outbound_relationships: Vec::new(),
|
||||
inbound_relationships: Vec::new(),
|
||||
indexes: Vec::new(),
|
||||
unique_constraints: Vec::new(),
|
||||
check_constraints: Vec::new(),
|
||||
};
|
||||
let out = render_structure(&desc).join("\n");
|
||||
assert!(!out.contains("Indexes:"), "got:\n{out}");
|
||||
@@ -1026,6 +1086,8 @@ mod tests {
|
||||
outbound_relationships: Vec::new(),
|
||||
inbound_relationships: Vec::new(),
|
||||
indexes: Vec::new(),
|
||||
unique_constraints: Vec::new(),
|
||||
check_constraints: Vec::new(),
|
||||
};
|
||||
let out = render_structure(&desc).join("\n");
|
||||
// The lowercase form of the SQLite type should appear.
|
||||
|
||||
Reference in New Issue
Block a user