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:
+10
@@ -1421,6 +1421,16 @@ impl App {
|
||||
RelationshipSelector::Named { .. } => (Operation::DropRelationship, None, None),
|
||||
},
|
||||
C::AddIndex { table, .. } => (Operation::AddIndex, Some(table.as_str()), None),
|
||||
C::AddConstraint { table, column, .. } => (
|
||||
Operation::AddConstraint,
|
||||
Some(table.as_str()),
|
||||
Some(column.as_str()),
|
||||
),
|
||||
C::DropConstraint { table, column, .. } => (
|
||||
Operation::DropConstraint,
|
||||
Some(table.as_str()),
|
||||
Some(column.as_str()),
|
||||
),
|
||||
C::DropIndex { selector } => match selector {
|
||||
IndexSelector::Columns { table, .. } => {
|
||||
(Operation::DropIndex, Some(table.as_str()), None)
|
||||
|
||||
+19
-10
@@ -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(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
+212
-7
@@ -13,7 +13,8 @@
|
||||
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{
|
||||
ChangeColumnMode, ColumnSpec, Command, Expr, IndexSelector, RelationshipSelector,
|
||||
ChangeColumnMode, ColumnSpec, Command, Constraint, ConstraintKind, Expr, IndexSelector,
|
||||
RelationshipSelector,
|
||||
};
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::grammar::{
|
||||
@@ -283,7 +284,8 @@ const DROP_INDEX: Node = Node::Seq(DROP_INDEX_NODES);
|
||||
// drop entry — `drop (table|column|relationship|index) ...`
|
||||
// =================================================================
|
||||
|
||||
const DROP_CHOICES: &[Node] = &[DROP_COLUMN, DROP_RELATIONSHIP, DROP_TABLE, DROP_INDEX];
|
||||
const DROP_CHOICES: &[Node] =
|
||||
&[DROP_COLUMN, DROP_RELATIONSHIP, DROP_TABLE, DROP_INDEX, DROP_CONSTRAINT];
|
||||
const DROP_SHAPE: Node = Node::Choice(DROP_CHOICES);
|
||||
|
||||
// =================================================================
|
||||
@@ -410,7 +412,7 @@ const ADD_INDEX: Node = Node::Seq(ADD_INDEX_NODES);
|
||||
// add entry — `add (column|1:n relationship|index) …`
|
||||
// =================================================================
|
||||
|
||||
const ADD_CHOICES: &[Node] = &[ADD_COLUMN, ADD_RELATIONSHIP, ADD_INDEX];
|
||||
const ADD_CHOICES: &[Node] = &[ADD_COLUMN, ADD_RELATIONSHIP, ADD_INDEX, ADD_CONSTRAINT];
|
||||
const ADD_SHAPE: Node = Node::Choice(ADD_CHOICES);
|
||||
|
||||
// =================================================================
|
||||
@@ -564,6 +566,7 @@ fn build_drop(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
})
|
||||
}
|
||||
}
|
||||
Some("constraint") => build_drop_constraint(path),
|
||||
Some("relationship") => {
|
||||
// Endpoints form has `from` as the third Word.
|
||||
let has_from = path
|
||||
@@ -634,6 +637,7 @@ fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
table: require_ident(path, "table_name")?,
|
||||
columns: collect_idents(path, "column_name"),
|
||||
}),
|
||||
Some("constraint") => build_add_constraint(path),
|
||||
_ => Err(ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown add subcommand".to_string())],
|
||||
@@ -764,6 +768,72 @@ fn build_change_column(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Build an `add constraint <constraint> to <T>.<col>` command
|
||||
/// (ADR-0029 §2.2). The `<constraint>` reuses the §2.1
|
||||
/// `COLUMN_CONSTRAINT` Choice, so exactly one of the four
|
||||
/// constraint kinds is matched; `collect_column_constraints`
|
||||
/// recovers it. The §9 redundancy and §5 dry-run checks are
|
||||
/// execution-time (the parser has no schema) and live in the
|
||||
/// database worker.
|
||||
fn build_add_constraint(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
let (not_null, unique, default, check) = collect_column_constraints(path)?;
|
||||
let constraint = if not_null {
|
||||
Constraint::NotNull
|
||||
} else if unique {
|
||||
Constraint::Unique
|
||||
} else if let Some(value) = default {
|
||||
Constraint::Default(value)
|
||||
} else if let Some(expr) = check {
|
||||
Constraint::Check(expr)
|
||||
} else {
|
||||
return Err(ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "add constraint needs a constraint".to_string())],
|
||||
});
|
||||
};
|
||||
Ok(Command::AddConstraint {
|
||||
table: require_ident(path, "table_name")?,
|
||||
column: require_ident(path, "column_name")?,
|
||||
constraint,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a `drop constraint <kind> from <T>.<col>` command
|
||||
/// (ADR-0029 §2.2). `drop` names only the kind — the
|
||||
/// `DROP_CONSTRAINT_KIND` Choice is payload-free, so the kind
|
||||
/// is recovered from which keyword(s) the path matched.
|
||||
fn build_drop_constraint(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
let words: Vec<&'static str> = path
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|i| match &i.kind {
|
||||
MatchedKind::Word(w) => Some(*w),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
// `not` appears only in the `not null` Seq, so its presence
|
||||
// alone identifies the kind.
|
||||
let kind = if words.contains(&"not") {
|
||||
ConstraintKind::NotNull
|
||||
} else if words.contains(&"unique") {
|
||||
ConstraintKind::Unique
|
||||
} else if words.contains(&"default") {
|
||||
ConstraintKind::Default
|
||||
} else if words.contains(&"check") {
|
||||
ConstraintKind::Check
|
||||
} else {
|
||||
return Err(ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "drop constraint needs a constraint kind".to_string())],
|
||||
});
|
||||
};
|
||||
Ok(Command::DropConstraint {
|
||||
table: require_ident(path, "table_name")?,
|
||||
column: require_ident(path, "column_name")?,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// CommandNodes
|
||||
// =================================================================
|
||||
@@ -778,6 +848,7 @@ pub static DROP: CommandNode = CommandNode {
|
||||
"parse.usage.drop_column",
|
||||
"parse.usage.drop_relationship",
|
||||
"parse.usage.drop_index",
|
||||
"parse.usage.drop_constraint",
|
||||
],};
|
||||
|
||||
pub static ADD: CommandNode = CommandNode {
|
||||
@@ -789,6 +860,7 @@ pub static ADD: CommandNode = CommandNode {
|
||||
"parse.usage.add_column",
|
||||
"parse.usage.add_relationship",
|
||||
"parse.usage.add_index",
|
||||
"parse.usage.add_constraint",
|
||||
],};
|
||||
|
||||
pub static RENAME: CommandNode = CommandNode {
|
||||
@@ -825,9 +897,10 @@ const COL_NAME: Node = Node::Hinted {
|
||||
};
|
||||
|
||||
// ADR-0029 column-constraint suffix — `not null`, `unique`,
|
||||
// `default <literal>`. (`check (<expr>)` joins in a later
|
||||
// ADR-0029 step.) One shared fragment: `create table` uses it
|
||||
// here; `add column` and `add constraint` reuse it later.
|
||||
// `default <literal>`, `check (<expr>)`. One shared fragment:
|
||||
// `create table` uses it here, `add column` reuses it as its
|
||||
// type suffix, and `add constraint` reuses the individual
|
||||
// `COLUMN_CONSTRAINT` Choice for its constraint slot.
|
||||
const NOT_NULL_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("not")),
|
||||
Node::Word(Word::keyword("null")),
|
||||
@@ -867,6 +940,53 @@ const COLUMN_CONSTRAINT_SUFFIX: Node = Node::Repeated {
|
||||
min: 0,
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// add_constraint / drop_constraint — `add constraint <constraint>
|
||||
// to <T>.<col>` / `drop constraint <kind> from <T>.<col>`
|
||||
// (ADR-0029 §2.2)
|
||||
// =================================================================
|
||||
|
||||
// Payload-free keyword nodes for `drop constraint` — naming the
|
||||
// kind is enough, since at most one constraint of each kind
|
||||
// exists per column. `not null` / `unique` reuse the §2.1
|
||||
// keyword-only nodes; `default` / `check` need bare-keyword
|
||||
// variants here (their §2.1 forms carry a literal / expression
|
||||
// payload that `drop` does not take).
|
||||
const DROP_DEFAULT_KEYWORD: Node = Node::Word(Word::keyword("default"));
|
||||
const DROP_CHECK_KEYWORD: Node = Node::Word(Word::keyword("check"));
|
||||
const DROP_CONSTRAINT_KIND_CHOICES: &[Node] = &[
|
||||
NOT_NULL_CONSTRAINT,
|
||||
UNIQUE_CONSTRAINT,
|
||||
DROP_DEFAULT_KEYWORD,
|
||||
DROP_CHECK_KEYWORD,
|
||||
];
|
||||
const DROP_CONSTRAINT_KIND: Node = Node::Choice(DROP_CONSTRAINT_KIND_CHOICES);
|
||||
|
||||
// The dotted `<Table>.<column>` target — the same `Ident '.'
|
||||
// Ident` shape `add 1:n relationship` uses for its endpoints.
|
||||
// `writes_table: true` on the table ident (via `TABLE_NAME_
|
||||
// EXISTING`) narrows the `.<column>` slot's completion
|
||||
// candidates to that table's columns.
|
||||
const CONSTRAINT_TARGET_NODES: &[Node] =
|
||||
&[TABLE_NAME_EXISTING, Node::Punct('.'), COLUMN_NAME];
|
||||
const CONSTRAINT_TARGET: Node = Node::Seq(CONSTRAINT_TARGET_NODES);
|
||||
|
||||
const ADD_CONSTRAINT_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("constraint")),
|
||||
COLUMN_CONSTRAINT,
|
||||
Node::Word(Word::keyword("to")),
|
||||
CONSTRAINT_TARGET,
|
||||
];
|
||||
const ADD_CONSTRAINT: Node = Node::Seq(ADD_CONSTRAINT_NODES);
|
||||
|
||||
const DROP_CONSTRAINT_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("constraint")),
|
||||
DROP_CONSTRAINT_KIND,
|
||||
Node::Word(Word::keyword("from")),
|
||||
CONSTRAINT_TARGET,
|
||||
];
|
||||
const DROP_CONSTRAINT: Node = Node::Seq(DROP_CONSTRAINT_NODES);
|
||||
|
||||
const COL_SPEC_NODES: &[Node] = &[
|
||||
COL_NAME,
|
||||
Node::Punct('('),
|
||||
@@ -1117,7 +1237,7 @@ pub static CREATE: CommandNode = CommandNode {
|
||||
|
||||
#[cfg(test)]
|
||||
mod constraint_tests {
|
||||
use super::Command;
|
||||
use super::{Command, Constraint, ConstraintKind};
|
||||
use crate::dsl::command::ColumnSpec;
|
||||
use crate::dsl::parser::parse_command;
|
||||
use crate::dsl::value::Value;
|
||||
@@ -1234,4 +1354,89 @@ mod constraint_tests {
|
||||
);
|
||||
assert!(cols[0].check.is_some());
|
||||
}
|
||||
|
||||
// --- `add constraint` / `drop constraint` (ADR-0029 §2.2) ---
|
||||
|
||||
#[test]
|
||||
fn add_constraint_not_null_parses() {
|
||||
match parse_command("add constraint not null to Users.email").expect("parse") {
|
||||
Command::AddConstraint {
|
||||
table,
|
||||
column,
|
||||
constraint,
|
||||
} => {
|
||||
assert_eq!(table, "Users");
|
||||
assert_eq!(column, "email");
|
||||
assert_eq!(constraint, Constraint::NotNull);
|
||||
}
|
||||
other => panic!("expected AddConstraint, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_constraint_unique_parses() {
|
||||
match parse_command("add constraint unique to Users.email").expect("parse") {
|
||||
Command::AddConstraint { constraint, .. } => {
|
||||
assert_eq!(constraint, Constraint::Unique);
|
||||
}
|
||||
other => panic!("expected AddConstraint, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_constraint_default_parses() {
|
||||
match parse_command("add constraint default 18 to Users.age").expect("parse") {
|
||||
Command::AddConstraint { constraint, .. } => {
|
||||
assert_eq!(
|
||||
constraint,
|
||||
Constraint::Default(Value::Number("18".to_string()))
|
||||
);
|
||||
}
|
||||
other => panic!("expected AddConstraint, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_constraint_check_parses() {
|
||||
match parse_command("add constraint check (age >= 0) to Users.age").expect("parse")
|
||||
{
|
||||
Command::AddConstraint {
|
||||
column, constraint, ..
|
||||
} => {
|
||||
assert_eq!(column, "age");
|
||||
assert!(matches!(constraint, Constraint::Check(_)));
|
||||
}
|
||||
other => panic!("expected AddConstraint, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_constraint_not_null_parses() {
|
||||
match parse_command("drop constraint not null from Users.email").expect("parse") {
|
||||
Command::DropConstraint {
|
||||
table,
|
||||
column,
|
||||
kind,
|
||||
} => {
|
||||
assert_eq!(table, "Users");
|
||||
assert_eq!(column, "email");
|
||||
assert_eq!(kind, ConstraintKind::NotNull);
|
||||
}
|
||||
other => panic!("expected DropConstraint, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_constraint_each_kind_parses() {
|
||||
for (input, expected) in [
|
||||
("drop constraint unique from T.c", ConstraintKind::Unique),
|
||||
("drop constraint default from T.c", ConstraintKind::Default),
|
||||
("drop constraint check from T.c", ConstraintKind::Check),
|
||||
] {
|
||||
match parse_command(input).expect("parse") {
|
||||
Command::DropConstraint { kind, .. } => assert_eq!(kind, expected),
|
||||
other => panic!("expected DropConstraint for {input:?}, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,6 +514,14 @@ mod usage_key_tests {
|
||||
let cases = [
|
||||
("add column to T: c (int)", "parse.usage.add_column"),
|
||||
("add index on T (c)", "parse.usage.add_index"),
|
||||
(
|
||||
"add constraint unique to T.c",
|
||||
"parse.usage.add_constraint",
|
||||
),
|
||||
(
|
||||
"drop constraint check from T.c",
|
||||
"parse.usage.drop_constraint",
|
||||
),
|
||||
(
|
||||
"add 1:n relationship from A.x to B.y",
|
||||
"parse.usage.add_relationship",
|
||||
|
||||
@@ -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", &[]),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1789,6 +1789,22 @@ async fn execute_command_typed(
|
||||
.drop_index(selector, src)
|
||||
.await
|
||||
.map(|d| CommandOutcome::Schema(Some(d))),
|
||||
Command::AddConstraint {
|
||||
table,
|
||||
column,
|
||||
constraint,
|
||||
} => database
|
||||
.add_constraint(table, column, constraint, src)
|
||||
.await
|
||||
.map(|d| CommandOutcome::Schema(Some(d))),
|
||||
Command::DropConstraint {
|
||||
table,
|
||||
column,
|
||||
kind,
|
||||
} => database
|
||||
.drop_constraint(table, column, kind, src)
|
||||
.await
|
||||
.map(|d| CommandOutcome::Schema(Some(d))),
|
||||
Command::ShowTable { name } => database
|
||||
.describe_table(name, src)
|
||||
.await
|
||||
|
||||
@@ -208,6 +208,8 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String {
|
||||
DropRelationship { .. } => "DropRelationship".into(),
|
||||
AddIndex { .. } => "AddIndex".into(),
|
||||
DropIndex { .. } => "DropIndex".into(),
|
||||
AddConstraint { .. } => "AddConstraint".into(),
|
||||
DropConstraint { .. } => "DropConstraint".into(),
|
||||
ShowTable { .. } => "ShowTable".into(),
|
||||
Insert { .. } => "Insert".into(),
|
||||
Update { .. } => "Update".into(),
|
||||
|
||||
+8
@@ -18,6 +18,10 @@ Assessment {
|
||||
text: "index",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "constraint",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "1:n",
|
||||
kind: Keyword,
|
||||
@@ -42,6 +46,10 @@ Assessment {
|
||||
text: "index",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "constraint",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "1:n",
|
||||
kind: Keyword,
|
||||
|
||||
+8
@@ -18,6 +18,10 @@ Assessment {
|
||||
text: "index",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "constraint",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "1:n",
|
||||
kind: Keyword,
|
||||
@@ -42,6 +46,10 @@ Assessment {
|
||||
text: "index",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "constraint",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "1:n",
|
||||
kind: Keyword,
|
||||
|
||||
+8
@@ -26,6 +26,10 @@ Assessment {
|
||||
text: "index",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "constraint",
|
||||
kind: Keyword,
|
||||
},
|
||||
],
|
||||
selected: None,
|
||||
},
|
||||
@@ -54,6 +58,10 @@ Assessment {
|
||||
text: "index",
|
||||
kind: Keyword,
|
||||
},
|
||||
Candidate {
|
||||
text: "constraint",
|
||||
kind: Keyword,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user