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