Skip to content

fix(sqlbuilder): scope set-op branch ORDER BY/LIMIT/OFFSET with parentheses - #46

Merged
KARTIKrocks merged 1 commit into
mainfrom
fix/sqlbuilder-set-op-scoping
Aug 1, 2026
Merged

fix(sqlbuilder): scope set-op branch ORDER BY/LIMIT/OFFSET with parentheses#46
KARTIKrocks merged 1 commit into
mainfrom
fix/sqlbuilder-set-op-scoping

Conversation

@KARTIKrocks

@KARTIKrocks KARTIKrocks commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Set operations (Union, UnionAll, Intersect, Except) concatenated each branch with no parentheses and let the left (receiver) branch's ORDER BY/LIMIT/OFFSET leak onto the enclosing statement. A union whose branches each carried their own ordering/limiting produced invalid SQL: two ORDER BYs and two LIMITs, with the right branch's clauses binding to the whole union instead of that branch.

Now any branch carrying ORDER BY/LIMIT/OFFSET is wrapped in parentheses, and the left branch's clauses are snapshotted to that branch when the set op is added. Ordering/limiting applied after the set op still applies to the whole statement. Plain branches, and same-operator nested branches with no ordering, are left unparenthesized (the latter avoids emitting a parenthesized compound select, which SQLite rejects).

Motivation

Fixes #

Changes

Checklist

  • fmt, vet, lint, test, build passes (make all)
  • New code has tests where appropriate
  • Breaking changes are documented

Summary by CodeRabbit

  • Bug Fixes
    • Improved set-operation queries so branch-level sorting, limits, and offsets apply to the correct query branch.
    • Added parentheses where needed to preserve expected query behavior, including nested and chained set operations.
    • Preserved set-operation behavior when queries are cloned or combined.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@KARTIKrocks, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d728e0b5-2de1-45d2-b8b0-fb4ace2fe069

📥 Commits

Reviewing files that changed from the base of the PR and between 0551ef4 and dbede9c.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !CHANGELOG.md
📒 Files selected for processing (3)
  • sqlbuilder/select.go
  • sqlbuilder/select_test.go
  • sqlbuilder/set_op.go
📝 Walkthrough

Walkthrough

Set operations now scope ORDER BY, LIMIT, and OFFSET to individual branches when required. Rendering adds parentheses and preserves outer clauses. Tests cover nested operations, argument rebasing, chaining, flattening, and cloning.

Changes

Set-operation clause scoping

Layer / File(s) Summary
Capture branch scope
sqlbuilder/select.go, sqlbuilder/set_op.go
Set operations capture existing ordering and pagination. Branches record when parentheses are required.
Render scoped operations
sqlbuilder/select.go
Build renders captured clauses before set operations. Shared helpers render ordering and pagination for branches and outer queries. Clone copies captured scope data.
Validate scoping behavior
sqlbuilder/select_test.go
Tests cover clause scope, parentheses, placeholder rebasing, nested and chained operations, flattening, and clone preservation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Poem

I’m a rabbit with queries to spare,
Scoping clauses with careful hare.
Branches get bounds,
Parentheses surround,
And cloned SQL hops clean through the air.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the problem and solution, but the Changes section is empty and the required checklist remains incomplete. List the key changes, provide a related issue or explain why none applies, and complete the checklist with verified results.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the set-operation scoping fix for branch ORDER BY, LIMIT, and OFFSET clauses.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sqlbuilder-set-op-scoping

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.86364% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
sqlbuilder/select.go 98.70% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a long-standing bug where set-operation branches (UNION, UNION ALL, INTERSECT, EXCEPT) emitted no parentheses, causing the left branch's ORDER BY / LIMIT / OFFSET to leak onto the enclosing statement and producing invalid SQL with duplicate clauses. It also adds SQL operator-precedence grouping so mixed-kind flat chains like a.Union(b).Intersect(c) emit (a UNION b) INTERSECT c rather than a UNION (b INTERSECT c).

  • Left-branch scoping (captureSetOpLeftScope + setOpLeft): on the first set op, any ORDER BY / LIMIT / OFFSET already on the receiver is snapshotted into a branchScope struct and cleared from the live builder; Build() then wraps just that branch in parentheses so those clauses bind to it, not the whole statement.
  • Right-branch scoping (setOpBranchNeedsParens): when the right-hand branch carries its own ORDER BY / LIMIT / OFFSET, or is itself a compound (has its own setOps), it is wrapped in parentheses at append time.
  • Precedence grouping (setOpWrapPoints): a scan over the set-op chain marks each UNION/EXCEPT → INTERSECT transition; leading parentheses are opened before the base branch and closed at those transition points, preserving fluent left-to-right evaluation order against SQL's INTERSECT-first binding rule.

Confidence Score: 4/5

Functionally safe to merge; the two new paren-injection mechanisms are correct in isolation and their interleaved interaction is also correct, but that combined path goes untested.

The core logic is sound after tracing all key paths: paren counts balance when both wrapOpen > 0 and leftParen = true, setOpBranchNeedsParens correctly handles the captured-scope case via len(b.setOps) > 0, and the precedence-grouping algorithm produces the right associativity for all chain shapes examined. The only gap is that no test exercises a left branch with ORDER BY/LIMIT combined with a mixed-kind operator chain that triggers wrapClose.

Files Needing Attention: The interaction between setOpLeft in select.go and the wrapOpen/wrapClose precedence machinery is the untested path worth a second look before merging.

Important Files Changed

Filename Overview
sqlbuilder/set_op.go Adds branchScope struct, setOpBranchNeedsParens, and setOpPrec helpers. Logic is correct: setOpBranchNeedsParens covers the captured-scope case implicitly via len(b.setOps) > 0.
sqlbuilder/select.go Core change: addSetOp, captureSetOpLeftScope, setOpWrapPoints, and updated Build/writeSetOps. The leftParen + wrapOpen paren-balance accounting is correct but the interaction has no test coverage.
sqlbuilder/select_test.go 13 new tests cover setOpLeft and wrapClose independently and combined nested/chain scenarios, but missing a test for setOpLeft + wrapClose together (left branch has ORDER BY + mixed-kind chain).
CHANGELOG.md Adds 0.27.0 entry describing both fixes accurately, including the SQLite limitation note.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["addSetOp(kind, other)"] --> B["captureSetOpLeftScope()"]
    B --> C{First set op AND\nhas ORDER BY/LIMIT/OFFSET?}
    C -- Yes --> D["Snapshot into setOpLeft\nClear live orderBy/limit/offset"]
    C -- No --> E["No-op"]
    D --> F["buildSelectPostgres(other)"]
    E --> F
    F --> G["setOpBranchNeedsParens(other)"]
    G --> H{other has ORDER BY\nor LIMIT/OFFSET\nor own setOps?}
    H -- Yes --> I["parenWrap = true"]
    H -- No --> J["parenWrap = false"]
    I --> K["Append setOp{kind, sql, parenWrap}"]
    J --> K
    K --> L["Build()"]
    L --> M["setOpWrapPoints()"]
    M --> N{"UNION/EXCEPT to INTERSECT transition?"}
    N -- Yes --> O["wrapClose[i]=true, open++"]
    N -- No --> P["wrapClose[i]=false"]
    O --> Q["Emit leading parens before base branch"]
    P --> Q
    Q --> R{setOpLeft != nil?}
    R -- Yes --> S["Wrap branch in parens with captured ORDER BY/LIMIT/OFFSET"]
    R -- No --> T["Write SELECT branch normally"]
    S --> U["writeSetOps: close precedence parens at wrapClose points"]
    T --> U
Loading

Reviews (2): Last reviewed commit: "fix(sqlbuilder): scope and group set ope..." | Re-trigger Greptile

Comment thread sqlbuilder/set_op.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sqlbuilder/select.go`:
- Around line 447-456: The set-operation chaining flow in SelectBuilder.addSetOp
currently loses grouping when consecutive operations use different setOpKind
values. Add coverage documenting the intended mixed Union/Intersect/Except
behavior, then update addSetOp and related set-operation rendering to either
preserve explicit parentheses around mixed-kind segments or reject cross-kind
chaining consistently; keep same-kind chaining behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7351f8b4-bd42-42e8-8e38-4356c3b0fa74

📥 Commits

Reviewing files that changed from the base of the PR and between e69b0a0 and 0551ef4.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !CHANGELOG.md
📒 Files selected for processing (3)
  • sqlbuilder/select.go
  • sqlbuilder/select_test.go
  • sqlbuilder/set_op.go

Comment thread sqlbuilder/select.go
Set operations (Union, UnionAll, Intersect, Except) mishandled branch
scope and chain grouping, producing invalid or silently wrong SQL.

Branch scope: each branch was concatenated with no parentheses, and the
left (receiver) branch's ORDER BY/LIMIT/OFFSET leaked onto the enclosing
statement. A union whose branches each carried their own ordering/limiting
emitted two ORDER BYs and two LIMITs (a syntax error), and the right
branch's clauses bound to the whole union instead of that branch. Any
branch carrying ORDER BY/LIMIT/OFFSET is now wrapped in parentheses, and
the left branch's clauses are snapshotted to that branch when the set op
is added; clauses added after the set op still apply to the whole
statement.

Chain grouping: a compound branch (e.g. a.UnionAll(b.Union(c))) was
flattened, silently regrouping it as (a UNION ALL b) UNION c. And because
SQL binds INTERSECT tighter than UNION/EXCEPT, mixed-kind flat chains like
a.Union(b).Intersect(c) rendered a UNION b INTERSECT c — read by SQL as
a UNION (b INTERSECT c) — not the fluent-intended (a UNION b) INTERSECT c.
Compound branches are now parenthesized, and the accumulated left segment
of a flat chain is parenthesized at each precedence rise. Same-precedence
chains (including all same-kind chains) are unchanged and stay flat.

These parenthesized compound forms target PostgreSQL/MySQL; SQLite does
not accept a parenthesized or per-branch-ordered compound select as a
set-op term, so nested/scoped/mixed-kind set ops are not expressible there.
@KARTIKrocks
KARTIKrocks force-pushed the fix/sqlbuilder-set-op-scoping branch from 0551ef4 to dbede9c Compare August 1, 2026 04:42
@KARTIKrocks
KARTIKrocks merged commit 1da7b2a into main Aug 1, 2026
12 checks passed
@KARTIKrocks
KARTIKrocks deleted the fix/sqlbuilder-set-op-scoping branch August 1, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants