Skip to content

Commit d7300f7

Browse files
jgilesclaude
andcommitted
Change emit_query_batch to use destination pointers instead of callbacks
Replace the callback-based Queue* API with destination pointers, similar to json.Unmarshal. This simplifies caller code by eliminating callback nesting - callers pass pointers where results should be written, and ExecuteBatch populates them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 16e715f commit d7300f7

File tree

8 files changed

+149
-151
lines changed

8 files changed

+149
-151
lines changed

docs/reference/config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ The `gen` mapping supports the following keys:
166166
- If true, emit a function per enum type
167167
that returns all valid enum values.
168168
- `emit_query_batch`:
169-
- If true, generate a `QueryBatch` type with `Queue*` methods that batch multiple different queries into a single round-trip. Uses pgx v5's `QueuedQuery` callback API. Only supported with `sql_package: pgx/v5`. Defaults to `false`.
169+
- If true, generate a `QueryBatch` type with `Queue*` methods that batch multiple different queries into a single round-trip. Each `Queue*` method accepts destination pointers where results are written when `ExecuteBatch` is called. Only supported with `sql_package: pgx/v5`. Defaults to `false`.
170170
- `emit_sql_as_comment`:
171171
- If true, emits the SQL statement as a code-block comment above the generated function, appending to any existing comments. Defaults to `false`.
172172
- `build_tags`:
@@ -453,7 +453,7 @@ Each mapping in the `packages` collection has the following keys:
453453
- If true, emit a function per enum type
454454
that returns all valid enum values.
455455
- `emit_query_batch`:
456-
- If true, generate a `QueryBatch` type with `Queue*` methods that batch multiple different queries into a single round-trip. Uses pgx v5's `QueuedQuery` callback API. Only supported with `sql_package: pgx/v5`. Defaults to `false`.
456+
- If true, generate a `QueryBatch` type with `Queue*` methods that batch multiple different queries into a single round-trip. Each `Queue*` method accepts destination pointers where results are written when `ExecuteBatch` is called. Only supported with `sql_package: pgx/v5`. Defaults to `false`.
457457
- `build_tags`:
458458
- If set, add a `//go:build <build_tags>` directive at the beginning of each generated Go file.
459459
- `json_tags_case_style`:

docs/reference/query-annotations.md

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,11 @@ The `:batchexec`, `:batchmany`, and `:batchone` annotations above batch the
230230
queries** into a single round-trip, use the `emit_query_batch` configuration
231231
option instead.
232232

233-
When `emit_query_batch` is enabled, sqlc generates a `QueryBatch` type that
234-
uses pgx v5's `QueuedQuery` callback API. Each regular query (`:one`, `:many`,
235-
`:exec`, `:execrows`, `:execresult`) gets a `Queue*` method on `QueryBatch`.
236-
All queued queries are sent in a single round-trip when `ExecuteBatch` is
237-
called.
233+
When `emit_query_batch` is enabled, sqlc generates a `QueryBatch` type with
234+
`Queue*` methods for each regular query (`:one`, `:many`, `:exec`, `:execrows`,
235+
`:execresult`). Each `Queue*` method accepts destination pointers where results
236+
are written when `ExecuteBatch` is called. All queued queries are sent in a
237+
single round-trip.
238238

239239
__NOTE: This option only works with PostgreSQL using the `pgx/v5` driver and outputting Go code.__
240240

@@ -268,23 +268,18 @@ UPDATE users SET name = $1 WHERE id = $2;
268268
// Generated QueryBatch API:
269269
batch := db.NewQueryBatch()
270270

271-
batch.QueueGetUser(userID, func(user db.User, found bool) error {
272-
if !found {
273-
return nil // no row matched
274-
}
275-
fmt.Println(user.Name)
276-
return nil
277-
})
271+
var user db.User
272+
var found bool
273+
batch.QueueGetUser(userID, &user, &found)
278274

279-
batch.QueueListUsers(func(users []db.User) error {
280-
fmt.Println("found", len(users), "users")
281-
return nil
282-
})
275+
var users []db.User
276+
batch.QueueListUsers(&users)
283277

284278
batch.QueueUpdateUser(db.UpdateUserParams{Name: "Alice", ID: 1})
285279

286280
// Send all queries in one round-trip:
287281
err := queries.ExecuteBatch(ctx, batch)
282+
// user, found, and users are now populated
288283
```
289284

290285
The `QueryBatch.Batch` field is exported so you can mix generated `Queue*`

internal/codegen/golang/templates/pgx/queryBatchCode.tmpl

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{{define "queryBatchCodePgx"}}
22

33
// QueryBatch allows queuing multiple queries to be executed in a single
4-
// round-trip using pgx v5's QueuedQuery callback pattern. Each Queue* method
5-
// calls pgx.Batch.Queue and registers a result callback (QueryRow, Query, or
6-
// Exec) that is invoked when ExecuteBatch processes the batch results.
7-
// For :exec queries, no callback is needed - errors propagate via ExecuteBatch.
4+
// round-trip using pgx v5's batch API. Each Queue* method calls
5+
// pgx.Batch.Queue and registers a result handler that writes to the provided
6+
// destination pointer(s) when ExecuteBatch processes the batch results.
7+
// For :exec queries, no destination is needed - errors propagate via ExecuteBatch.
88
//
99
// The Batch field is exported to allow interoperability: callers can mix
1010
// generated Queue* calls with custom pgx batch operations on the same
@@ -29,45 +29,40 @@ func (q *Queries) ExecuteBatch(ctx context.Context, {{if $.EmitMethodsWithDBArgu
2929
{{if and (ne .Cmd ":copyfrom") (ne (hasPrefix .Cmd ":batch") true)}}
3030
{{if eq .Cmd ":one"}}
3131
// Queue{{.MethodName}} queues {{.MethodName}} for batch execution.
32-
// The callback fn is called when ExecuteBatch is called. The second parameter
33-
// is false if the row was not found (no error is returned in this case).
34-
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}fn func({{.Ret.DefineType}}, bool) error) {
32+
// The result is written to dest when ExecuteBatch is called.
33+
// If no row is found, *ok is set to false (no error is returned in this case).
34+
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}dest *{{.Ret.DefineType}}, ok *bool) {
3535
b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).QueryRow(func(row pgx.Row) error {
3636
var {{.Ret.Name}} {{.Ret.Type}}
3737
err := row.Scan({{.Ret.Scan}})
3838
if err != nil {
3939
if errors.Is(err, pgx.ErrNoRows) {
40-
return fn({{.Ret.ReturnName}}, false)
40+
*ok = false
41+
return nil
4142
}
4243
return err
4344
}
44-
return fn({{.Ret.ReturnName}}, true)
45+
*dest = {{.Ret.ReturnName}}
46+
*ok = true
47+
return nil
4548
})
4649
}
4750
{{end}}
4851

4952
{{if eq .Cmd ":many"}}
5053
// Queue{{.MethodName}} queues {{.MethodName}} for batch execution.
51-
// The callback fn is called with the results when ExecuteBatch is called.
52-
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}fn func([]{{.Ret.DefineType}}) error) {
54+
// The results are appended to *dest when ExecuteBatch is called.
55+
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}dest *[]{{.Ret.DefineType}}) {
5356
b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Query(func(rows pgx.Rows) error {
5457
defer rows.Close()
55-
{{- if $.EmitEmptySlices}}
56-
items := []{{.Ret.DefineType}}{}
57-
{{else}}
58-
var items []{{.Ret.DefineType}}
59-
{{end -}}
6058
for rows.Next() {
6159
var {{.Ret.Name}} {{.Ret.Type}}
6260
if err := rows.Scan({{.Ret.Scan}}); err != nil {
6361
return err
6462
}
65-
items = append(items, {{.Ret.ReturnName}})
63+
*dest = append(*dest, {{.Ret.ReturnName}})
6664
}
67-
if err := rows.Err(); err != nil {
68-
return err
69-
}
70-
return fn(items)
65+
return rows.Err()
7166
})
7267
}
7368
{{end}}
@@ -81,20 +76,22 @@ func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}) {
8176

8277
{{if eq .Cmd ":execrows"}}
8378
// Queue{{.MethodName}} queues {{.MethodName}} for batch execution.
84-
// The callback fn is called with the number of rows affected when ExecuteBatch is called.
85-
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}fn func(int64) error) {
79+
// The number of rows affected is written to dest when ExecuteBatch is called.
80+
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}dest *int64) {
8681
b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Exec(func(ct pgconn.CommandTag) error {
87-
return fn(ct.RowsAffected())
82+
*dest = ct.RowsAffected()
83+
return nil
8884
})
8985
}
9086
{{end}}
9187

9288
{{if eq .Cmd ":execresult"}}
9389
// Queue{{.MethodName}} queues {{.MethodName}} for batch execution.
94-
// The callback fn is called with the command tag when ExecuteBatch is called.
95-
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}fn func(pgconn.CommandTag) error) {
90+
// The command tag is written to dest when ExecuteBatch is called.
91+
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}dest *pgconn.CommandTag) {
9692
b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Exec(func(ct pgconn.CommandTag) error {
97-
return fn(ct)
93+
*dest = ct
94+
return nil
9895
})
9996
}
10097
{{end}}

internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query_batch.sql.go

Lines changed: 32 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query_batch.sql.go

Lines changed: 16 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)