Skip to content

Releases: vtuanjs/sqlc-gen-go

v2.3.0-stable

07 Apr 10:13

Choose a tag to compare

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

06 Apr 07:44

Choose a tag to compare

Enhance performance + Test cov 68.1%

Full Changelog: v2.1.0-stable...v2.2.0-stable

v2.1.0-stable

06 Apr 04:29

Choose a tag to compare

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 a dynCompiledQuery, then Build() is called per request (no per-call string scanning)
  • Manual — hand-written strings.Builder with fmt.Fprintf per 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 DynamicSQL and matches manual performance
    for the no-optional case (151 ns vs 121 ns manual, vs 2,209 ns DynamicSQL).
  • For the all-optional large query, PreCompiled is actually 2.3x faster than manual
    (817 ns vs 1,919 ns), because manual uses fmt.Fprintf per condition while
    Build writes 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; PreCompiled overhead is ~150–800 ns —
    effectively free in practice.

v2.0.0-stable

05 Apr 15:40
Immutable release. Only release title and notes can be modified.

Choose a tag to compare

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.go contains its own struct, constructor, methods, and interface.
  • db.go only keeps DBTX; querier.go is 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 — nil means "skip this clause"
  • Adds a bool field for flag-only parameters (e.g. ORDER BY toggles that appear only in :if annotations, not as col = $N predicate 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
  id ASC,  -- :if @id_asc
  id 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_queries is enabled: the directive is added to each *.sql.go file.
  • Otherwise: the directive is added only to the querier.go file.
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

05 Apr 15:26
Immutable release. Only release title and notes can be modified.

Choose a tag to compare

Full Changelog: v1.7.5...v1.7.6

v1.7.5

05 Apr 13:18
Immutable release. Only release title and notes can be modified.

Choose a tag to compare

Full Changelog: v1.7.4...v1.7.5

v1.7.4

04 Apr 17:08

Choose a tag to compare

Full Changelog: v1.7.3...v1.7.4

v1.7.3

04 Apr 13:01
47a7214

Choose a tag to compare

What's Changed

Full Changelog: v1.7.2...v1.7.3

v1.7.2

04 Mar 02:15

Choose a tag to compare

Full Changelog: v1.7.1...v1.7.2

v1.7.1

03 Mar 15:16

Choose a tag to compare

Full Changelog: v1.7.0...v1.7.1