Matrix: insert Form B coverage

12 tests across schema_serial_pk / text_pk / multi_table / every_type.
Pins (a) Form B skips auto-gen columns from the slot list (regression
for handoff-12 §B fix); (b) wrong-count value lists are now flagged
at typing time, not only at submit (the previous commit's fix); and
(c) per-type slot prose advances correctly through every Type variant.
This commit is contained in:
claude@clouddev1
2026-05-15 20:31:01 +00:00
parent fffb44ff4f
commit a9a04cff97
14 changed files with 588 additions and 3 deletions
+236 -1
View File
@@ -1 +1,236 @@
//! Submodule stub — populated in subsequent tasks.
//! 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)
});
// First column slot must name `Name`, not the skipped
// `id`.
assert!(
prose.contains("Name"),
"Form B should advance to first non-auto column `Name`, got prose: {prose:?}",
);
assert!(
!prose.contains("`id`"),
"Form B should NOT prompt for the auto-gen `id`, 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 a = assess_at_end("insert into Customers values ('Alice')", &schema);
// Only one value supplied; Form B for Customers needs two.
// The grammar's typed slot list expects another `,<value>`
// before the `)`. Classify as DefiniteError or Incomplete
// (which one depends on whether the closing `)` is past
// the missing slot).
assert!(
!matches!(a.state, InputState::Valid),
"input with too-few values must NOT be Valid, got {:?}",
a.state,
);
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 a = assess_at_end(
"insert into Customers values (1, 'Alice', 'a@b.c')",
&schema,
);
assert!(
!matches!(a.state, InputState::Valid),
"Form B with a value-for-serial must be invalid, got {:?}",
a.state,
);
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.
// =========================================================
#[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);
}