//! 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); }