Matrix: typing-surface infrastructure + insert Form A coverage

Per docs/handoff/20260515-handoff-12.md §1. Systematic per-position
coverage of (state, hint, completion, parse_result) across canonical
schema shapes; submodule per command family. Insert Form A covers 23
cursor positions across serial-PK, text-PK, multi-table, and
every-Type schemas. Both bugs fixed in the previous commit were
surfaced by these tests.

Shared helpers under tests/typing_surface/mod.rs: 5 canonical schema
shapes, assess() helper, property-assertion shortcuts, and a snap!
macro that wraps insta with a stable per-cell suffix.

859 -> 885 tests passing; 1 ignored (pre-existing doc-test).
This commit is contained in:
claude@clouddev1
2026-05-15 20:06:58 +00:00
parent 0b15ce0306
commit 24e641bc21
38 changed files with 1641 additions and 0 deletions
+348
View File
@@ -0,0 +1,348 @@
//! Matrix coverage for `insert into T (cols) values (vals)`
//! (Form A — explicit column list).
//!
//! Form A is the most complex insert form: the column list
//! `(cols)` constrains which values are required and in what
//! order, and the dispatch must mirror `do_insert`'s `user_cols`
//! contract (ADR-0018 §3, handoff-12 §B). Bugs unique to Form A
//! from handoff-12:
//!
//! - **E2**: `insert into T (` showed no column candidates
//! because the value-literal suppression masked the
//! `Ident{Columns}` expectation. Fixed in commit 619a8bd.
//! - **D2**: `insert into T (a, b, c) values (1, 2, 3` (no
//! closing paren) classified as `DefiniteErrorAt(<values>)`
//! because `walk_optional` was rolling back the partial value
//! list. Fixed in commit 5815918.
//!
//! Every meaningful cursor position is exercised against the
//! relevant schema shapes; per-cell snapshots pin behaviour
//! and explicit property assertions document the invariants
//! that matter most.
use crate::typing_surface::*;
use rdbms_playground::input_render::InputState;
// =========================================================
// Cursor at the entry-word boundary.
// =========================================================
#[test]
fn after_insert_keyword_expects_into() {
let schema = schema_serial_pk();
let a = assess_at_end("insert", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
crate::snap!("after_insert_keyword", a);
}
#[test]
fn after_insert_space_expects_into() {
let schema = schema_serial_pk();
let a = assess_at_end("insert ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["into"]);
crate::snap!("after_insert_space", a);
}
#[test]
fn after_into_offers_table_candidates() {
let schema = schema_serial_pk();
let a = assess_at_end("insert into ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["Customers"]);
crate::snap!("after_into_serial_pk", a);
}
#[test]
fn after_into_with_multi_table_offers_both() {
let schema = schema_multi_table();
let a = assess_at_end("insert into ", &schema);
assert_candidate_present(&a, &["Customers", "Orders"]);
crate::snap!("after_into_multi_table", a);
}
// =========================================================
// Cursor immediately after the table-name boundary.
//
// At `insert into Customers ` the walker must offer two
// branches: `values` (Form B/A pivot) and `(` (Form A/C pivot).
// Handoff §D1 fixed this: the `(` punctuation now surfaces as
// a `CandidateKind::Punct` candidate.
// =========================================================
#[test]
fn after_table_name_offers_values_and_open_paren() {
let schema = schema_serial_pk();
let a = assess_at_end("insert into Customers ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["values", "("]);
crate::snap!("after_table_name", a);
}
// =========================================================
// Form A path enters at the `(`.
//
// The §1 bug E2 was that `insert into T (` showed nothing —
// no column candidates surfaced. Reproduces here as a property
// assertion (must include at least one column from the table)
// and the inverse leakage check (must NOT include columns
// from sibling tables).
// =========================================================
#[test]
fn after_open_paren_offers_active_table_columns_only() {
let schema = schema_multi_table();
let a = assess_at_end("insert into Customers (", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["id", "Name"]);
assert_no_candidate_named(&a, &["OrderId", "CustId", "Total"]);
crate::snap!("after_open_paren_multi_table", a);
}
#[test]
fn after_open_paren_serial_pk_offers_all_columns_including_serial() {
let schema = schema_serial_pk();
let a = assess_at_end("insert into Customers (", &schema);
assert_candidate_present(&a, &["id", "Name", "Email"]);
crate::snap!("after_open_paren_serial_pk", a);
}
#[test]
fn after_open_paren_text_pk_offers_text_pk_columns() {
let schema = schema_text_pk();
let a = assess_at_end("insert into Items (", &schema);
assert_candidate_present(&a, &["Code", "Title"]);
crate::snap!("after_open_paren_text_pk", a);
}
// =========================================================
// Mid-column-list positions.
// =========================================================
#[test]
fn mid_column_list_after_comma_offers_remaining_columns() {
let schema = schema_serial_pk();
let a = assess_at_end("insert into Customers (id, ", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["Name", "Email"]);
crate::snap!("after_comma_in_column_list", a);
}
#[test]
fn after_close_paren_expects_values_keyword() {
let schema = schema_serial_pk();
let a = assess_at_end("insert into Customers (Name)", &schema);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["values"]);
crate::snap!("after_close_paren", a);
}
#[test]
fn after_values_keyword_expects_open_paren() {
let schema = schema_serial_pk();
// Trailing space so we're past the `values` word boundary
// — without it the partial-prefix logic re-offers `values`
// itself as the candidate that matches the typed prefix.
let a = assess_at_end(
"insert into Customers (Name) values ",
&schema,
);
assert!(matches!(a.state, InputState::IncompleteAtEof));
assert_candidate_present(&a, &["("]);
crate::snap!("after_values_keyword", a);
}
// =========================================================
// Value-list positions — typed-slot prose.
// =========================================================
#[test]
fn after_values_open_paren_form_a_text_column_prose_names_column() {
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers (Name) values (",
&schema,
);
assert!(
hint_prose_contains(&a, "Name"),
"expected column name in prose, got {:?}",
a.hint,
);
assert!(
hint_prose_contains(&a, "quoted string"),
"expected text-slot prose, got {:?}",
a.hint,
);
crate::snap!("after_values_open_paren_text_col", a);
}
#[test]
fn after_values_open_paren_form_a_serial_column_offers_null_to_auto_generate() {
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers (id, Name) values (",
&schema,
);
let prose = hint_prose(&a)
.unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint));
assert!(prose.contains("id"), "prose should name `id`, got {prose:?}");
assert!(
prose.contains("null") && prose.contains("auto-generate"),
"prose should mention `null` to auto-generate, got {prose:?}",
);
crate::snap!("after_values_open_paren_serial_col", a);
}
#[test]
fn mid_value_list_after_comma_advances_to_next_column_prose() {
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers (Name, Email) values ('Alice', ",
&schema,
);
let prose = hint_prose(&a)
.unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint));
assert!(
prose.contains("Email"),
"prose should name `Email`, got {prose:?}",
);
crate::snap!("after_first_value_advance_to_email", a);
}
// =========================================================
// In-progress Form A — regression for handoff §1 bug D2.
//
// `insert into Orders (...) values (..., ...` (no closing
// paren) must classify as IncompleteAtEof, not
// DefiniteErrorAt.
// =========================================================
#[test]
fn form_a_in_progress_values_list_is_incomplete_not_definite_error() {
let schema = schema_multi_table();
let a = assess_at_end(
"insert into Orders (OrderId, CustId, Total) values (42, 89, 17.59",
&schema,
);
assert!(
matches!(a.state, InputState::IncompleteAtEof),
"expected IncompleteAtEof (bug §1-D2 regression), got {:?}",
a.state,
);
crate::snap!("form_a_in_progress_no_red", a);
}
#[test]
fn form_a_in_progress_with_one_value_is_incomplete() {
let schema = schema_multi_table();
let a = assess_at_end(
"insert into Orders (OrderId, CustId, Total) values (42",
&schema,
);
assert!(matches!(a.state, InputState::IncompleteAtEof));
crate::snap!("form_a_in_progress_one_value", a);
}
// =========================================================
// Form A: complete inputs parse to Insert.
// =========================================================
#[test]
fn form_a_complete_parses_to_insert() {
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers (Name, Email) values ('Alice', 'a@b.c')",
&schema,
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
crate::snap!("form_a_complete_two_values", a);
}
#[test]
fn form_a_complete_with_serial_in_list_parses() {
let schema = schema_serial_pk();
let a = assess_at_end(
"insert into Customers (id, Name, Email) values (1, 'Alice', 'a@b.c')",
&schema,
);
assert!(matches!(a.state, InputState::Valid));
assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
crate::snap!("form_a_complete_with_serial", a);
}
// =========================================================
// Per-column-type prose across the Type axis (every_type
// schema). Each call to `Things.<col>` value position
// exercises one Type variant's catalog prose.
// =========================================================
#[test]
fn form_a_int_slot_prose_says_integer() {
let schema = schema_every_type();
let a = assess_at_end("insert into Things (k) values (", &schema);
let prose = hint_prose(&a).unwrap_or_else(|| {
panic!("expected Prose for int slot, got {:?}", a.hint)
});
assert!(
prose.contains("integer"),
"int-slot prose should say `integer`, got {prose:?}",
);
crate::snap!("form_a_int_slot", a);
}
#[test]
fn form_a_date_slot_prose_says_yyyy_mm_dd() {
let schema = schema_every_type();
let a = assess_at_end("insert into Things (dt) values (", &schema);
let prose = hint_prose(&a).unwrap_or_else(|| {
panic!("expected Prose for date slot, got {:?}", a.hint)
});
assert!(
prose.contains("YYYY-MM-DD"),
"date-slot prose should reference YYYY-MM-DD format, got {prose:?}",
);
crate::snap!("form_a_date_slot", a);
}
#[test]
fn form_a_bool_slot_prose_mentions_true_false() {
let schema = schema_every_type();
let a = assess_at_end("insert into Things (b) values (", &schema);
let prose = hint_prose(&a).unwrap_or_else(|| {
panic!("expected Prose for bool slot, got {:?}", a.hint)
});
assert!(
prose.contains("true") && prose.contains("false"),
"bool-slot prose should mention `true`/`false`, got {prose:?}",
);
crate::snap!("form_a_bool_slot", a);
}
#[test]
fn form_a_shortid_slot_prose_mentions_null_to_auto_generate() {
let schema = schema_every_type();
let a = assess_at_end("insert into Things (sid) values (", &schema);
let prose = hint_prose(&a).unwrap_or_else(|| {
panic!("expected Prose for shortid slot, got {:?}", a.hint)
});
assert!(
prose.contains("null") && prose.contains("auto-generate"),
"shortid-slot prose should mention `null` to auto-generate, got {prose:?}",
);
crate::snap!("form_a_shortid_slot", a);
}
// =========================================================
// Column-name leakage: Form A's column list must not offer
// columns from sibling tables in a multi-table schema.
// =========================================================
#[test]
fn form_a_open_paren_no_leakage_from_other_tables() {
let schema = schema_multi_table();
let a = assess_at_end("insert into Orders (", &schema);
assert_no_candidate_named(&a, &["Name"]);
assert_candidate_present(&a, &["OrderId", "CustId", "Total"]);
crate::snap!("form_a_open_paren_orders", a);
}