Files
rdbms-playground/tests/typing_surface/insert_form_c.rs
T
claude@clouddev1 10e5197c19 feat: bring simple-mode insert arity diagnostics to parity with advanced
A wrong-count simple-mode insert now shows the friendly per-column arity
message at typing time (instead of a bare "expected `,`/`)`") and is
blocked from dispatch at submit — unifying simple and advanced mode onto
the one ADR-0027 model (structural parse + ERROR diagnostic), where they
had diverged.

Grammar: a simple-mode-only arity gate (dsl_insert_value_list) routes a
wrong-count DSL insert tuple to the type-blind fallback so it matches
structurally and the per-tuple arity diagnostic fires. The gate is gated
to simple mode, so advanced behaviour is unchanged. count_tuple_values
and the target-column selection (insert_target_columns) are now shared
by both grammars.

Diagnostic: dml_insert_arity_diagnostics is mode-aware — advanced Form B
expects all columns; simple Form B/C expects the user-fillable columns
(serial/shortid auto-fill). It counts the DSL Form A role and scans the
keyword-less Form C tuple. New catalog keys name the fillable/auto split
and the all-auto-table case.

Submit: a wrong-count DSL insert now parses Ok + carries the ERROR
diagnostic, so a unified Ok-arm pre-flight (dsl_insert_count_mismatch_notes)
blocks dispatch and teaches; the previous Err-arm note retires.
advanced_alternative_note's gate now reads the validity verdict so it
still fires for the parse-Ok-with-error shape.

Docs: ADR-0036 Amendment 2 (+ README index) and requirements.md H1a.
2026-05-29 20:45:21 +00:00

181 lines
6.4 KiB
Rust

//! Matrix coverage for `insert into T (vals)` (Form C — bare
//! value list, no `values` keyword).
//!
//! Form C and Form B produce the identical AST and dispatch
//! identically (`Insert { columns: None, … }`). As of the
//! Form-C type-awareness work (handoff-14), Form C's paren is
//! resolved by the `insert_first_paren` lookahead: a value
//! literal as the first token routes the contents through the
//! typed `column_value_list` — the same per-column typed slots
//! Form B uses. So Form C values are now type-checked and
//! count-checked at parse time, not only at bind time.
//!
//! An identifier (column name) as the first token, or an empty
//! paren, routes to Form A instead — `insert into T (Name)`
//! still surfaces the "did you mean Form A?" recovery.
use crate::typing_surface::*;
use rdbms_playground::input_render::InputState;
// =========================================================
// Form C happy path: type-correct values parse to Insert.
// =========================================================
#[test]
fn form_c_text_pk_correct_values_parses() {
// Items(Code:text, Title:text) — Form C expects two text
// values (no auto-gen columns to skip).
let schema = schema_text_pk();
let a = assess_at_end(
"insert into Items ('SKU-1', 'Widget')",
&schema,
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
crate::snap!("form_c_text_pk_valid", a);
}
#[test]
fn form_c_serial_pk_correct_values_parses() {
// Customers(id:serial, Name:text, Email:text) — Form C
// skips the serial `id`, expects two text values.
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers ('Alice', 'a@b.c')",
&schema,
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
crate::snap!("form_c_serial_pk_valid", a);
}
#[test]
fn form_c_with_null_value_parses() {
// null is type-compatible with any slot.
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers (null, 'a@b.c')",
&schema,
);
assert!(matches!(a.state, InputState::Valid));
crate::snap!("form_c_null_value", a);
}
// =========================================================
// Form C is now type-aware (the §2.2 limitation is fixed).
// =========================================================
#[test]
fn form_c_rejects_number_for_text_column() {
// `3.14` lands in the Name(text) slot — the typed slot
// rejects it at parse time. Before Form-C type-awareness
// this parsed Valid and only failed at bind time.
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers (3.14, 'a@b.c')",
&schema,
);
assert!(
!matches!(a.state, InputState::Valid),
"Form C should now type-check `3.14` against Name(text), got {:?}",
a.state,
);
crate::snap!("form_c_type_mismatch", a);
}
#[test]
fn form_c_wrong_value_count_is_invalid() {
// Customers Form C expects exactly two values (id:serial skipped).
// Three values is a count mismatch. As of issue #17 it parses
// structurally (so the friendly arity diagnostic fires); the
// user-facing invalidity is the validity verdict (`[ERR]`).
let schema = schema_serial_pk();
let input = "insert into Customers ('Alice', 'a@b.c', 'extra')";
let a = assess_at_end(input, &schema);
assert_eq!(
rdbms_playground::dsl::walker::input_verdict_in_mode(
input,
Some(&schema),
rdbms_playground::mode::Mode::Simple,
),
Some(rdbms_playground::dsl::walker::Severity::Error),
"Form C with too many values must light the [ERR] verdict",
);
crate::snap!("form_c_wrong_count", a);
}
// =========================================================
// Form C typed-slot prose — the per-column hint Form B has
// is now available in Form C too.
// =========================================================
#[test]
fn form_c_second_slot_shows_typed_prose_for_column() {
// First token `'Alice'` is a string literal → Form C. At
// the second slot the hint names the Email column.
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers ('Alice', ",
&schema,
);
let prose = hint_prose(&a).unwrap_or_else(|| {
panic!("expected Prose at Form C second slot, got {:?}", a.hint)
});
assert!(
prose.contains("Email"),
"Form C second slot should name `Email`, got prose: {prose:?}",
);
crate::snap!("form_c_typed_prose", a);
}
// =========================================================
// In-progress Form C classifies as IncompleteAtEof.
// =========================================================
#[test]
fn form_c_in_progress_after_comma_is_incomplete() {
let schema = schema_serial_pk();
let a = assess_at_end("insert into Customers ('Alice', ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
crate::snap!("form_c_in_progress_after_comma", a);
}
#[test]
fn form_c_in_progress_without_close_paren_is_incomplete() {
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers ('Alice', 'a@b.c'",
&schema,
);
assert!(matches!(a.state, InputState::IncompleteAtEof));
crate::snap!("form_c_in_progress_no_close", a);
}
// =========================================================
// Form A recovery: a column-name identifier as the first
// paren token routes to Form A — `insert into T (Name)`
// without `values` flags as Form-A-in-progress.
// =========================================================
#[test]
fn form_c_with_column_shaped_item_flags_as_form_a_in_progress() {
let schema = schema_serial_pk();
let a = assess_at_end("insert into Customers (Name)", &schema);
assert!(
matches!(a.state, InputState::IncompleteAtEof),
"expected IncompleteAtEof (Form A recovery), got {:?}",
a.state,
);
assert_candidate_present(&a, &["values"]);
crate::snap!("form_c_column_shaped_recovery", a);
}
#[test]
fn form_c_with_two_columns_flags_as_form_a_in_progress() {
let schema = schema_serial_pk();
let a = assess_at_end("insert into Customers (Name, Email)", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["values"]);
crate::snap!("form_c_two_columns_recovery", a);
}