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
+80
View File
@@ -53,6 +53,60 @@ impl ColumnSpec {
}
}
/// A column-level constraint with its payload (ADR-0029 §3).
///
/// Produced by `add constraint <constraint> to <T>.<col>`.
/// `Default` / `Check` carry the value / expression; `NotNull`
/// and `Unique` are payload-free.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Constraint {
NotNull,
Unique,
Default(Value),
Check(Expr),
}
impl Constraint {
/// The bare constraint kind, dropping any payload — used for
/// the `[ok]` summary line and log output.
#[must_use]
pub const fn kind(&self) -> ConstraintKind {
match self {
Self::NotNull => ConstraintKind::NotNull,
Self::Unique => ConstraintKind::Unique,
Self::Default(_) => ConstraintKind::Default,
Self::Check(_) => ConstraintKind::Check,
}
}
}
/// The kind of a column-level constraint, without a payload.
///
/// Produced by `drop constraint <kind> from <T>.<col>`
/// (ADR-0029 §3) — naming the kind is enough, since at most one
/// constraint of each kind exists per column.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintKind {
NotNull,
Unique,
Default,
Check,
}
impl ConstraintKind {
/// Upper-case SQL-style label for user-facing messages
/// (`NOT NULL`, `UNIQUE`, `DEFAULT`, `CHECK`).
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::NotNull => "NOT NULL",
Self::Unique => "UNIQUE",
Self::Default => "DEFAULT",
Self::Check => "CHECK",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
CreateTable {
@@ -150,6 +204,22 @@ pub enum Command {
DropIndex {
selector: IndexSelector,
},
/// Add a column-level constraint to an existing column
/// (ADR-0029 §2.2). Applied through the rebuild-table
/// primitive after a §5 dry-run guards populated columns.
AddConstraint {
table: String,
column: String,
constraint: Constraint,
},
/// Remove a column-level constraint from an existing column
/// (ADR-0029 §2.2). Naming the `kind` is enough — at most
/// one constraint of each kind exists per column.
DropConstraint {
table: String,
column: String,
kind: ConstraintKind,
},
/// Re-display a table's structure in the output. Doesn't
/// change schema; useful when the user wants to look at a
/// table they aren't currently DDL'ing on.
@@ -498,6 +568,8 @@ impl Command {
Self::DropRelationship { .. } => "drop relationship",
Self::AddIndex { .. } => "add index",
Self::DropIndex { .. } => "drop index",
Self::AddConstraint { .. } => "add constraint",
Self::DropConstraint { .. } => "drop constraint",
Self::ShowTable { .. } => "show table",
Self::Insert { .. } => "insert into",
Self::Update { .. } => "update",
@@ -536,6 +608,8 @@ impl Command {
| Self::DropColumn { table, .. }
| Self::RenameColumn { table, .. }
| Self::ChangeColumnType { table, .. }
| Self::AddConstraint { table, .. }
| Self::DropConstraint { table, .. }
| Self::Insert { table, .. }
| Self::Update { table, .. }
| Self::Delete { table, .. } => table,
@@ -598,6 +672,12 @@ impl Command {
"from {parent_table}.{parent_column} to {child_table}.{child_column}"
),
},
// A constraint command's subject is the dotted
// `<table>.<column>` it acts on (ADR-0029 §2.2).
Self::AddConstraint { table, column, .. }
| Self::DropConstraint { table, column, .. } => {
format!("{table}.{column}")
}
_ => self.target_table().to_string(),
}
}