sqlc — queries from the schema
Three files as tabs: sqlc.yaml → internal/db/queries/users.sql → internal/db/connect.go — the config that points sqlc at your schema, the query you write, and the pool the generated code runs on.
1. sqlc.yaml — the one line that makes the whole "no drift" promise real:
sql:
- engine: "postgresql"
schema: "db/migrations" # ← reads YOUR goose migrations
queries: "internal/db/queries"
gen:
go:
out: "internal/db"
sql_package: "pgx/v5"
schema: "db/migrations" — sqlc learns the shape of users from the same migration file goose runs. There is no second definition to keep in sync.
2. internal/db/queries/users.sql — you write SQL, annotated with a name and result shape:
-- name: CountUsers :one
SELECT count(*) FROM users;
-- name: CountUsers :one tells sqlc: generate a Go method called CountUsers that returns one row.
3. internal/db/connect.go — a pgx connection pool, verified with a Ping so a misconfigured URL fails loudly at startup, not on the first request.
Generate the Go. Now run:
sqlc generate
sqlc reads the schema + the query and writes internal/db/db.go, models.go, and users.sql.go — generated code you don't edit (like Templ's _templ.go). You get, for free and fully typed:
// in the generated users.sql.go — you wrote the SQL, sqlc wrote this:
func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countUsers)
var count int64
err := row.Scan(&count)
return count, err
}
And a User struct in models.go whose fields mirror the columns exactly — id UUID, email TEXT, password_hash TEXT, created_at TIMESTAMPTZ — because sqlc read them from your migration. You never hand-wrote that struct, and it can never disagree with the table.
Never hand-edit the generated files.
db.go,models.go,users.sql.goall carry a// Code generated by sqlc. DO NOT EDIT.header — and they mean it. Change the SQL, runsqlc generateagain. The generated code is a function of your schema and queries; editing it by hand reintroduces exactly the drift sqlc exists to prevent.