README
¶
sqlc-gen-go
A sqlc plugin that generates type-safe Go database access code from SQL. Runs as a WASM plugin (recommended) or standalone binary.
Usage
version: '2'
plugins:
- name: golang
wasm:
url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v1.7.5/sqlc-gen-go.wasm
sha256: 372861f69cf9e49509e1b6f8395620bfc7cc2d35732a656644055d3c720d1aa3
sql:
- schema: schema.sql
queries: query.sql
engine: postgresql
codegen:
- plugin: golang
out: db
options:
package: db
sql_package: pgx/v5
Building from source
make all # produces bin/sqlc-gen-go and bin/sqlc-gen-go.wasm
To use a local build:
plugins:
- name: golang
wasm:
url: file:///path/to/bin/sqlc-gen-go.wasm
sha256: "" # optional since sqlc v1.24.0
Migrating from sqlc's built-in Go codegen
Two changes are required:
- Add a top-level
pluginsentry pointing to the WASM plugin. - Replace
gen.gowithcodegen, referencing the plugin by name. Move all options into theoptionsblock;outmoves up one level.
Before:
sql:
- engine: postgresql
gen:
go:
package: db
out: db
emit_json_tags: true
After:
plugins:
- name: golang
wasm:
url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v1.7.4/sqlc-gen-go.wasm
sha256: 372861f69cf9e49509e1b6f8395620bfc7cc2d35732a656644055d3c720d1aa3
sql:
- engine: postgresql
codegen:
- plugin: golang
out: db
options:
package: db
emit_json_tags: true
Global overrides/go move to options/<plugin-name>:
options:
golang:
rename:
id: "Identifier"
overrides:
- db_type: "timestamptz"
nullable: true
engine: postgresql
go_type:
import: "gopkg.in/guregu/null.v4"
package: "null"
type: "Time"
Advanced Options
emit_per_file_queries
Each SQL source file gets its own struct and interface instead of a shared Queries/Querier.
| SQL file | Struct | Interface |
|---|---|---|
users.sql |
UsersQueries |
UsersQuerier |
user_orders.sql |
UserOrdersQueries |
UserOrdersQuerier |
options:
emit_interface: true
emit_per_file_queries: true
- Each
*.sql.gocontains its own struct, constructor, methods, and interface. db.goonly keepsDBTX;querier.gois not generated.- Incompatible with
emit_prepared_queries.
emit_err_nil_if_no_rows
:one queries return nil, nil instead of nil, sql.ErrNoRows when no row is found.
options:
emit_err_nil_if_no_rows: true
emit_tracing
Injects custom code at the start of every query method. Supports {{.MethodName}} and {{.StructName}} template variables.
options:
emit_tracing:
import: "go.opentelemetry.io/otel"
package: "otel"
code:
- "ctx, span := otel.Tracer(\"{{.StructName}}\").Start(ctx, \"{{.MethodName}}\")"
- "defer span.End()"
| Field | Description |
|---|---|
import |
Import path of the tracing package |
package |
Package alias (if different from the last path segment) |
code |
Lines to inject; each is a Go template |
emit_dynamic_filter
Enables optional WHERE/ORDER BY clauses controlled at runtime via -- :if @param annotations in SQL.
When a parameter is marked with :if, the generated code:
- Makes the parameter a pointer (
*T) in the params struct —nilmeans "skip this clause" - Adds a
boolfield for flag-only parameters (e.g. ORDER BY toggles that appear only in:ifannotations, not ascol = $Npredicate values) - Calls the generated
DynamicSQL()helper at runtime to strip inactive lines and renumber placeholders
options:
emit_dynamic_filter: true
SQL annotations
-- name: SearchUsers :many
SELECT * FROM users
WHERE
1 = 1
AND email = @email -- :if @email -- omit if email is nil
AND phone = @phone -- :if @phone -- omit if phone is nil
AND EXISTS ( -- :if @has_orders -- flag-only bool
SELECT 1 FROM orders
WHERE orders.user_id = users.id
AND orders.created_at >= @orders_since -- :if @orders_since
)
ORDER BY id ASC;
-- name: SearchUsersOrdered :many
SELECT * FROM users
WHERE
1 = 1
AND email = @email -- :if @email
ORDER BY
created_at DESC, -- :if @order_created_at_desc
name ASC, -- :if @order_name_asc
id ASC;
Note: For complex cases with ORDER BY, we recommend using CASE statements because they do not affect query performance like WHERE clauses:
CASE WHEN @id_asc::bool THEN id END ASC, -- :if @id_asc
CASE WHEN @id_desc::bool THEN id END DESC -- :if @id_desc
Generated Go
type SearchUsersParams struct {
Email *string // nil → clause skipped
Phone *string // nil → clause skipped
OrdersSince *time.Time // nil → clause skipped
HasOrders bool // false → EXISTS block skipped
}
func (q *SearchQueries) SearchUsers(ctx context.Context, db DBTX, arg SearchUsersParams) ([]*User, error) {
...
dynQuery, dynArgs := DynamicSQL(SearchUsers, []any{arg.Email, arg.Phone, arg.OrdersSince, arg.HasOrders})
rows, err := db.Query(ctx, dynQuery, dynArgs...)
...
}
Annotation rules
| Syntax | Behaviour |
|---|---|
AND col = @param -- :if @param |
Inline — skip line if param is nil/false |
AND (a = @a AND b = @b) -- :if @a @b |
Multi-condition — skip if any param is inactive |
-- :if @flag (standalone) |
Block — skip the next line if flag is false |
A DynamicSQL helper is emitted into dynfilter.go in the output package. After filtering, remaining $N placeholders are renumbered sequentially and the args slice is trimmed to match, preventing "expected N arguments, got M" errors.
go_generate_mock
Adds a //go:generate directive for mock generation. $GOFILE expands to the current filename at generate time.
- When
emit_per_file_queriesis enabled: the directive is added to each*.sql.gofile. - Otherwise: the directive is added only to the
querier.gofile.
options:
go_generate_mock: "mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock"
Running go generate ./... produces a mock per SQL file (with emit_per_file_queries):
| Source | Mock |
|---|---|
users.sql.go |
mock/users.sql.go |
orders.sql.go |
mock/orders.sql.go |