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:
+111
-8
@@ -1394,20 +1394,91 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn phase_d_insert_with_schema_accepts_typed_values_per_column() {
|
||||
// Form B: the grammar dispatches one slot per
|
||||
// non-auto-generated column — the serial `id` is
|
||||
// skipped because the dispatch path (`db::do_insert`)
|
||||
// auto-fills it (ADR-0018 §3).
|
||||
let schema = schema_with(
|
||||
"Customers",
|
||||
&[("id", Type::Serial), ("Name", Type::Text), ("Active", Type::Bool)],
|
||||
);
|
||||
// 3 columns: int, text, bool. Each value matches its slot.
|
||||
// 2 user-typed values: Name (text), Active (bool).
|
||||
let cmd = parse_command_with_schema(
|
||||
"insert into Customers values (1, 'Alice', true)",
|
||||
"insert into Customers values ('Alice', true)",
|
||||
&schema,
|
||||
)
|
||||
.expect("parse");
|
||||
match cmd {
|
||||
Command::Insert { table, values, .. } => {
|
||||
assert_eq!(table, "Customers");
|
||||
assert_eq!(values.len(), 3);
|
||||
assert_eq!(values.len(), 2);
|
||||
}
|
||||
other => panic!("expected Insert, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_d_insert_form_b_skips_serial_column() {
|
||||
// Form B: `insert into <T> values (…)` excludes
|
||||
// auto-generated columns from the value list. Supplying
|
||||
// a value for the serial column is a count mismatch.
|
||||
let schema = schema_with(
|
||||
"Customers",
|
||||
&[("id", Type::Serial), ("Name", Type::Text)],
|
||||
);
|
||||
// Two values where Form B expects one (Name only):
|
||||
let err = parse_command_with_schema(
|
||||
"insert into Customers values (1, 'Alice')",
|
||||
&schema,
|
||||
)
|
||||
.expect_err("Form B should reject user-supplied serial");
|
||||
match err {
|
||||
crate::dsl::ParseError::Invalid { .. } => {}
|
||||
other => panic!("expected Invalid, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_d_insert_form_a_accepts_serial_when_listed() {
|
||||
// Form A: user explicitly lists `id`. The dispatch path
|
||||
// accepts user-supplied serial values when they're in
|
||||
// the explicit column list; the grammar mirrors that.
|
||||
let schema = schema_with(
|
||||
"Customers",
|
||||
&[("id", Type::Serial), ("Name", Type::Text)],
|
||||
);
|
||||
let cmd = parse_command_with_schema(
|
||||
"insert into Customers (id, Name) values (1, 'Alice')",
|
||||
&schema,
|
||||
)
|
||||
.expect("parse");
|
||||
match cmd {
|
||||
Command::Insert { columns, values, .. } => {
|
||||
assert_eq!(columns.as_deref(), Some(&["id".to_string(), "Name".to_string()][..]));
|
||||
assert_eq!(values.len(), 2);
|
||||
}
|
||||
other => panic!("expected Insert, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_d_insert_form_a_filters_to_user_listed_columns() {
|
||||
// Form A: listing only Name should accept exactly one
|
||||
// value (for Name), even though the table has more
|
||||
// columns.
|
||||
let schema = schema_with(
|
||||
"Customers",
|
||||
&[("id", Type::Serial), ("Name", Type::Text), ("Active", Type::Bool)],
|
||||
);
|
||||
let cmd = parse_command_with_schema(
|
||||
"insert into Customers (Name) values ('Alice')",
|
||||
&schema,
|
||||
)
|
||||
.expect("parse");
|
||||
match cmd {
|
||||
Command::Insert { columns, values, .. } => {
|
||||
assert_eq!(columns.as_deref(), Some(&["Name".to_string()][..]));
|
||||
assert_eq!(values.len(), 1);
|
||||
}
|
||||
other => panic!("expected Insert, got {other:?}"),
|
||||
}
|
||||
@@ -1654,9 +1725,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_hint_for_each_type_routes_to_correct_catalog_key() {
|
||||
// Confirm each Type maps to its expected catalog key
|
||||
// via insert at a single-column table.
|
||||
fn typed_hint_for_each_user_settable_type_routes_via_form_b() {
|
||||
// Form B (`insert into T values (…)`) excludes auto-
|
||||
// generated columns from the value list — so only the
|
||||
// user-settable types appear at this position.
|
||||
for (ty, key) in [
|
||||
(Type::Int, "hint.value_slot_int"),
|
||||
(Type::Real, "hint.value_slot_real"),
|
||||
@@ -1666,8 +1738,6 @@ mod tests {
|
||||
(Type::Date, "hint.value_slot_date"),
|
||||
(Type::DateTime, "hint.value_slot_datetime"),
|
||||
(Type::Blob, "hint.value_slot_blob"),
|
||||
(Type::Serial, "hint.value_slot_serial"),
|
||||
(Type::ShortId, "hint.value_slot_shortid"),
|
||||
] {
|
||||
let schema = schema_with("T", &[("c", ty)]);
|
||||
let mode = hint_mode_at_input_with_schema("insert into T values (", &schema);
|
||||
@@ -1678,6 +1748,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_hint_for_auto_generated_types_routes_via_form_a() {
|
||||
// Serial / shortid columns can be set by the user only
|
||||
// in Form A (`insert into T (col) values (…)`) — Form B
|
||||
// skips them because the dispatch path auto-fills.
|
||||
for (ty, key) in [
|
||||
(Type::Serial, "hint.value_slot_serial"),
|
||||
(Type::ShortId, "hint.value_slot_shortid"),
|
||||
] {
|
||||
let schema = schema_with("T", &[("c", ty)]);
|
||||
let mode =
|
||||
hint_mode_at_input_with_schema("insert into T (c) values (", &schema);
|
||||
assert!(
|
||||
matches!(mode, Some(HintMode::ProseOnly(k)) if k == key),
|
||||
"expected ProseOnly({key}) for type {ty:?}, got {mode:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_hint_form_b_skips_serial_column_to_generic_or_text_neighbor() {
|
||||
// A serial-only table in Form B has nothing for the user
|
||||
// to type — column_value_list returns the schemaless
|
||||
// fallback, so the hint at the first value position is
|
||||
// the generic value-literal prose.
|
||||
let schema = schema_with("T", &[("id", Type::Serial)]);
|
||||
let mode = hint_mode_at_input_with_schema("insert into T values (", &schema);
|
||||
assert!(
|
||||
matches!(mode, Some(HintMode::ProseOnly("hint.value_literal_slot"))),
|
||||
"got {mode:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_d_update_multi_assignment_uses_per_column_types() {
|
||||
let schema = schema_with(
|
||||
|
||||
Reference in New Issue
Block a user