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:
+69
-22
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user