Skip to content

Commit 8e0e965

Browse files
MagicalTuxclaude
andcommitted
feat(alter): scope-aware RENAME COLUMN in multi-table view/trigger bodies (A-rn3-edge)
When a view or trigger body reaches the renamed column through sources where that column name is NOT globally unique, the flat global-uniqueness prover could not tell whether a bare `old` bound to the renamed table or another scope's table, so it bailed — leaving the body byte-unchanged. For a nested subquery whose FROM owns `old` while an outer source shares the name, that was a silent wrong-results bug: e.g. `SELECT a, (SELECT c FROM u) FROM t` after `ALTER TABLE u RENAME COLUMN c TO cc` kept a stale `(SELECT c FROM u)` that then re-bound bare `c` to the outer `t.c`. Add a scope-aware fallback that runs when the flat check declines. It walks the body with a scope stack and resolves each bare `old` innermost-scope-first to its owning table: if every bare `old` binds to the renamed table it rewrites all bare tokens; if none does, only the qualified `renamed.old` refs rewrite; a genuinely mixed body (the same bare name binding to different tables in one statement) still declines untouched. No `Expr::Column` source-span refactor is needed — the same "uniqueness/scope, not spans" insight as the multi-source slice, now generalized across nesting levels. New helpers (exec/mod.rs): `walk_own_scope_columns` (exhaustive own-scope column walk incl. `IN (SELECT)` LHS and inline `OVER(...)` parts), `resolve_bare_owner`, `resolve_exprs_bare_owners`, `collect_bare_old_owners`, `scope_bare_old_decision` (views) and `scope_bare_old_decision_trigger` / `collect_trigger_stmt_bare_owners` (triggers, honouring the UPDATE/DELETE target as the outer scope). Wired into `view_global_unique_quals` / `trigger_global_unique_quals`. Byte-exact vs sqlite3 3.50.4 for views (scalar / EXISTS / IN-SELECT subqueries, joins, N-level nesting, qualified-only rewrites) and trigger bodies (INSERT … SELECT/VALUES-subquery, UPDATE/DELETE target scope). The now-passing cases move from the `bail` invariants into the differential-equality sets; each test keeps a genuinely-mixed case as the remaining residual. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7137f29 commit 8e0e965

5 files changed

Lines changed: 448 additions & 61 deletions

File tree

ROADMAP.md

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -977,21 +977,35 @@ exhausted for bounded (single-fix) work.
977977
`sqlite3Multiply128/160`) — a ~300-line table-heavy port for an obscure
978978
extension flag. Deferred as low-ROI; see [[printf-bang-float-decode]].
979979

980-
- **A-rn3-edge — RENAME COLUMN in genuinely multi-table view/trigger bodies.**
981-
The token rewrite already handles single-source-with-subqueries bodies and
980+
- **A-rn3-edge — RENAME COLUMN in genuinely multi-table view/trigger bodies.
981+
Mostly DONE (scope-aware, no span refactor); only the *mixed* case residual.**
982+
The token rewrite handles single-source-with-subqueries bodies and
982983
nested-subquery / cross-object reaches via the global-uniqueness provers
983984
(`view_global_unique_quals` / `trigger_global_unique_quals`): a bare `old` is
984985
rewritten when that column name is unique across *every* base source at any
985-
nesting level. What remains is the genuinely **ambiguous** case — a bare ref to
986-
a column that *several* in-scope base tables own, where only the occurrence in
987-
the renamed table's scope should change. The rewrite currently bails (leaves the
988-
body unchanged — never corrupts) because the AST has no per-column-ref span.
989-
Two steps:
990-
- **A-rn3-edge-1** *(enabling refactor)* — add a source span (byte range) to
991-
`Expr::Column`. The sibling `schema` field it once shared with the now-landed
992-
3-part qualifier check is already in place, so this is just the span.
993-
- **A-rn3-edge-2** — use the span for scope-aware rename: resolve each bare ref
994-
to its owning table and rewrite only the matching occurrences.
986+
nesting level. The remaining case was a bare ref to a column that *several*
987+
in-scope base tables own — where only the occurrence in the renamed table's
988+
scope should change.
989+
- **Solved without the span refactor** by a *scope-aware* fallback that runs
990+
when the flat global-uniqueness check declines
991+
(`scope_bare_old_decision` / `scope_bare_old_decision_trigger`, with the
992+
shared `walk_own_scope_columns` / `resolve_bare_owner` /
993+
`collect_bare_old_owners` / `resolve_exprs_bare_owners` machinery). It walks
994+
the body with a scope stack and resolves each bare `old` innermost-scope-first
995+
to its owning table: if *every* bare `old` binds to the renamed table it
996+
rewrites all bare tokens; if *none* does, only the qualified `renamed.old`
997+
refs rewrite; the AST need never grow a per-column-ref span. Covers views
998+
(scalar / `EXISTS` / `IN (SELECT)` subqueries, joins, N-level nesting,
999+
correlation) and trigger bodies (`INSERT … SELECT`/`VALUES`-subquery,
1000+
`UPDATE`/`DELETE` with the target as the outer scope, the `WHEN` guard).
1001+
- **Residual: the genuinely *mixed* body** — the *same* bare column name
1002+
binding to *different* tables in one statement (e.g.
1003+
`SELECT a FROM t WHERE a IN (SELECT a FROM u)` renaming `u.a`, where SQLite
1004+
rewrites only the inner `a`). That still needs per-occurrence source spans on
1005+
`Expr::Column`; the scope pass declines it and leaves the object
1006+
byte-identical (never a half-renamed body). Tests: the `bail` invariants in
1007+
`tests/rename_column_view_subquery.rs`, `tests/view_rename_column_subquery.rs`,
1008+
`tests/rename_column_trigger_subquery.rs`.
9951009

9961010
- **A-prepare-correlated — prepare-time validation in correlated subquery bodies. DONE.**
9971011
The eager (prepare-time, row-independent) validators cover the common scopes —
@@ -1445,9 +1459,10 @@ reasonable order:
14451459
then OS file locks, the WAL `-shm` index, and a thread-safe `Connection`).
14461460
4. **D2e-encoder / dbpage-2 / D5 / D6** — ecosystem surfaces; pick the unblocked
14471461
ones (dbpage-2 is oracle-blocked, D2e-encoder needs the fts5 writer source).
1448-
5. **Track A leftovers** — the `Expr::Column` enrichment (source span + schema
1449-
field) that unblocks both **A-rn3-edge** and the 3-part-qualifier check, plus
1450-
the statement-level prepare pass for the lazy-validation gaps.
1462+
5. **Track A leftovers** — an `Expr::Column` source-span enrichment would let the
1463+
**A-rn3-edge** *mixed*-body residual rewrite per-occurrence (the rest of
1464+
A-rn3-edge is now done scope-aware, no span needed); plus the statement-level
1465+
prepare pass for the lazy-validation gaps.
14511466
6. **`EXPLAIN QUERY PLAN` fidelity (Track B) — essentially closed.** The whole B9
14521467
cluster shipped in 2026-07 (B9a incl. the seekable-`IN` render, B9c–B9g, the B9d
14531468
subset). What remains is deferred by design: the cost-model index-choice items
@@ -1459,6 +1474,8 @@ reasonable order:
14591474
`sqlite_stat4` (diverge from / unverifiable against the stat1-only oracle);
14601475
**B1c** RIGHT/FULL inner seeks (correct via materialization); **D7** the C-API
14611476
shim (needs `unsafe`; a sibling crate); **dbpage-2** (oracle-blocked); the
1462-
**A-rn3-edge** ambiguous-ref case (needs per-column-ref spans); and the FTS5
1463-
large-scale encoder sub-cases (need the fts5 writer source). Build-specific oracle
1477+
**A-rn3-edge** *mixed*-body residual only (the same bare name binding to two
1478+
tables in one statement — needs per-column-ref spans; the rest is done
1479+
scope-aware); and the FTS5 large-scale encoder sub-cases (need the fts5 writer
1480+
source). Build-specific oracle
14641481
quirks we intentionally do NOT match are recorded in §6.

0 commit comments

Comments
 (0)