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