10e5197c19
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.
328 lines
12 KiB
Rust
328 lines
12 KiB
Rust
//! Matrix coverage for `insert into T values (vals)` (Form B —
|
|
//! no column list; the dispatcher auto-fills column names per
|
|
//! the schema, *skipping auto-generated columns* per ADR-0018
|
|
//! §3).
|
|
//!
|
|
//! The handoff §B fix this session was that Form B's
|
|
//! `column_value_list` mirrors `do_insert`'s `user_cols`
|
|
//! contract — so the slot list excludes serial/shortid columns
|
|
//! and the hint prose at the first value position names the
|
|
//! first non-auto-gen column, not the (skipped) `id`.
|
|
|
|
use crate::typing_surface::*;
|
|
use rdbms_playground::input_render::InputState;
|
|
|
|
// =========================================================
|
|
// Entry positions overlap with Form A — covered there. This
|
|
// file picks up from the `values` keyword.
|
|
// =========================================================
|
|
|
|
#[test]
|
|
fn after_values_space_offers_open_paren() {
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end("insert into Customers values ", &schema);
|
|
assert!(matches!(a.state, InputState::IncompleteAtEof));
|
|
assert_candidate_present(&a, &["("]);
|
|
crate::snap!("after_values_space", a);
|
|
}
|
|
|
|
// =========================================================
|
|
// First value position. The key §B regression: for a table
|
|
// whose first column is `id:serial`, Form B's slot list
|
|
// excludes `id`, so the prose must name the *first non-auto*
|
|
// column — `Name` for schema_serial_pk.
|
|
// =========================================================
|
|
|
|
#[test]
|
|
fn form_b_first_value_skips_serial_column() {
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end("insert into Customers values (", &schema);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose at first Form B value slot, got {:?}", a.hint)
|
|
});
|
|
// The value slot itself must be keyed on `Name` — the first
|
|
// non-auto column — not on the skipped `id`.
|
|
assert!(
|
|
prose.starts_with("for `Name`"),
|
|
"Form B's first value slot should be for `Name`, got prose: {prose:?}",
|
|
);
|
|
// `id` appears only inside the trailing pedagogical note
|
|
// (`id` auto-generated — skipped here …), never as the slot
|
|
// the user is being prompted to fill.
|
|
assert!(
|
|
!prose.starts_with("for `id`"),
|
|
"Form B must not prompt for the auto-gen `id` as a value slot, got prose: {prose:?}",
|
|
);
|
|
crate::snap!("form_b_first_value_serial_pk", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_b_first_value_text_pk_names_first_column() {
|
|
let schema = schema_text_pk();
|
|
let a = assess_at_end("insert into Items values (", &schema);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose, got {:?}", a.hint)
|
|
});
|
|
assert!(
|
|
prose.contains("Code"),
|
|
"Form B should name the PK column `Code`, got prose: {prose:?}",
|
|
);
|
|
crate::snap!("form_b_first_value_text_pk", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_b_first_value_every_type_first_column_is_int() {
|
|
// The first column in schema_every_type is `k:int`. Prose
|
|
// must say `integer` and name `k`.
|
|
let schema = schema_every_type();
|
|
let a = assess_at_end("insert into Things values (", &schema);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose, got {:?}", a.hint)
|
|
});
|
|
assert!(
|
|
prose.contains("k"),
|
|
"should name column `k`, got prose: {prose:?}",
|
|
);
|
|
assert!(
|
|
prose.contains("integer"),
|
|
"should say `integer`, got prose: {prose:?}",
|
|
);
|
|
crate::snap!("form_b_first_value_every_type", a);
|
|
}
|
|
|
|
// =========================================================
|
|
// Second-value position — the slot list advances to the next
|
|
// non-auto column. For schema_serial_pk this is `Email`.
|
|
// =========================================================
|
|
|
|
#[test]
|
|
fn form_b_after_first_value_advances_to_next_column() {
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end(
|
|
"insert into Customers values ('Alice', ",
|
|
&schema,
|
|
);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose at second slot, got {:?}", a.hint)
|
|
});
|
|
assert!(
|
|
prose.contains("Email"),
|
|
"second slot should name `Email`, got prose: {prose:?}",
|
|
);
|
|
crate::snap!("form_b_second_value", a);
|
|
}
|
|
|
|
// =========================================================
|
|
// In-progress Form B values — must classify as
|
|
// IncompleteAtEof. Regression for the matrix-found bug in
|
|
// walk_repeated (fixed alongside this matrix).
|
|
// =========================================================
|
|
|
|
#[test]
|
|
fn form_b_in_progress_after_comma_is_incomplete() {
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end(
|
|
"insert into Customers values ('Alice', ",
|
|
&schema,
|
|
);
|
|
assert!(
|
|
matches!(a.state, InputState::IncompleteAtEof),
|
|
"in-progress Form B should be Incomplete, got {:?}",
|
|
a.state,
|
|
);
|
|
crate::snap!("form_b_in_progress_after_comma", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_b_in_progress_without_closing_paren_is_incomplete() {
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end(
|
|
"insert into Customers values ('Alice', 'a@b.c'",
|
|
&schema,
|
|
);
|
|
assert!(matches!(a.state, InputState::IncompleteAtEof));
|
|
crate::snap!("form_b_in_progress_no_close_paren", a);
|
|
}
|
|
|
|
// =========================================================
|
|
// Form B with the wrong number of values is rejected.
|
|
// schema_serial_pk has Customers(id:serial, Name:text,
|
|
// Email:text). Form B excludes id → exactly 2 values
|
|
// required.
|
|
// =========================================================
|
|
|
|
#[test]
|
|
fn form_b_with_too_few_values_is_invalid_at_close_paren() {
|
|
let schema = schema_serial_pk();
|
|
let input = "insert into Customers values ('Alice')";
|
|
let a = assess_at_end(input, &schema);
|
|
// Only one value supplied; Form B for Customers needs two. As of
|
|
// issue #17 a wrong-count tuple parses *structurally* (so the
|
|
// friendly arity diagnostic can fire) — the user-facing invalidity
|
|
// is the validity verdict (the `[ERR]` indicator), not the
|
|
// structural `state`.
|
|
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),
|
|
"too-few values must light the [ERR] verdict",
|
|
);
|
|
crate::snap!("form_b_too_few_values", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_b_with_extra_value_for_serial_column_is_invalid() {
|
|
// Form B excludes serial. Supplying a value for `id` here
|
|
// (treating it as the first slot) means an extra value
|
|
// overall — Customers has 3 columns but Form B accepts 2.
|
|
let schema = schema_serial_pk();
|
|
let input = "insert into Customers values (1, 'Alice', 'a@b.c')";
|
|
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 B with a value-for-serial must light the [ERR] verdict",
|
|
);
|
|
crate::snap!("form_b_extra_serial_value", a);
|
|
}
|
|
|
|
// =========================================================
|
|
// Form B happy path: correct number of values parses to
|
|
// Insert.
|
|
// =========================================================
|
|
|
|
#[test]
|
|
fn form_b_with_correct_values_parses() {
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end(
|
|
"insert into Customers values ('Alice', 'a@b.c')",
|
|
&schema,
|
|
);
|
|
assert!(matches!(a.state, InputState::Valid));
|
|
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
|
|
crate::snap!("form_b_valid", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_b_text_pk_with_correct_values_parses() {
|
|
let schema = schema_text_pk();
|
|
let a = assess_at_end(
|
|
"insert into Items values ('SKU-1', 'Widget')",
|
|
&schema,
|
|
);
|
|
assert!(matches!(a.state, InputState::Valid));
|
|
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
|
|
crate::snap!("form_b_text_pk_valid", a);
|
|
}
|
|
|
|
// =========================================================
|
|
// Date / DateTime / Bool slot prose: schema_every_type
|
|
// exercises each Type variant against a Form B values
|
|
// position.
|
|
// =========================================================
|
|
|
|
// =========================================================
|
|
// Pedagogical Form-A pointer (handoff-12 §2.2).
|
|
//
|
|
// At the FIRST value slot of a Form B insert whose table has
|
|
// auto-generated columns, the hint must mention the skipped
|
|
// column(s) and point at the explicit-column form.
|
|
// =========================================================
|
|
|
|
#[test]
|
|
fn form_b_first_slot_mentions_skipped_serial_column() {
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end("insert into Customers values (", &schema);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose at first Form B slot, got {:?}", a.hint)
|
|
});
|
|
// Names the skipped auto-gen column.
|
|
assert!(
|
|
prose.contains("`id`"),
|
|
"first-slot hint should mention skipped `id`, got: {prose:?}",
|
|
);
|
|
// Points at the explicit-column escape hatch.
|
|
assert!(
|
|
prose.contains("auto-generated") && prose.contains("list columns"),
|
|
"first-slot hint should explain the Form-A escape, got: {prose:?}",
|
|
);
|
|
crate::snap!("form_b_first_slot_skip_note", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_b_second_slot_omits_skip_note() {
|
|
// The note fires once, at the first slot only — not at
|
|
// every comma.
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end(
|
|
"insert into Customers values ('Alice', ",
|
|
&schema,
|
|
);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose at second slot, got {:?}", a.hint)
|
|
});
|
|
assert!(
|
|
!prose.contains("auto-generated"),
|
|
"second-slot hint must NOT repeat the skip note, got: {prose:?}",
|
|
);
|
|
crate::snap!("form_b_second_slot_no_skip_note", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_b_text_pk_has_no_skip_note() {
|
|
// No auto-gen columns → no skip note.
|
|
let schema = schema_text_pk();
|
|
let a = assess_at_end("insert into Items values (", &schema);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose, got {:?}", a.hint)
|
|
});
|
|
assert!(
|
|
!prose.contains("auto-generated"),
|
|
"text-PK table has no auto-gen column — no skip note expected, got: {prose:?}",
|
|
);
|
|
crate::snap!("form_b_text_pk_no_skip_note", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_a_first_slot_has_no_skip_note() {
|
|
// Form A lists columns explicitly — the user is in control,
|
|
// no pedagogical pointer needed.
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end(
|
|
"insert into Customers (Name) values (",
|
|
&schema,
|
|
);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose, got {:?}", a.hint)
|
|
});
|
|
assert!(
|
|
!prose.contains("auto-generated"),
|
|
"Form A must not show the Form-B skip note, got: {prose:?}",
|
|
);
|
|
crate::snap!("form_a_no_skip_note", a);
|
|
}
|
|
|
|
#[test]
|
|
fn form_b_advances_through_every_type_first_to_real() {
|
|
// Things' second column is `r:real`. After typing the
|
|
// first value, prose must name `r` and say `number`.
|
|
let schema = schema_every_type();
|
|
let a = assess_at_end("insert into Things values (1, ", &schema);
|
|
let prose = hint_prose(&a).unwrap_or_else(|| {
|
|
panic!("expected Prose at 2nd slot, got {:?}", a.hint)
|
|
});
|
|
assert!(prose.contains("r"), "should name `r`, got prose: {prose:?}");
|
|
assert!(
|
|
prose.contains("number"),
|
|
"real-slot prose should say `number`, got prose: {prose:?}",
|
|
);
|
|
crate::snap!("form_b_every_type_second_slot", a);
|
|
}
|