24e641bc21
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).
349 lines
12 KiB
Rust
349 lines
12 KiB
Rust
//! 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);
|
|
}
|