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
+15
View File
@@ -207,12 +207,14 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
// code, not the catalog, because spacing is alignment-
// sensitive in the multi-entry case.
("parse.usage.add_column", &[]),
("parse.usage.add_constraint", &[]),
("parse.usage.add_index", &[]),
("parse.usage.add_relationship", &[]),
("parse.usage.change_column", &[]),
("parse.usage.create_table", &[]),
("parse.usage.delete", &[]),
("parse.usage.drop_column", &[]),
("parse.usage.drop_constraint", &[]),
("parse.usage.drop_index", &[]),
("parse.usage.drop_relationship", &[]),
("parse.usage.drop_table", &[]),
@@ -385,6 +387,19 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
&["count", "action", "child_table", "rel", "on_delete"],
),
// ---- change-column dry-run diagnostics (per ADR-0017) ----
// ---- add-constraint dry-run diagnostics (per ADR-0029 §5) ----
(
"db.diagnostic.add_check_summary",
&["table", "column", "total", "rule"],
),
(
"db.diagnostic.add_not_null_summary",
&["table", "column", "total"],
),
(
"db.diagnostic.add_unique_summary",
&["table", "column", "total"],
),
("db.diagnostic.force_conversion_hint", &[]),
("db.diagnostic.header_becomes", &[]),
("db.diagnostic.header_from", &[]),
+17
View File
@@ -432,6 +432,7 @@ parse:
drop_index: |-
drop index <Name>
drop index on <Table> (<col>[, ...])
drop_constraint: "drop constraint (not null | unique | default | check) from <Table>.<col>"
add_column: "add column [to] [table] <Table>: <Name> (<Type>)"
add_relationship: |-
add 1:n relationship [as <Name>]
@@ -439,6 +440,11 @@ parse:
[on delete <action>] [on update <action>]
[--create-fk]
add_index: "add index [as <Name>] on <Table> (<col>[, ...])"
add_constraint: |-
add constraint not null to <Table>.<col>
add constraint unique to <Table>.<col>
add constraint default <value> to <Table>.<col>
add constraint check (<expr>) to <Table>.<col>
rename_column: "rename column [in] [table] <Table>: <Old> to <New>"
change_column: |-
change column [in] [table] <Table>: <Name> (<Type>)
@@ -727,6 +733,17 @@ db:
# Follow-up suggestion appended to the lossy diagnostic
# (only — incompatibles can't be force-overridden).
force_conversion_hint: "if you want to execute this conversion in spite of the problems, re-run with `--force-conversion`."
# `add constraint ...` dry-run refusals (ADR-0029 §5).
# Surface when the column's existing rows would violate the
# constraint being added; the offending rows follow in a
# diagnostic table. There is no force override — the user
# fixes the data and retries.
add_not_null_summary: |-
Cannot add NOT NULL to `{table}.{column}`: {total} row(s) hold a null value.
add_unique_summary: |-
Cannot add UNIQUE to `{table}.{column}`: {total} value(s) appear in more than one row.
add_check_summary: |-
Cannot add this CHECK to `{table}.{column}`: {total} row(s) do not satisfy `{rule}`.
# ---- DSL command success summaries (ADR-0019 §9 sweep) --------------
ok:
+4
View File
@@ -67,6 +67,8 @@ pub enum Operation {
DropRelationship,
AddIndex,
DropIndex,
AddConstraint,
DropConstraint,
Query,
Rebuild,
Replay,
@@ -96,6 +98,8 @@ impl Operation {
Self::DropRelationship => "drop relationship",
Self::AddIndex => "add index",
Self::DropIndex => "drop index",
Self::AddConstraint => "add constraint",
Self::DropConstraint => "drop constraint",
Self::Query => "query",
Self::Rebuild => "rebuild",
Self::Replay => "replay",