add column: column constraints — NOT NULL / UNIQUE / DEFAULT (ADR-0029 §6)
`add column` now accepts the shared constraint suffix and the worker honours it — the surface where NOT NULL / UNIQUE actually matter, on non-PK columns. - Grammar: `ADD_COLUMN_NODES` gains the constraint-suffix fragment; `collect_column_constraints` folds it into `Command::AddColumn`. - `do_add_column` routes per ADR-0029 §6: SQLite's `ALTER TABLE ADD COLUMN` cannot express `UNIQUE` and requires a default for `NOT NULL`, so those go through the rebuild primitive (`do_add_constrained_column_via_rebuild`); plain cases keep the ALTER path with the constraint suffix appended. - Pre-flight refusals, before any SQL write: a NOT NULL column with no default added to a populated table; a UNIQUE column with a default added to a multi-row table; a default on a `serial` / `shortid` column. CHECK is still deferred to the next commit. 1193 tests pass (+9); clippy clean.
This commit is contained in:
+80
-5
@@ -15,6 +15,7 @@ use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{
|
||||
ChangeColumnMode, ColumnSpec, Command, IndexSelector, RelationshipSelector,
|
||||
};
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, HintMode, IdentSource, Node, ValidationError, Word,
|
||||
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
|
||||
@@ -299,6 +300,9 @@ const ADD_COLUMN_NODES: &[Node] = &[
|
||||
Node::Punct('('),
|
||||
TYPE_SLOT,
|
||||
Node::Punct(')'),
|
||||
// ADR-0029: the constraint suffix — shared with `create
|
||||
// table`'s column spec.
|
||||
COLUMN_CONSTRAINT_SUFFIX,
|
||||
];
|
||||
const ADD_COLUMN: Node = Node::Seq(ADD_COLUMN_NODES);
|
||||
|
||||
@@ -612,15 +616,15 @@ fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown type".to_string())],
|
||||
})?;
|
||||
let (not_null, unique, default) = collect_column_constraints(path)?;
|
||||
Ok(Command::AddColumn {
|
||||
table: require_ident(path, "table_name")?,
|
||||
column: require_ident(path, "column_name")?,
|
||||
ty,
|
||||
// Constraint suffix is wired in once the
|
||||
// constraint grammar lands (ADR-0029).
|
||||
not_null: false,
|
||||
unique: false,
|
||||
default: None,
|
||||
not_null,
|
||||
unique,
|
||||
default,
|
||||
// CHECK joins in a later ADR-0029 step.
|
||||
check: None,
|
||||
})
|
||||
}
|
||||
@@ -890,6 +894,47 @@ const CREATE_TABLE_NODES: &[Node] = &[
|
||||
];
|
||||
const CREATE_TABLE: Node = Node::Seq(CREATE_TABLE_NODES);
|
||||
|
||||
/// Collect the ADR-0029 constraint suffix from a
|
||||
/// single-column command's matched path (`add column`),
|
||||
/// returning the `(not_null, unique, default)` triple. The
|
||||
/// scan reacts only to the four constraint keywords, so
|
||||
/// passing the whole path is safe. (`create table`'s
|
||||
/// multi-column collection is inline in `build_create_table`.)
|
||||
fn collect_column_constraints(
|
||||
path: &MatchedPath,
|
||||
) -> Result<(bool, bool, Option<Value>), ValidationError> {
|
||||
let mut not_null = false;
|
||||
let mut unique = false;
|
||||
let mut default = None;
|
||||
let mut items = path.items.iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
match &item.kind {
|
||||
MatchedKind::Word("not") => {
|
||||
if matches!(
|
||||
items.peek().map(|i| &i.kind),
|
||||
Some(MatchedKind::Word("null"))
|
||||
) {
|
||||
items.next();
|
||||
not_null = true;
|
||||
}
|
||||
}
|
||||
MatchedKind::Word("unique") => unique = true,
|
||||
MatchedKind::Word("default") => {
|
||||
let value = items
|
||||
.next()
|
||||
.and_then(crate::dsl::grammar::data::item_to_value)
|
||||
.ok_or_else(|| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "default needs a value".to_string())],
|
||||
})?;
|
||||
default = Some(value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok((not_null, unique, default))
|
||||
}
|
||||
|
||||
/// The friendly error for declaring a constraint a
|
||||
/// primary-key column already implies (ADR-0029 §9).
|
||||
fn redundant_pk_constraint(column: &str, constraint: &str) -> ValidationError {
|
||||
@@ -1077,4 +1122,34 @@ mod constraint_tests {
|
||||
assert_eq!(cols.len(), 2);
|
||||
assert!(cols.iter().all(|c| !c.not_null && !c.unique && c.default.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_column_parses_its_constraint_suffix() {
|
||||
match parse_command("add column to T: tier (int) not null default 0")
|
||||
.expect("add column should parse")
|
||||
{
|
||||
Command::AddColumn {
|
||||
not_null,
|
||||
unique,
|
||||
default,
|
||||
..
|
||||
} => {
|
||||
assert!(not_null);
|
||||
assert!(!unique);
|
||||
assert_eq!(default, Some(Value::Number("0".to_string())));
|
||||
}
|
||||
other => panic!("expected AddColumn, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_column_parses_a_unique_constraint() {
|
||||
match parse_command("add column to T: email (text) unique").expect("parse") {
|
||||
Command::AddColumn { unique, not_null, .. } => {
|
||||
assert!(unique);
|
||||
assert!(!not_null);
|
||||
}
|
||||
other => panic!("expected AddColumn, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user