diff --git a/tests/typing_surface/insert_form_b.rs b/tests/typing_surface/insert_form_b.rs index b2d5ff4..f2b1bcd 100644 --- a/tests/typing_surface/insert_form_b.rs +++ b/tests/typing_surface/insert_form_b.rs @@ -1 +1,236 @@ -//! Submodule stub — populated in subsequent tasks. +//! 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); +} diff --git a/tests/typing_surface/mod.rs b/tests/typing_surface/mod.rs index 109b70f..751906b 100644 --- a/tests/typing_surface/mod.rs +++ b/tests/typing_surface/mod.rs @@ -18,7 +18,7 @@ use rdbms_playground::completion::{ use rdbms_playground::dsl::parser::parse_command_with_schema; use rdbms_playground::dsl::types::Type; use rdbms_playground::input_render::{ - AmbientHint, InputState, ambient_hint, classify_input, + AmbientHint, InputState, ambient_hint, classify_input_with_schema, }; pub mod insert_form_a; @@ -168,7 +168,7 @@ pub struct Assessment { /// Assess the typing surface at the given cell. pub fn assess(input: &str, cursor: usize, schema: &SchemaCache) -> Assessment { - let state = classify_input(input); + let state = classify_input_with_schema(input, schema); let hint = ambient_hint(input, cursor, None, schema); let completion = candidates_at_cursor(input, cursor, schema); let parse_result = match parse_command_with_schema(input, schema) { diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__after_values_space_offers_open_paren@after_values_space.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__after_values_space_offers_open_paren@after_values_space.snap new file mode 100644 index 0000000..ba94cb2 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__after_values_space_offers_open_paren@after_values_space.snap @@ -0,0 +1,39 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers values \" cursor=29" +expression: "& a" +--- +Assessment { + input: "insert into Customers values ", + cursor: 29, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "(", + kind: Punct, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 29, + 29, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "(", + kind: Punct, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_advances_through_every_type_first_to_real@form_b_every_type_second_slot.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_advances_through_every_type_first_to_real@form_b_every_type_second_slot.snap new file mode 100644 index 0000000..b3324ac --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_advances_through_every_type_first_to_real@form_b_every_type_second_slot.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Things values (1, \" cursor=30" +expression: "& a" +--- +Assessment { + input: "insert into Things values (1, ", + cursor: 30, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `r`: Type a number (e.g. 3.14, -0.5) or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 30, + 30, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_after_first_value_advances_to_next_column@form_b_second_value.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_after_first_value_advances_to_next_column@form_b_second_value.snap new file mode 100644 index 0000000..e710bab --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_after_first_value_advances_to_next_column@form_b_second_value.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers values ('Alice', \" cursor=39" +expression: "& a" +--- +Assessment { + input: "insert into Customers values ('Alice', ", + cursor: 39, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `Email`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 39, + 39, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_every_type_first_column_is_int@form_b_first_value_every_type.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_every_type_first_column_is_int@form_b_first_value_every_type.snap new file mode 100644 index 0000000..230eb7c --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_every_type_first_column_is_int@form_b_first_value_every_type.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Things values (\" cursor=27" +expression: "& a" +--- +Assessment { + input: "insert into Things values (", + cursor: 27, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `k`: Type an integer (e.g. 42, -7) or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 27, + 27, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_skips_serial_column@form_b_first_value_serial_pk.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_skips_serial_column@form_b_first_value_serial_pk.snap new file mode 100644 index 0000000..648de8a --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_skips_serial_column@form_b_first_value_serial_pk.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers values (\" cursor=30" +expression: "& a" +--- +Assessment { + input: "insert into Customers values (", + cursor: 30, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `Name`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 30, + 30, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_text_pk_names_first_column@form_b_first_value_text_pk.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_text_pk_names_first_column@form_b_first_value_text_pk.snap new file mode 100644 index 0000000..ba19d21 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_value_text_pk_names_first_column@form_b_first_value_text_pk.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Items values (\" cursor=26" +expression: "& a" +--- +Assessment { + input: "insert into Items values (", + cursor: 26, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `Code`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 26, + 26, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_in_progress_after_comma_is_incomplete@form_b_in_progress_after_comma.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_in_progress_after_comma_is_incomplete@form_b_in_progress_after_comma.snap new file mode 100644 index 0000000..e710bab --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_in_progress_after_comma_is_incomplete@form_b_in_progress_after_comma.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers values ('Alice', \" cursor=39" +expression: "& a" +--- +Assessment { + input: "insert into Customers values ('Alice', ", + cursor: 39, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `Email`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 39, + 39, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_in_progress_without_closing_paren_is_incomplete@form_b_in_progress_no_close_paren.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_in_progress_without_closing_paren_is_incomplete@form_b_in_progress_no_close_paren.snap new file mode 100644 index 0000000..9a40533 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_in_progress_without_closing_paren_is_incomplete@form_b_in_progress_no_close_paren.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers values ('Alice', 'a@b.c'\" cursor=46" +expression: "& a" +--- +Assessment { + input: "insert into Customers values ('Alice', 'a@b.c'", + cursor: 46, + state: IncompleteAtEof, + hint: Some( + Prose( + "Next: `)`", + ), + ), + completion: None, + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_text_pk_with_correct_values_parses@form_b_text_pk_valid.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_text_pk_with_correct_values_parses@form_b_text_pk_valid.snap new file mode 100644 index 0000000..a62c717 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_text_pk_with_correct_values_parses@form_b_text_pk_valid.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Items values ('SKU-1', 'Widget')\" cursor=44" +expression: "& a" +--- +Assessment { + input: "insert into Items values ('SKU-1', 'Widget')", + cursor: 44, + state: Valid, + hint: Some( + Prose( + "Submit with Enter", + ), + ), + completion: None, + parse_result: Ok( + "Insert", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_correct_values_parses@form_b_valid.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_correct_values_parses@form_b_valid.snap new file mode 100644 index 0000000..df0f7ed --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_correct_values_parses@form_b_valid.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers values ('Alice', 'a@b.c')\" cursor=47" +expression: "& a" +--- +Assessment { + input: "insert into Customers values ('Alice', 'a@b.c')", + cursor: 47, + state: Valid, + hint: Some( + Prose( + "Submit with Enter", + ), + ), + completion: None, + parse_result: Ok( + "Insert", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_extra_value_for_serial_column_is_invalid@form_b_extra_serial_value.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_extra_value_for_serial_column_is_invalid@form_b_extra_serial_value.snap new file mode 100644 index 0000000..a238ad9 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_extra_value_for_serial_column_is_invalid@form_b_extra_serial_value.snap @@ -0,0 +1,35 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers values (1, 'Alice', 'a@b.c')\" cursor=50" +expression: "& a" +--- +Assessment { + input: "insert into Customers values (1, 'Alice', 'a@b.c')", + cursor: 50, + state: DefiniteErrorAt( + 30, + ), + hint: Some( + Prose( + "for `Name`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 50, + 50, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(definite)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_too_few_values_is_invalid_at_close_paren@form_b_too_few_values.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_too_few_values_is_invalid_at_close_paren@form_b_too_few_values.snap new file mode 100644 index 0000000..d6936e0 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_with_too_few_values_is_invalid_at_close_paren@form_b_too_few_values.snap @@ -0,0 +1,21 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers values ('Alice')\" cursor=38" +expression: "& a" +--- +Assessment { + input: "insert into Customers values ('Alice')", + cursor: 38, + state: DefiniteErrorAt( + 37, + ), + hint: Some( + Prose( + "Submit with Enter", + ), + ), + completion: None, + parse_result: Err( + "Invalid(definite)", + ), +}