grammar+db: 3h — UPSERT ON CONFLICT DO NOTHING / DO UPDATE (ADR-0033 §9)
on_conflict_clause on SQL_INSERT_SHAPE: optional (col,…) conflict target (distinct conflict_target_column role so it never enters listed_columns), DO NOTHING / DO UPDATE SET … [WHERE …]. `do` is factored out of the action Choice so nothing/update disambiguate without tripping the walk_seq/walk_choice shared-prefix trap (ADR-0033 Amendment 1). Worker runs the UPSERT verbatim (SQLite native); no new execution path. build_sql_insert: row_source now stops before the FIRST trailing clause — ON CONFLICT (3h) or RETURNING (3g) — and do_sql_insert's shortid auto-fill rewrite re-appends the whole trailing tail, so an auto-filled INSERT keeps its ON CONFLICT / RETURNING. excluded pseudo-table (§9): resolves to the target's columns inside the DO UPDATE action and completes at `excluded.|`, but stays flagged as unknown_qualifier in VALUES / RETURNING / non-upsert statements. Diagnostic pass scopes it by the DO UPDATE byte-range (update token → RETURNING/end); completion resolves it against the INSERT target's current_table_columns. NOTE: scoping uses byte-range rather than the plan's prescribed from_scope TableBinding push — same behaviour, no walker scope-frame change. Tests (+13): grammar accept/reject; DO NOTHING / DO UPDATE-excluded / no-target execution + persistence; auto-fill × ON CONFLICT with a REAL unique conflict (proves the clause survives the rewrite, not a no-op); excluded resolves in DO UPDATE SET + WHERE, flagged in VALUES (incl. same statement), unknown column under excluded; excluded.| completion; conflict-target not in listed_columns. 1576 pass / 0 fail / 1 ignored. Clippy clean. Dev sql_insert entry word still removed in 3j. Known follow-up (tracked for 3i): UPSERT DO UPDATE bare column refs (SET LHS / WHERE) are not schema-validated, unlike regular UPDATE — the INSERT target isn't a diagnostic binding. Fits 3i's cross-cut SET/WHERE validation scope.
This commit is contained in:
+38
-6
@@ -374,12 +374,26 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
let qualified_columns: Option<Vec<String>> = prefix_qualifier
|
||||
.as_ref()
|
||||
.map(|q| {
|
||||
resolve_qualifier_columns_in(
|
||||
q,
|
||||
resolution_from_scope,
|
||||
resolution_cte_bindings,
|
||||
cache,
|
||||
)
|
||||
// ADR-0033 §9: `excluded.|` inside an `INSERT … ON
|
||||
// CONFLICT … DO UPDATE` completes to the target table's
|
||||
// columns — `excluded` mirrors the would-be-inserted row.
|
||||
// The target's columns are the INSERT's
|
||||
// `current_table_columns` (set by the target-table slot).
|
||||
// The diagnostic pass enforces the strict DO-UPDATE
|
||||
// byte-range; completion is the softer surface and offers
|
||||
// the columns whenever the INSERT target is in hand.
|
||||
if q.eq_ignore_ascii_case("excluded")
|
||||
&& let Some(cols) = current_table_columns
|
||||
{
|
||||
cols.iter().map(|c| c.name.clone()).collect()
|
||||
} else {
|
||||
resolve_qualifier_columns_in(
|
||||
q,
|
||||
resolution_from_scope,
|
||||
resolution_cte_bindings,
|
||||
cache,
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let expected = if probe.expected.is_empty() {
|
||||
@@ -2119,6 +2133,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excluded_prefix_completes_to_target_columns() {
|
||||
// ADR-0033 §9: `excluded.|` inside a DO UPDATE action
|
||||
// completes to the INSERT target table's columns.
|
||||
let cache = two_table_schema();
|
||||
let input = "sqlinsert into a (id, name) values (1, 'x') \
|
||||
on conflict (id) do update set name = excluded.";
|
||||
let cs = cands_with(input, input.len(), &cache);
|
||||
assert!(
|
||||
cs.contains(&"id".to_string()) && cs.contains(&"name".to_string()),
|
||||
"excluded.| should offer the target table's columns; got {cs:?}",
|
||||
);
|
||||
assert!(
|
||||
!cs.contains(&"total".to_string()),
|
||||
"a column from an unrelated table must not appear; got {cs:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_prefix_with_partial_filters_prefix() {
|
||||
// `select a.na` — only `name` (starts with `na`).
|
||||
|
||||
Reference in New Issue
Block a user