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),
|
RelationshipSelector::Named { .. } => (Operation::DropRelationship, None, None),
|
||||||
},
|
},
|
||||||
C::AddIndex { table, .. } => (Operation::AddIndex, Some(table.as_str()), 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 {
|
C::DropIndex { selector } => match selector {
|
||||||
IndexSelector::Columns { table, .. } => {
|
IndexSelector::Columns { table, .. } => {
|
||||||
(Operation::DropIndex, Some(table.as_str()), None)
|
(Operation::DropIndex, Some(table.as_str()), None)
|
||||||
|
|||||||
+19
-10
@@ -844,17 +844,19 @@ mod tests {
|
|||||||
fn multi_candidate_position_offers_add_subcommands() {
|
fn multi_candidate_position_offers_add_subcommands() {
|
||||||
// After `add ` the parser expects `column` (for
|
// After `add ` the parser expects `column` (for
|
||||||
// `add column ...`), `index` (for `add index ...`,
|
// `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
|
// `add 1:n relationship ...`). The completion engine
|
||||||
// sections keyword candidates (`column`, `index`)
|
// sections keyword candidates ahead of the `1:n`
|
||||||
// ahead of the `1:n` composite literal, so the literal
|
// composite literal, so the literal sorts last even
|
||||||
// sorts last even though `add 1:n` is declared second.
|
// though `add 1:n` is declared second.
|
||||||
let cs = cands("add ", 4);
|
let cs = cands("add ", 4);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cs,
|
cs,
|
||||||
vec![
|
vec![
|
||||||
"column".to_string(),
|
"column".to_string(),
|
||||||
"index".to_string(),
|
"index".to_string(),
|
||||||
|
"constraint".to_string(),
|
||||||
"1:n".to_string(),
|
"1:n".to_string(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -1113,10 +1115,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn drop_offers_all_four_subcommands() {
|
fn drop_offers_all_five_subcommands() {
|
||||||
// `drop` branches: column / relationship / table / index
|
// `drop` branches: column / relationship / table / index
|
||||||
// (ADR-0025). Candidates follow grammar declaration
|
// (ADR-0025) / constraint (ADR-0029 §2.2). Candidates
|
||||||
// order, so `index` — added last — appears last.
|
// follow grammar declaration order, so `constraint` —
|
||||||
|
// added last — appears last.
|
||||||
let cs = cands("drop ", 5);
|
let cs = cands("drop ", 5);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cs,
|
cs,
|
||||||
@@ -1125,6 +1128,7 @@ mod tests {
|
|||||||
"relationship".to_string(),
|
"relationship".to_string(),
|
||||||
"table".to_string(),
|
"table".to_string(),
|
||||||
"index".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.sort_by(|a, b| a.text.cmp(&b.text));
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
// `add ` exposes `column`, `1:n` and `index` — the
|
// `add ` exposes `column`, `1:n`, `index` and
|
||||||
// alphabetic ranker reorders them.
|
// `constraint` — the alphabetic ranker reorders them.
|
||||||
let cache = SchemaCache::default();
|
let cache = SchemaCache::default();
|
||||||
let comp = candidates_at_cursor_with("add ", 4, &cache, alphabetic_ranker)
|
let comp = candidates_at_cursor_with("add ", 4, &cache, alphabetic_ranker)
|
||||||
.expect("some completion");
|
.expect("some completion");
|
||||||
let texts: Vec<String> = comp.candidates.into_iter().map(|c| c.text).collect();
|
let texts: Vec<String> = comp.candidates.into_iter().map(|c| c.text).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
texts,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
CreateTable {
|
CreateTable {
|
||||||
@@ -150,6 +204,22 @@ pub enum Command {
|
|||||||
DropIndex {
|
DropIndex {
|
||||||
selector: IndexSelector,
|
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
|
/// Re-display a table's structure in the output. Doesn't
|
||||||
/// change schema; useful when the user wants to look at a
|
/// change schema; useful when the user wants to look at a
|
||||||
/// table they aren't currently DDL'ing on.
|
/// table they aren't currently DDL'ing on.
|
||||||
@@ -498,6 +568,8 @@ impl Command {
|
|||||||
Self::DropRelationship { .. } => "drop relationship",
|
Self::DropRelationship { .. } => "drop relationship",
|
||||||
Self::AddIndex { .. } => "add index",
|
Self::AddIndex { .. } => "add index",
|
||||||
Self::DropIndex { .. } => "drop index",
|
Self::DropIndex { .. } => "drop index",
|
||||||
|
Self::AddConstraint { .. } => "add constraint",
|
||||||
|
Self::DropConstraint { .. } => "drop constraint",
|
||||||
Self::ShowTable { .. } => "show table",
|
Self::ShowTable { .. } => "show table",
|
||||||
Self::Insert { .. } => "insert into",
|
Self::Insert { .. } => "insert into",
|
||||||
Self::Update { .. } => "update",
|
Self::Update { .. } => "update",
|
||||||
@@ -536,6 +608,8 @@ impl Command {
|
|||||||
| Self::DropColumn { table, .. }
|
| Self::DropColumn { table, .. }
|
||||||
| Self::RenameColumn { table, .. }
|
| Self::RenameColumn { table, .. }
|
||||||
| Self::ChangeColumnType { table, .. }
|
| Self::ChangeColumnType { table, .. }
|
||||||
|
| Self::AddConstraint { table, .. }
|
||||||
|
| Self::DropConstraint { table, .. }
|
||||||
| Self::Insert { table, .. }
|
| Self::Insert { table, .. }
|
||||||
| Self::Update { table, .. }
|
| Self::Update { table, .. }
|
||||||
| Self::Delete { table, .. } => table,
|
| Self::Delete { table, .. } => table,
|
||||||
@@ -598,6 +672,12 @@ impl Command {
|
|||||||
"from {parent_table}.{parent_column} to {child_table}.{child_column}"
|
"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(),
|
_ => self.target_table().to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+212
-7
@@ -13,7 +13,8 @@
|
|||||||
|
|
||||||
use crate::dsl::action::ReferentialAction;
|
use crate::dsl::action::ReferentialAction;
|
||||||
use crate::dsl::command::{
|
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::value::Value;
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
@@ -283,7 +284,8 @@ const DROP_INDEX: Node = Node::Seq(DROP_INDEX_NODES);
|
|||||||
// drop entry — `drop (table|column|relationship|index) ...`
|
// 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);
|
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) …`
|
// 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);
|
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") => {
|
Some("relationship") => {
|
||||||
// Endpoints form has `from` as the third Word.
|
// Endpoints form has `from` as the third Word.
|
||||||
let has_from = path
|
let has_from = path
|
||||||
@@ -634,6 +637,7 @@ fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
|
|||||||
table: require_ident(path, "table_name")?,
|
table: require_ident(path, "table_name")?,
|
||||||
columns: collect_idents(path, "column_name"),
|
columns: collect_idents(path, "column_name"),
|
||||||
}),
|
}),
|
||||||
|
Some("constraint") => build_add_constraint(path),
|
||||||
_ => Err(ValidationError {
|
_ => Err(ValidationError {
|
||||||
message_key: "parse.error_wrapper",
|
message_key: "parse.error_wrapper",
|
||||||
args: vec![("detail", "unknown add subcommand".to_string())],
|
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
|
// CommandNodes
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -778,6 +848,7 @@ pub static DROP: CommandNode = CommandNode {
|
|||||||
"parse.usage.drop_column",
|
"parse.usage.drop_column",
|
||||||
"parse.usage.drop_relationship",
|
"parse.usage.drop_relationship",
|
||||||
"parse.usage.drop_index",
|
"parse.usage.drop_index",
|
||||||
|
"parse.usage.drop_constraint",
|
||||||
],};
|
],};
|
||||||
|
|
||||||
pub static ADD: CommandNode = CommandNode {
|
pub static ADD: CommandNode = CommandNode {
|
||||||
@@ -789,6 +860,7 @@ pub static ADD: CommandNode = CommandNode {
|
|||||||
"parse.usage.add_column",
|
"parse.usage.add_column",
|
||||||
"parse.usage.add_relationship",
|
"parse.usage.add_relationship",
|
||||||
"parse.usage.add_index",
|
"parse.usage.add_index",
|
||||||
|
"parse.usage.add_constraint",
|
||||||
],};
|
],};
|
||||||
|
|
||||||
pub static RENAME: CommandNode = CommandNode {
|
pub static RENAME: CommandNode = CommandNode {
|
||||||
@@ -825,9 +897,10 @@ const COL_NAME: Node = Node::Hinted {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ADR-0029 column-constraint suffix — `not null`, `unique`,
|
// ADR-0029 column-constraint suffix — `not null`, `unique`,
|
||||||
// `default <literal>`. (`check (<expr>)` joins in a later
|
// `default <literal>`, `check (<expr>)`. One shared fragment:
|
||||||
// ADR-0029 step.) One shared fragment: `create table` uses it
|
// `create table` uses it here, `add column` reuses it as its
|
||||||
// here; `add column` and `add constraint` reuse it later.
|
// type suffix, and `add constraint` reuses the individual
|
||||||
|
// `COLUMN_CONSTRAINT` Choice for its constraint slot.
|
||||||
const NOT_NULL_NODES: &[Node] = &[
|
const NOT_NULL_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("not")),
|
Node::Word(Word::keyword("not")),
|
||||||
Node::Word(Word::keyword("null")),
|
Node::Word(Word::keyword("null")),
|
||||||
@@ -867,6 +940,53 @@ const COLUMN_CONSTRAINT_SUFFIX: Node = Node::Repeated {
|
|||||||
min: 0,
|
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] = &[
|
const COL_SPEC_NODES: &[Node] = &[
|
||||||
COL_NAME,
|
COL_NAME,
|
||||||
Node::Punct('('),
|
Node::Punct('('),
|
||||||
@@ -1117,7 +1237,7 @@ pub static CREATE: CommandNode = CommandNode {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod constraint_tests {
|
mod constraint_tests {
|
||||||
use super::Command;
|
use super::{Command, Constraint, ConstraintKind};
|
||||||
use crate::dsl::command::ColumnSpec;
|
use crate::dsl::command::ColumnSpec;
|
||||||
use crate::dsl::parser::parse_command;
|
use crate::dsl::parser::parse_command;
|
||||||
use crate::dsl::value::Value;
|
use crate::dsl::value::Value;
|
||||||
@@ -1234,4 +1354,89 @@ mod constraint_tests {
|
|||||||
);
|
);
|
||||||
assert!(cols[0].check.is_some());
|
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 = [
|
let cases = [
|
||||||
("add column to T: c (int)", "parse.usage.add_column"),
|
("add column to T: c (int)", "parse.usage.add_column"),
|
||||||
("add index on T (c)", "parse.usage.add_index"),
|
("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",
|
"add 1:n relationship from A.x to B.y",
|
||||||
"parse.usage.add_relationship",
|
"parse.usage.add_relationship",
|
||||||
|
|||||||
@@ -207,12 +207,14 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
// code, not the catalog, because spacing is alignment-
|
// code, not the catalog, because spacing is alignment-
|
||||||
// sensitive in the multi-entry case.
|
// sensitive in the multi-entry case.
|
||||||
("parse.usage.add_column", &[]),
|
("parse.usage.add_column", &[]),
|
||||||
|
("parse.usage.add_constraint", &[]),
|
||||||
("parse.usage.add_index", &[]),
|
("parse.usage.add_index", &[]),
|
||||||
("parse.usage.add_relationship", &[]),
|
("parse.usage.add_relationship", &[]),
|
||||||
("parse.usage.change_column", &[]),
|
("parse.usage.change_column", &[]),
|
||||||
("parse.usage.create_table", &[]),
|
("parse.usage.create_table", &[]),
|
||||||
("parse.usage.delete", &[]),
|
("parse.usage.delete", &[]),
|
||||||
("parse.usage.drop_column", &[]),
|
("parse.usage.drop_column", &[]),
|
||||||
|
("parse.usage.drop_constraint", &[]),
|
||||||
("parse.usage.drop_index", &[]),
|
("parse.usage.drop_index", &[]),
|
||||||
("parse.usage.drop_relationship", &[]),
|
("parse.usage.drop_relationship", &[]),
|
||||||
("parse.usage.drop_table", &[]),
|
("parse.usage.drop_table", &[]),
|
||||||
@@ -385,6 +387,19 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
&["count", "action", "child_table", "rel", "on_delete"],
|
&["count", "action", "child_table", "rel", "on_delete"],
|
||||||
),
|
),
|
||||||
// ---- change-column dry-run diagnostics (per ADR-0017) ----
|
// ---- 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.force_conversion_hint", &[]),
|
||||||
("db.diagnostic.header_becomes", &[]),
|
("db.diagnostic.header_becomes", &[]),
|
||||||
("db.diagnostic.header_from", &[]),
|
("db.diagnostic.header_from", &[]),
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ parse:
|
|||||||
drop_index: |-
|
drop_index: |-
|
||||||
drop index <Name>
|
drop index <Name>
|
||||||
drop index on <Table> (<col>[, ...])
|
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_column: "add column [to] [table] <Table>: <Name> (<Type>)"
|
||||||
add_relationship: |-
|
add_relationship: |-
|
||||||
add 1:n relationship [as <Name>]
|
add 1:n relationship [as <Name>]
|
||||||
@@ -439,6 +440,11 @@ parse:
|
|||||||
[on delete <action>] [on update <action>]
|
[on delete <action>] [on update <action>]
|
||||||
[--create-fk]
|
[--create-fk]
|
||||||
add_index: "add index [as <Name>] on <Table> (<col>[, ...])"
|
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>"
|
rename_column: "rename column [in] [table] <Table>: <Old> to <New>"
|
||||||
change_column: |-
|
change_column: |-
|
||||||
change column [in] [table] <Table>: <Name> (<Type>)
|
change column [in] [table] <Table>: <Name> (<Type>)
|
||||||
@@ -727,6 +733,17 @@ db:
|
|||||||
# Follow-up suggestion appended to the lossy diagnostic
|
# Follow-up suggestion appended to the lossy diagnostic
|
||||||
# (only — incompatibles can't be force-overridden).
|
# (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`."
|
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) --------------
|
# ---- DSL command success summaries (ADR-0019 §9 sweep) --------------
|
||||||
ok:
|
ok:
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ pub enum Operation {
|
|||||||
DropRelationship,
|
DropRelationship,
|
||||||
AddIndex,
|
AddIndex,
|
||||||
DropIndex,
|
DropIndex,
|
||||||
|
AddConstraint,
|
||||||
|
DropConstraint,
|
||||||
Query,
|
Query,
|
||||||
Rebuild,
|
Rebuild,
|
||||||
Replay,
|
Replay,
|
||||||
@@ -96,6 +98,8 @@ impl Operation {
|
|||||||
Self::DropRelationship => "drop relationship",
|
Self::DropRelationship => "drop relationship",
|
||||||
Self::AddIndex => "add index",
|
Self::AddIndex => "add index",
|
||||||
Self::DropIndex => "drop index",
|
Self::DropIndex => "drop index",
|
||||||
|
Self::AddConstraint => "add constraint",
|
||||||
|
Self::DropConstraint => "drop constraint",
|
||||||
Self::Query => "query",
|
Self::Query => "query",
|
||||||
Self::Rebuild => "rebuild",
|
Self::Rebuild => "rebuild",
|
||||||
Self::Replay => "replay",
|
Self::Replay => "replay",
|
||||||
|
|||||||
@@ -1789,6 +1789,22 @@ async fn execute_command_typed(
|
|||||||
.drop_index(selector, src)
|
.drop_index(selector, src)
|
||||||
.await
|
.await
|
||||||
.map(|d| CommandOutcome::Schema(Some(d))),
|
.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
|
Command::ShowTable { name } => database
|
||||||
.describe_table(name, src)
|
.describe_table(name, src)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String {
|
|||||||
DropRelationship { .. } => "DropRelationship".into(),
|
DropRelationship { .. } => "DropRelationship".into(),
|
||||||
AddIndex { .. } => "AddIndex".into(),
|
AddIndex { .. } => "AddIndex".into(),
|
||||||
DropIndex { .. } => "DropIndex".into(),
|
DropIndex { .. } => "DropIndex".into(),
|
||||||
|
AddConstraint { .. } => "AddConstraint".into(),
|
||||||
|
DropConstraint { .. } => "DropConstraint".into(),
|
||||||
ShowTable { .. } => "ShowTable".into(),
|
ShowTable { .. } => "ShowTable".into(),
|
||||||
Insert { .. } => "Insert".into(),
|
Insert { .. } => "Insert".into(),
|
||||||
Update { .. } => "Update".into(),
|
Update { .. } => "Update".into(),
|
||||||
|
|||||||
+8
@@ -18,6 +18,10 @@ Assessment {
|
|||||||
text: "index",
|
text: "index",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
},
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "constraint",
|
||||||
|
kind: Keyword,
|
||||||
|
},
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "1:n",
|
text: "1:n",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
@@ -42,6 +46,10 @@ Assessment {
|
|||||||
text: "index",
|
text: "index",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
},
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "constraint",
|
||||||
|
kind: Keyword,
|
||||||
|
},
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "1:n",
|
text: "1:n",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
|
|||||||
+8
@@ -18,6 +18,10 @@ Assessment {
|
|||||||
text: "index",
|
text: "index",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
},
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "constraint",
|
||||||
|
kind: Keyword,
|
||||||
|
},
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "1:n",
|
text: "1:n",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
@@ -42,6 +46,10 @@ Assessment {
|
|||||||
text: "index",
|
text: "index",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
},
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "constraint",
|
||||||
|
kind: Keyword,
|
||||||
|
},
|
||||||
Candidate {
|
Candidate {
|
||||||
text: "1:n",
|
text: "1:n",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
|
|||||||
+8
@@ -26,6 +26,10 @@ Assessment {
|
|||||||
text: "index",
|
text: "index",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
},
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "constraint",
|
||||||
|
kind: Keyword,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
selected: None,
|
selected: None,
|
||||||
},
|
},
|
||||||
@@ -54,6 +58,10 @@ Assessment {
|
|||||||
text: "index",
|
text: "index",
|
||||||
kind: Keyword,
|
kind: Keyword,
|
||||||
},
|
},
|
||||||
|
Candidate {
|
||||||
|
text: "constraint",
|
||||||
|
kind: Keyword,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user