90e3f5dbfb
Form C (`insert into T (vals)`) shared the `(` opener with Form A, so its paren was an untyped Repeated(Choice(literal, ident)) — values weren't type- or count-checked at parse time (handoff-12 §2.2). New Node::Lookahead variant: a factory that peeks the source. The insert first-paren factory inspects the first token — a value literal routes the contents through the typed column_value_list (Form B dispatch contract: per-non-auto-column typed slots); an identifier or empty paren routes to a Form A column-name list. So Form C now gets the same per-column typed slots, hints, and parse-time type/count checking Form B has. The explicit-Choice-branch split is impossible here (committed-choice semantics commit after `(` matches); lookahead is the only route, and DynamicSubgrammar factories couldn't see the source. Node::Lookahead is not memoized — its output depends on source — but it returns only a small node (a Repeated, or a thin DynamicSubgrammar wrapper that delegates to the memoized column_value_list). `insert into T (` now cleanly shows Form A column candidates instead of mixed Form-A/C suggestions. Form C matrix tests updated for the type-aware behaviour.
178 lines
6.1 KiB
Rust
178 lines
6.1 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 — caught at
|
|
// parse time now.
|
|
let schema = schema_serial_pk();
|
|
let a = assess_at_end(
|
|
"insert into Customers ('Alice', 'a@b.c', 'extra')",
|
|
&schema,
|
|
);
|
|
assert!(
|
|
!matches!(a.state, InputState::Valid),
|
|
"Form C with too many values must be invalid, got {:?}",
|
|
a.state,
|
|
);
|
|
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);
|
|
}
|