fix: stop DML binder paths from silently losing writes and reads#72
Open
takaebato wants to merge 6 commits into
Open
fix: stop DML binder paths from silently losing writes and reads#72takaebato wants to merge 6 commits into
takaebato wants to merge 6 commits into
Conversation
Six related binder bugs, all making UPDATE / INSERT / SELECT INTO surfaces silently incomplete: - A qualified SET target whose qualifier named no writable relation dropped the whole assignment - its RHS reads, its lineage, and (for a sole assignment) the entire UPDATE from the write surfaces. It now reads as a PostgreSQL composite subfield on a single sink (SET address.city = ... writes t.address, root-prefixed spelling included) and surfaces unattributed (table: None, Unresolved) among several writable relations or for unrepresentable paths. The tuple form inherits the fix, restoring its reads/lineage symmetry. - T-SQL UPDATE <alias> ... FROM t AS <alias> fabricated a phantom write target named after the alias and made the alias's own references ambiguous. The FROM relations now bind first (mirroring DELETE's USING order) and the target resolves through them. - SELECT INTO wrapped its CreateTableAs around the statement's WITH, burying a data-modifying CTE's INSERT inside the root's input where the write-root collection never descends; the CTE's write vanished from every surface. The WITH now stays outermost. - PG's INSERT INTO t AS c upsert alias was ignored, leaving the conflict action's c.n references unresolved; it now rides the conflict and RETURNING scopes. - The UPDATE FROM / DELETE USING loops kept joined relations but dropped their USING / NATURAL merge columns, so an unqualified reference to one stayed Ambiguous instead of fanning in as in a SELECT. - MySQL UPDATE ... ORDER BY keys were never bound (DELETE's were). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
✅ PR title follows the Conventional Commits spec. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #72 +/- ##
==========================================
+ Coverage 95.19% 95.21% +0.01%
==========================================
Files 27 28 +1
Lines 6179 6307 +128
Branches 6179 6307 +128
==========================================
+ Hits 5882 6005 +123
- Misses 206 209 +3
- Partials 91 93 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The struct reading of an unmatched SET qualifier is exact for PostgreSQL-family dialects (their SET grammar forbids relation qualifiers, so the leading segment is always a column) but wrong for MySQL / MSSQL / SQLite / ClickHouse, where no struct columns exist and such a qualifier can only be a mistyped table path - the previous structural heuristic fabricated a column write there. Introduces DialectCapabilities, resolution-rule switches derived once from the parsing dialect and threaded into the binder alongside the identifier style (the first dialect-dependent resolution rule; new ones become a one-line switch). Table-qualifier-only dialects now surface the unmatched target unattributed (table: None, Unresolved); struct-capable dialects - and GenericDialect, where struct-field SET syntax almost always carries PostgreSQL-family intent - keep the struct reading. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ilities struct Replace the precomputed DialectCapabilities bool-field struct with the dialect itself on the Binder: extraction threads `&dyn Dialect` down to build_with_diagnostics, and each dialect-dependent resolution rule is a question method in the new binder/dialect.rs concern file (struct_set_targets() carries the engine-verified rationale). Bind code never downcasts the dialect ad hoc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Match the supports_* naming of sqlparser's own Dialect capability methods, which this file extends in spirit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The source-scope merge kept only the relations, dropping a parenthesized join's USING / NATURAL merge columns — the same bug just fixed in the UPDATE ... FROM and DELETE ... USING loops, one site over. An unqualified reference to the merge column now fans in like the same join in a SELECT. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two corners of the unmatched-qualifier fallback, both verified against PostgreSQL 18. Root-prefix detection compared the leading segment against the target's name textually, so under an aliased target (UPDATE t AS x) the shadowed name neither stripped nor read as a struct column and the sole assignment fell unattributed, erasing the UPDATE from the table-level write surface; the check is now scope addressability (the alias when aliased, the bare name otherwise), and a shadowed leading segment is a struct column, exactly as PostgreSQL reads it. And the struct reading was capped at two segments, leaving a nested path (SET address.city.zip) unattributed; PostgreSQL puts no bound on the path depth, so neither do we — the leading column is written whatever the depth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Six related binder bugs, all making UPDATE / INSERT / SELECT INTO surfaces silently incomplete, plus the first dialect-semantics mechanism.
UPDATE t SET address.city = old_city || '-x'lost the RHS reads, the lineage, and — as the sole assignment — the entire UPDATE fromwritesand the CRUD buckets, with no diagnostic. The dotted path now resolves by dialect: in struct-capable dialects (PostgreSQL family — whose SET grammar forbids relation qualifiers outright, so the leading segment is always a column, verified on PostgreSQL 18 — plus BigQuery / Hive / DuckDB / Generic) it reads as a struct subfield path on the single sink, at any depth (SET address.cityandSET address.city.zipboth writet.address; a leading segment that addresses the root in scope — the alias when aliased, the bare name otherwise — is a stripped prefix, and a shadowed name is a column:UPDATE t AS x SET t.awrites columnt, exactly as PostgreSQL 18 reads it); in table-qualifier-only dialects (MySQL / MSSQL / SQLite / ClickHouse, where such a qualifier can only be a mistyped table path), among several writable relations, and for unrepresentable paths it surfaces unattributed (table: None,Unresolved). The assignment is never discarded. The tuple form inherits the fix, which also restores its reads/lineage symmetry.binder/dialect.rsconcern file (supports_struct_set_targets()is the first). Bind code never downcasts the dialect ad hoc; a future dialect-dependent rule is one more method there.UPDATE <alias> ... FROM t AS <alias>fabricated a phantom write target. The bare target name bound as a tablea, so writes pinned a nonexistent relation whilea.idbecame ambiguous between the phantom and the real one. When the join-less bare target names a FROM alias, the FROM relations now bind first (mirroring DELETE's USING order) and the target resolves through them:writes: t.x,a.idreadst.id.SELECT INTOburied a data-modifying CTE's write.WITH c AS (INSERT INTO t1 ... RETURNING a) SELECT a INTO t2 FROM cwrapped theCreateTableAsaround the statement'sWITH, hiding the CTE's INSERT inside the root's input where the write-root collection never descends —t1's write vanished from writes, CRUD, and lineage. TheWITHnow stays outermost, as PostgreSQL's top-level-only rule for data-modifying CTEs implies.INSERT INTO counters AS cupsert alias was ignored, leaving the conflict action'sc.nunresolved (no sink read, no lineage source). The alias now rides the conflict and RETURNING scopes.UPDATE ... FROM/DELETE ... USINGloops dropped merge columns. The joined relations were kept but theirUSING/NATURALmerge columns were not, so an unqualified reference to one stayedAmbiguousinstead of fanning in to the joined sides as the same join does in a SELECT A parenthesized-join MERGE source had the same drop, one site over — fixed alike.UPDATE ... ORDER BYkeys were never bound (DELETE's were), losing their reads.Tests
🤖 Generated with Claude Code