This skill enforces sqlc 1.31+ conventions for Go projects with PostgreSQL. The rule: SQL is the source of truth, Go calls go through the generated Querier interface only.
Apply only when sqlc is configured (sqlc.yaml exists). If the project uses sqlx / raw database/sql everywhere, STOP and ask the user before introducing sqlc.
sqlc.yaml requirements
version: "2"
sql:
- engine: postgresql
queries: db/queries
schema: db/migrations
gen:
go:
package: db
out: db/sqlc
emit_interface: true
emit_json_tags: true
emit_db_tags: false
emit_pointers_for_null_types: true
emit_empty_slices: true
sql_package: pgx/v5
version: "2"required. v1 is deprecated.emit_interface: truegenerates theQuerierinterface so handlers can mock easily.sql_package: pgx/v5preferred overdatabase/sqlfor PostgreSQL.schema:points to migrations, not to a hand-written schema file. sqlc parses migrations to derive the schema.
Query naming convention
| Prefix | Returns | Example |
|---|---|---|
Get |
exactly one row, error if missing | GetUserByID |
Find |
one row or nil, no error if missing |
FindUserByEmail |
List |
many rows | ListUsersByOrg |
Count |
scalar count | CountActiveUsers |
Create |
inserts and returns the created row | CreateUser |
Update |
updates and returns the updated row | UpdateUserEmail |
Delete |
deletes, no return | DeleteUser |
Each query starts with -- name: <PascalCase> :one|:many|:exec|:execrows:
-- name: GetUserByID :one
SELECT id, email, created_at FROM users WHERE id = $1;
-- name: ListUsersByOrg :many
SELECT id, email FROM users WHERE org_id = $1 ORDER BY created_at DESC;
-- name: CreateUser :one
INSERT INTO users (email, org_id) VALUES ($1, $2) RETURNING *;
Forbidden patterns
- Hand-written
db.Query("SELECT ...")/db.Exec("INSERT ...")from Go when sqlc could generate it. Add a query todb/queries/*.sqland runsqlc generate. version: "1"insqlc.yaml. Migrate to v2.- Modifying generated files in
db/sqlc/(e.g.models.go,queries.sql.go). They will be overwritten. Modify the SQL source and regenerate. - Defining the schema twice (once in migrations, once in a separate
schema.sql). Point sqlc at migrations. - Mixing pgx and
database/sqlin the same project. Pick one—pgx/v5preferred. - String concatenation to build dynamic queries. Use
sqlc.arg(),sqlc.embed(), or write multiple named queries. - Query names that do not start with one of the prefixes above.
FetchUser,RetrieveUserByEmail— pick the canonical prefix. - A new query when an existing one in
db/queries/*.sqlalready covers the use case. grep query names and the underlyingSELECT/INSERT/UPDATE/DELETEfirst; reuse or extend if found.
When sqlc cannot express what you need
sqlc handles most CRUD plus aggregates, CTEs, and window functions. If you need:
- runtime dynamic column selection
- variable-arity
IN ()clauses without an array - prepared statement caching control
STOP and report:
Need to express [query shape]. sqlc patterns checked: [
sqlc.arg,sqlc.embed,ANY($1::int[])]. None covers [specific gap]. Approve one of: (A) restructure withANY(?::type[])forIN, (B) add a hand-written method on the*db.Queriesreceiver in a separate non-generated file (with comment), (C) different approach.
After every SQL change
- Run
sqlc vet(linting against the schema) - Run
sqlc generate - Commit both the SQL change and the generated Go output
- Run
go test ./...
Verification (grep after every sqlc-related change)
# config version
grep -nE '^version:\s*"?1' **/sqlc.yaml **/sqlc.yml 2>/dev/null
# hand-written SQL calls (should be sqlc queries)
grep -rnE 'db\.(Query|QueryRow|Exec)\(["`]\s*(SELECT|INSERT|UPDATE|DELETE)' --include='*.go' .
# query naming convention
grep -nE '^-- name:\s+\w+' db/queries/*.sql | grep -vE ':\s+(Get|Find|List|Count|Create|Update|Delete)\w+'
Reference: https://docs.sqlc.dev/en/latest/reference/config.html
Source: zhaojiannet/claude-skills — distributed by TomeVault.