diff --git a/src/dsl/grammar/data.rs b/src/dsl/grammar/data.rs index 854f339..950c1c0 100644 --- a/src/dsl/grammar/data.rs +++ b/src/dsl/grammar/data.rs @@ -18,9 +18,10 @@ use crate::dsl::command::{Command, RowFilter}; use crate::dsl::grammar::{ - CommandNode, HintMode, IdentSource, Node, ValidationError, Word, + CommandNode, IdentSource, Node, ValidationError, Word, shared::{column_value_list, current_column_value}, }; +use crate::dsl::walker::context::WalkContext; use crate::dsl::value::Value; use crate::dsl::walker::outcome::{MatchedItem, MatchedKind, MatchedPath}; @@ -52,25 +53,6 @@ const TABLE_NAME_INSERT: Node = Node::Ident { writes_user_listed_column: false, }; -// `value_literal` — null / true / false / number / string. The -// chumsky-side equivalent (`value_literal()` in dsl/parser.rs). -const VALUE_LITERAL_CHOICES: &[Node] = &[ - Node::Word(Word::keyword("null")), - Node::Word(Word::keyword("true")), - Node::Word(Word::keyword("false")), - Node::NumberLit { validator: None }, - Node::StringLit, -]; -const VALUE_LITERAL_INNER: Node = Node::Choice(VALUE_LITERAL_CHOICES); -/// Value-literal slot with the `ProseOnly` HintMode -/// (ADR-0024 §HintMode-per-node) — the hint resolver surfaces -/// the generic "Type a value: …" prose rather than the -/// misleading `null`/`true`/`false` candidate trio. -const VALUE_LITERAL: Node = Node::Hinted { - mode: HintMode::ProseOnly("hint.value_literal_slot"), - inner: &VALUE_LITERAL_INNER, -}; - // ================================================================= // show — `show (data|table) ` // ================================================================= @@ -97,43 +79,85 @@ const SHOW_SHAPE: Node = Node::Choice(SHOW_CHOICES); // ================================================================= // // Forms A (with column list) and C (bare value list) both start -// with `(`. To avoid the walker's "first commit wins" semantics -// rejecting Form C when the inner content is values rather than -// column names, the inside of the first paren is parsed as a -// repeated `Choice(Ident, ValueLiteral)`. The AST builder then -// disambiguates: if a `values` keyword follows the first paren, -// the inner content was column names; otherwise it was values. +// with `(`. The walker's "first commit wins" Choice semantics +// can't pick between them after the `(` matches, so the first +// paren's contents are resolved by a `Node::Lookahead` factory +// (`insert_first_paren`): it peeks the first token to decide. +// +// - First token is a value literal (number / string / +// null / true / false) → Form C → the typed `column_value_list` +// (same dispatch contract as Form B — ADR-0024 §Phase D Form-C +// type-awareness). Form C values are now type-checked at parse +// time, not only at bind time. +// - Otherwise (column-name identifier, or an empty paren) → +// Form A → a repeated column-name list. The idents write +// `WalkContext::user_listed_columns` so the trailing +// `values (…)` slots mirror the user's selection. -const INSERT_PAREN_ITEM_CHOICES: &[Node] = &[ - // VALUE_LITERAL first so that `true`/`false`/`null` match - // their Word branch rather than the broader Ident{Columns} - // catch-all (consume_ident doesn't filter against the - // keyword set; without this ordering, `(true)` would lex - // as a column-name list). - VALUE_LITERAL, - Node::Ident { - source: IdentSource::Columns, - role: "insert_first_item", - validator: None, - highlight_override: None, - writes_table: false, - writes_column: false, - // Form A signal: when the user lists explicit columns - // in `insert into (col1, col2, …)`, the walker - // appends each matched name to - // `WalkContext::user_listed_columns`. The inner - // `values (…)` slot list then mirrors that user - // selection instead of the auto-filtered default - // (ADR-0024 §Phase D §column_value_list). - writes_user_listed_column: true, - }, -]; -const INSERT_PAREN_ITEM: Node = Node::Choice(INSERT_PAREN_ITEM_CHOICES); -const INSERT_PAREN_LIST: Node = Node::Repeated { - inner: &INSERT_PAREN_ITEM, - separator: Some(&Node::Punct(',')), - min: 1, +/// Form A's column-name slot. `static` (not `const`) so the +/// `insert_first_paren` factory can take a `&'static` reference +/// to it when building the repeated list at walk time. +static FORM_A_COLUMN: Node = Node::Ident { + source: IdentSource::Columns, + role: "insert_first_item", + validator: None, + highlight_override: None, + writes_table: false, + writes_column: false, + writes_user_listed_column: true, }; +static INSERT_COMMA: Node = Node::Punct(','); + +/// First-paren resolver (ADR-0024 §Phase D Form-C type-awareness). +/// Peeks the first token after `(` to route to Form A's +/// column-name list or Form C's typed value list. +fn insert_first_paren(_ctx: &WalkContext, source: &str, pos: usize) -> Node { + if first_paren_item_is_value_literal(source, pos) { + // Form C — bare value list. `column_value_list` with no + // user-listed columns dispatches per non-auto-generated + // column, exactly as Form B does. + Node::DynamicSubgrammar(column_value_list) + } else { + // Form A (or Form A in progress / empty paren). + Node::Repeated { + inner: &FORM_A_COLUMN, + separator: Some(&INSERT_COMMA), + min: 1, + } + } +} + +/// True when the first token after the insert `(` is a +/// value literal — the signal that the paren is a Form C value +/// list rather than a Form A column-name list. An empty paren +/// or an identifier-shaped token (a column name) returns false. +fn first_paren_item_is_value_literal(source: &str, pos: usize) -> bool { + use crate::dsl::walker::lex_helpers::{ + consume_ident, consume_number_literal, consume_string_literal, + skip_whitespace, + }; + let p = skip_whitespace(source, pos); + if p >= source.len() { + return false; // empty paren — treat as Form A + } + if consume_string_literal(source, p).is_some() { + return true; + } + if consume_number_literal(source, p).is_some() { + return true; + } + if let Some((s, e)) = consume_ident(source, p) { + let word = &source[s..e]; + // `null` / `true` / `false` are value literals; any + // other identifier is a column name (Form A). + return word.eq_ignore_ascii_case("null") + || word.eq_ignore_ascii_case("true") + || word.eq_ignore_ascii_case("false"); + } + false // punctuation (e.g. `)`) — treat as Form A +} + +const INSERT_PAREN_LIST: Node = Node::Lookahead(insert_first_paren); /// Schema-aware value list: when the walker has a populated /// `current_table_columns`, unfolds to a `Seq` of typed slots diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs index 6d63ffa..0a0ff7e 100644 --- a/src/dsl/grammar/mod.rs +++ b/src/dsl/grammar/mod.rs @@ -286,9 +286,20 @@ pub enum Node { min: usize, }, /// Resolves at walk time using the active `WalkContext`. - /// Phase D+ uses this for `column_value_list`. + /// Phase D+ uses this for `column_value_list`. The factory + /// is pure in `ctx`, so the walker memoizes the resolution + /// (one leak per distinct schema shape). #[allow(dead_code)] DynamicSubgrammar(fn(&WalkContext) -> Self), + /// Like `DynamicSubgrammar` but the factory also sees the + /// source and the current byte position, so it can look + /// ahead. Used by the insert first-paren to discriminate + /// Form A (`(cols) values (...)`) from Form C (`(vals)`) + /// before walking the contents — Form C then routes through + /// the typed `column_value_list` (ADR-0024 §Phase D, Form C + /// type-awareness). Not memoized: the output depends on the + /// source, not just `ctx`. + Lookahead(fn(&WalkContext, &str, usize) -> Self), /// Typed value-literal slot (ADR-0024 §Phase D §typed-value-slots). /// /// Walks `inner` to consume the literal but records the diff --git a/src/dsl/walker/driver.rs b/src/dsl/walker/driver.rs index 32158a3..6b615a9 100644 --- a/src/dsl/walker/driver.rs +++ b/src/dsl/walker/driver.rs @@ -212,6 +212,20 @@ fn walk_node_inner( let resolved = resolve_dynamic(*factory, ctx); walk_node(source, pos, resolved, ctx, path, per_byte) } + Node::Lookahead(factory) => { + // ADR-0024 §Phase D Form-C type-awareness: the + // factory peeks the source at `pos` (e.g. to tell a + // Form A column list from a Form C value list) and + // returns the shape to walk. Not memoized — the + // result depends on the source — but the factory + // returns a small node (a Repeated, or a thin + // DynamicSubgrammar wrapper that delegates to the + // memoized `column_value_list`), so the per-walk + // leak is a few bytes, not a whole typed tree. + let resolved: &'static Node = + Box::leak(Box::new(factory(ctx, source, pos))); + walk_node(source, pos, resolved, ctx, path, per_byte) + } Node::TypedValueSlot { ty, column_name, diff --git a/tests/typing_surface/insert_form_c.rs b/tests/typing_surface/insert_form_c.rs index b719230..43ff4ab 100644 --- a/tests/typing_surface/insert_form_c.rs +++ b/tests/typing_surface/insert_form_c.rs @@ -1,24 +1,30 @@ //! Matrix coverage for `insert into T (vals)` (Form C — bare //! value list, no `values` keyword). //! -//! Form C shares the `( ... )` opener with Form A but resolves -//! the paren contents as values rather than column names. Per -//! handoff-12 §2.2 the Form C path is *type-unaware* — its -//! grammar uses the schemaless `INSERT_PAREN_LIST` shape, not -//! the typed `column_value_list`. Type validation happens at -//! bind time, not parse time. +//! 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. //! -//! The previous commit's Form C/A disambiguation means -//! column-shaped items (idents) inside the parens now flag as -//! "did you mean Form A?". This file pins both the happy-path -//! (literals only) and the Form-A-recovery (column-shaped -//! items). +//! 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_with_text_literals_parses() { +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')", @@ -26,32 +32,128 @@ fn form_c_with_text_literals_parses() { ); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Insert")); - crate::snap!("form_c_text_literals", a); + crate::snap!("form_c_text_pk_valid", a); } #[test] -fn form_c_with_mixed_literals_parses() { +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 (1, 'Alice', 'a@b.c')", + "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_mixed_literals", a); + crate::snap!("form_c_serial_pk_valid", a); } #[test] -fn form_c_with_null_first_parses() { +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, 'Alice', 'a@b.c')", + "insert into Customers (null, 'a@b.c')", &schema, ); assert!(matches!(a.state, InputState::Valid)); - crate::snap!("form_c_null_first", a); + 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(); @@ -73,37 +175,3 @@ fn form_c_with_two_columns_flags_as_form_a_in_progress() { assert_candidate_present(&a, &["values"]); crate::snap!("form_c_two_columns_recovery", a); } - -#[test] -fn form_c_type_unaware_grammar_accepts_decimal_for_int_column() { - // Form C's grammar uses INSERT_PAREN_LIST (the pre-Phase-D - // schemaless choice), so type mismatches aren't caught at - // parse time. Bind time catches them. Handoff §2.2 - // documents this as known. - let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers (3.14, 'Alice', 'a@b.c')", - &schema, - ); - assert!(matches!(a.state, InputState::Valid)); - crate::snap!("form_c_type_unaware", a); -} - -#[test] -fn form_c_in_progress_after_comma_is_incomplete() { - let schema = schema_serial_pk(); - let a = assess_at_end("insert into Customers (1, ", &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 (1, 'Alice'", - &schema, - ); - assert!(matches!(a.state, InputState::IncompleteAtEof)); - crate::snap!("form_c_in_progress_no_close", a); -} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_offers_active_table_columns_only@after_open_paren_multi_table.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_offers_active_table_columns_only@after_open_paren_multi_table.snap index a94feff..1e6452a 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_offers_active_table_columns_only@after_open_paren_multi_table.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_offers_active_table_columns_only@after_open_paren_multi_table.snap @@ -8,9 +8,19 @@ Assessment { cursor: 23, state: IncompleteAtEof, hint: Some( - Prose( - "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", - ), + Candidates { + items: [ + Candidate { + text: "Name", + kind: Identifier, + }, + Candidate { + text: "id", + kind: Identifier, + }, + ], + selected: None, + }, ), completion: Some( Completion { @@ -20,18 +30,6 @@ Assessment { ), partial_prefix: "", candidates: [ - Candidate { - text: "null", - kind: Keyword, - }, - Candidate { - text: "true", - kind: Keyword, - }, - Candidate { - text: "false", - kind: Keyword, - }, Candidate { text: "Name", kind: Identifier, diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_serial_pk_offers_all_columns_including_serial@after_open_paren_serial_pk.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_serial_pk_offers_all_columns_including_serial@after_open_paren_serial_pk.snap index 595b5f9..35e9163 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_serial_pk_offers_all_columns_including_serial@after_open_paren_serial_pk.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_serial_pk_offers_all_columns_including_serial@after_open_paren_serial_pk.snap @@ -8,9 +8,23 @@ Assessment { cursor: 23, state: IncompleteAtEof, hint: Some( - Prose( - "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", - ), + Candidates { + items: [ + Candidate { + text: "Email", + kind: Identifier, + }, + Candidate { + text: "Name", + kind: Identifier, + }, + Candidate { + text: "id", + kind: Identifier, + }, + ], + selected: None, + }, ), completion: Some( Completion { @@ -20,18 +34,6 @@ Assessment { ), partial_prefix: "", candidates: [ - Candidate { - text: "null", - kind: Keyword, - }, - Candidate { - text: "true", - kind: Keyword, - }, - Candidate { - text: "false", - kind: Keyword, - }, Candidate { text: "Email", kind: Identifier, diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_text_pk_offers_text_pk_columns@after_open_paren_text_pk.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_text_pk_offers_text_pk_columns@after_open_paren_text_pk.snap index 08402a4..f78d80a 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_text_pk_offers_text_pk_columns@after_open_paren_text_pk.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_text_pk_offers_text_pk_columns@after_open_paren_text_pk.snap @@ -8,9 +8,19 @@ Assessment { cursor: 19, state: IncompleteAtEof, hint: Some( - Prose( - "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", - ), + Candidates { + items: [ + Candidate { + text: "Code", + kind: Identifier, + }, + Candidate { + text: "Title", + kind: Identifier, + }, + ], + selected: None, + }, ), completion: Some( Completion { @@ -20,18 +30,6 @@ Assessment { ), partial_prefix: "", candidates: [ - Candidate { - text: "null", - kind: Keyword, - }, - Candidate { - text: "true", - kind: Keyword, - }, - Candidate { - text: "false", - kind: Keyword, - }, Candidate { text: "Code", kind: Identifier, diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_open_paren_no_leakage_from_other_tables@form_a_open_paren_orders.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_open_paren_no_leakage_from_other_tables@form_a_open_paren_orders.snap index 1762cd6..985fcaa 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_open_paren_no_leakage_from_other_tables@form_a_open_paren_orders.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_open_paren_no_leakage_from_other_tables@form_a_open_paren_orders.snap @@ -8,9 +8,23 @@ Assessment { cursor: 20, state: IncompleteAtEof, hint: Some( - Prose( - "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", - ), + Candidates { + items: [ + Candidate { + text: "CustId", + kind: Identifier, + }, + Candidate { + text: "OrderId", + kind: Identifier, + }, + Candidate { + text: "Total", + kind: Identifier, + }, + ], + selected: None, + }, ), completion: Some( Completion { @@ -20,18 +34,6 @@ Assessment { ), partial_prefix: "", candidates: [ - Candidate { - text: "null", - kind: Keyword, - }, - Candidate { - text: "true", - kind: Keyword, - }, - Candidate { - text: "false", - kind: Keyword, - }, Candidate { text: "CustId", kind: Identifier, diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_column_list_after_comma_offers_remaining_columns@after_comma_in_column_list.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_column_list_after_comma_offers_remaining_columns@after_comma_in_column_list.snap index 9dd870b..a7612f5 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_column_list_after_comma_offers_remaining_columns@after_comma_in_column_list.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_column_list_after_comma_offers_remaining_columns@after_comma_in_column_list.snap @@ -8,9 +8,23 @@ Assessment { cursor: 27, state: IncompleteAtEof, hint: Some( - Prose( - "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", - ), + Candidates { + items: [ + Candidate { + text: "Email", + kind: Identifier, + }, + Candidate { + text: "Name", + kind: Identifier, + }, + Candidate { + text: "id", + kind: Identifier, + }, + ], + selected: None, + }, ), completion: Some( Completion { @@ -20,18 +34,6 @@ Assessment { ), partial_prefix: "", candidates: [ - Candidate { - text: "null", - kind: Keyword, - }, - Candidate { - text: "true", - kind: Keyword, - }, - Candidate { - text: "false", - kind: Keyword, - }, Candidate { text: "Email", kind: Identifier, diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_in_progress_after_comma_is_incomplete@form_c_in_progress_after_comma.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_in_progress_after_comma_is_incomplete@form_c_in_progress_after_comma.snap index 1c6bde2..988e06a 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_in_progress_after_comma_is_incomplete@form_c_in_progress_after_comma.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_in_progress_after_comma_is_incomplete@form_c_in_progress_after_comma.snap @@ -1,22 +1,22 @@ --- source: tests/typing_surface/insert_form_c.rs -description: "input=\"insert into Customers (1, \" cursor=26" +description: "input=\"insert into Customers ('Alice', \" cursor=32" expression: "& a" --- Assessment { - input: "insert into Customers (1, ", - cursor: 26, + input: "insert into Customers ('Alice', ", + cursor: 32, state: IncompleteAtEof, hint: Some( Prose( - "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", + "for `Email`: Type a quoted string (e.g. 'Alice') or null", ), ), completion: Some( Completion { replaced_range: ( - 26, - 26, + 32, + 32, ), partial_prefix: "", candidates: [ @@ -24,26 +24,6 @@ Assessment { text: "null", kind: Keyword, }, - Candidate { - text: "true", - kind: Keyword, - }, - Candidate { - text: "false", - kind: Keyword, - }, - Candidate { - text: "Email", - kind: Identifier, - }, - Candidate { - text: "Name", - kind: Identifier, - }, - Candidate { - text: "id", - kind: Identifier, - }, ], }, ), diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_in_progress_without_close_paren_is_incomplete@form_c_in_progress_no_close.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_in_progress_without_close_paren_is_incomplete@form_c_in_progress_no_close.snap index 255428b..d79c3b1 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_in_progress_without_close_paren_is_incomplete@form_c_in_progress_no_close.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_in_progress_without_close_paren_is_incomplete@form_c_in_progress_no_close.snap @@ -1,11 +1,11 @@ --- source: tests/typing_surface/insert_form_c.rs -description: "input=\"insert into Customers (1, 'Alice'\" cursor=33" +description: "input=\"insert into Customers ('Alice', 'a@b.c'\" cursor=39" expression: "& a" --- Assessment { - input: "insert into Customers (1, 'Alice'", - cursor: 33, + input: "insert into Customers ('Alice', 'a@b.c'", + cursor: 39, state: IncompleteAtEof, hint: Some( Prose( diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_rejects_number_for_text_column@form_c_type_mismatch.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_rejects_number_for_text_column@form_c_type_mismatch.snap new file mode 100644 index 0000000..fe9911f --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_rejects_number_for_text_column@form_c_type_mismatch.snap @@ -0,0 +1,35 @@ +--- +source: tests/typing_surface/insert_form_c.rs +description: "input=\"insert into Customers (3.14, 'a@b.c')\" cursor=37" +expression: "& a" +--- +Assessment { + input: "insert into Customers (3.14, 'a@b.c')", + cursor: 37, + state: DefiniteErrorAt( + 23, + ), + hint: Some( + Prose( + "for `Name`: Type a quoted string (e.g. 'Alice') or null (`id` auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 37, + 37, + ), + 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_c__form_c_second_slot_shows_typed_prose_for_column@form_c_typed_prose.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_second_slot_shows_typed_prose_for_column@form_c_typed_prose.snap new file mode 100644 index 0000000..988e06a --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_second_slot_shows_typed_prose_for_column@form_c_typed_prose.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_c.rs +description: "input=\"insert into Customers ('Alice', \" cursor=32" +expression: "& a" +--- +Assessment { + input: "insert into Customers ('Alice', ", + cursor: 32, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `Email`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 32, + 32, + ), + 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_c__form_c_with_mixed_literals_parses@form_c_mixed_literals.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_serial_pk_correct_values_parses@form_c_serial_pk_valid.snap similarity index 78% rename from tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_mixed_literals_parses@form_c_mixed_literals.snap rename to tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_serial_pk_correct_values_parses@form_c_serial_pk_valid.snap index 98c5eea..76646dd 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_mixed_literals_parses@form_c_mixed_literals.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_serial_pk_correct_values_parses@form_c_serial_pk_valid.snap @@ -1,11 +1,11 @@ --- source: tests/typing_surface/insert_form_c.rs -description: "input=\"insert into Customers (1, 'Alice', 'a@b.c')\" cursor=43" +description: "input=\"insert into Customers ('Alice', 'a@b.c')\" cursor=40" expression: "& a" --- Assessment { - input: "insert into Customers (1, 'Alice', 'a@b.c')", - cursor: 43, + input: "insert into Customers ('Alice', 'a@b.c')", + cursor: 40, state: Valid, hint: Some( Candidates { @@ -21,8 +21,8 @@ Assessment { completion: Some( Completion { replaced_range: ( - 43, - 43, + 40, + 40, ), partial_prefix: "", candidates: [ diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_text_literals_parses@form_c_text_literals.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_text_pk_correct_values_parses@form_c_text_pk_valid.snap similarity index 100% rename from tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_text_literals_parses@form_c_text_literals.snap rename to tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_text_pk_correct_values_parses@form_c_text_pk_valid.snap diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_null_first_parses@form_c_null_first.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_null_first_parses@form_c_null_first.snap deleted file mode 100644 index 4feb267..0000000 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_null_first_parses@form_c_null_first.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: tests/typing_surface/insert_form_c.rs -description: "input=\"insert into Customers (null, 'Alice', 'a@b.c')\" cursor=46" -expression: "& a" ---- -Assessment { - input: "insert into Customers (null, 'Alice', 'a@b.c')", - cursor: 46, - state: Valid, - hint: Some( - Candidates { - items: [ - Candidate { - text: "values", - kind: Keyword, - }, - ], - selected: None, - }, - ), - completion: Some( - Completion { - replaced_range: ( - 46, - 46, - ), - partial_prefix: "", - candidates: [ - Candidate { - text: "values", - kind: Keyword, - }, - ], - }, - ), - parse_result: Ok( - "Insert", - ), -} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_type_unaware_grammar_accepts_decimal_for_int_column@form_c_type_unaware.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_null_value_parses@form_c_null_value.snap similarity index 77% rename from tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_type_unaware_grammar_accepts_decimal_for_int_column@form_c_type_unaware.snap rename to tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_null_value_parses@form_c_null_value.snap index 4f6daa8..e43b8be 100644 --- a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_type_unaware_grammar_accepts_decimal_for_int_column@form_c_type_unaware.snap +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_with_null_value_parses@form_c_null_value.snap @@ -1,11 +1,11 @@ --- source: tests/typing_surface/insert_form_c.rs -description: "input=\"insert into Customers (3.14, 'Alice', 'a@b.c')\" cursor=46" +description: "input=\"insert into Customers (null, 'a@b.c')\" cursor=37" expression: "& a" --- Assessment { - input: "insert into Customers (3.14, 'Alice', 'a@b.c')", - cursor: 46, + input: "insert into Customers (null, 'a@b.c')", + cursor: 37, state: Valid, hint: Some( Candidates { @@ -21,8 +21,8 @@ Assessment { completion: Some( Completion { replaced_range: ( - 46, - 46, + 37, + 37, ), partial_prefix: "", candidates: [ diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_wrong_value_count_is_invalid@form_c_wrong_count.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_wrong_value_count_is_invalid@form_c_wrong_count.snap new file mode 100644 index 0000000..60ffa80 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_c__form_c_wrong_value_count_is_invalid@form_c_wrong_count.snap @@ -0,0 +1,21 @@ +--- +source: tests/typing_surface/insert_form_c.rs +description: "input=\"insert into Customers ('Alice', 'a@b.c', 'extra')\" cursor=49" +expression: "& a" +--- +Assessment { + input: "insert into Customers ('Alice', 'a@b.c', 'extra')", + cursor: 49, + state: DefiniteErrorAt( + 39, + ), + hint: Some( + Prose( + "Submit with Enter", + ), + ), + completion: None, + parse_result: Err( + "Invalid(definite)", + ), +}