Repository navigation
Evaluate UNION / EXCEPT / INTERSECT chains left to right to close #202 - #2582
Open
joshuablac wants to merge 1 commit into
Open
joshuablac wants to merge 1 commit into
joshuablac wants to merge 1 commit into
Conversation
…SQL#202 The parser nests a chain of set operations to the right, and each SELECT applied its operator to the full result of the rest of the chain, so A EXCEPT B EXCEPT C was computed as A EXCEPT (B EXCEPT C). In addition, (A EXCEPT B) UNION C lost the inner operation, because the outer clause was merged into the parenthesized SELECT and replaced or hid its own one. - Compile: the chain is flattened into a list of set operations applied left to right, with INTERSECT binding tighter than UNION and EXCEPT, as in SQL. A parenthesized operand stays a group. - Grammar: a parenthesized right operand is marked (parenthesizedOperand), and an outer set operation on a parenthesized SELECT that already has one is kept apart (outerSetOps) instead of overwriting it. - Execution: the four existing UNION ALL / UNION / EXCEPT / INTERSECT branches are moved unchanged into applySetOp(), called once per operation. A query with a single set operation runs the same code. test339 (9, 10, 12), test7 (I, J, K) and test2414 (P) asserted the old right-to-left results; they now expect the SQL results. Co-Authored-By: Claude Opus 5.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.
Closes #202
Problem
Chains of
UNION/UNION ALL/EXCEPT/INTERSECTare evaluated from right to left. In addition, a parenthesized chain on the left loses an operation. Withe1 = {1,2,3,4},e2 = {1,5},e3 = {3,5},e4 = {2,5}:develope1 EXCEPT e2 EXCEPT e3(the issue){2,3,4}{2,4}e1 UNION e2 EXCEPT e3{1,2,3,4}{1,2,4}e1 EXCEPT e2 UNION e3{2,4}{2,3,4,5}e1 INTERSECT e2 UNION e3{1,3}{1,3,5}e2 UNION ALL e3 EXCEPT e4{1,3,5}{1,3}e1 EXCEPT e2 EXCEPT e3 EXCEPT e4{2,3,4}{4}e1 UNION e2 INTERSECT e3 EXCEPT e4{1,2,3,4}{1,3,4}(e1 EXCEPT e2) EXCEPT e3{1,2,4}(theEXCEPT e2is lost){2,4}(e1 EXCEPT e2) UNION e3{1,2,3,4,5}(theEXCEPTis lost){2,3,4,5}Already correct on
develop, and unchanged:e1 UNION e2 INTERSECT e3,e1 EXCEPT e2 INTERSECT e3ande1 INTERSECT e2 EXCEPT e3e1 EXCEPT (e2 EXCEPT e3)UNIONor onlyUNION ALLSQL evaluates
UNIONandEXCEPTfrom left to right, withINTERSECTbinding tighter. PostgreSQL, SQL Server and MySQL all follow this. I cross-checked every case above in SQLite. It agrees on all of them except whereINTERSECTfollows aUNION/EXCEPT: SQLite deliberately gives all compound operators the same precedence. For those cases this PR keeps the standard precedence, whichtest339(cases 11 and 13) already asserts.Cause
A op1 B op2 C→A.op1 = B,B.op2 = C). Each SELECT applied its operator to the complete result of the rest of the chain, soA op1 (B op2 C).(A op1 B) op2 C, theParenthesizedSelect UnionClause/LPAR Select RPAR UnionClauseactions merged the outer clause into the inner SELECT withyy.extend. That replaced the inner operation when both used the same operator, and otherwise left two ofunion/except/… set, of which only one was used.Fix
Grammar (
src/alasqlparser.jison, parser regenerated with the pinned jison 0.4.18; the diff toalasqlparser.jsis only those actions):parenthesizedOperand, so its own set operations stay a group.yy.extendSetOp(insrc/28yy.js): when a parenthesized SELECT already has a set operation, the outer clause is kept inouterSetOpsinstead of overwriting it.Compile (
src/40select.js):compileSetOpsflattens the chain into a list of{kind, corresponding, fn}, which is applied to the result from left to right:INTERSECTthat follows aUNION/EXCEPTjoins that operation's right operand.CORRESPONDINGflag.The existing behavior of using the right operand's
ORDER BYwhen the query has none is kept unchanged.Execution (
src/38query.js): the four existing UNION ALL / UNION / EXCEPT / INTERSECT branches are moved unchanged intoapplySetOp(query, kind, fn, corresponding), which runs once per operation. A query with a single set operation therefore runs the same code as before. Two related details:ORDER BYafter such a chain is always for the whole result.queryfn3now declares the loop variables that used to be hoisted from the moved code.To check that single set operations are unaffected, I ran 44 queries against
developand against this branch: each operator with and withoutCORRESPONDING,SELECT *,FROM ?,ORDER BY/LIMIT/OFFSETafter the operation, parenthesized operands with their ownORDER BY/LIMIT(the #2417 patterns), subqueries inFROMand inIN (...). All 44 results are identical.Tests
Added
test/test202-B.js(test202.jsis taken):SELECT * … EXCEPT … EXCEPTINTERSECTprecedenceORDER BYafter a chainCORRESPONDINGAll 6 fail on
developand pass with this change.Updated tests that asserted the right-to-left results:
test339, case 9:a UNION b EXCEPT c→[3, 4]test339, case 10:a EXCEPT b UNION c→[1, 2, 3, 5]test339, case 12:a INTERSECT b UNION c→[1, 2, 5]test7, cases I, J and K. Their comments already noted that the expected values were "the current behavior" and not the correct result. I and K now get the result their comments describe. J followsINTERSECTprecedence:10 UNION ALL (20 INTERSECT 20) UNION ALL 30.test2414, case P:((… a = 1) UNION (… a = 2 ORDER BY a)) UNION … a > 2 ORDER BY a DESCnow includes thea = 2row that was dropped before:[4, 3, 2, 1].Full suite (
bun test, same command asyarn test-only): 2255 pass, 213 skip. The only failures are the 2 CLI tests intest2149.js, which fail the same way on unmodifieddevelopin my local setup (a space in my checkout path, and the version-injection step ofbuild.shnot running).Notes
Select.toString()is unchanged, so it still prints chains without parentheses. Nothing in the code re-parses that output.A UNION (B) EXCEPT C(a parenthesized operand followed by another operation) still fails to parse, as ondevelop.alasqlparser.js. Several open PRs also change the grammar, so whichever merges second needsyarn jisonagain.This fix was written with AI assistance (Claude Code) and verified locally as described above.
🤖 Generated with Claude Code