Releases: vtuanjs/sqlc-gen-go
v2.3.0-stable
Feature: Support top-level block annotation for emit_dynamic_filter
A standalone -- :if @flag annotation followed by a line that opens a paren block (e.g. AND EXISTS () now skips the entire block atomically, not just the opening line.
v2.2.0-stable
Enhance performance + Test cov 68.1%
Full Changelog: v2.1.0-stable...v2.2.0-stable
v2.1.0-stable
Full Changelog: v2.0.0-stable...v2.1.0-stable
DynamicSQL vs PreCompiled vs Manual benchmark
Compares three approaches for dynamic SQL construction:
DynamicSQL— one-shot helper; parses the annotated SQL on every call (backward compat)PreCompiled— generated code path; SQL is parsed once at package init into adynCompiledQuery, thenBuild()is called per request (no per-call string scanning)- Manual — hand-written
strings.Builderwithfmt.Fprintfper condition
Two query sizes are tested:
- Small:
SearchUsers— 1 required param + 4 optional conditions - Large: 1 required param + 20 optional conditions
Run
cd example
go test ./bench -bench=. -benchmem -count=3 -run='^$'Results (Intel Core i7-11800H @ 2.30GHz)
Small query (5 params, 4 optional)
| Benchmark | ns/op | req/s | B/op | allocs/op |
|---|---|---|---|---|
| DynamicSQL — no optional | 2,285 | ~438 K | 3,688 | 46 |
| PreCompiled — no optional | 180 | ~5.56 M | 304 | 3 |
| Manual — no optional | 138 | ~7.25 M | 368 | 5 |
| DynamicSQL — all optional | 2,478 | ~403 K | 4,168 | 49 |
| PreCompiled — all optional | 409 | ~2.44 M | 784 | 6 |
| Manual — all optional | 442 | ~2.26 M | 688 | 6 |
Large query (21 params, 20 optional)
| Benchmark | ns/op | req/s | B/op | allocs/op |
|---|---|---|---|---|
| DynamicSQL — no optional | 5,375 | ~186 K | 7,472 | 119 |
| PreCompiled — no optional | 226 | ~4.43 M | 304 | 3 |
| Manual — no optional | 198 | ~5.05 M | 640 | 5 |
| DynamicSQL — all optional | 6,843 | ~146 K | 9,552 | 126 |
| PreCompiled — all optional | 944 | ~1.06 M | 2,384 | 10 |
| Manual — all optional | 2,473 | ~404 K | 1,921 | 27 |
How it works
When emit_dynamic_filter: true is set, the generator now emits a package-level
pre-compiled query variable alongside each dynamic-filter query:
// Generated once at package init — zero per-call parsing cost
var _searchUsersDynQ = dynCompile(SearchUsers)
func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]User, error) {
dynQuery, dynArgs := _searchUsersDynQ.Build([]any{arg.Name, arg.Email, arg.Phone, ...})
rows, err := q.db.Query(ctx, dynQuery, dynArgs...)
// ...
}dynCompile parses the -- :if $N markers once into a list of pre-split segments.
Build then just iterates those segments, checks conditions with indexed array lookups,
and writes the pre-split string parts — no regex, no string scanning.
DynamicSQL(sql, args) is kept for backward compatibility and one-off use;
it still parses on every call.
Takeaways
- PreCompiled is ~14–25x faster than
DynamicSQLand matches manual performance
for the no-optional case (151 ns vs 121 ns manual, vs 2,209 nsDynamicSQL). - For the all-optional large query, PreCompiled is actually 2.3x faster than manual
(817 ns vs 1,919 ns), because manual usesfmt.Fprintfper condition while
Buildwrites pre-split string literals directly. - Allocations drop from 46–126 down to 3–10, matching or beating manual.
- A typical DB round-trip is ~1 ms;
PreCompiledoverhead is ~150–800 ns —
effectively free in practice.
v2.0.0-stable
Stable version
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: trueemit_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: trueSQL 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
id ASC, -- :if @id_asc
id DESC -- :if @id_descGenerated 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 |
Test Coverage
You can see all tests at: https://github.com/vtuanjs/sqlc-gen-go/tree/main/example
v1.7.6
Full Changelog: v1.7.5...v1.7.6
v1.7.5
Full Changelog: v1.7.4...v1.7.5
v1.7.4
Full Changelog: v1.7.3...v1.7.4
v1.7.3
v1.7.2
Full Changelog: v1.7.1...v1.7.2
v1.7.1
Full Changelog: v1.7.0...v1.7.1