Skip to content

⚡ Bolt: optimize query compilation via argument capacity hints#155

Closed
cungminh2710 wants to merge 1 commit into
mainfrom
bolt/compile-optimization-8099639359604793530
Closed

⚡ Bolt: optimize query compilation via argument capacity hints#155
cungminh2710 wants to merge 1 commit into
mainfrom
bolt/compile-optimization-8099639359604793530

Conversation

@cungminh2710

Copy link
Copy Markdown
Contributor

💡 What: Optimized the SQL query compilation hot-path by providing argument count hints to the compilation context and hoisting redundant object creation.

🎯 Why: To reduce heap allocations and pressure on the garbage collector during query building, especially for complex queries or large IN clauses that previously caused multiple args slice re-allocations.

📊 Impact: Reduces allocations in complex queries. For a SELECT with 1000 items in an IN clause, redundant slice growths for both args and argPlan (when named placeholders are used) are avoided.

🔬 Measurement: Verified using the new BenchmarkSelectToSQL/InClause1000 and existing benchmarks in pkg/rain/compile_bench_test.go. All functional tests passed.


PR created automatically by Jules for task 8099639359604793530 started by @cungminh2710

Reduce redundant slice re-allocations during SQL query compilation by
utilizing `ensureArgsCapacity` hints in several expression rendering
paths:

- `writeJoinedPredicates` (WHERE/HAVING)
- `BinaryExpr` (arithmetic)
- `ConcatExpr`
- `InExpr` (including hoisting of `expressionContext`)
- `BetweenExpr`
- `LogicalExpr` (AND/OR)
- `CaseExpr`
- `CoalesceExpr`
- `writeRaw`

Also added `BenchmarkSelectToSQL/InClause1000` to measure compilation
performance for large parameter sets. In-clause allocations were
slightly reduced by avoiding multiple internal slice growths.

Co-authored-by: cungminh2710 <8063319+cungminh2710@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR optimizes the SQL query compilation hot-path in pkg/rain/query_compile.go by introducing ensureArgsCapacity call-sites before each expression type's loop and hoisting the expressionContext struct allocation outside the IN clause value loop.

  • ensureArgsCapacity adoption: Pre-allocation hints are added for InExpr, BinaryExpr, ConcatExpr, BetweenExpr, LogicalExpr, CaseExpr, CoalesceExpr, and raw SQL. The helper allocates exactly the minimum needed capacity without a growth factor, meaning sequential small-hint calls that exceed the pool's initial cap=32 can trigger O(N) individual reallocations instead of amortized O(log N) growth.
  • expressionContext hoisting: Semantically correct — the struct is a value type passed by value and len(value.Values) is constant over the loop.
  • New benchmark: BenchmarkSelectToSQL/InClause1000 correctly covers the motivating scenario with proper b.ResetTimer() placement.

Confidence Score: 3/5

Safe to merge for the IN clause path but carries a regression risk for queries with many small sub-expressions that collectively exceed the pool capacity.

The optimization works well for the targeted IN(1000) case, but applying the exact-capacity helper uniformly with small fixed hints (2 for BinaryExpr, 3 for BetweenExpr) means complex queries with many nested expressions can see more reallocations than the old bare-append path. The existing benchmark only exercises the IN clause path, leaving the regression path unverified.

pkg/rain/query_compile.go — specifically the ensureArgsCapacity implementation and its call-sites with small fixed hints.

Important Files Changed

Filename Overview
pkg/rain/query_compile.go Adds ensureArgsCapacity call-sites across all expression types and hoists expressionContext out of the IN clause loop; the helper allocates exact minimum capacity rather than using an amortized growth factor, which can cause O(N) reallocations for queries with many small sub-expressions.
pkg/rain/compile_bench_test.go Adds an InClause1000 benchmark sub-case that validates the motivating optimization scenario; setup correctly uses b.ResetTimer() after slice preparation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[writeExpressionInContext] --> B{Expression type}
    B -->|InExpr| C["ensureArgsCapacity(len(values)+1)"]
    B -->|BinaryExpr| D["ensureArgsCapacity(2)"]
    B -->|LogicalExpr| E["ensureArgsCapacity(len(exprs))"]
    B -->|CaseExpr| F["ensureArgsCapacity(pairs*2 + optionals)"]
    B -->|BetweenExpr| H["ensureArgsCapacity(3)"]
    C --> J{cap less than len plus n?}
    D --> J
    E --> J
    F --> J
    H --> J
    J -->|No| K[No allocation]
    J -->|Yes| L["make slice with exact capacity = needed"]
    L --> M[copy and reassign c.args]
    K --> N[writeAny appends val via Go append]
    M --> N
    N --> O[compiledQuery snapshot]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[writeExpressionInContext] --> B{Expression type}
    B -->|InExpr| C["ensureArgsCapacity(len(values)+1)"]
    B -->|BinaryExpr| D["ensureArgsCapacity(2)"]
    B -->|LogicalExpr| E["ensureArgsCapacity(len(exprs))"]
    B -->|CaseExpr| F["ensureArgsCapacity(pairs*2 + optionals)"]
    B -->|BetweenExpr| H["ensureArgsCapacity(3)"]
    C --> J{cap less than len plus n?}
    D --> J
    E --> J
    F --> J
    H --> J
    J -->|No| K[No allocation]
    J -->|Yes| L["make slice with exact capacity = needed"]
    L --> M[copy and reassign c.args]
    K --> N[writeAny appends val via Go append]
    M --> N
    N --> O[compiledQuery snapshot]
Loading

Comments Outside Diff (1)

  1. pkg/rain/query_compile.go, line 205-218 (link)

    P1 ensureArgsCapacity allocates the exact minimum capacity needed (needed) rather than using an amortized growth factor. When multiple small-hint calls occur within one query compilation pass (e.g., a chain of nested BinaryExpr, LogicalExpr, and CoalesceExpr nodes), each call that just barely exceeds the current cap triggers a fresh allocation of exactly the new minimum, producing O(N) individual copies rather than the O(log N) amortized growth that Go's append would provide. A concrete failure path: a query with 20 BinaryExpr predicates (40 total args) that passes the initial pool cap of 32 would trigger a new allocation every two args appended — 4 extra allocations instead of the 1 doubling that bare append gives.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: pkg/rain/query_compile.go
    Line: 205-218
    
    Comment:
    `ensureArgsCapacity` allocates the exact minimum capacity needed (`needed`) rather than using an amortized growth factor. When multiple small-hint calls occur within one query compilation pass (e.g., a chain of nested `BinaryExpr`, `LogicalExpr`, and `CoalesceExpr` nodes), each call that just barely exceeds the current cap triggers a fresh allocation of exactly the new minimum, producing O(N) individual copies rather than the O(log N) amortized growth that Go's `append` would provide. A concrete failure path: a query with 20 `BinaryExpr` predicates (40 total args) that passes the initial pool cap of 32 would trigger a new allocation every two args appended — 4 extra allocations instead of the 1 doubling that bare `append` gives.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Codex

Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
pkg/rain/query_compile.go:205-218
`ensureArgsCapacity` allocates the exact minimum capacity needed (`needed`) rather than using an amortized growth factor. When multiple small-hint calls occur within one query compilation pass (e.g., a chain of nested `BinaryExpr`, `LogicalExpr`, and `CoalesceExpr` nodes), each call that just barely exceeds the current cap triggers a fresh allocation of exactly the new minimum, producing O(N) individual copies rather than the O(log N) amortized growth that Go's `append` would provide. A concrete failure path: a query with 20 `BinaryExpr` predicates (40 total args) that passes the initial pool cap of 32 would trigger a new allocation every two args appended — 4 extra allocations instead of the 1 doubling that bare `append` gives.

```suggestion
func (c *compileContext) ensureArgsCapacity(n int) {
	if c.hasNames {
		if needed := len(c.argPlan) + n; cap(c.argPlan) < needed {
			newCap := max(needed, cap(c.argPlan)*2)
			newPlan := make([]compiledArg, len(c.argPlan), newCap)
			copy(newPlan, c.argPlan)
			c.argPlan = newPlan
		}
	}
	if needed := len(c.args) + n; cap(c.args) < needed {
		newCap := max(needed, cap(c.args)*2)
		newArgs := make([]any, len(c.args), newCap)
		copy(newArgs, c.args)
		c.args = newArgs
	}
}
```

### Issue 2 of 2
pkg/rain/query_compile.go:311
**Capacity hint treats each predicate as contributing exactly one arg**

`ensureArgsCapacity(len(predicates))` assumes one arg per predicate, but a single predicate can easily contribute many more — an `IN(1000)` predicate contributes 1000, a `BETWEEN` contributes 2, a nested `AND`/`OR` contributes the sum of its children. When these compound predicates are involved, the outer hint is immediately exhausted by the very first child, leaving all subsequent children's `ensureArgsCapacity` calls to each allocate exactly their own minimum. This doesn't affect correctness, but for WHERE clauses mixing simple and complex predicates the allocation savings can fall well short of what was intended.

Reviews (1): Last reviewed commit: "refactor(rain): optimize query compilati..." | Re-trigger Greptile

Comment thread pkg/rain/query_compile.go
return c.writePredicate(predicates[0])
}

c.ensureArgsCapacity(len(predicates))

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.

P2 Capacity hint treats each predicate as contributing exactly one arg

ensureArgsCapacity(len(predicates)) assumes one arg per predicate, but a single predicate can easily contribute many more — an IN(1000) predicate contributes 1000, a BETWEEN contributes 2, a nested AND/OR contributes the sum of its children. When these compound predicates are involved, the outer hint is immediately exhausted by the very first child, leaving all subsequent children's ensureArgsCapacity calls to each allocate exactly their own minimum. This doesn't affect correctness, but for WHERE clauses mixing simple and complex predicates the allocation savings can fall well short of what was intended.

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/rain/query_compile.go
Line: 311

Comment:
**Capacity hint treats each predicate as contributing exactly one arg**

`ensureArgsCapacity(len(predicates))` assumes one arg per predicate, but a single predicate can easily contribute many more — an `IN(1000)` predicate contributes 1000, a `BETWEEN` contributes 2, a nested `AND`/`OR` contributes the sum of its children. When these compound predicates are involved, the outer hint is immediately exhausted by the very first child, leaving all subsequent children's `ensureArgsCapacity` calls to each allocate exactly their own minimum. This doesn't affect correctness, but for WHERE clauses mixing simple and complex predicates the allocation savings can fall well short of what was intended.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

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.

1 participant