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
+8
View File
@@ -50,8 +50,16 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
// ---- CHECK violations ----
("error.check.insert.headline", &["table", "column"]),
("error.check.insert.hint", &["column"]),
(
"error.check.insert.hint_with_rule",
&["value", "rule", "column"],
),
("error.check.update.headline", &["table", "column"]),
("error.check.update.hint", &["column"]),
(
"error.check.update.hint_with_rule",
&["value", "rule", "column"],
),
// ---- FK violations (anchor: "referenced by") ----
(
"error.foreign_key.child_side.insert.headline",
+7 -4
View File
@@ -85,17 +85,20 @@ error:
headline: "`{table}.{column}` cannot be null."
hint: "The `{column}` column is required — pick a non-null value, or do not include `{column}` in your `set` list."
# CHECK constraint violations. Placeholder coverage —
# the playground does not emit CHECK constraints today
# (track C3), but the catalog is wired so the wording
# is ready when constraint-management lands.
# CHECK constraint violations (ADR-0029 §10). When the
# runtime resolves the column's compiled CHECK expression,
# `hint_with_rule` names both the offending value and the
# rule; the plain `hint` is the fallback when enrichment
# could not resolve the rule.
check:
insert:
headline: "check constraint refused `{table}.{column}`."
hint: "A check constraint requires `{column}` to satisfy a rule the inserted value did not."
hint_with_rule: "The value {value} breaks the rule `{rule}` — `{column}` must satisfy it."
update:
headline: "check constraint refused `{table}.{column}`."
hint: "A check constraint requires `{column}` to satisfy a rule the new value did not."
hint_with_rule: "The new value {value} breaks the rule `{rule}` — `{column}` must satisfy it."
# Type mismatch — engine-side STRICT refusal of a wrong-shape
# value. Mostly the `change column ... --dont-convert` path
+69 -22
View File
@@ -145,6 +145,10 @@ pub struct FailureContext {
/// §7. Rendered through the bordered diagnostic-table
/// renderer when present.
pub diagnostic_table: Option<DiagnosticTable>,
/// For a `CHECK` violation: the column's compiled `CHECK`
/// expression, resolved from the schema (ADR-0029 §10). Lets
/// the friendly error name the rule the value broke.
pub check_rule: Option<String>,
}
/// Context the translator uses to pick catalog keys and fill
@@ -174,6 +178,10 @@ pub struct TranslateContext {
/// Pinpointed offending row(s); rendered onto the
/// `FriendlyError::diagnostic_table` field when present.
pub diagnostic_table: Option<DiagnosticTable>,
/// For a `CHECK` violation: the column's compiled `CHECK`
/// expression (ADR-0029 §10). When present, the friendly
/// error names the rule the value broke.
pub check_rule: Option<String>,
pub verbosity: Verbosity,
}
@@ -207,6 +215,7 @@ impl TranslateContext {
target_type: None,
value: facts.value,
diagnostic_table: facts.diagnostic_table,
check_rule: facts.check_rule,
verbosity,
}
}
@@ -489,30 +498,47 @@ fn translate_not_null(message: &str, ctx: &TranslateContext) -> FriendlyError {
// ---- CHECK -----------------------------------------------------
fn translate_check(_message: &str, ctx: &TranslateContext) -> FriendlyError {
// The engine reports CHECK constraint failures by constraint
// name, not by column. We don't have user-named CHECK
// constraints today, so the message is rarely informative.
// Surface what we have via context.
// The engine reports a `CHECK` failure by the column the
// constraint sits on; the runtime's enrichment resolves the
// table, the offending value, and — the teaching moment —
// the rule itself (ADR-0029 §10). When the rule could not
// be resolved, the plain hint stands on its own.
let table = ctx_table(ctx);
let column = ctx_column(ctx);
match ctx.operation {
Some(Operation::Update) => fe(
t!(
"error.check.update.headline",
table = table,
column = column
),
verbose_hint(ctx, t!("error.check.update.hint", column = column)),
),
_ => fe(
t!(
"error.check.insert.headline",
table = table,
column = column
),
verbose_hint(ctx, t!("error.check.insert.hint", column = column)),
),
}
let is_update = matches!(ctx.operation, Some(Operation::Update));
let headline = if is_update {
t!("error.check.update.headline", table = table, column = column)
} else {
t!("error.check.insert.headline", table = table, column = column)
};
let hint = ctx.check_rule.as_ref().map_or_else(
|| {
if is_update {
t!("error.check.update.hint", column = column)
} else {
t!("error.check.insert.hint", column = column)
}
},
|rule| {
let value = ctx_value(ctx);
if is_update {
t!(
"error.check.update.hint_with_rule",
value = value,
rule = rule,
column = column
)
} else {
t!(
"error.check.insert.hint_with_rule",
value = value,
rule = rule,
column = column
)
}
},
);
fe(headline, verbose_hint(ctx, hint))
}
// ---- not_found / already_exists --------------------------------
@@ -857,6 +883,27 @@ mod tests {
assert!(f.headline.contains("age"));
}
#[test]
fn check_with_a_resolved_rule_names_the_rule_and_value() {
// ADR-0029 §10: when enrichment resolves the column's
// compiled CHECK expression and the offending value,
// the hint names both.
let err = sqlite(
"CHECK constraint failed: score",
SqliteErrorKind::UniqueViolation,
);
let mut ctx = ctx_with(Operation::Insert);
ctx.table = Some("T".to_string());
ctx.column = Some("score".to_string());
ctx.value = Some("-5".to_string());
ctx.check_rule = Some("\"score\" >= 0".to_string());
let f = translate(&err, &ctx);
assert!(f.headline.contains("check constraint refused"));
let hint = f.hint.expect("a verbose hint");
assert!(hint.contains("-5"), "the offending value: {hint}");
assert!(hint.contains("\"score\" >= 0"), "the rule: {hint}");
}
// ---- not_found ----
#[test]