constraints: add constraint / drop constraint on existing columns (ADR-0029 §2.2)

Adds the two commands for modifying a column's constraints after
creation, completing ADR-0029's §2.2 surface.

Grammar (dsl/grammar/ddl.rs): `add constraint <constraint> to
<T>.<col>` reuses the §2.1 COLUMN_CONSTRAINT choice; `drop
constraint <kind> from <T>.<col>` names only the kind. Both join
the `add` / `drop` choices, discriminated by the `constraint`
form word.

AST (dsl/command.rs): `Command::AddConstraint` / `DropConstraint`
plus the `Constraint` / `ConstraintKind` enums.

Worker (db.rs): `do_add_constraint` / `do_drop_constraint` apply
the change through the rebuild-table primitive. `add` runs the §5
dry-run first — `not null` / `unique` / `check` against a
populated column are refused, before any write, with a
pretty-printed table of offending rows. §9 redundant-on-PK
declarations and §6 `default` on an auto-generated column are
friendly refusals; dropping a constraint the column does not
carry is likewise refused.

Also fixes schema_to_ddl, which suppressed UNIQUE for every PK
column — a compound-PK member is not individually unique, so an
explicit UNIQUE on it must survive the rebuild.

23 tests added (6 grammar, 17 worker); 3 completion-test and 3
matrix snapshots updated for the new `constraint` subcommand.
This commit is contained in:
claude@clouddev1
2026-05-19 18:31:57 +00:00
parent 102dff08c4
commit abce1188f2
14 changed files with 1360 additions and 29 deletions
+19 -10
View File
@@ -844,17 +844,19 @@ mod tests {
fn multi_candidate_position_offers_add_subcommands() {
// After `add ` the parser expects `column` (for
// `add column ...`), `index` (for `add index ...`,
// ADR-0025), and `1` (the opener for
// ADR-0025), `constraint` (for `add constraint ...`,
// ADR-0029 §2.2), and `1` (the opener for
// `add 1:n relationship ...`). The completion engine
// sections keyword candidates (`column`, `index`)
// ahead of the `1:n` composite literal, so the literal
// sorts last even though `add 1:n` is declared second.
// sections keyword candidates ahead of the `1:n`
// composite literal, so the literal sorts last even
// though `add 1:n` is declared second.
let cs = cands("add ", 4);
assert_eq!(
cs,
vec![
"column".to_string(),
"index".to_string(),
"constraint".to_string(),
"1:n".to_string(),
]
);
@@ -1113,10 +1115,11 @@ mod tests {
}
#[test]
fn drop_offers_all_four_subcommands() {
fn drop_offers_all_five_subcommands() {
// `drop` branches: column / relationship / table / index
// (ADR-0025). Candidates follow grammar declaration
// order, so `index` — added last — appears last.
// (ADR-0025) / constraint (ADR-0029 §2.2). Candidates
// follow grammar declaration order, so `constraint` —
// added last — appears last.
let cs = cands("drop ", 5);
assert_eq!(
cs,
@@ -1125,6 +1128,7 @@ mod tests {
"relationship".to_string(),
"table".to_string(),
"index".to_string(),
"constraint".to_string(),
],
);
}
@@ -1686,15 +1690,20 @@ mod tests {
c.sort_by(|a, b| a.text.cmp(&b.text));
c
}
// `add ` exposes `column`, `1:n` and `index` — the
// alphabetic ranker reorders them.
// `add ` exposes `column`, `1:n`, `index` and
// `constraint` — the alphabetic ranker reorders them.
let cache = SchemaCache::default();
let comp = candidates_at_cursor_with("add ", 4, &cache, alphabetic_ranker)
.expect("some completion");
let texts: Vec<String> = comp.candidates.into_iter().map(|c| c.text).collect();
assert_eq!(
texts,
vec!["1:n".to_string(), "column".to_string(), "index".to_string()]
vec![
"1:n".to_string(),
"column".to_string(),
"constraint".to_string(),
"index".to_string(),
]
);
}