Phase D: insert value list mirrors do_insert's user_cols contract
Bug: hint at \`insert into Customers values (\` for a Customers
table with id:serial PK suggested typing an integer for \`id\`,
but the dispatch path (\`db::do_insert\`) deliberately doesn't
accept user-supplied values for auto-generated columns in
Form B. The grammar prompted for a value the dispatch would
refuse.
The fix aligns Phase D's \`column_value_list\` dynamic sub-grammar
with do_insert's three forms (ADR-0014 + ADR-0018 §3):
- **Form A** \`insert into <T> (col1, col2, …) values (…)\` —
user explicitly lists columns. Slot list mirrors that
selection; serial / shortid columns CAN appear if the user
lists them.
- **Form B** \`insert into <T> values (…)\` — bare values. Slot
list = non-auto-generated columns of the table in
declaration order. Serial / shortid get auto-filled by the
dispatch; the grammar doesn't prompt for them.
- **Form C** \`insert into <T> (v1, v2, …)\` — bare value list.
Not affected by this change (column_value_list isn't on this
path; Form C's literals route through the schemaless
INSERT_PAREN_LIST).
Implementation:
\`WalkContext.user_listed_columns: Option<Vec<String>>\` — when
\`Some\`, signals Form A; \`None\` is Form B. Populated by walking
the first paren's column-list idents.
\`Node::Ident.writes_user_listed_column: bool\` — new field;
\`true\` on the INSERT_PAREN_ITEM's Ident child. When the
walker matches that ident in Form A, it appends the
schema-canonical column name (case-corrected against the
schema) to user_listed_columns.
\`column_value_list\` factory:
- If user_listed_columns is Some → resolve each name from the
schema; one typed slot per listed column.
- Else → filter current_table_columns to non-auto-generated;
one typed slot per remaining column.
- Empty result → fall back to the schemaless value-literal
list (a serial-only table in Form B has nothing for the
user to type).
Tests:
- New \`phase_d_insert_form_b_skips_serial_column\` confirms the
bug: \`insert into Customers values (1, 'Alice')\` against a
Customers with serial id rejects at parse time (Form B
expects 1 value for Name, not 2).
- New \`phase_d_insert_form_a_accepts_serial_when_listed\`
confirms \`insert into Customers (id, Name) values (1, 'Alice')\`
works.
- New \`phase_d_insert_form_a_filters_to_user_listed_columns\`
confirms partial Form A (\`(Name) values ('Alice')\`).
- Updated \`phase_d_insert_with_schema_accepts_typed_values_per_column\`
to match the new Form B contract (2 user-typed values, not 3).
- Updated typed-hint test matrix split into form-B (8 types)
and form-A (serial / shortid).
- New \`typed_hint_form_b_skips_serial_column_to_generic_or_text_neighbor\`
pins the fallback behavior for a serial-only table.
For the user: \`insert into Customers values (\` for a Customers
with \`(id:serial, Name:text, Email:text)\` now hints
\`for \`Name\`: Type a quoted string …\` (skipping id entirely)
and accepts exactly 2 values. To set the serial explicitly,
use Form A: \`insert into Customers (id, Name, Email) values
(1, 'Alice', 'a@b.c')\`.
Tests: 851 passing, 0 failing, 1 ignored. Clippy clean.
This commit is contained in:
@@ -94,6 +94,7 @@ pub fn walk_node(
|
||||
highlight_override: _,
|
||||
writes_table,
|
||||
writes_column,
|
||||
writes_user_listed_column,
|
||||
} => walk_ident(
|
||||
source,
|
||||
pos,
|
||||
@@ -102,6 +103,7 @@ pub fn walk_node(
|
||||
*validator,
|
||||
*writes_table,
|
||||
*writes_column,
|
||||
*writes_user_listed_column,
|
||||
ctx,
|
||||
path,
|
||||
per_byte,
|
||||
@@ -241,6 +243,7 @@ fn walk_ident(
|
||||
validator: Option<crate::dsl::grammar::IdentValidator>,
|
||||
writes_table: bool,
|
||||
writes_column: bool,
|
||||
writes_user_listed_column: bool,
|
||||
ctx: &mut WalkContext,
|
||||
path: &mut MatchedPath,
|
||||
per_byte: &mut Vec<ByteClass>,
|
||||
@@ -289,6 +292,28 @@ fn walk_ident(
|
||||
.map(|c| c.name.clone())
|
||||
.or_else(|| Some(text.clone()));
|
||||
}
|
||||
if writes_user_listed_column
|
||||
&& matches!(src, crate::dsl::grammar::IdentSource::Columns)
|
||||
{
|
||||
// Form A: `insert into <T> (col1, col2, …)`. Append the
|
||||
// matched column name to user_listed_columns so the
|
||||
// inner `values (…)` slot list mirrors the user's
|
||||
// explicit selection. Schema-canonical name wins over
|
||||
// user's spelling so downstream lookups (typed slot
|
||||
// dispatch, hint rendering) are consistent.
|
||||
let canonical = ctx
|
||||
.current_table_columns
|
||||
.as_ref()
|
||||
.and_then(|cols| {
|
||||
cols.iter()
|
||||
.find(|c| c.name.eq_ignore_ascii_case(&text))
|
||||
.map(|c| c.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| text.clone());
|
||||
ctx.user_listed_columns
|
||||
.get_or_insert_with(Vec::new)
|
||||
.push(canonical);
|
||||
}
|
||||
path.push(MatchedItem {
|
||||
kind: MatchedKind::Ident { role },
|
||||
text,
|
||||
|
||||
Reference in New Issue
Block a user