constraints: CHECK-violation friendly error + typing-surface matrix (ADR-0029 §10)

Completes ADR-0029's implementation: the friendly-error layer
now names the rule a CHECK violation broke, and the
typing-surface matrix covers the whole constraint grammar.

CHECK-violation friendly error (ADR-0029 §10):
- enrich_dsl_failure gains a CHECK branch — it reads the column
  from the engine's `CHECK constraint failed: <column>`
  message, then resolves the table, the offending value, and
  the column's compiled CHECK expression.
- FailureContext / TranslateContext carry the resolved
  check_rule; translate_check renders "the value <v> breaks the
  rule `<rule>`" when it is known, falling back to the plain
  hint otherwise.

Typing-surface matrix: a new `constraints` submodule, 14 cells
covering the create-table / add-column constraint suffix and
the add-constraint / drop-constraint commands (174 → 188).

16 tests added (1 translate unit, 1 enrichment integration, 14
matrix cells).
This commit is contained in:
claude@clouddev1
2026-05-19 18:54:48 +00:00
parent abce1188f2
commit 5e97f6ac6a
22 changed files with 915 additions and 26 deletions
+149
View File
@@ -0,0 +1,149 @@
//! Matrix coverage for ADR-0029 column constraints — the
//! `not null` / `unique` / `default` / `check` suffix on
//! `create table` & `add column`, and the `add constraint` /
//! `drop constraint` commands (ADR-0029 §2).
use crate::typing_surface::*;
use rdbms_playground::input_render::InputState;
// --- create table / add column constraint suffix --------------
#[test]
fn create_table_default_suffix_parses() {
let a = assess_at_end(
"create table Books with pk isbn(text) default '000'",
&schema_empty(),
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("CreateTable"));
crate::snap!("create_table_default_suffix", a);
}
#[test]
fn create_table_check_suffix_parses() {
let a = assess_at_end(
"create table Ages with pk age(int) check (age >= 0)",
&schema_empty(),
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("CreateTable"));
crate::snap!("create_table_check_suffix", a);
}
#[test]
fn add_column_check_suffix_parses() {
let a = assess_at_end(
"add column to Customers: note (text) check (note like 'A%')",
&schema_multi_table(),
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("AddColumn"));
crate::snap!("add_column_check_suffix", a);
}
#[test]
fn add_column_after_type_offers_constraint_keywords() {
let a = assess_at_end(
"add column to Customers: note (text) ",
&schema_multi_table(),
);
assert_candidate_present(&a, &["not", "unique", "default", "check"]);
crate::snap!("add_column_constraint_suffix", a);
}
// --- add constraint -------------------------------------------
#[test]
fn after_add_offers_constraint_branch() {
let a = assess_at_end("add ", &schema_multi_table());
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["constraint"]);
crate::snap!("after_add_constraint_branch", a);
}
#[test]
fn add_constraint_offers_the_four_kinds() {
let a = assess_at_end("add constraint ", &schema_multi_table());
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["not", "unique", "default", "check"]);
crate::snap!("add_constraint_kinds", a);
}
#[test]
fn add_constraint_to_offers_table_names() {
let a = assess_at_end("add constraint not null to ", &schema_multi_table());
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["Customers", "Orders"]);
crate::snap!("add_constraint_to_tables", a);
}
#[test]
fn add_constraint_dot_narrows_to_table_columns() {
let a = assess_at_end("add constraint unique to Orders.", &schema_multi_table());
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["OrderId", "CustId", "Total"]);
assert_no_candidate_named(&a, &["id", "Name"]);
crate::snap!("add_constraint_dot_columns", a);
}
#[test]
fn complete_add_constraint_not_null_parses() {
let a = assess_at_end(
"add constraint not null to Customers.Name",
&schema_multi_table(),
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("AddConstraint"));
crate::snap!("add_constraint_not_null_complete", a);
}
#[test]
fn complete_add_constraint_default_parses() {
let a = assess_at_end(
"add constraint default 0 to Orders.Total",
&schema_multi_table(),
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("AddConstraint"));
crate::snap!("add_constraint_default_complete", a);
}
#[test]
fn complete_add_constraint_check_parses() {
let a = assess_at_end(
"add constraint check (Total >= 0) to Orders.Total",
&schema_multi_table(),
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("AddConstraint"));
crate::snap!("add_constraint_check_complete", a);
}
// --- drop constraint ------------------------------------------
#[test]
fn after_drop_offers_constraint_branch() {
let a = assess_at_end("drop ", &schema_multi_table());
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["constraint"]);
crate::snap!("after_drop_constraint_branch", a);
}
#[test]
fn drop_constraint_offers_the_four_kinds() {
let a = assess_at_end("drop constraint ", &schema_multi_table());
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["not", "unique", "default", "check"]);
crate::snap!("drop_constraint_kinds", a);
}
#[test]
fn complete_drop_constraint_parses() {
let a = assess_at_end(
"drop constraint unique from Customers.Name",
&schema_multi_table(),
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("DropConstraint"));
crate::snap!("drop_constraint_complete", a);
}
+1
View File
@@ -35,6 +35,7 @@ pub mod drop_column;
pub mod drop_relationship;
pub mod add_relationship;
pub mod index_ops;
pub mod constraints;
pub mod rename_change_column;
pub mod app_commands;
pub mod candidate_ordering;
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add column to Customers: note (text) \" cursor=37"
expression: "& a"
---
Assessment {
input: "add column to Customers: note (text) ",
cursor: 37,
state: Valid,
hint: Some(
Candidates {
items: [
Candidate {
text: "not",
kind: Keyword,
},
Candidate {
text: "unique",
kind: Keyword,
},
Candidate {
text: "default",
kind: Keyword,
},
Candidate {
text: "check",
kind: Keyword,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
37,
37,
),
partial_prefix: "",
candidates: [
Candidate {
text: "not",
kind: Keyword,
},
Candidate {
text: "unique",
kind: Keyword,
},
Candidate {
text: "default",
kind: Keyword,
},
Candidate {
text: "check",
kind: Keyword,
},
],
},
),
parse_result: Ok(
"AddColumn",
),
}
@@ -0,0 +1,45 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add column to Customers: note (text) check (note like 'A%')\" cursor=59"
expression: "& a"
---
Assessment {
input: "add column to Customers: note (text) check (note like 'A%')",
cursor: 59,
state: Valid,
hint: Some(
Prose(
"no such column `note` on table `Customers`",
),
),
completion: Some(
Completion {
replaced_range: (
59,
59,
),
partial_prefix: "",
candidates: [
Candidate {
text: "not",
kind: Keyword,
},
Candidate {
text: "unique",
kind: Keyword,
},
Candidate {
text: "default",
kind: Keyword,
},
Candidate {
text: "check",
kind: Keyword,
},
],
},
),
parse_result: Ok(
"AddColumn",
),
}
@@ -0,0 +1,55 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add constraint unique to Orders.\" cursor=32"
expression: "& a"
---
Assessment {
input: "add constraint unique to Orders.",
cursor: 32,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "CustId",
kind: Identifier,
},
Candidate {
text: "OrderId",
kind: Identifier,
},
Candidate {
text: "Total",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
32,
32,
),
partial_prefix: "",
candidates: [
Candidate {
text: "CustId",
kind: Identifier,
},
Candidate {
text: "OrderId",
kind: Identifier,
},
Candidate {
text: "Total",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add constraint \" cursor=15"
expression: "& a"
---
Assessment {
input: "add constraint ",
cursor: 15,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "not",
kind: Keyword,
},
Candidate {
text: "unique",
kind: Keyword,
},
Candidate {
text: "default",
kind: Keyword,
},
Candidate {
text: "check",
kind: Keyword,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
15,
15,
),
partial_prefix: "",
candidates: [
Candidate {
text: "not",
kind: Keyword,
},
Candidate {
text: "unique",
kind: Keyword,
},
Candidate {
text: "default",
kind: Keyword,
},
Candidate {
text: "check",
kind: Keyword,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,47 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add constraint not null to \" cursor=27"
expression: "& a"
---
Assessment {
input: "add constraint not null to ",
cursor: 27,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
27,
27,
),
partial_prefix: "",
candidates: [
Candidate {
text: "Customers",
kind: Identifier,
},
Candidate {
text: "Orders",
kind: Identifier,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add \" cursor=4"
expression: "& a"
---
Assessment {
input: "add ",
cursor: 4,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "column",
kind: Keyword,
},
Candidate {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
Candidate {
text: "1:n",
kind: Keyword,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
4,
4,
),
partial_prefix: "",
candidates: [
Candidate {
text: "column",
kind: Keyword,
},
Candidate {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
Candidate {
text: "1:n",
kind: Keyword,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,71 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"drop \" cursor=5"
expression: "& a"
---
Assessment {
input: "drop ",
cursor: 5,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "column",
kind: Keyword,
},
Candidate {
text: "relationship",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
5,
5,
),
partial_prefix: "",
candidates: [
Candidate {
text: "column",
kind: Keyword,
},
Candidate {
text: "relationship",
kind: Keyword,
},
Candidate {
text: "table",
kind: Keyword,
},
Candidate {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}
@@ -0,0 +1,19 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add constraint check (Total >= 0) to Orders.Total\" cursor=49"
expression: "& a"
---
Assessment {
input: "add constraint check (Total >= 0) to Orders.Total",
cursor: 49,
state: Valid,
hint: Some(
Prose(
"Submit with Enter",
),
),
completion: None,
parse_result: Ok(
"AddConstraint",
),
}
@@ -0,0 +1,19 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add constraint default 0 to Orders.Total\" cursor=40"
expression: "& a"
---
Assessment {
input: "add constraint default 0 to Orders.Total",
cursor: 40,
state: Valid,
hint: Some(
Prose(
"Submit with Enter",
),
),
completion: None,
parse_result: Ok(
"AddConstraint",
),
}
@@ -0,0 +1,19 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"add constraint not null to Customers.Name\" cursor=41"
expression: "& a"
---
Assessment {
input: "add constraint not null to Customers.Name",
cursor: 41,
state: Valid,
hint: Some(
Prose(
"Submit with Enter",
),
),
completion: None,
parse_result: Ok(
"AddConstraint",
),
}
@@ -0,0 +1,19 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"drop constraint unique from Customers.Name\" cursor=42"
expression: "& a"
---
Assessment {
input: "drop constraint unique from Customers.Name",
cursor: 42,
state: Valid,
hint: Some(
Prose(
"Submit with Enter",
),
),
completion: None,
parse_result: Ok(
"DropConstraint",
),
}
@@ -0,0 +1,19 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"create table Ages with pk age(int) check (age >= 0)\" cursor=51"
expression: "& a"
---
Assessment {
input: "create table Ages with pk age(int) check (age >= 0)",
cursor: 51,
state: Valid,
hint: Some(
Prose(
"Submit with Enter",
),
),
completion: None,
parse_result: Ok(
"CreateTable",
),
}
@@ -0,0 +1,19 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"create table Books with pk isbn(text) default '000'\" cursor=51"
expression: "& a"
---
Assessment {
input: "create table Books with pk isbn(text) default '000'",
cursor: 51,
state: Valid,
hint: Some(
Prose(
"Submit with Enter",
),
),
completion: None,
parse_result: Ok(
"CreateTable",
),
}
@@ -0,0 +1,63 @@
---
source: tests/typing_surface/constraints.rs
description: "input=\"drop constraint \" cursor=16"
expression: "& a"
---
Assessment {
input: "drop constraint ",
cursor: 16,
state: IncompleteAtEof,
hint: Some(
Candidates {
items: [
Candidate {
text: "not",
kind: Keyword,
},
Candidate {
text: "unique",
kind: Keyword,
},
Candidate {
text: "default",
kind: Keyword,
},
Candidate {
text: "check",
kind: Keyword,
},
],
selected: None,
},
),
completion: Some(
Completion {
replaced_range: (
16,
16,
),
partial_prefix: "",
candidates: [
Candidate {
text: "not",
kind: Keyword,
},
Candidate {
text: "unique",
kind: Keyword,
},
Candidate {
text: "default",
kind: Keyword,
},
Candidate {
text: "check",
kind: Keyword,
},
],
},
),
parse_result: Err(
"Invalid(at_eof)",
),
}