⚡ Bolt: reduce query execution allocations by removing redundant argument slice copies#97
Conversation
…ment slice copies This optimization removes a redundant heap allocation (slice copy) in compiledQuery.literalArgs() and compiledQuery.bind() when the query does not contain named placeholders. Since q.args is a fresh slice created during query compilation and is not modified by the internal runners, it can be safely returned directly. Performance Impact: - Reduces allocations by 1 per query execution. - BenchmarkSQLiteSelectPointLookup/small: 42 -> 41 allocs/op. Co-authored-by: cungminh2710 <8063319+cungminh2710@users.noreply.github.com>
|
👋 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 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 SummaryThis PR removes the
Confidence Score: 4/5Safe to merge — no current caller modifies the returned slice, and database/sql never writes back to the variadic args array. The change achieves its allocation-reduction goal correctly for all existing paths. Every call site that consumes the return value of Only
|
| Filename | Overview |
|---|---|
| pkg/rain/query_compile.go | Removes defensive copies from literalArgs() and the !hasNames branch of bind(), returning q.args directly; safe for all current internal callers but silently changes the ownership contract for the public ToSQL() API and concurrent prepared-query execution paths. |
Sequence Diagram
sequenceDiagram
participant Caller
participant PreparedQuery
participant compiledQuery
participant db as database/sql
Note over PreparedQuery,compiledQuery: Prepared at open time — compiledQuery stored in struct
Caller->>PreparedQuery: "Exec(ctx, PreparedArgs{})"
PreparedQuery->>compiledQuery: bind(args) [!hasNames path]
Note over compiledQuery: Before PR: return append([]any(nil), q.args...)
Note over compiledQuery: After PR: return q.args — shared backing array
compiledQuery-->>PreparedQuery: bound ([]any)
PreparedQuery->>db: stmt.ExecContext(ctx, bound...)
Note over db: Creates internal []driver.NamedValue copy — does NOT modify bound
db-->>PreparedQuery: sql.Result
Note over Caller,db: Concurrent second call — same compiledQuery.args backing array returned
Caller->>PreparedQuery: "Exec(ctx, PreparedArgs{})"
PreparedQuery->>compiledQuery: bind(args)
compiledQuery-->>PreparedQuery: same q.args reference
PreparedQuery->>db: stmt.ExecContext(ctx, bound...)
db-->>PreparedQuery: sql.Result
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
pkg/rain/query_compile.go:37-40
**Removed copy changes the slice-ownership contract for prepared queries**
For prepared types (`PreparedInsertQuery`, `PreparedUpdateQuery`, `PreparedDeleteQuery`, `PreparedSelectQuery`), the `compiledQuery` is compiled once and stored in the struct; every subsequent call to `Exec`/`Scan` calls `bind()` on that stored value. With this change, the `!hasNames` fast-path now hands back a reference to the same backing array (`p.compiled.args`) on every call. That's safe today because no current caller writes to the returned slice before passing it to `ExecContext`/`QueryContext`. However, the old comment explicitly named this as protection against "shared state footguns", and removing it silently drops the guard. If any future code path modifies `bound` in-place between `bind()` and the DB call (e.g., driver-specific wrapping, middleware, or retry logic), it will corrupt the stored `compiledQuery.args` for all subsequent calls — a data-race-free but logically corrupting mutation with no immediate panic. A `// NOTE: callers must not modify the returned slice` comment would preserve the intent without the allocation cost.
Reviews (1): Last reviewed commit: "⚡ Bolt: reduce query execution allocatio..." | Re-trigger Greptile
| // OPTIMIZATION: Return the pre-calculated arguments directly. Since q.args | ||
| // is a fresh slice created during compilation and query runners do not | ||
| // modify it, we can safely avoid the redundant copy. | ||
| return q.args, nil |
There was a problem hiding this comment.
Removed copy changes the slice-ownership contract for prepared queries
For prepared types (PreparedInsertQuery, PreparedUpdateQuery, PreparedDeleteQuery, PreparedSelectQuery), the compiledQuery is compiled once and stored in the struct; every subsequent call to Exec/Scan calls bind() on that stored value. With this change, the !hasNames fast-path now hands back a reference to the same backing array (p.compiled.args) on every call. That's safe today because no current caller writes to the returned slice before passing it to ExecContext/QueryContext. However, the old comment explicitly named this as protection against "shared state footguns", and removing it silently drops the guard. If any future code path modifies bound in-place between bind() and the DB call (e.g., driver-specific wrapping, middleware, or retry logic), it will corrupt the stored compiledQuery.args for all subsequent calls — a data-race-free but logically corrupting mutation with no immediate panic. A // NOTE: callers must not modify the returned slice comment would preserve the intent without the allocation cost.
Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/rain/query_compile.go
Line: 37-40
Comment:
**Removed copy changes the slice-ownership contract for prepared queries**
For prepared types (`PreparedInsertQuery`, `PreparedUpdateQuery`, `PreparedDeleteQuery`, `PreparedSelectQuery`), the `compiledQuery` is compiled once and stored in the struct; every subsequent call to `Exec`/`Scan` calls `bind()` on that stored value. With this change, the `!hasNames` fast-path now hands back a reference to the same backing array (`p.compiled.args`) on every call. That's safe today because no current caller writes to the returned slice before passing it to `ExecContext`/`QueryContext`. However, the old comment explicitly named this as protection against "shared state footguns", and removing it silently drops the guard. If any future code path modifies `bound` in-place between `bind()` and the DB call (e.g., driver-specific wrapping, middleware, or retry logic), it will corrupt the stored `compiledQuery.args` for all subsequent calls — a data-race-free but logically corrupting mutation with no immediate panic. A `// NOTE: callers must not modify the returned slice` comment would preserve the intent without the allocation cost.
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!
💡 What: Removed redundant slice copies in
compiledQuery.literalArgs()andcompiledQuery.bind()for queries that do not use named placeholders.🎯 Why: Each query execution was performing an unnecessary heap allocation to copy the pre-calculated argument slice, even though the internal query runners do not modify it.
📊 Impact: Reduces heap allocations by 1 per query execution (e.g., from 42 to 41 for point lookups).
🔬 Measurement: Verified with
go test -bench BenchmarkSQLiteSelectPointLookup/small -benchmem ./pkg/rain.PR created automatically by Jules for task 13585493257413164491 started by @cungminh2710