Skip to content
gohacks.dev
Databases

An sqlc Cheat Sheet for Queries Config and Overrides

An sqlc cheat sheet for Go and PostgreSQL: query annotations, parameters, optional filters, arrays, enums, embed, config options and commands, each run

8 min read
Summarize in AI
Flat illustration of a database beside the table it holds.

This is a reference for the sqlc settings you reach for every week, with the program that ran each one. The schema is authors, books and a status enum, on PostgreSQL 18.6. The explanations are in the sqlc tutorial, query annotations and config. For where it gets awkward, see limitations. The same kind of sheet for the other tools is the GORM cheat sheet and the Ent cheat sheet.

Tested with Go 1.25.6, sqlc v1.30.0, pgx v5.11.0, PostgreSQL 18.6 in Docker, macOS on Apple Silicon (arm64)

The project

schema.sqlText
create type status as enum ('draft', 'live');
create table authors (
  id    bigserial primary key,
  name  text not null,
  email text not null unique,
  bio   text
);
create table books (
  id        bigserial primary key,
  author_id bigint not null references authors(id),
  title     text not null,
  pages     int not null check (pages > 0),
  status    status not null default 'draft'
);
query.sqlText
-- name: CreateAuthor :one
insert into authors (name, email, bio) values ($1, $2, $3) returning *;

-- name: GetAuthor :one
select * from authors where id = $1;

-- name: ListBooks :many
select * from books where author_id = $1 order by id;

-- name: BooksByStatus :many
select id, title from books where status = sqlc.arg(status)::status order by id;

-- name: SearchBooks :many
select id, title from books
where (sqlc.narg('author_id')::bigint is null or author_id = sqlc.narg('author_id'))
  and (sqlc.narg('min_pages')::int is null or pages >= sqlc.narg('min_pages'))
order by id;

-- name: BooksByIDs :many
select id, title from books where id = any($1::bigint[]) order by id;

-- name: AddBook :one
insert into books (author_id, title, pages) values ($1, $2, $3) returning id;

-- name: Publish :execrows
update books set status = 'live' where id = $1 and status = 'draft';

-- name: RenameAuthor :exec
update authors set name = $2 where id = $1;

-- name: UpsertAuthor :one
insert into authors (name, email) values ($1, $2)
on conflict (email) do update set name = excluded.name
returning id;

-- name: CountByStatus :many
select status, count(*) as n from books group by status order by status;

-- name: BookWithAuthor :one
select sqlc.embed(books), sqlc.embed(authors) from books join authors on authors.id = books.author_id where books.id = $1;
sqlc.yamlText
version: "2"
sql:
  - engine: postgresql
    schema: schema.sql
    queries: query.sql
    gen:
      go:
        package: db
        out: db
        sql_package: pgx/v5
        emit_json_tags: true
        emit_empty_slices: true
        emit_pointers_for_null_types: true
main.goGo
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"

	"example.test/lab/db"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

func must(err error) {
	if err != nil {
		log.SetFlags(0)
		log.Fatal(err)
	}
}

func main() {
	ctx := context.Background()
	base := "postgres://postgres:[email protected]:55432/lab?sslmode=disable"
	admin, err := pgxpool.New(ctx, base)
	must(err)
	ddl, _ := os.ReadFile("schema.sql")
	_, err = admin.Exec(ctx, "drop schema if exists l0705 cascade; create schema l0705; set search_path = l0705; "+string(ddl))
	must(err)
	pool, err := pgxpool.New(ctx, base+"&search_path=l0705")
	must(err)
	q := db.New(pool)

	bio := "writes Go"
	ada, err := q.CreateAuthor(ctx, db.CreateAuthorParams{Name: "Ada", Email: "[email protected]", Bio: &bio})
	must(err)
	bob, _ := q.CreateAuthor(ctx, db.CreateAuthorParams{Name: "Bob", Email: "[email protected]"})
	fmt.Println(":one with returning *:", ada.ID, ada.Name, "bio:", *ada.Bio, "| nil bio is a nil pointer:", bob.Bio == nil)

	for i, pages := range []int32{120, 300, 450} {
		id, err := q.AddBook(ctx, db.AddBookParams{AuthorID: ada.ID, Title: fmt.Sprintf("Book %d", i+1), Pages: pages})
		must(err)
		_ = id
	}
	books, _ := q.ListBooks(ctx, ada.ID)
	none, _ := q.ListBooks(ctx, bob.ID)
	fmt.Println(":many:", len(books), "books | empty result is a non nil slice:", none != nil, len(none))

	n, _ := q.Publish(ctx, books[1].ID)
	again, _ := q.Publish(ctx, books[1].ID)
	fmt.Println(":execrows publish twice:", n, again)

	live, _ := q.BooksByStatus(ctx, db.StatusLive)
	fmt.Println("enum parameter:", len(live), "live |", db.StatusLive, db.StatusDraft)

	min := int32(200)
	sr, _ := q.SearchBooks(ctx, db.SearchBooksParams{AuthorID: &ada.ID, MinPages: &min})
	all, _ := q.SearchBooks(ctx, db.SearchBooksParams{})
	fmt.Println("narg filters: with both", len(sr), "| with none", len(all))

	byIDs, _ := q.BooksByIDs(ctx, []int64{books[0].ID, books[2].ID})
	fmt.Println("any($1::bigint[]):", len(byIDs))

	must(q.RenameAuthor(ctx, db.RenameAuthorParams{ID: ada.ID, Name: "Ada L"}))
	id2, _ := q.UpsertAuthor(ctx, db.UpsertAuthorParams{Name: "Ada Lovelace", Email: "[email protected]"})
	got, _ := q.GetAuthor(ctx, ada.ID)
	fmt.Println(":exec and upsert:", id2 == ada.ID, got.Name)

	counts, _ := q.CountByStatus(ctx)
	for _, c := range counts {
		fmt.Println("group by:", c.Status, c.N)
	}

	bw, _ := q.BookWithAuthor(ctx, books[0].ID)
	fmt.Println("sqlc.embed:", bw.Book.Title, "by", bw.Author.Name)

	_, err = q.GetAuthor(ctx, 999)
	fmt.Println("missing row is pgx.ErrNoRows:", errors.Is(err, pgx.ErrNoRows))

	tx, _ := pool.Begin(ctx)
	qtx := q.WithTx(tx)
	qtx.RenameAuthor(ctx, db.RenameAuthorParams{ID: ada.ID, Name: "rolled back"})
	tx.Rollback(ctx)
	back, _ := q.GetAuthor(ctx, ada.ID)
	fmt.Println("WithTx then rollback:", back.Name)
}
Shell
set -e
~/go/bin/sqlc generate
go mod tidy > /dev/null 2>&1
go run .
Text
:one with returning *: 1 Ada bio: writes Go | nil bio is a nil pointer: true
:many: 3 books | empty result is a non nil slice: true 0
:execrows publish twice: 1 0
enum parameter: 1 live | live draft
narg filters: with both 2 | with none 3
any($1::bigint[]): 2
:exec and upsert: true Ada Lovelace
group by: draft 2
group by: live 1
sqlc.embed: Book 1 by Ada Lovelace
missing row is pgx.ErrNoRows: true
WithTx then rollback: Ada Lovelace

Commands

NeedCommand
Installgo install github.com/sqlc-dev/sqlc/cmd/[email protected]
Generate Gosqlc generate
Check without writing filessqlc compile
Run vet rulessqlc vet
Fail if generated code is stalesqlc diff
Start a projectsqlc init

Query annotations

AnnotationGenerated method returnsIn the run
:oneone row, pgx.ErrNoRows if noneCreateAuthor returned the new author; a missing id gave ErrNoRows
:manya sliceListBooks returned 3 for Ada and an empty slice for Bob
:execerrorRenameAuthor
:execrowsrows affectedPublish returned 1 then 0
:execresultthe driver's resultnot used here
:copyfromrows copied (pgx)covered in batch and COPY
:batchexec, :batchone, :batchmanypgx batch (pgx only)same article

The output :execrows publish twice: 1 0 is the pattern for "idempotent update": the where status = 'draft' clause makes the second call a no-op and the count says so.

Parameters

NeedSQLGenerated
Positionalwhere id = $1a plain argument
Namedwhere id = sqlc.arg(id)an argument named id
Nullable namedsqlc.narg('author_id')a pointer or pgtype type
Cast to fix a typesqlc.arg(status)::statusthe enum type
Arrayany($1::bigint[])[]int64
Several parametersvalues ($1, $2, $3)a Params struct

Name parameters. Without names, two or more parameters become a struct with fields called Column1, Column2. The optional filter pattern:

where (sqlc.narg('author_id')::bigint is null or author_id = sqlc.narg('author_id'))

passed with both values set, SearchBooks returned 2 books, and with both nil, all 3 (narg filters: with both 2 | with none 3). The array form took []int64{...} and returned 2 rows (any($1::bigint[]): 2).

Result types

SQL typeGo typeNote
bigint, integerint64, int32
textstring
nullable textpgtype.Text, or *string with emit_pointers_for_null_typesthe run used pointers
timestamptzpgtype.Timestamptzoverride to time.Time (overrides)
uuidpgtype.UUIDoverride to google/uuid
jsonb[]byteoverride to a struct
enuma named string type with constantsdb.StatusLive, db.StatusDraft
array_agg(x)interface{}cast: array_agg(x)::bigint[]
count(*)int64

In the run, Bob's missing bio came back as a nil pointer (nil bio is a nil pointer: true), Ada's as *string with the text, and the enum passed as a parameter (db.StatusLive) selected 1 live book.

Joins and embedding

NeedSQL
All columns of one tableselect books.* from ... or select * from books
Both tables as nested structsselect sqlc.embed(books), sqlc.embed(authors) from ...
Explicit, flatselect b.id as book_id, a.name as author_name ...

The embed query returned a BookWithAuthorRow with Book and Author fields: sqlc.embed: Book 1 by Ada Lovelace. Avoid embed on the nullable side of a left join, which fails to scan a NULL (joins).

Transactions and errors

NeedCode
Begintx, _ := pool.Begin(ctx)
Bind queriesqtx := q.WithTx(tx)
Endtx.Commit(ctx) or defer tx.Rollback(ctx)
Not founderrors.Is(err, pgx.ErrNoRows)
Unique violationerrors.As(err, &pgErr) with pgErr.Code == "23505"

The run renamed an author inside a transaction and rolled back: WithTx then rollback: Ada Lovelace, the name from before. The missing row test printed true.

sqlc.yaml options

OptionEffect
sql_package: pgx/v5generate for pgx, with pgtype and batch support
sql_package: database/sqlgenerate for the standard library
emit_json_tags: trueadd json tags to structs
emit_pointers_for_null_types: true*string instead of pgtype.Text
emit_empty_slices: true:many returns []T{} not nil
emit_interface: truegenerate a Querier interface (for mocks)
emit_exact_table_names: truedo not singularise table names
emit_prepared_queries: trueprepare statements (database/sql)
overridesmap types (guide)
renamerename generated fields

emit_empty_slices shows in the output: empty result is a non nil slice: true 0. A nil slice marshals to null in JSON and an empty one to [], which is the reason to turn it on for API code.

A project layout that scales

Keep SQL in one directory per bounded area (sql/schema, sql/queries), point schema at the migration files so the generator reads the same SQL production runs, and generate into one package per area. Check the generated code in, so a clone builds without sqlc, and let CI run sqlc diff to catch a stale copy. Put the pinned version of sqlc in a tools file or the Makefile so every developer and the CI job run the same generator, because a newer version writes different code.

Habits

  1. Name every parameter and cast every non obvious one.
  2. Return explicit columns from joins.
  3. Make updates idempotent and read :execrows.
  4. Run compile, vet and diff in CI.
  5. Test every query against a real database.

The full reference is the sqlc documentation, and the configuration file is described in the sqlc configuration reference.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.