From f1ff5970bf42dcd272d15a44e549f8e1f8eb847b Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Fri, 15 May 2026 21:30:03 +0000 Subject: [PATCH] Hint: pedagogical Form-A pointer at Form B's first value slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handoff-12 §2.2: Form B `insert into T values (…)` silently skips auto-generated columns from the value list, so a user who wants to set a serial/shortid column explicitly could only discover Form A by reading help. Now the hint at the first Form B value slot appends a note naming the skipped column(s) and pointing at the explicit-column form. hint_resolution_at_input derives the skipped columns from the post-walk WalkContext (Form B = no user_listed_columns + table has serial/shortid columns) and reports them on HintResolution; the note fires only at the first slot so it doesn't repeat at every comma. ambient_hint composes it onto the per-column prose. --- src/dsl/walker/mod.rs | 137 +++++++++++++++--- src/friendly/keys.rs | 1 + src/friendly/strings/en-US.yaml | 5 + src/input_render.rs | 22 ++- tests/typing_surface/insert_form_b.rs | 96 +++++++++++- ..._has_no_skip_note@form_a_no_skip_note.snap | 33 +++++ ...al_column@form_b_first_slot_skip_note.snap | 33 +++++ ..._is_int@form_b_first_value_every_type.snap | 2 +- ...l_column@form_b_first_value_serial_pk.snap | 2 +- ..._note@form_b_second_slot_no_skip_note.snap | 33 +++++ ...skip_note@form_b_text_pk_no_skip_note.snap | 33 +++++ ..._is_invalid@form_b_extra_serial_value.snap | 2 +- 12 files changed, 366 insertions(+), 33 deletions(-) create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_a_first_slot_has_no_skip_note@form_a_no_skip_note.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_slot_mentions_skipped_serial_column@form_b_first_slot_skip_note.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_second_slot_omits_skip_note@form_b_second_slot_no_skip_note.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_text_pk_has_no_skip_note@form_b_text_pk_no_skip_note.snap diff --git a/src/dsl/walker/mod.rs b/src/dsl/walker/mod.rs index 7409962..5c4e6c5 100644 --- a/src/dsl/walker/mod.rs +++ b/src/dsl/walker/mod.rs @@ -74,6 +74,15 @@ pub fn hint_mode_at_input_with_schema( pub struct HintResolution { pub mode: crate::dsl::grammar::HintMode, pub column: Option, + /// Auto-generated columns (serial / shortid) that Form B + /// `insert into values (…)` silently skips from the + /// value list (ADR-0018 §3). Populated *only* at the first + /// value slot of a Form B insert whose table has such + /// columns — empty everywhere else. The renderer appends a + /// pedagogical note pointing the user at Form A so the + /// skipped column is discoverable without reading help + /// (handoff-12 §2.2). + pub form_b_autogen_skipped: Vec, } /// Single-walk hint resolver (ADR-0024 §Phase D §typed-value-slots). @@ -89,16 +98,22 @@ pub fn hint_resolution_at_input( use crate::dsl::grammar::{HintMode, IdentSource}; use crate::dsl::walker::outcome::Expectation; - let (expected, pending_type, pending_column) = - expected_for_hint_with_full_ctx(source, schema); - if expected.is_empty() { + let snap = expected_for_hint_snapshot(source, schema); + if snap.expected.is_empty() { return None; } + let expected = snap.expected; - if let Some(ty) = pending_type { + if let Some(ty) = snap.pending_value_type { return Some(HintResolution { mode: HintMode::ProseOnly(catalog_key_for_value_type(ty)), - column: pending_column, + form_b_autogen_skipped: form_b_autogen_skipped( + source, + snap.user_listed_columns.as_ref(), + snap.current_table_columns.as_ref(), + snap.pending_value_column.as_deref(), + ), + column: snap.pending_value_column, }); } @@ -116,6 +131,7 @@ pub fn hint_resolution_at_input( return Some(HintResolution { mode: HintMode::ProseOnly("hint.value_literal_slot"), column: None, + form_b_autogen_skipped: Vec::new(), }); } @@ -132,12 +148,60 @@ pub fn hint_resolution_at_input( return Some(HintResolution { mode: HintMode::ForceProse("hint.ambient_typing_name"), column: None, + form_b_autogen_skipped: Vec::new(), }); } None } +/// Auto-generated columns a Form B insert skips from its value +/// list — but only when the cursor sits at the *first* value +/// slot, so the pedagogical note fires once per command rather +/// than at every comma. +/// +/// Returns empty unless: the command is an `insert`; no explicit +/// column list was given (Form B — `user_listed` is `None`); the +/// table has serial / shortid columns; and `pending_column` is +/// the first non-auto-generated column (the first slot). +fn form_b_autogen_skipped( + source: &str, + user_listed: Option<&Vec>, + table_columns: Option<&Vec>, + pending_column: Option<&str>, +) -> Vec { + use crate::dsl::types::Type; + + // Form A (explicit column list) and non-insert commands + // (`update T set …` value slots also leave user_listed + // None) are excluded — the note is insert-Form-B only. + if user_listed.is_some() { + return Vec::new(); + } + if !source.trim_start().to_ascii_lowercase().starts_with("insert") { + return Vec::new(); + } + let Some(cols) = table_columns else { + return Vec::new(); + }; + let is_auto = |t: Type| matches!(t, Type::Serial | Type::ShortId); + let skipped: Vec = cols + .iter() + .filter(|c| is_auto(c.user_type)) + .map(|c| c.name.clone()) + .collect(); + if skipped.is_empty() { + return Vec::new(); + } + // Fire only at the first value slot — i.e. when the slot's + // column is the first non-auto-generated column. + let first_non_auto = cols.iter().find(|c| !is_auto(c.user_type)); + match (first_non_auto, pending_column) { + (Some(first), Some(pending)) if first.name == pending => skipped, + _ => Vec::new(), + } +} + fn hint_mode_at_input_inner( source: &str, schema: Option<&crate::completion::SchemaCache>, @@ -350,41 +414,62 @@ pub fn expected_at_input(source: &str) -> Vec { /// surfaced. Used by the hint resolver to distinguish "must /// type more" from "could continue", and to dispatch per-type /// prose when the cursor is inside a typed value slot. +/// Post-walk snapshot the hint resolver needs: the strict +/// expected set plus the `WalkContext` fields that survive the +/// walk and feed per-column / pedagogical prose. +struct HintWalkSnapshot { + expected: Vec, + pending_value_type: Option, + pending_value_column: Option, + current_table_columns: Option>, + /// `Some` when the input used Form A's explicit column list. + /// `None` for Form B (`insert into T values …`) and for + /// every non-insert command. + user_listed_columns: Option>, +} + fn expected_for_hint_with_ctx( source: &str, schema: Option<&crate::completion::SchemaCache>, ) -> (Vec, Option) { - let (expected, ty, _col) = expected_for_hint_with_full_ctx(source, schema); - (expected, ty) + let snap = expected_for_hint_snapshot(source, schema); + (snap.expected, snap.pending_value_type) } -fn expected_for_hint_with_full_ctx( +fn expected_for_hint_snapshot( source: &str, schema: Option<&crate::completion::SchemaCache>, -) -> ( - Vec, - Option, - Option, -) { +) -> HintWalkSnapshot { use crate::dsl::grammar::REGISTRY; - if source.trim().is_empty() { - let expected = REGISTRY + let entry_words = || -> Vec { + REGISTRY .iter() .map(|c| outcome::Expectation::Word(c.entry.primary)) - .collect(); - return (expected, None, None); + .collect() + }; + + if source.trim().is_empty() { + return HintWalkSnapshot { + expected: entry_words(), + pending_value_type: None, + pending_value_column: None, + current_table_columns: None, + user_listed_columns: None, + }; } let mut ctx = schema.map_or_else(context::WalkContext::new, |s| { context::WalkContext::with_schema(s) }); let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx); let Some(result) = result else { - let expected = REGISTRY - .iter() - .map(|c| outcome::Expectation::Word(c.entry.primary)) - .collect(); - return (expected, None, None); + return HintWalkSnapshot { + expected: entry_words(), + pending_value_type: None, + pending_value_column: None, + current_table_columns: None, + user_listed_columns: None, + }; }; let expected = match result.outcome { outcome::WalkOutcome::Match { .. } | outcome::WalkOutcome::ValidationFailed { .. } => { @@ -393,7 +478,13 @@ fn expected_for_hint_with_full_ctx( outcome::WalkOutcome::Incomplete { expected, .. } | outcome::WalkOutcome::Mismatch { expected, .. } => expected, }; - (expected, ctx.pending_value_type, ctx.pending_value_column) + HintWalkSnapshot { + expected, + pending_value_type: ctx.pending_value_type, + pending_value_column: ctx.pending_value_column, + current_table_columns: ctx.current_table_columns, + user_listed_columns: ctx.user_listed_columns, + } } /// Public walk entry. `bound` is `EndOfInput` for parse; diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index 277b6e6..f3c8f9b 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -150,6 +150,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("hint.value_slot_shortid", &[]), ("hint.value_slot_text", &[]), ("hint.value_slot_for_column", &["column", "detail"]), + ("hint.value_slot_autogen_skipped", &["columns"]), // ---- Parse error rendering ---- ("parse.available_commands", &["commands"]), ("parse.caret", &["padding"]), diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index 6263223..9c7868c 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -312,6 +312,11 @@ hint: # actual column name so the user sees "for `Email`: Type a # quoted string …" instead of the generic type prose. value_slot_for_column: "for `{column}`: {detail}" + # Pedagogical note appended at the first value slot of a + # Form B `insert into T values (…)` when T has auto-generated + # columns the value list skips — points the user at the + # explicit-column form so the skipped column is discoverable. + value_slot_autogen_skipped: "({columns} auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)" parse: # Wrapper around chumsky's structural error message. The diff --git a/src/input_render.rs b/src/input_render.rs index fb01cd8..3c839a1 100644 --- a/src/input_render.rs +++ b/src/input_render.rs @@ -237,7 +237,8 @@ pub fn ambient_hint( // candidate completion (e.g. `n` → `null`) applies. if cursor_partial_is_empty(input, cursor) { let detail = crate::friendly::translate(key, &[]); - let composed = match resolution.and_then(|r| r.column) { + let resolution = resolution.expect("matched on resolution.mode"); + let mut composed = match resolution.column { Some(column) => crate::t!( "hint.value_slot_for_column", column = column, @@ -245,6 +246,25 @@ pub fn ambient_hint( ), None => detail, }; + // Form B pedagogical note: when the first value + // slot of `insert into T values (…)` is reached + // and T has auto-generated columns the value list + // skips, point the user at the explicit-column + // form so the skipped column is discoverable + // (handoff-12 §2.2). + if !resolution.form_b_autogen_skipped.is_empty() { + let columns = resolution + .form_b_autogen_skipped + .iter() + .map(|c| format!("`{c}`")) + .collect::>() + .join(", "); + composed.push(' '); + composed.push_str(&crate::t!( + "hint.value_slot_autogen_skipped", + columns = columns + )); + } return Some(AmbientHint::Prose(composed)); } } diff --git a/tests/typing_surface/insert_form_b.rs b/tests/typing_surface/insert_form_b.rs index f2b1bcd..49ca8e2 100644 --- a/tests/typing_surface/insert_form_b.rs +++ b/tests/typing_surface/insert_form_b.rs @@ -40,15 +40,18 @@ fn form_b_first_value_skips_serial_column() { 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`. + // The value slot itself must be keyed on `Name` — the first + // non-auto column — not on the skipped `id`. assert!( - prose.contains("Name"), - "Form B should advance to first non-auto column `Name`, got prose: {prose:?}", + prose.starts_with("for `Name`"), + "Form B's first value slot should be for `Name`, got prose: {prose:?}", ); + // `id` appears only inside the trailing pedagogical note + // (`id` auto-generated — skipped here …), never as the slot + // the user is being prompted to fill. assert!( - !prose.contains("`id`"), - "Form B should NOT prompt for the auto-gen `id`, got prose: {prose:?}", + !prose.starts_with("for `id`"), + "Form B must not prompt for the auto-gen `id` as a value slot, got prose: {prose:?}", ); crate::snap!("form_b_first_value_serial_pk", a); } @@ -218,6 +221,87 @@ fn form_b_text_pk_with_correct_values_parses() { // position. // ========================================================= +// ========================================================= +// Pedagogical Form-A pointer (handoff-12 §2.2). +// +// At the FIRST value slot of a Form B insert whose table has +// auto-generated columns, the hint must mention the skipped +// column(s) and point at the explicit-column form. +// ========================================================= + +#[test] +fn form_b_first_slot_mentions_skipped_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 slot, got {:?}", a.hint) + }); + // Names the skipped auto-gen column. + assert!( + prose.contains("`id`"), + "first-slot hint should mention skipped `id`, got: {prose:?}", + ); + // Points at the explicit-column escape hatch. + assert!( + prose.contains("auto-generated") && prose.contains("list columns"), + "first-slot hint should explain the Form-A escape, got: {prose:?}", + ); + crate::snap!("form_b_first_slot_skip_note", a); +} + +#[test] +fn form_b_second_slot_omits_skip_note() { + // The note fires once, at the first slot only — not at + // every comma. + 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("auto-generated"), + "second-slot hint must NOT repeat the skip note, got: {prose:?}", + ); + crate::snap!("form_b_second_slot_no_skip_note", a); +} + +#[test] +fn form_b_text_pk_has_no_skip_note() { + // No auto-gen columns → no skip note. + 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("auto-generated"), + "text-PK table has no auto-gen column — no skip note expected, got: {prose:?}", + ); + crate::snap!("form_b_text_pk_no_skip_note", a); +} + +#[test] +fn form_a_first_slot_has_no_skip_note() { + // Form A lists columns explicitly — the user is in control, + // no pedagogical pointer needed. + let schema = schema_serial_pk(); + let a = assess_at_end( + "insert into Customers (Name) values (", + &schema, + ); + let prose = hint_prose(&a).unwrap_or_else(|| { + panic!("expected Prose, got {:?}", a.hint) + }); + assert!( + !prose.contains("auto-generated"), + "Form A must not show the Form-B skip note, got: {prose:?}", + ); + crate::snap!("form_a_no_skip_note", a); +} + #[test] fn form_b_advances_through_every_type_first_to_real() { // Things' second column is `r:real`. After typing the diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_a_first_slot_has_no_skip_note@form_a_no_skip_note.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_a_first_slot_has_no_skip_note@form_a_no_skip_note.snap new file mode 100644 index 0000000..2a382f4 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_a_first_slot_has_no_skip_note@form_a_no_skip_note.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_b.rs +description: "input=\"insert into Customers (Name) values (\" cursor=37" +expression: "& a" +--- +Assessment { + input: "insert into Customers (Name) values (", + cursor: 37, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `Name`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 37, + 37, + ), + 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_slot_mentions_skipped_serial_column@form_b_first_slot_skip_note.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_slot_mentions_skipped_serial_column@form_b_first_slot_skip_note.snap new file mode 100644 index 0000000..ec8533e --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_first_slot_mentions_skipped_serial_column@form_b_first_slot_skip_note.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 (`id` auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)", + ), + ), + 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_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 index 230eb7c..56cad15 100644 --- 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 @@ -9,7 +9,7 @@ Assessment { state: IncompleteAtEof, hint: Some( Prose( - "for `k`: Type an integer (e.g. 42, -7) or null", + "for `k`: Type an integer (e.g. 42, -7) or null (`sid`, `auto` auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)", ), ), completion: Some( 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 index 648de8a..ec8533e 100644 --- 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 @@ -9,7 +9,7 @@ Assessment { state: IncompleteAtEof, hint: Some( Prose( - "for `Name`: Type a quoted string (e.g. 'Alice') or null", + "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( diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_second_slot_omits_skip_note@form_b_second_slot_no_skip_note.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_second_slot_omits_skip_note@form_b_second_slot_no_skip_note.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_second_slot_omits_skip_note@form_b_second_slot_no_skip_note.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_text_pk_has_no_skip_note@form_b_text_pk_no_skip_note.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_b__form_b_text_pk_has_no_skip_note@form_b_text_pk_no_skip_note.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_text_pk_has_no_skip_note@form_b_text_pk_no_skip_note.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_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 index a238ad9..40d5c40 100644 --- 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 @@ -11,7 +11,7 @@ Assessment { ), hint: Some( Prose( - "for `Name`: Type a quoted string (e.g. 'Alice') or null", + "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(