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

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
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'
);-- 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;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: truepackage 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)
}set -e
~/go/bin/sqlc generate
go mod tidy > /dev/null 2>&1
go run .: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 LovelaceCommands
| Need | Command |
|---|---|
| Install | go install github.com/sqlc-dev/sqlc/cmd/[email protected] |
| Generate Go | sqlc generate |
| Check without writing files | sqlc compile |
| Run vet rules | sqlc vet |
| Fail if generated code is stale | sqlc diff |
| Start a project | sqlc init |
Query annotations
| Annotation | Generated method returns | In the run |
|---|---|---|
:one | one row, pgx.ErrNoRows if none | CreateAuthor returned the new author; a missing id gave ErrNoRows |
:many | a slice | ListBooks returned 3 for Ada and an empty slice for Bob |
:exec | error | RenameAuthor |
:execrows | rows affected | Publish returned 1 then 0 |
:execresult | the driver's result | not used here |
:copyfrom | rows copied (pgx) | covered in batch and COPY |
:batchexec, :batchone, :batchmany | pgx 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
| Need | SQL | Generated |
|---|---|---|
| Positional | where id = $1 | a plain argument |
| Named | where id = sqlc.arg(id) | an argument named id |
| Nullable named | sqlc.narg('author_id') | a pointer or pgtype type |
| Cast to fix a type | sqlc.arg(status)::status | the enum type |
| Array | any($1::bigint[]) | []int64 |
| Several parameters | values ($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 type | Go type | Note |
|---|---|---|
bigint, integer | int64, int32 | |
text | string | |
nullable text | pgtype.Text, or *string with emit_pointers_for_null_types | the run used pointers |
timestamptz | pgtype.Timestamptz | override to time.Time (overrides) |
uuid | pgtype.UUID | override to google/uuid |
jsonb | []byte | override to a struct |
| enum | a named string type with constants | db.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
| Need | SQL |
|---|---|
| All columns of one table | select books.* from ... or select * from books |
| Both tables as nested structs | select sqlc.embed(books), sqlc.embed(authors) from ... |
| Explicit, flat | select 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
| Need | Code |
|---|---|
| Begin | tx, _ := pool.Begin(ctx) |
| Bind queries | qtx := q.WithTx(tx) |
| End | tx.Commit(ctx) or defer tx.Rollback(ctx) |
| Not found | errors.Is(err, pgx.ErrNoRows) |
| Unique violation | errors.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
| Option | Effect |
|---|---|
sql_package: pgx/v5 | generate for pgx, with pgtype and batch support |
sql_package: database/sql | generate for the standard library |
emit_json_tags: true | add 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: true | generate a Querier interface (for mocks) |
emit_exact_table_names: true | do not singularise table names |
emit_prepared_queries: true | prepare statements (database/sql) |
overrides | map types (guide) |
rename | rename 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
- Name every parameter and cast every non obvious one.
- Return explicit columns from joins.
- Make updates idempotent and read
:execrows. - Run
compile,vetanddiffin CI. - 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.


