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/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"] +} 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