Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. Each `Queue*` method accepts destination pointers where results are written when `ExecuteBatch` is called. 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`:
Expand All @@ -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`:
Expand Down Expand Up @@ -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. Each `Queue*` method accepts destination pointers where results are written when `ExecuteBatch` is called. Only supported with `sql_package: pgx/v5`. Defaults to `false`.
- `build_tags`:
- If set, add a `//go:build <build_tags>` directive at the beginning of each generated Go file.
- `json_tags_case_style`:
Expand All @@ -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`:
Expand Down
63 changes: 63 additions & 0 deletions docs/reference/query-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,69 @@ 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 with
`Queue*` methods for each regular query (`:one`, `:many`, `:exec`, `:execrows`,
`:execresult`). Each `Queue*` method accepts destination pointers where results
are written when `ExecuteBatch` is called. All queued queries are sent in a
single round-trip.

__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()

var user db.User
var found bool
batch.QueueGetUser(userID, &user, &found)

var users []db.User
batch.QueueListUsers(&users)

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

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

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)
Expand Down
22 changes: 19 additions & 3 deletions internal/codegen/golang/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type tmplCtx struct {
EmitAllEnumValues bool
UsesCopyFrom bool
UsesBatch bool
EmitQueryBatch bool
OmitSqlcVersion bool
BuildTags string
WrapErrors bool
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down
65 changes: 65 additions & 0 deletions internal/codegen/golang/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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))
}
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions internal/codegen/golang/opts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
}
Expand Down
100 changes: 100 additions & 0 deletions internal/codegen/golang/templates/pgx/queryBatchCode.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{{define "queryBatchCodePgx"}}

// QueryBatch allows queuing multiple queries to be executed in a single
// round-trip using pgx v5's batch API. Each Queue* method calls
// pgx.Batch.Queue and registers a result handler that writes to the provided
// destination pointer(s) when ExecuteBatch processes the batch results.
// For :exec queries, no destination 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 result is written to dest when ExecuteBatch is called.
// If no row is found, *ok is set to false (no error is returned in this case).
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}dest *{{.Ret.DefineType}}, ok *bool) {
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) {
*ok = false
return nil
}
return err
}
*dest = {{.Ret.ReturnName}}
*ok = true
return nil
})
}
{{end}}

{{if eq .Cmd ":many"}}
// Queue{{.MethodName}} queues {{.MethodName}} for batch execution.
// The results are appended to *dest when ExecuteBatch is called.
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}dest *[]{{.Ret.DefineType}}) {
b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Query(func(rows pgx.Rows) error {
defer rows.Close()
for rows.Next() {
var {{.Ret.Name}} {{.Ret.Type}}
if err := rows.Scan({{.Ret.Scan}}); err != nil {
return err
}
*dest = append(*dest, {{.Ret.ReturnName}})
}
return rows.Err()
})
}
{{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 number of rows affected is written to dest when ExecuteBatch is called.
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}dest *int64) {
b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Exec(func(ct pgconn.CommandTag) error {
*dest = ct.RowsAffected()
return nil
})
}
{{end}}

{{if eq .Cmd ":execresult"}}
// Queue{{.MethodName}} queues {{.MethodName}} for batch execution.
// The command tag is written to dest when ExecuteBatch is called.
func (b *QueryBatch) Queue{{.MethodName}}({{.Arg.Pair}}{{if .Arg.Pair}}, {{end}}dest *pgconn.CommandTag) {
b.Batch.Queue({{.ConstantName}}, {{.Arg.Params}}).Exec(func(ct pgconn.CommandTag) error {
*dest = ct
return nil
})
}
{{end}}
{{end}}
{{end}}
{{end}}
29 changes: 29 additions & 0 deletions internal/codegen/golang/templates/template.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Loading
Loading