constraints: CHECK — check (<expr>) at create table & add column (ADR-0029)
The fourth constraint. `check ( <expr> )` reuses the ADR-0026 WHERE-expression grammar via `Subgrammar`, so a check is written in the same language as a `where` filter. - Grammar: a `CHECK_CONSTRAINT` arm joins the shared constraint-suffix Choice; `consume_check_expr` extracts the parenthesised expression (paren-depth aware) into `ColumnSpec.check` / `Command::AddColumn.check`. - Storage: the parsed `Expr` is compiled once to inline SQL (`compile_check_sql` — `compile_expr` + ADR-0028's param-inliner) and stored in that form everywhere — a new `check_expr` column in `__rdbms_playground_columns`, `project.yaml`'s `ColumnSchema.check`, and the column DDL emitted by `do_create_table` / `schema_to_ddl`. - `add column … check` routes through the rebuild primitive (SQLite's `ALTER … ADD COLUMN` cannot carry it); a CHECK on a serial/shortid column is create-table-only and refused at add-column with a friendly message. - `describe` surfaces the CHECK. ADR-0029 §7/§8 updated to the SQL-form decision — double-quoted identifiers, consistent with ADR-0028's `explain` display SQL. 1201 tests pass (+8); clippy clean.
This commit is contained in:
+92
-10
@@ -13,7 +13,7 @@
|
||||
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{
|
||||
ChangeColumnMode, ColumnSpec, Command, IndexSelector, RelationshipSelector,
|
||||
ChangeColumnMode, ColumnSpec, Command, Expr, IndexSelector, RelationshipSelector,
|
||||
};
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::grammar::{
|
||||
@@ -27,7 +27,7 @@ use crate::dsl::grammar::{
|
||||
/// candidates (ADR-0024 §HintMode-per-node).
|
||||
const NEW_NAME_HINT: HintMode = HintMode::ForceProse("hint.ambient_typing_name");
|
||||
use crate::dsl::types::Type;
|
||||
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
|
||||
use crate::dsl::walker::outcome::{MatchedItem, MatchedKind, MatchedPath};
|
||||
|
||||
// =================================================================
|
||||
// Building blocks
|
||||
@@ -616,7 +616,8 @@ 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)?;
|
||||
let (not_null, unique, default, check) =
|
||||
collect_column_constraints(path)?;
|
||||
Ok(Command::AddColumn {
|
||||
table: require_ident(path, "table_name")?,
|
||||
column: require_ident(path, "column_name")?,
|
||||
@@ -624,8 +625,7 @@ fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
not_null,
|
||||
unique,
|
||||
default,
|
||||
// CHECK joins in a later ADR-0029 step.
|
||||
check: None,
|
||||
check,
|
||||
})
|
||||
}
|
||||
Some("1") => build_add_relationship(path),
|
||||
@@ -842,8 +842,20 @@ const DEFAULT_CONSTRAINT_NODES: &[Node] = &[
|
||||
];
|
||||
const DEFAULT_CONSTRAINT: Node = Node::Seq(DEFAULT_CONSTRAINT_NODES);
|
||||
|
||||
// `check ( <expr> )` — the expression is the ADR-0026 WHERE
|
||||
// grammar, reached through `Subgrammar` (ADR-0029 §2.1). The
|
||||
// parentheses match SQL's `CHECK (…)` and give the parser an
|
||||
// unambiguous end for the expression.
|
||||
const CHECK_CONSTRAINT_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("check")),
|
||||
Node::Punct('('),
|
||||
Node::Subgrammar(&super::expr::OR_EXPR),
|
||||
Node::Punct(')'),
|
||||
];
|
||||
const CHECK_CONSTRAINT: Node = Node::Seq(CHECK_CONSTRAINT_NODES);
|
||||
|
||||
const COLUMN_CONSTRAINT_CHOICES: &[Node] =
|
||||
&[NOT_NULL_CONSTRAINT, UNIQUE_CONSTRAINT, DEFAULT_CONSTRAINT];
|
||||
&[NOT_NULL_CONSTRAINT, UNIQUE_CONSTRAINT, DEFAULT_CONSTRAINT, CHECK_CONSTRAINT];
|
||||
const COLUMN_CONSTRAINT: Node = Node::Choice(COLUMN_CONSTRAINT_CHOICES);
|
||||
|
||||
/// Zero-or-more constraints — the suffix after a column's
|
||||
@@ -894,18 +906,49 @@ const CREATE_TABLE_NODES: &[Node] = &[
|
||||
];
|
||||
const CREATE_TABLE: Node = Node::Seq(CREATE_TABLE_NODES);
|
||||
|
||||
/// Consume a `check` constraint's `( <expr> )` from `items`,
|
||||
/// which must be positioned just after the `Word("check")`,
|
||||
/// and build the ADR-0026 expression (ADR-0029 §2.1). The
|
||||
/// grammar's `Seq` guarantees the surrounding `(` … `)`;
|
||||
/// paren depth handles a parenthesised sub-expression inside.
|
||||
fn consume_check_expr(
|
||||
items: &mut std::iter::Peekable<std::slice::Iter<'_, MatchedItem>>,
|
||||
) -> Result<Expr, ValidationError> {
|
||||
items.next(); // the opening `(`
|
||||
let mut depth = 1usize;
|
||||
let mut expr_items: Vec<MatchedItem> = Vec::new();
|
||||
for inner in items.by_ref() {
|
||||
match &inner.kind {
|
||||
MatchedKind::Punct('(') => {
|
||||
depth += 1;
|
||||
expr_items.push(inner.clone());
|
||||
}
|
||||
MatchedKind::Punct(')') => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
expr_items.push(inner.clone());
|
||||
}
|
||||
_ => expr_items.push(inner.clone()),
|
||||
}
|
||||
}
|
||||
super::expr::build_expr(&expr_items)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// returning the `(not_null, unique, default, check)` tuple.
|
||||
/// The scan reacts only to the 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> {
|
||||
) -> Result<(bool, bool, Option<Value>, Option<Expr>), ValidationError> {
|
||||
let mut not_null = false;
|
||||
let mut unique = false;
|
||||
let mut default = None;
|
||||
let mut check = None;
|
||||
let mut items = path.items.iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
match &item.kind {
|
||||
@@ -929,10 +972,13 @@ fn collect_column_constraints(
|
||||
})?;
|
||||
default = Some(value);
|
||||
}
|
||||
MatchedKind::Word("check") => {
|
||||
check = Some(consume_check_expr(&mut items)?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok((not_null, unique, default))
|
||||
Ok((not_null, unique, default, check))
|
||||
}
|
||||
|
||||
/// The friendly error for declaring a constraint a
|
||||
@@ -1005,6 +1051,13 @@ fn build_create_table(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
last.default = Some(value);
|
||||
}
|
||||
}
|
||||
// `check ( <expr> )` (ADR-0029 §2.1).
|
||||
MatchedKind::Word("check") => {
|
||||
let expr = consume_check_expr(&mut items)?;
|
||||
if let Some(last) = columns.last_mut() {
|
||||
last.check = Some(expr);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1152,4 +1205,33 @@ mod constraint_tests {
|
||||
other => panic!("expected AddColumn, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_table_parses_a_check_constraint() {
|
||||
let cols = create_columns("create table T with pk age(int) check (age >= 0)");
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert!(cols[0].check.is_some(), "the column carries a CHECK");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_column_parses_a_check_constraint() {
|
||||
match parse_command("add column to T: age (int) check (age >= 0 and age < 150)")
|
||||
.expect("parse")
|
||||
{
|
||||
Command::AddColumn { check, .. } => {
|
||||
assert!(check.is_some(), "the column carries a CHECK");
|
||||
}
|
||||
other => panic!("expected AddColumn, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_with_a_parenthesised_sub_expression_parses() {
|
||||
// The check's own parens plus a nested group — the
|
||||
// builder's paren-depth scan must pair them correctly.
|
||||
let cols = create_columns(
|
||||
"create table T with pk n(int) check ((n > 0) or (n < -10))",
|
||||
);
|
||||
assert!(cols[0].check.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user