fix(sql): AND binds tighter than OR in every clause (ANDOR) - #407
Merged
Merged
Conversation
Story ANDOR: a condition that mixes AND and OR is evaluated with SQL precedence. - The condition reducer applies SQL precedence (AND before OR); a written group, NESTED/CHILD/PARENT bodies included, stays one operand. - WHERE (and DELETE / UPDATE, which share it): a child bool shares its parent only on the same operator, so an OR branch is no longer made optional beside a filter. - HAVING: the bucket_selector script parenthesises an OR under an AND; the selector no longer strips "1 == 1" from the whole script (it ate params.max_c1 == 1); a one-key HAVING mix is accepted exactly when the include/exclude lists give SQL's answer. - NOT at the head of an AND run folds into its condition (ISNULL <-> ISNOTNULL included); on MATCH it is refused by name with a rewrite. A MATCH-bearing OR group under an AND stays a required clause. - The rendered SQL keeps the parentheses precedence needs. - Docs: precedence sections, NOT examples and 31 examples corrected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fupelaqu
marked this pull request as ready for review
October 1, 2026 03:19
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
A condition that mixes
ANDandORwithout parentheses is now evaluated with SQL precedence (ANDbinds tighter thanOR) in every clause. Before,A OR B AND Cwas read as(A OR B) AND C, and the Elasticsearchboolquery made theORbranch optional beside a filter: every unparenthesised mix inWHEREreturned wrong rows, on every major.NESTED(…)/CHILD(…)/PARENT(…)bodies, stays one operand.WHERE(andDELETE/UPDATE, which share it): a childboolshares its parent only when it has the same operator, so anORbranch is never optional beside afilter. AMATCH-bearingORgroup written under anANDstays one required clause.HAVING:bucket_selectorscript parenthesises anORunder anAND;1 == 1from the whole script (it ateparams.max_c1 == 1, soHAVING MAX(c1) = 1failed);HAVING k = 'v1' OR k = 'v2' AND NOT k = 'v3'is accepted exactly when the include/exclude lists give SQL's answer.NOTat the head of anANDrun folds into its condition (ISNULL↔ISNOTNULLincluded). OnMATCHit is refused by name, with a rewrite that parses.NOTsections, plus 31 examples that did not parse, corrected.Verification
main: 0 violations.main.sql/test1,749 · bridges 263 / 256 ·core/test1,147 ·macrosTests/test30.NOT ISNULLagainstmain(same rows wheremainwas right),HAVING MAX(c1) = 1, and the one-keyHAVINGrows. The other majors run in CI.Release notes
AND/ORmixes follow SQL precedence inWHERE,CASE,HAVING,DELETEandUPDATE.DELETEandUPDATEused to change the wrong documents; that damage cannot be recovered automatically.CREATE OR REPLACEskips an unchanged render);INSERT … SELECT, CTAS andCOPYresults need re-running.NOT ISNULL(x)/NOT ISNOTNULL(x)afterAND/ORnow meanISNOTNULL(x)/ISNULL(x).a OR NOT ISNULL(x)used to run asAND NOT, and aCASE WHENwithNOT ISNULLnegated the whole condition.a AND NOT ISNULL(x)returns the same rows with a different query.NOTonMATCHis refused by name, with a rewrite.x AND MATCH(a, b) AGAINST(…)is now required (it was optional). A relevance-ordered result can change, since aMATCHmay move between filter and scoring clauses.HAVINGover a metric whose script contained1 == 1(MAX(c1) = 1,= 10,IN (1, 2)) now works. A view built over such aHAVINGmay need re-creating.HAVINGmixes are accepted exactly when Elasticsearch's lists give SQL's answer; the others are refused.NOT c = 1instead ofc NOT = 1, and parentheses on rebuilt trees. Chains of four or more conditions with one operator regroup: same query, differentCASEscript text, so ingest pipelines are re-created on the nextALTER.selectorScript); no downstream rebuild.Known limitations (not changed here)
NOTbeforeLIKE/IN/BETWEEN/ISparses only as the second item of a pair (a AND b AND NOT c LIKE 'x%'is rejected). Documented.HAVING ISNULL(MAX(x))never holds for a group withoutx, and aMATCHover an aggregate inHAVINGis dropped from the script: pre-existing, to be fixed separately.🤖 Generated with Claude Code