From da8a80c4dd8e6c337ba80c1a011298b2066a757e Mon Sep 17 00:00:00 2001 From: Josh Giles Date: Sun, 15 Mar 2026 15:28:48 -0400 Subject: [PATCH 1/2] Support batching different queries together. Fix #4195 by adding an emit_query_batch codegen option that creates a QueryBatch type when using pgx v5. When emit_query_batch: true is set in config (pgx/v5 only), sqlc generates: - A QueryBatch struct wrapping pgx.Batch - NewQueryBatch() constructor - Queue* methods for each query (:one, :many, :exec, :execrows, :execresult) - ExecuteBatch() on Queries to send all queued queries in one round-trip This expands the batching support of existing :batchone/:batchmany/:batchexec annotations, which required separate query definitions and only supported baching the same query with different parameters. Batching multiple instances of the same query with different parameters is also supported via the new interface. While there is probably not a good reason to use both in the same package, you can generate and use both without error for backwards compatibility. Key design decisions - Follows pgx v5's recommended QueuedQuery callback pattern - QueryBatch.Batch is exported so users can mix generated Queue* calls with custom pgx batch operations - :exec queries have no callback (consistent with pgx - errors propagate via Close/ExecuteBatch) - :one callbacks receive a bool indicating whether a row was found Changes - internal/codegen/golang/gen.go - Wire up EmitQueryBatch option, file generation, and validation - internal/codegen/golang/imports.go - Add queryBatchImports() with import logic that skips struct field types (struct definitions live in query.sql.go, not the batch file). Extract queryUsesType() from inline closure for reuse. - internal/codegen/golang/opts/options.go - Add EmitQueryBatch and OutputQueryBatchFileName options - internal/codegen/golang/templates/pgx/queryBatchCode.tmpl - New template for all query batch methods - internal/codegen/golang/templates/template.tmpl - Add queryBatchFile and queryBatchCode template definitions Test coverage - emit_query_batch - Full test with :one, :many, :exec, :execrows, :execresult - emit_query_batch_db_arg - With emit_methods_with_db_argument: true - emit_query_batch_minimal - Non-struct return types (verifies import handling for e.g. pgtype.Timestamptz) - emit_query_batch_overrides - With custom type overrides (verifies batch file doesn't import types only used in struct fields) - emit_query_batch_with_batch - Combines old-style :batchexec with emit_query_batch (verifies both batch.go and query_batch.sql.go coexist correctly) Documentation - docs/reference/config.md - Added emit_query_batch and output_query_batch_file_name - docs/reference/query-annotations.md - New section with usage examples contrasting with :batch* annotations --- docs/reference/config.md | 8 ++ docs/reference/query-annotations.md | 68 +++++++++ internal/codegen/golang/gen.go | 22 ++- internal/codegen/golang/imports.go | 65 +++++++++ internal/codegen/golang/opts/options.go | 2 + .../golang/templates/pgx/queryBatchCode.tmpl | 103 ++++++++++++++ .../codegen/golang/templates/template.tmpl | 29 ++++ .../postgresql/pgx/v5/go/db.go | 33 +++++ .../postgresql/pgx/v5/go/models.go | 16 +++ .../postgresql/pgx/v5/go/query.sql.go | 113 +++++++++++++++ .../postgresql/pgx/v5/go/query_batch.sql.go | 129 ++++++++++++++++++ .../postgresql/pgx/v5/query.sql | 17 +++ .../postgresql/pgx/v5/schema.sql | 7 + .../postgresql/pgx/v5/sqlc.yaml | 11 ++ .../postgresql/pgx/v5/go/db.go | 26 ++++ .../postgresql/pgx/v5/go/models.go | 10 ++ .../postgresql/pgx/v5/go/query.sql.go | 45 ++++++ .../postgresql/pgx/v5/go/query_batch.sql.go | 75 ++++++++++ .../postgresql/pgx/v5/query.sql | 5 + .../postgresql/pgx/v5/schema.sql | 4 + .../postgresql/pgx/v5/sqlc.yaml | 12 ++ .../postgresql/pgx/v5/go/db.go | 33 +++++ .../postgresql/pgx/v5/go/models.go | 15 ++ .../postgresql/pgx/v5/go/query.sql.go | 72 ++++++++++ .../postgresql/pgx/v5/go/query_batch.sql.go | 98 +++++++++++++ .../postgresql/pgx/v5/query.sql | 11 ++ .../postgresql/pgx/v5/schema.sql | 5 + .../postgresql/pgx/v5/sqlc.yaml | 11 ++ .../postgresql/pgx/v5/go/db.go | 33 +++++ .../postgresql/pgx/v5/go/models.go | 15 ++ .../postgresql/pgx/v5/go/query.sql.go | 77 +++++++++++ .../postgresql/pgx/v5/go/query_batch.sql.go | 97 +++++++++++++ .../postgresql/pgx/v5/query.sql | 11 ++ .../postgresql/pgx/v5/schema.sql | 5 + .../postgresql/pgx/v5/sqlc.yaml | 16 +++ .../postgresql/pgx/v5/go/batch.go | 66 +++++++++ .../postgresql/pgx/v5/go/db.go | 33 +++++ .../postgresql/pgx/v5/go/models.go | 10 ++ .../postgresql/pgx/v5/go/query.sql.go | 54 ++++++++ .../postgresql/pgx/v5/go/query_batch.sql.go | 80 +++++++++++ .../postgresql/pgx/v5/query.sql | 11 ++ .../postgresql/pgx/v5/schema.sql | 4 + .../postgresql/pgx/v5/sqlc.yaml | 11 ++ 43 files changed, 1565 insertions(+), 3 deletions(-) create mode 100644 internal/codegen/golang/templates/pgx/queryBatchCode.tmpl create mode 100644 internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/db.go create mode 100644 internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/models.go create mode 100644 internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query_batch.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/query.sql create mode 100644 internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/schema.sql create mode 100644 internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/sqlc.yaml create mode 100644 internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/db.go create mode 100644 internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/models.go create mode 100644 internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query_batch.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/query.sql create mode 100644 internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/schema.sql create mode 100644 internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/sqlc.yaml create mode 100644 internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/db.go create mode 100644 internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/models.go create mode 100644 internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/query.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/query_batch.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/query.sql create mode 100644 internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/schema.sql create mode 100644 internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/sqlc.yaml create mode 100644 internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/db.go create mode 100644 internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/models.go create mode 100644 internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/query.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/query_batch.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/query.sql create mode 100644 internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/schema.sql create mode 100644 internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/sqlc.yaml create mode 100644 internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/batch.go create mode 100644 internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/db.go create mode 100644 internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/models.go create mode 100644 internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/query.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/query_batch.sql.go create mode 100644 internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/query.sql create mode 100644 internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/schema.sql create mode 100644 internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/sqlc.yaml diff --git a/docs/reference/config.md b/docs/reference/config.md index ff8bcd0890..9b493a5f2f 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -165,6 +165,8 @@ The `gen` mapping supports the following keys: - `emit_all_enum_values`: - If true, emit a function per enum type that returns all valid enum values. +- `emit_query_batch`: + - 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`. - `emit_sql_as_comment`: - If true, emits the SQL statement as a code-block comment above the generated function, appending to any existing comments. Defaults to `false`. - `build_tags`: @@ -179,6 +181,8 @@ The `gen` mapping supports the following keys: - If `true`, sqlc won't generate table and enum structs that aren't used in queries for a given package. Defaults to `false`. - `output_batch_file_name`: - Customize the name of the batch file. Defaults to `batch.go`. +- `output_query_batch_file_name`: + - Customize the name of the query batch file. Defaults to `query_batch.sql.go`. - `output_db_file_name`: - Customize the name of the db file. Defaults to `db.go`. - `output_models_file_name`: @@ -448,6 +452,8 @@ Each mapping in the `packages` collection has the following keys: - `emit_all_enum_values`: - If true, emit a function per enum type that returns all valid enum values. +- `emit_query_batch`: + - 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`. - `build_tags`: - If set, add a `//go:build ` directive at the beginning of each generated Go file. - `json_tags_case_style`: @@ -456,6 +462,8 @@ Each mapping in the `packages` collection has the following keys: - If `true`, sqlc won't generate table and enum structs that aren't used in queries for a given package. Defaults to `false`. - `output_batch_file_name`: - Customize the name of the batch file. Defaults to `batch.go`. +- `output_query_batch_file_name`: + - Customize the name of the query batch file. Defaults to `query_batch.sql.go`. - `output_db_file_name`: - Customize the name of the db file. Defaults to `db.go`. - `output_models_file_name`: diff --git a/docs/reference/query-annotations.md b/docs/reference/query-annotations.md index 4fabe05aae..6e6c553c60 100644 --- a/docs/reference/query-annotations.md +++ b/docs/reference/query-annotations.md @@ -223,6 +223,74 @@ func (b *CreateBookBatchResults) Close() error { } ``` +## `emit_query_batch` (batching different queries) + +The `:batchexec`, `:batchmany`, and `:batchone` annotations above batch the +**same query** with different parameters. If you need to batch **different +queries** into a single round-trip, use the `emit_query_batch` configuration +option instead. + +When `emit_query_batch` is enabled, sqlc generates a `QueryBatch` type that +uses pgx v5's `QueuedQuery` callback API. Each regular query (`:one`, `:many`, +`:exec`, `:execrows`, `:execresult`) gets a `Queue*` method on `QueryBatch`. +All queued queries are sent in a single round-trip when `ExecuteBatch` is +called. + +__NOTE: This option only works with PostgreSQL using the `pgx/v5` driver and outputting Go code.__ + +```yaml +# sqlc.yaml +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "db" + out: "db" + sql_package: "pgx/v5" + emit_query_batch: true +``` + +```sql +-- name: GetUser :one +SELECT * FROM users WHERE id = $1; + +-- name: ListUsers :many +SELECT * FROM users ORDER BY id; + +-- name: UpdateUser :exec +UPDATE users SET name = $1 WHERE id = $2; +``` + +```go +// Generated QueryBatch API: +batch := db.NewQueryBatch() + +batch.QueueGetUser(userID, func(user db.User, found bool) error { + if !found { + return nil // no row matched + } + fmt.Println(user.Name) + return nil +}) + +batch.QueueListUsers(func(users []db.User) error { + fmt.Println("found", len(users), "users") + return nil +}) + +batch.QueueUpdateUser(db.UpdateUserParams{Name: "Alice", ID: 1}) + +// Send all queries in one round-trip: +err := queries.ExecuteBatch(ctx, batch) +``` + +The `QueryBatch.Batch` field is exported so you can mix generated `Queue*` +calls with custom pgx batch operations on the same `pgx.Batch`. This feature +can be used alongside `:batch*` annotations in the same package. + ## `:copyfrom` __NOTE: This command is driver and package specific, see [how to insert](../howto/insert.md#using-copyfrom) diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index 7df56a0a41..9b996244f5 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -39,6 +39,7 @@ type tmplCtx struct { EmitAllEnumValues bool UsesCopyFrom bool UsesBatch bool + EmitQueryBatch bool OmitSqlcVersion bool BuildTags string WrapErrors bool @@ -182,7 +183,8 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, EmitEnumValidMethod: options.EmitEnumValidMethod, EmitAllEnumValues: options.EmitAllEnumValues, UsesCopyFrom: usesCopyFrom(queries), - UsesBatch: usesBatch(queries), + UsesBatch: usesBatch(queries) || options.EmitQueryBatch, + EmitQueryBatch: options.EmitQueryBatch, SQLDriver: parseDriver(options.SqlPackage), Q: "`", Package: options.Package, @@ -205,10 +207,14 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, tctx.SQLDriver = opts.SQLDriverGoSQLDriverMySQL } - if tctx.UsesBatch && !tctx.SQLDriver.IsPGX() { + if usesBatch(queries) && !tctx.SQLDriver.IsPGX() { return nil, errors.New(":batch* commands are only supported by pgx") } + if options.EmitQueryBatch && tctx.SQLDriver != opts.SQLDriverPGXV5 { + return nil, errors.New("emit_query_batch is only supported by pgx/v5") + } + funcMap := template.FuncMap{ "lowerTitle": sdk.LowerTitle, "comment": sdk.DoubleSlashComment, @@ -289,6 +295,11 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, batchFileName = options.OutputBatchFileName } + queryBatchFileName := "query_batch.sql.go" + if options.OutputQueryBatchFileName != "" { + queryBatchFileName = options.OutputQueryBatchFileName + } + if err := execute(dbFileName, "dbFile"); err != nil { return nil, err } @@ -305,11 +316,16 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, return nil, err } } - if tctx.UsesBatch { + if usesBatch(queries) { if err := execute(batchFileName, "batchFile"); err != nil { return nil, err } } + if tctx.EmitQueryBatch { + if err := execute(queryBatchFileName, "queryBatchFile"); err != nil { + return nil, err + } + } files := map[string]struct{}{} for _, gq := range queries { diff --git a/internal/codegen/golang/imports.go b/internal/codegen/golang/imports.go index ccca4f603c..59c46c10eb 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -101,6 +101,10 @@ func (i *importer) Imports(filename string) [][]ImportSpec { if i.Options.OutputBatchFileName != "" { batchFileName = i.Options.OutputBatchFileName } + queryBatchFileName := "query_batch.sql.go" + if i.Options.OutputQueryBatchFileName != "" { + queryBatchFileName = i.Options.OutputQueryBatchFileName + } switch filename { case dbFileName: @@ -113,6 +117,8 @@ func (i *importer) Imports(filename string) [][]ImportSpec { return mergeImports(i.copyfromImports()) case batchFileName: return mergeImports(i.batchImports()) + case queryBatchFileName: + return mergeImports(i.queryBatchImports()) default: return mergeImports(i.queryImports(filename)) } @@ -506,6 +512,65 @@ func hasPrefixIgnoringSliceAndPointerPrefix(s, prefix string) bool { return strings.HasPrefix(trimmedS, trimmedPrefix) } +func (i *importer) queryBatchImports() fileImports { + // Filter to only non-batch, non-copyfrom queries + regularQueries := make([]Query, 0, len(i.Queries)) + for _, q := range i.Queries { + if q.Cmd != metadata.CmdCopyFrom && !usesBatch([]Query{q}) { + regularQueries = append(regularQueries, q) + } + } + std, pkg := buildImports(i.Options, regularQueries, queryBatchUsesType(regularQueries)) + + for _, q := range regularQueries { + switch q.Cmd { + case metadata.CmdOne: + // :one queries use errors.Is for pgx.ErrNoRows check + std["errors"] = struct{}{} + case metadata.CmdExecRows, metadata.CmdExecResult: + // Exec queries need pgconn.CommandTag to handle results. + // metadata.CmdExecLastId is unsupported in Postgres. + pkg[ImportSpec{Path: "github.com/jackc/pgx/v5/pgconn"}] = struct{}{} + } + } + + // context is always needed for ExecuteBatch + std["context"] = struct{}{} + // pgx/v5 is always needed for pgx.Batch and pgx.Rows + pkg[ImportSpec{Path: "github.com/jackc/pgx/v5"}] = struct{}{} + return sortedImports(std, pkg) +} + +// queryBatchUsesType returns a predicate that checks whether a type name is +// directly referenced in the generated query batch file. This skips struct +// field types because struct definitions live in query.sql.go, not +// query_batch.sql.go. The batch file only references structs by name. +func queryBatchUsesType(queries []Query) func(string) bool { + return func(name string) bool { + for _, q := range queries { + if q.hasRetType() { + // Only check non-struct return types. Struct definitions + // live in the query file, not the batch file. + if !q.Ret.EmitStruct() { + if hasPrefixIgnoringSliceAndPointerPrefix(q.Ret.Type(), name) { + return true + } + } + } + // Only check non-struct arg types. Struct args appear as + // the struct name in the function signature, not field types. + if !q.Arg.EmitStruct() { + for _, f := range q.Arg.Pairs() { + if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) { + return true + } + } + } + } + return false + } +} + func replaceConflictedArg(imports [][]ImportSpec, queries []Query) []Query { m := make(map[string]struct{}) for _, is := range imports { diff --git a/internal/codegen/golang/opts/options.go b/internal/codegen/golang/opts/options.go index 0d5d51c2dd..af99b78da2 100644 --- a/internal/codegen/golang/opts/options.go +++ b/internal/codegen/golang/opts/options.go @@ -45,6 +45,8 @@ type Options struct { OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"` BuildTags string `json:"build_tags,omitempty" yaml:"build_tags"` Initialisms *[]string `json:"initialisms,omitempty" yaml:"initialisms"` + EmitQueryBatch bool `json:"emit_query_batch,omitempty" yaml:"emit_query_batch"` + OutputQueryBatchFileName string `json:"output_query_batch_file_name,omitempty" yaml:"output_query_batch_file_name"` InitialismsMap map[string]struct{} `json:"-" yaml:"-"` } diff --git a/internal/codegen/golang/templates/pgx/queryBatchCode.tmpl b/internal/codegen/golang/templates/pgx/queryBatchCode.tmpl new file mode 100644 index 0000000000..5291d21723 --- /dev/null +++ b/internal/codegen/golang/templates/pgx/queryBatchCode.tmpl @@ -0,0 +1,103 @@ +{{define "queryBatchCodePgx"}} + +// QueryBatch allows queuing multiple queries to be executed in a single +// round-trip using pgx v5's QueuedQuery callback pattern. Each Queue* method +// calls pgx.Batch.Queue and registers a result callback (QueryRow, Query, or +// Exec) that is invoked when ExecuteBatch processes the batch results. +// For :exec queries, no callback is needed - errors propagate via ExecuteBatch. +// +// The Batch field is exported to allow interoperability: callers can mix +// generated Queue* calls with custom pgx batch operations on the same +// underlying pgx.Batch. +type QueryBatch struct { + Batch *pgx.Batch +} + +// NewQueryBatch creates a new QueryBatch. +func NewQueryBatch() *QueryBatch { + return &QueryBatch{ + Batch: &pgx.Batch{}, + } +} + +// ExecuteBatch sends all queued queries and closes the batch. +func (q *Queries) ExecuteBatch(ctx context.Context, {{if $.EmitMethodsWithDBArgument}}db DBTX, {{end}}batch *QueryBatch) error { + return {{if $.EmitMethodsWithDBArgument}}db{{else}}q.db{{end}}.SendBatch(ctx, batch.Batch).Close() +} + +{{range .GoQueries}} +{{if and (ne .Cmd ":copyfrom") (ne (hasPrefix .Cmd ":batch") true)}} +{{if eq .Cmd ":one"}} +// Queue{{.MethodName}} queues {{.MethodName}} for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}fn func({{.Ret.DefineType}}, bool) error) { + b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).QueryRow(func(row pgx.Row) error { + var {{.Ret.Name}} {{.Ret.Type}} + err := row.Scan({{.Ret.Scan}}) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn({{.Ret.ReturnName}}, false) + } + return err + } + return fn({{.Ret.ReturnName}}, true) + }) +} +{{end}} + +{{if eq .Cmd ":many"}} +// Queue{{.MethodName}} queues {{.MethodName}} for batch execution. +// The callback fn is called with the results when ExecuteBatch is called. +func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}fn func([]{{.Ret.DefineType}}) error) { + b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Query(func(rows pgx.Rows) error { + defer rows.Close() + {{- if $.EmitEmptySlices}} + items := []{{.Ret.DefineType}}{} + {{else}} + var items []{{.Ret.DefineType}} + {{end -}} + for rows.Next() { + var {{.Ret.Name}} {{.Ret.Type}} + if err := rows.Scan({{.Ret.Scan}}); err != nil { + return err + } + items = append(items, {{.Ret.ReturnName}}) + } + if err := rows.Err(); err != nil { + return err + } + return fn(items) + }) +} +{{end}} + +{{if eq .Cmd ":exec"}} +// Queue{{.MethodName}} queues {{.MethodName}} for batch execution. +func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}) { + b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}) +} +{{end}} + +{{if eq .Cmd ":execrows"}} +// Queue{{.MethodName}} queues {{.MethodName}} for batch execution. +// The callback fn is called with the number of rows affected when ExecuteBatch is called. +func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}fn func(int64) error) { + b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Exec(func(ct pgconn.CommandTag) error { + return fn(ct.RowsAffected()) + }) +} +{{end}} + +{{if eq .Cmd ":execresult"}} +// Queue{{.MethodName}} queues {{.MethodName}} for batch execution. +// The callback fn is called with the command tag when ExecuteBatch is called. +func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}fn func(pgconn.CommandTag) error) { + b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Exec(func(ct pgconn.CommandTag) error { + return fn(ct) + }) +} +{{end}} +{{end}} +{{end}} +{{end}} diff --git a/internal/codegen/golang/templates/template.tmpl b/internal/codegen/golang/templates/template.tmpl index afd50c01ac..9a8ce8c926 100644 --- a/internal/codegen/golang/templates/template.tmpl +++ b/internal/codegen/golang/templates/template.tmpl @@ -252,3 +252,32 @@ import ( {{- template "batchCodePgx" .}} {{end}} {{end}} + +{{define "queryBatchFile"}} +{{if .BuildTags}} +//go:build {{.BuildTags}} + +{{end}}// Code generated by sqlc. DO NOT EDIT. +{{if not .OmitSqlcVersion}}// versions: +// sqlc {{.SqlcVersion}} +{{end}}// source: {{.SourceName}} + +package {{.Package}} + +{{ if hasImports .SourceName }} +import ( + {{range imports .SourceName}} + {{range .}}{{.}} + {{end}} + {{end}} +) +{{end}} + +{{template "queryBatchCode" . }} +{{end}} + +{{define "queryBatchCode"}} +{{if .SQLDriver.IsPGX }} + {{- template "queryBatchCodePgx" .}} +{{end}} +{{end}} diff --git a/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..9a44027379 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/db.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row + SendBatch(context.Context, *pgx.Batch) pgx.BatchResults +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..bf637fb012 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type MyschemaUser struct { + ID int32 + Name string + Email string + CreatedAt pgtype.Timestamptz +} diff --git a/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..7600155aac --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,113 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5/pgconn" +) + +const archiveUser = `-- name: ArchiveUser :execresult +UPDATE myschema.users SET name = 'archived' WHERE id = $1 +` + +func (q *Queries) ArchiveUser(ctx context.Context, id int32) (pgconn.CommandTag, error) { + return q.db.Exec(ctx, archiveUser, id) +} + +const createUser = `-- name: CreateUser :one +INSERT INTO myschema.users (name, email) VALUES ($1, $2) RETURNING id, name, email, created_at +` + +type CreateUserParams struct { + Name string + Email string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (MyschemaUser, error) { + row := q.db.QueryRow(ctx, createUser, arg.Name, arg.Email) + var i MyschemaUser + err := row.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ) + return i, err +} + +const deleteUser = `-- name: DeleteUser :execrows +DELETE FROM myschema.users WHERE id = $1 +` + +func (q *Queries) DeleteUser(ctx context.Context, id int32) (int64, error) { + result, err := q.db.Exec(ctx, deleteUser, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const getUser = `-- name: GetUser :one +SELECT id, name, email, created_at FROM myschema.users WHERE id = $1 +` + +func (q *Queries) GetUser(ctx context.Context, id int32) (MyschemaUser, error) { + row := q.db.QueryRow(ctx, getUser, id) + var i MyschemaUser + err := row.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ) + return i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT id, name, email, created_at FROM myschema.users ORDER BY id +` + +func (q *Queries) ListUsers(ctx context.Context) ([]MyschemaUser, error) { + rows, err := q.db.Query(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MyschemaUser + for rows.Next() { + var i MyschemaUser + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateUser = `-- name: UpdateUser :exec +UPDATE myschema.users SET name = $1, email = $2 WHERE id = $3 +` + +type UpdateUserParams struct { + Name string + Email string + ID int32 +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { + _, err := q.db.Exec(ctx, updateUser, arg.Name, arg.Email, arg.ID) + return err +} diff --git a/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query_batch.sql.go b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query_batch.sql.go new file mode 100644 index 0000000000..fa0a9691da --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/go/query_batch.sql.go @@ -0,0 +1,129 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query_batch.sql.go + +package querytest + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// QueryBatch allows queuing multiple queries to be executed in a single +// round-trip using pgx v5's QueuedQuery callback pattern. Each Queue* method +// calls pgx.Batch.Queue and registers a result callback (QueryRow, Query, or +// Exec) that is invoked when ExecuteBatch processes the batch results. +// For :exec queries, no callback is needed - errors propagate via ExecuteBatch. +// +// The Batch field is exported to allow interoperability: callers can mix +// generated Queue* calls with custom pgx batch operations on the same +// underlying pgx.Batch. +type QueryBatch struct { + Batch *pgx.Batch +} + +// NewQueryBatch creates a new QueryBatch. +func NewQueryBatch() *QueryBatch { + return &QueryBatch{ + Batch: &pgx.Batch{}, + } +} + +// ExecuteBatch sends all queued queries and closes the batch. +func (q *Queries) ExecuteBatch(ctx context.Context, batch *QueryBatch) error { + return q.db.SendBatch(ctx, batch.Batch).Close() +} + +// QueueArchiveUser queues ArchiveUser for batch execution. +// The callback fn is called with the command tag when ExecuteBatch is called. +func (b *QueryBatch) QueueArchiveUser(id int32, fn func(pgconn.CommandTag) error) { + b.Batch.Queue(archiveUser, id).Exec(func(ct pgconn.CommandTag) error { + return fn(ct) + }) +} + +// QueueCreateUser queues CreateUser for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) QueueCreateUser(arg CreateUserParams, fn func(MyschemaUser, bool) error) { + b.Batch.Queue(createUser, arg.Name, arg.Email).QueryRow(func(row pgx.Row) error { + var i MyschemaUser + err := row.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn(i, false) + } + return err + } + return fn(i, true) + }) +} + +// QueueDeleteUser queues DeleteUser for batch execution. +// The callback fn is called with the number of rows affected when ExecuteBatch is called. +func (b *QueryBatch) QueueDeleteUser(id int32, fn func(int64) error) { + b.Batch.Queue(deleteUser, id).Exec(func(ct pgconn.CommandTag) error { + return fn(ct.RowsAffected()) + }) +} + +// QueueGetUser queues GetUser for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) QueueGetUser(id int32, fn func(MyschemaUser, bool) error) { + b.Batch.Queue(getUser, id).QueryRow(func(row pgx.Row) error { + var i MyschemaUser + err := row.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn(i, false) + } + return err + } + return fn(i, true) + }) +} + +// QueueListUsers queues ListUsers for batch execution. +// The callback fn is called with the results when ExecuteBatch is called. +func (b *QueryBatch) QueueListUsers(fn func([]MyschemaUser) error) { + b.Batch.Queue(listUsers).Query(func(rows pgx.Rows) error { + defer rows.Close() + var items []MyschemaUser + for rows.Next() { + var i MyschemaUser + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ); err != nil { + return err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return err + } + return fn(items) + }) +} + +// QueueUpdateUser queues UpdateUser for batch execution. +func (b *QueryBatch) QueueUpdateUser(arg UpdateUserParams) { + b.Batch.Queue(updateUser, arg.Name, arg.Email, arg.ID) +} diff --git a/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..3b3360cb77 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/query.sql @@ -0,0 +1,17 @@ +-- name: GetUser :one +SELECT * FROM myschema.users WHERE id = $1; + +-- name: ListUsers :many +SELECT * FROM myschema.users ORDER BY id; + +-- name: CreateUser :one +INSERT INTO myschema.users (name, email) VALUES ($1, $2) RETURNING *; + +-- name: UpdateUser :exec +UPDATE myschema.users SET name = $1, email = $2 WHERE id = $3; + +-- name: DeleteUser :execrows +DELETE FROM myschema.users WHERE id = $1; + +-- name: ArchiveUser :execresult +UPDATE myschema.users SET name = 'archived' WHERE id = $1; diff --git a/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..7fd7a7295f --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/schema.sql @@ -0,0 +1,7 @@ +CREATE SCHEMA myschema; +CREATE TABLE myschema.users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL, + created_at timestamptz +); diff --git a/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..f1cd440531 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch/postgresql/pgx/v5/sqlc.yaml @@ -0,0 +1,11 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "pgx/v5" + emit_query_batch: true diff --git a/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..8f35638eea --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/db.go @@ -0,0 +1,26 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row + SendBatch(context.Context, *pgx.Batch) pgx.BatchResults +} + +func New() *Queries { + return &Queries{} +} + +type Queries struct { +} diff --git a/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..871639874e --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/models.go @@ -0,0 +1,10 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +type User struct { + ID int32 + Name string +} diff --git a/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..afb19ec0bc --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getUser = `-- name: GetUser :one +SELECT id, name FROM users WHERE id = $1 +` + +func (q *Queries) GetUser(ctx context.Context, db DBTX, id int32) (User, error) { + row := db.QueryRow(ctx, getUser, id) + var i User + err := row.Scan(&i.ID, &i.Name) + return i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT id, name FROM users ORDER BY id +` + +func (q *Queries) ListUsers(ctx context.Context, db DBTX) ([]User, error) { + rows, err := db.Query(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query_batch.sql.go b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query_batch.sql.go new file mode 100644 index 0000000000..3afedd14cb --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/go/query_batch.sql.go @@ -0,0 +1,75 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query_batch.sql.go + +package querytest + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" +) + +// QueryBatch allows queuing multiple queries to be executed in a single +// round-trip using pgx v5's QueuedQuery callback pattern. Each Queue* method +// calls pgx.Batch.Queue and registers a result callback (QueryRow, Query, or +// Exec) that is invoked when ExecuteBatch processes the batch results. +// For :exec queries, no callback is needed - errors propagate via ExecuteBatch. +// +// The Batch field is exported to allow interoperability: callers can mix +// generated Queue* calls with custom pgx batch operations on the same +// underlying pgx.Batch. +type QueryBatch struct { + Batch *pgx.Batch +} + +// NewQueryBatch creates a new QueryBatch. +func NewQueryBatch() *QueryBatch { + return &QueryBatch{ + Batch: &pgx.Batch{}, + } +} + +// ExecuteBatch sends all queued queries and closes the batch. +func (q *Queries) ExecuteBatch(ctx context.Context, db DBTX, batch *QueryBatch) error { + return db.SendBatch(ctx, batch.Batch).Close() +} + +// QueueGetUser queues GetUser for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) QueueGetUser(id int32, fn func(User, bool) error) { + b.Batch.Queue(getUser, id).QueryRow(func(row pgx.Row) error { + var i User + err := row.Scan(&i.ID, &i.Name) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn(i, false) + } + return err + } + return fn(i, true) + }) +} + +// QueueListUsers queues ListUsers for batch execution. +// The callback fn is called with the results when ExecuteBatch is called. +func (b *QueryBatch) QueueListUsers(fn func([]User) error) { + b.Batch.Queue(listUsers).Query(func(rows pgx.Rows) error { + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return err + } + return fn(items) + }) +} diff --git a/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..c73ec35262 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/query.sql @@ -0,0 +1,5 @@ +-- name: GetUser :one +SELECT * FROM users WHERE id = $1; + +-- name: ListUsers :many +SELECT * FROM users ORDER BY id; diff --git a/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..c775404825 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/schema.sql @@ -0,0 +1,4 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); diff --git a/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..4f8f6dadcc --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_db_arg/postgresql/pgx/v5/sqlc.yaml @@ -0,0 +1,12 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "pgx/v5" + emit_query_batch: true + emit_methods_with_db_argument: true diff --git a/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..9a44027379 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/db.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row + SendBatch(context.Context, *pgx.Batch) pgx.BatchResults +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..6039374fc3 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type User struct { + ID int32 + Name string + CreatedAt pgtype.Timestamptz +} diff --git a/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..77b8b63540 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,72 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const getUser = `-- name: GetUser :one +SELECT id, name, created_at FROM users WHERE id = $1 +` + +func (q *Queries) GetUser(ctx context.Context, id int32) (User, error) { + row := q.db.QueryRow(ctx, getUser, id) + var i User + err := row.Scan(&i.ID, &i.Name, &i.CreatedAt) + return i, err +} + +const getUserCreatedAt = `-- name: GetUserCreatedAt :one +SELECT created_at FROM users WHERE id = $1 +` + +func (q *Queries) GetUserCreatedAt(ctx context.Context, id int32) (pgtype.Timestamptz, error) { + row := q.db.QueryRow(ctx, getUserCreatedAt, id) + var created_at pgtype.Timestamptz + err := row.Scan(&created_at) + return created_at, err +} + +const listUsers = `-- name: ListUsers :many +SELECT id, name, created_at FROM users ORDER BY id +` + +func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { + rows, err := q.db.Query(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name, &i.CreatedAt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateUser = `-- name: UpdateUser :exec +UPDATE users SET name = $1 WHERE id = $2 +` + +type UpdateUserParams struct { + Name string + ID int32 +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { + _, err := q.db.Exec(ctx, updateUser, arg.Name, arg.ID) + return err +} diff --git a/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/query_batch.sql.go b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/query_batch.sql.go new file mode 100644 index 0000000000..048edc5c90 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/go/query_batch.sql.go @@ -0,0 +1,98 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query_batch.sql.go + +package querytest + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" +) + +// QueryBatch allows queuing multiple queries to be executed in a single +// round-trip using pgx v5's QueuedQuery callback pattern. Each Queue* method +// calls pgx.Batch.Queue and registers a result callback (QueryRow, Query, or +// Exec) that is invoked when ExecuteBatch processes the batch results. +// For :exec queries, no callback is needed - errors propagate via ExecuteBatch. +// +// The Batch field is exported to allow interoperability: callers can mix +// generated Queue* calls with custom pgx batch operations on the same +// underlying pgx.Batch. +type QueryBatch struct { + Batch *pgx.Batch +} + +// NewQueryBatch creates a new QueryBatch. +func NewQueryBatch() *QueryBatch { + return &QueryBatch{ + Batch: &pgx.Batch{}, + } +} + +// ExecuteBatch sends all queued queries and closes the batch. +func (q *Queries) ExecuteBatch(ctx context.Context, batch *QueryBatch) error { + return q.db.SendBatch(ctx, batch.Batch).Close() +} + +// QueueGetUser queues GetUser for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) QueueGetUser(id int32, fn func(User, bool) error) { + b.Batch.Queue(getUser, id).QueryRow(func(row pgx.Row) error { + var i User + err := row.Scan(&i.ID, &i.Name, &i.CreatedAt) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn(i, false) + } + return err + } + return fn(i, true) + }) +} + +// QueueGetUserCreatedAt queues GetUserCreatedAt for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) QueueGetUserCreatedAt(id int32, fn func(pgtype.Timestamptz, bool) error) { + b.Batch.Queue(getUserCreatedAt, id).QueryRow(func(row pgx.Row) error { + var created_at pgtype.Timestamptz + err := row.Scan(&created_at) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn(created_at, false) + } + return err + } + return fn(created_at, true) + }) +} + +// QueueListUsers queues ListUsers for batch execution. +// The callback fn is called with the results when ExecuteBatch is called. +func (b *QueryBatch) QueueListUsers(fn func([]User) error) { + b.Batch.Queue(listUsers).Query(func(rows pgx.Rows) error { + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name, &i.CreatedAt); err != nil { + return err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return err + } + return fn(items) + }) +} + +// QueueUpdateUser queues UpdateUser for batch execution. +func (b *QueryBatch) QueueUpdateUser(arg UpdateUserParams) { + b.Batch.Queue(updateUser, arg.Name, arg.ID) +} diff --git a/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..6f040751d8 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/query.sql @@ -0,0 +1,11 @@ +-- name: GetUser :one +SELECT * FROM users WHERE id = $1; + +-- name: GetUserCreatedAt :one +SELECT created_at FROM users WHERE id = $1; + +-- name: ListUsers :many +SELECT * FROM users ORDER BY id; + +-- name: UpdateUser :exec +UPDATE users SET name = $1 WHERE id = $2; diff --git a/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..94cca6f384 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + created_at timestamptz +); diff --git a/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..f1cd440531 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_minimal/postgresql/pgx/v5/sqlc.yaml @@ -0,0 +1,11 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "pgx/v5" + emit_query_batch: true diff --git a/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..9a44027379 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/db.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row + SendBatch(context.Context, *pgx.Batch) pgx.BatchResults +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..2b734616bf --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "github.com/sqlc-dev/sqlc-testdata/pkg" +) + +type Account struct { + ID int32 + Name string + Balance pkg.CustomType +} diff --git a/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..cb9b34ddfe --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,77 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" + + "github.com/sqlc-dev/sqlc-testdata/pkg" +) + +const createAccount = `-- name: CreateAccount :one +INSERT INTO accounts (name, balance) VALUES ($1, $2) RETURNING id, name, balance +` + +type CreateAccountParams struct { + Name string + Balance pkg.CustomType +} + +func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) { + row := q.db.QueryRow(ctx, createAccount, arg.Name, arg.Balance) + var i Account + err := row.Scan(&i.ID, &i.Name, &i.Balance) + return i, err +} + +const getAccount = `-- name: GetAccount :one +SELECT id, name, balance FROM accounts WHERE id = $1 +` + +func (q *Queries) GetAccount(ctx context.Context, id int32) (Account, error) { + row := q.db.QueryRow(ctx, getAccount, id) + var i Account + err := row.Scan(&i.ID, &i.Name, &i.Balance) + return i, err +} + +const listAccounts = `-- name: ListAccounts :many +SELECT id, name, balance FROM accounts ORDER BY id +` + +func (q *Queries) ListAccounts(ctx context.Context) ([]Account, error) { + rows, err := q.db.Query(ctx, listAccounts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Account + for rows.Next() { + var i Account + if err := rows.Scan(&i.ID, &i.Name, &i.Balance); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateBalance = `-- name: UpdateBalance :exec +UPDATE accounts SET balance = $1 WHERE id = $2 +` + +type UpdateBalanceParams struct { + Balance pkg.CustomType + ID int32 +} + +func (q *Queries) UpdateBalance(ctx context.Context, arg UpdateBalanceParams) error { + _, err := q.db.Exec(ctx, updateBalance, arg.Balance, arg.ID) + return err +} diff --git a/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/query_batch.sql.go b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/query_batch.sql.go new file mode 100644 index 0000000000..13f20160fa --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/go/query_batch.sql.go @@ -0,0 +1,97 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query_batch.sql.go + +package querytest + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" +) + +// QueryBatch allows queuing multiple queries to be executed in a single +// round-trip using pgx v5's QueuedQuery callback pattern. Each Queue* method +// calls pgx.Batch.Queue and registers a result callback (QueryRow, Query, or +// Exec) that is invoked when ExecuteBatch processes the batch results. +// For :exec queries, no callback is needed - errors propagate via ExecuteBatch. +// +// The Batch field is exported to allow interoperability: callers can mix +// generated Queue* calls with custom pgx batch operations on the same +// underlying pgx.Batch. +type QueryBatch struct { + Batch *pgx.Batch +} + +// NewQueryBatch creates a new QueryBatch. +func NewQueryBatch() *QueryBatch { + return &QueryBatch{ + Batch: &pgx.Batch{}, + } +} + +// ExecuteBatch sends all queued queries and closes the batch. +func (q *Queries) ExecuteBatch(ctx context.Context, batch *QueryBatch) error { + return q.db.SendBatch(ctx, batch.Batch).Close() +} + +// QueueCreateAccount queues CreateAccount for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) QueueCreateAccount(arg CreateAccountParams, fn func(Account, bool) error) { + b.Batch.Queue(createAccount, arg.Name, arg.Balance).QueryRow(func(row pgx.Row) error { + var i Account + err := row.Scan(&i.ID, &i.Name, &i.Balance) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn(i, false) + } + return err + } + return fn(i, true) + }) +} + +// QueueGetAccount queues GetAccount for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) QueueGetAccount(id int32, fn func(Account, bool) error) { + b.Batch.Queue(getAccount, id).QueryRow(func(row pgx.Row) error { + var i Account + err := row.Scan(&i.ID, &i.Name, &i.Balance) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn(i, false) + } + return err + } + return fn(i, true) + }) +} + +// QueueListAccounts queues ListAccounts for batch execution. +// The callback fn is called with the results when ExecuteBatch is called. +func (b *QueryBatch) QueueListAccounts(fn func([]Account) error) { + b.Batch.Queue(listAccounts).Query(func(rows pgx.Rows) error { + defer rows.Close() + var items []Account + for rows.Next() { + var i Account + if err := rows.Scan(&i.ID, &i.Name, &i.Balance); err != nil { + return err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return err + } + return fn(items) + }) +} + +// QueueUpdateBalance queues UpdateBalance for batch execution. +func (b *QueryBatch) QueueUpdateBalance(arg UpdateBalanceParams) { + b.Batch.Queue(updateBalance, arg.Balance, arg.ID) +} diff --git a/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..a548f86de0 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/query.sql @@ -0,0 +1,11 @@ +-- name: GetAccount :one +SELECT * FROM accounts WHERE id = $1; + +-- name: ListAccounts :many +SELECT * FROM accounts ORDER BY id; + +-- name: CreateAccount :one +INSERT INTO accounts (name, balance) VALUES ($1, $2) RETURNING *; + +-- name: UpdateBalance :exec +UPDATE accounts SET balance = $1 WHERE id = $2; diff --git a/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..8a73846bd4 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + balance NUMERIC NOT NULL +); diff --git a/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..bc3e85f0e4 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/sqlc.yaml @@ -0,0 +1,16 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "pgx/v5" + emit_query_batch: true + overrides: + - db_type: "pg_catalog.numeric" + go_type: + import: "github.com/sqlc-dev/sqlc-testdata/pkg" + type: "CustomType" diff --git a/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/batch.go b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/batch.go new file mode 100644 index 0000000000..cd77731a88 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/batch.go @@ -0,0 +1,66 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: batch.go + +package querytest + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" +) + +var ( + ErrBatchAlreadyClosed = errors.New("batch already closed") +) + +const batchUpdateUser = `-- name: BatchUpdateUser :batchexec +UPDATE users SET name = $1 WHERE id = $2 +` + +type BatchUpdateUserBatchResults struct { + br pgx.BatchResults + tot int + closed bool +} + +type BatchUpdateUserParams struct { + Name string + ID int32 +} + +func (q *Queries) BatchUpdateUser(ctx context.Context, arg []BatchUpdateUserParams) *BatchUpdateUserBatchResults { + batch := &pgx.Batch{} + for _, a := range arg { + vals := []interface{}{ + a.Name, + a.ID, + } + batch.Queue(batchUpdateUser, vals...) + } + br := q.db.SendBatch(ctx, batch) + return &BatchUpdateUserBatchResults{br, len(arg), false} +} + +func (b *BatchUpdateUserBatchResults) Exec(f func(int, error)) { + defer b.br.Close() + for t := 0; t < b.tot; t++ { + if b.closed { + if f != nil { + f(t, ErrBatchAlreadyClosed) + } + continue + } + _, err := b.br.Exec() + if f != nil { + f(t, err) + } + } +} + +func (b *BatchUpdateUserBatchResults) Close() error { + b.closed = true + return b.br.Close() +} diff --git a/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..9a44027379 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/db.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row + SendBatch(context.Context, *pgx.Batch) pgx.BatchResults +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..871639874e --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/models.go @@ -0,0 +1,10 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +type User struct { + ID int32 + Name string +} diff --git a/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..e8cbb2fda8 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,54 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM users WHERE id = $1 +` + +func (q *Queries) DeleteUser(ctx context.Context, id int32) error { + _, err := q.db.Exec(ctx, deleteUser, id) + return err +} + +const getUser = `-- name: GetUser :one +SELECT id, name FROM users WHERE id = $1 +` + +func (q *Queries) GetUser(ctx context.Context, id int32) (User, error) { + row := q.db.QueryRow(ctx, getUser, id) + var i User + err := row.Scan(&i.ID, &i.Name) + return i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT id, name FROM users ORDER BY id +` + +func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { + rows, err := q.db.Query(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/query_batch.sql.go b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/query_batch.sql.go new file mode 100644 index 0000000000..a19acb9e49 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/go/query_batch.sql.go @@ -0,0 +1,80 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query_batch.sql.go + +package querytest + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" +) + +// QueryBatch allows queuing multiple queries to be executed in a single +// round-trip using pgx v5's QueuedQuery callback pattern. Each Queue* method +// calls pgx.Batch.Queue and registers a result callback (QueryRow, Query, or +// Exec) that is invoked when ExecuteBatch processes the batch results. +// For :exec queries, no callback is needed - errors propagate via ExecuteBatch. +// +// The Batch field is exported to allow interoperability: callers can mix +// generated Queue* calls with custom pgx batch operations on the same +// underlying pgx.Batch. +type QueryBatch struct { + Batch *pgx.Batch +} + +// NewQueryBatch creates a new QueryBatch. +func NewQueryBatch() *QueryBatch { + return &QueryBatch{ + Batch: &pgx.Batch{}, + } +} + +// ExecuteBatch sends all queued queries and closes the batch. +func (q *Queries) ExecuteBatch(ctx context.Context, batch *QueryBatch) error { + return q.db.SendBatch(ctx, batch.Batch).Close() +} + +// QueueDeleteUser queues DeleteUser for batch execution. +func (b *QueryBatch) QueueDeleteUser(id int32) { + b.Batch.Queue(deleteUser, id) +} + +// QueueGetUser queues GetUser for batch execution. +// The callback fn is called when ExecuteBatch is called. The second parameter +// is false if the row was not found (no error is returned in this case). +func (b *QueryBatch) QueueGetUser(id int32, fn func(User, bool) error) { + b.Batch.Queue(getUser, id).QueryRow(func(row pgx.Row) error { + var i User + err := row.Scan(&i.ID, &i.Name) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return fn(i, false) + } + return err + } + return fn(i, true) + }) +} + +// QueueListUsers queues ListUsers for batch execution. +// The callback fn is called with the results when ExecuteBatch is called. +func (b *QueryBatch) QueueListUsers(fn func([]User) error) { + b.Batch.Queue(listUsers).Query(func(rows pgx.Rows) error { + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return err + } + return fn(items) + }) +} diff --git a/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..a70644946e --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/query.sql @@ -0,0 +1,11 @@ +-- name: GetUser :one +SELECT * FROM users WHERE id = $1; + +-- name: ListUsers :many +SELECT * FROM users ORDER BY id; + +-- name: DeleteUser :exec +DELETE FROM users WHERE id = $1; + +-- name: BatchUpdateUser :batchexec +UPDATE users SET name = $1 WHERE id = $2; diff --git a/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..c775404825 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/schema.sql @@ -0,0 +1,4 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); diff --git a/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..f1cd440531 --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_with_batch/postgresql/pgx/v5/sqlc.yaml @@ -0,0 +1,11 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "pgx/v5" + emit_query_batch: true From 16e715f229953b1367ea10817c102c03c32f784a Mon Sep 17 00:00:00 2001 From: Josh Giles Date: Sun, 15 Mar 2026 16:32:46 -0400 Subject: [PATCH 2/2] Only run emit_query_batch_overrides in base context. --- .../emit_query_batch_overrides/postgresql/pgx/v5/exec.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/exec.json diff --git a/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/exec.json b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/emit_query_batch_overrides/postgresql/pgx/v5/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +}