Fix: sqlc Not Working — sqlc.yaml v2, pgx/v5 Driver, Nullable Types, JSONB, and Array Parameters
Part of: Database Errors
Quick Answer
How to fix Go sqlc errors — sqlc.yaml v2 config schema, pgx/v5 vs database/sql driver choice, sql.NullString vs pointer types, JSONB and UUID overrides, ANY($1::int[]) slice params, and migration ordering.
The Error
You run sqlc generate and it complains about your config:
$ sqlc generate
# error parsing config: version is requiredOr it generates code but the types don’t match what you wrote:
-- query.sql
-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;// generated.go
type GetUserRow struct {
ID int32 // You wanted int64
Name sql.NullString // You wanted string
Email sql.NullString // You wanted *string
}Or your pgx-specific types panic at runtime:
panic: cannot find encode plan for type pgtype.JSONBOr passing a slice to IN (...) doesn’t work:
SELECT * FROM users WHERE id IN ($1); -- Can't pass []intWhy This Happens
sqlc parses your SQL, generates Go code with named functions per query, and statically types arguments and results. Most issues come from one of:
- Wrong config version.
sqlc.yamlhas v1 and v2 schemas with very different shapes. v2 is current; v1 is deprecated. Mixing them silently produces wrong output. - Driver mismatch.
database/sqlandpgx/v5have different type maps. Picking the wrongsql_packagein config generates types your runtime can’t use. - Schema vs query inference. sqlc infers nullability and types from your
CREATE TABLE(schema). If your column isemail TEXT(nullable), the generated type issql.NullString. To get a pointer or non-null type, change the schema or override. - Postgres-specific syntax for slices. SQL
IN ($1)with a Go slice doesn’t work in any driver. sqlc requiresANY($1::int[])for slice parameters.
A second layer of confusion comes from where sqlc sits in the broader Go database-tooling space. Many users adopt it expecting a Prisma-like ORM and are surprised when it does nothing at runtime. sqlc is a pure code generator — it reads SQL, emits Go functions, and exits. There is no migration runner, no connection management, no fluent query builder, no entity tracking. Errors that look like “sqlc is broken” are often the absence of a feature it was never meant to ship. If your migrations aren’t applied, sqlc still happily generates code from stale schema files and produces compile-time-correct Go that hits a database without those tables.
A third layer is the gap between the schema sqlc parses and the schema your database runs. sqlc’s parser lags Postgres on some recent syntax (generated columns, certain partitioning DDL, window-function edges). When parsing fails on a column, sqlc emits a struct missing that field — no compile error, just a silent omission. Diff generated structs against \d table before trusting them.
Fix 1: Use the v2 Config Schema
# sqlc.yaml
version: "2"
sql:
- schema: "db/schema.sql"
queries: "db/queries.sql"
engine: "postgresql"
gen:
go:
package: "db"
out: "internal/db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_pointers_for_null_types: true
emit_prepared_queries: false
emit_interface: trueThree sections inside sql:
schema— path(s) to yourCREATE TABLEstatements.queries— path(s) to your-- name: X :one|:many|:execqueries.gen.go— output config.
For multi-database projects, list multiple sql blocks:
version: "2"
sql:
- { schema: "db/main/schema.sql", queries: "db/main/queries.sql", engine: "postgresql", gen: { go: { package: "main_db", out: "internal/main_db" } } }
- { schema: "db/analytics/schema.sql", queries: "db/analytics/queries.sql", engine: "postgresql", gen: { go: { package: "analytics_db", out: "internal/analytics_db" } } }Pro Tip: Set emit_pointers_for_null_types: true. Without it, every nullable column becomes sql.NullString / sql.NullInt64 / etc. — verbose and inconvenient. Pointers (*string, *int64) compose better and JSON-encode cleanly.
Fix 2: Pick the Right Driver
For new projects, use pgx/v5:
sql_package: "pgx/v5"This generates code that uses pgxpool.Pool and pgx.Rows directly. Faster than database/sql and supports the full Postgres feature set (JSONB, arrays, ranges, copy-from, custom types).
For projects still on database/sql:
sql_package: "database/sql"The generated code uses sql.DB, sql.Rows, sql.NullString. Works with any driver implementing database/sql (lib/pq, jackc/pgx via stdlib wrapper).
Common Mistake: Choosing pgx/v5 but importing lib/pq in your driver setup, or vice versa. The sqlc-generated code is tied to the driver — mismatched runtime fails at the type level.
Fix 3: Override Column Types
For UUID columns from github.com/google/uuid:
sql:
- schema: "..."
queries: "..."
gen:
go:
# ...
overrides:
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
- db_type: "uuid"
nullable: true
go_type: "github.com/google/uuid.NullUUID"For JSONB columns mapped to a typed struct:
overrides:
- column: "users.preferences"
go_type:
import: "github.com/myorg/types"
package: "types"
type: "UserPreferences"The column matches table.column. Use db_type for type-wide overrides; column for per-field.
For timestamp columns with time.Time:
overrides:
- db_type: "timestamptz"
go_type: "time.Time"
- db_type: "timestamptz"
nullable: true
go_type: "*time.Time"Pro Tip: Keep all overrides in one block. sqlc applies them in order — earlier rules can be shadowed by later ones with the same db_type/column.
Fix 4: Use ANY($1::int[]) for Slice Parameters
IN ($1) doesn’t accept slices in any Postgres Go driver. Use the Postgres array operator:
-- name: GetUsersByIDs :many
SELECT * FROM users WHERE id = ANY($1::bigint[]);sqlc generates a function that takes []int64:
users, err := q.GetUsersByIDs(ctx, []int64{1, 2, 3})For text arrays:
-- name: GetUsersByEmails :many
SELECT * FROM users WHERE email = ANY($1::text[]);For multi-column lookups, use a row constructor:
-- name: GetUsersByCompositeKeys :many
SELECT * FROM users
WHERE (org_id, user_id) IN (
SELECT * FROM unnest($1::int[], $2::int[]) AS t(o, u)
);Common Mistake: Writing WHERE id IN (sqlc.slice('ids')) thinking sqlc will expand it. sqlc does have a sqlc.slice() helper, but for Postgres-with-array support the cleaner pattern is ANY($1::type[]).
Fix 5: Query Annotations and Return Types
Every query needs a -- name: X :TYPE annotation:
-- name: CreateUser :one
INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *;
-- name: ListUsers :many
SELECT * FROM users ORDER BY id;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
-- name: CountUsers :one
SELECT COUNT(*) FROM users;Return type modifiers:
:one— exactly one row.ErrNoRowsif empty.:many— zero or more rows. Returns a slice.:exec— no rows returned. Returns onlyerror.:execrows— returns affected rows count + error.:execresult— returnssql.Result+ error.:batchone/:batchmany/:batchexec— pgx batch (multiple queries in one network round-trip).
For batch queries with pgx:
-- name: GetUserByID :batchone
SELECT * FROM users WHERE id = $1;batch := q.GetUserByID(ctx, []int64{1, 2, 3})
batch.QueryRow(func(i int, user User, err error) {
// Called once per item
})
batch.Close()Batches send all queries in one network call — massive speedup for N+1 patterns.
Fix 6: Embed Complex JOINs with :json or Struct Composition
For queries that return data from multiple tables, sqlc generates a flat struct by default:
-- name: GetOrderWithUser :one
SELECT o.*, u.name AS user_name, u.email AS user_email
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.id = $1;type GetOrderWithUserRow struct {
ID int64
Total int64
UserID int64
UserName string
UserEmail string
}For better separation, use sqlc.embed:
-- name: GetOrderWithUser :one
SELECT sqlc.embed(o), sqlc.embed(u)
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.id = $1;type GetOrderWithUserRow struct {
Order Order
User User
}sqlc.embed(alias) expands to the alias’s full column list and groups them into a nested struct on the Go side.
Pro Tip: For aggregations returning JSON arrays, output JSONB and let sqlc generate []byte:
-- name: GetUserWithPosts :one
SELECT u.*, json_agg(p.*) AS posts
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE u.id = $1
GROUP BY u.id;Then unmarshal into a typed slice in Go:
row, _ := q.GetUserWithPosts(ctx, id)
var posts []Post
json.Unmarshal(row.Posts, &posts)Fix 7: COPY FROM for Bulk Insert
For inserting thousands of rows, sqlc can generate COPY FROM (pgx only):
-- name: CreateUsers :copyfrom
INSERT INTO users (name, email) VALUES ($1, $2);Generates:
err := q.CreateUsers(ctx, []CreateUsersParams{
{Name: "Alice", Email: "[email protected]"},
{Name: "Bob", Email: "[email protected]"},
// ... 10000 more ...
})Under the hood, this uses Postgres’s COPY FROM STDIN protocol — 10-100x faster than individual INSERTs for bulk loads.
Common Mistake: Using :copyfrom for small inserts. For <100 rows, the overhead vs :many INSERT ... RETURNING is comparable. Reserve :copyfrom for genuinely large bulk loads where you don’t need RETURNING.
Fix 8: Set Up Migrations and Generate Schema
sqlc reads CREATE TABLE statements to infer types — keep them in sync with your migrations:
db/
├── schema.sql # All CREATE TABLEs (sqlc reads this)
├── queries.sql # All -- name: X queries
└── migrations/
├── 0001_users.up.sql
├── 0001_users.down.sql
├── 0002_orders.up.sql
└── 0002_orders.down.sqlGenerate schema.sql from migrations periodically:
# Concat all up migrations (with a migration tool):
cat db/migrations/*.up.sql > db/schema.sqlOr use a migration tool like golang-migrate, goose, or atlas that can dump the final schema.
For sqlc to use migrations directly (skip the consolidated schema):
sql:
- schema: "db/migrations"
queries: "db/queries.sql"
engine: "postgresql"
gen: { go: { package: "db", out: "internal/db" } }sqlc reads all .sql files in the directory in alphabetical order. Migration filenames need to sort correctly (0001_, 0002_, etc.).
sqlc vs xo vs gnorm vs Atlas: Which Go SQL Codegen Should You Use?
sqlc isn’t the only “SQL to Go” tool. The category splits along two axes: what the tool considers the source of truth (queries, schema, or migrations) and how much it generates (just types, or whole CRUD layers).
sqlc is query-first. You write SQL queries, sqlc parses them, infers the parameter and result types from your schema, and emits one Go function per query. There is no runtime library, no entity tracking, no automatic CRUD. The generated code is plain database/sql or pgx/v5 calls. This makes sqlc ideal when you want full control over SQL and minimal magic — the tradeoff is that you write every query by hand, including the boring SELECT * FROM users WHERE id = $1 boilerplate.
xo is schema-first. Point it at a live database (or DDL file) and it generates a struct per table plus basic CRUD methods (Insert, Update, Delete, ByID). Custom queries are still hand-written and registered separately. xo is closer to a thin “active record” pattern — good for projects that want CRUD for free and only drop to SQL for the interesting cases. It’s less popular than sqlc today but still maintained, and the generated code is friendlier to read than sqlc’s flat function lists.
gnorm is template-driven. It introspects the schema and runs your Go templates over the metadata, so you decide exactly what gets generated. Most flexible, most work — you maintain the templates. Teams use gnorm for DAOs that look unlike sqlc or xo output, or to target languages besides Go from the same schema.
Atlas is schema management rather than code generation. It diffs your declarative schema (HCL or SQL) against the database and applies migrations. No Go type generation. Many sqlc users pair the two: Atlas owns schema and migrations; sqlc generates Go from the schema files Atlas manages.
If you’re starting fresh: use sqlc for queries plus Atlas (or golang-migrate) for schema changes. If you want CRUD without writing it, xo. If you need exotic generated code, gnorm. If you want a full ORM with relationships and lazy loading, look outside the codegen category entirely — Ent or GORM are the Go equivalents of Hibernate/Active Record and trade SQL transparency for developer ergonomics.
Still Not Working?
A few less-obvious failures:
- Generated file has no imports. Run
goimports -w internal/dbafter generate, or setgo.sumcorrectly. sqlc generates standard imports but missing them leaves you with an unused package error. pgxpool.Pooldoesn’t satisfy the generated interface. Withemit_interface: true, sqlc generates aDBTXinterface that requiresQuery/QueryRow/Exec.pgxpool.Poolsatisfies it forpgx/v5. If your code uses*sql.DBsomewhere, you need to bridge.go generate ./...doesn’t run sqlc. sqlc isn’t a Go generator by default. Add//go:generate sqlc generateat the top of a file or runsqlc generatedirectly in your build.- JSONB columns return wrong type. Without an override, JSONB is
[]byte. Override tojson.RawMessage,map[string]interface{}, or your custom struct. - CI build fails:
sqlc not found. Install sqlc in CI explicitly viago installor download the binary. It’s not a Go dependency. - Tests that hit a real DB fail with permission errors. Test DB user needs
SELECT/INSERT/UPDATE/DELETEon the relevant tables. sqlc-generated code doesn’t bypass permissions. - Long compile times after generation. A query with
sqlc.embedgenerates large structs. For 100+ queries with embeds, generated code can be hundreds of KB. Split queries across multiple files and packages. pgvectorcolumns fail. No built-in support. Override with a custom type that implementspgtype.Codec, and register the codec with your pool at startup.- Generated code diffs on every CI run. sqlc emits files with version comments and import order that depends on its parser state. Pin sqlc’s version in
go.modor a.tool-versionsfile and runsqlc generatein CI as a verification step, not a generation step. If the diff is non-empty, fail the build. go testworks locally but fails in CI withpq: column does not exist. Your local schema has a column the migrations haven’t added in CI. Always run migrations against the CI database before tests, and never editschema.sqlby hand without a matching migration file.- Generated structs miss columns added by
ALTER TABLEmigrations. Ifschema.sqlis built by concatenating migrations in alphabetical order, anALTER TABLE users ADD COLUMNin a later migration won’t merge into the originalCREATE TABLE. Either keep one canonicalCREATE TABLEper table updated in place, or have sqlc read the migrations directory directly so it sees both DDL statements in order.
For related Go and database tooling issues, see Go module not found, Postgres relation does not exist, TypeORM query failed, and Prisma migration failed.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: pgvector Not Working — Extension Install, Index Not Used, and Dimension Errors
How to fix pgvector errors — extension does not exist CREATE EXTENSION vector, dimension mismatch on insert, HNSW index not used slow queries, distance operator confusion, psycopg register adapter, and ivfflat vs hnsw selection.
Fix: AWS RDS Proxy Not Working — Endpoint, IAM Auth, Connection Pinning, and Lambda VPC
How to fix AWS RDS Proxy errors — IAM authentication token mismatch, connection pinning blocking reuse, Lambda VPC routing, Secrets Manager rotation, max_connections, read/write splitter, and TLS requirement.
Fix: BigQuery Not Working — Partitioning, Slots, Streaming Inserts, and Cost Surprises
How to fix BigQuery errors — query scans full table without partition filter, slot contention with on-demand pricing, streaming insert quota exceeded, DML row limits, service account auth, and INFORMATION_SCHEMA for monitoring.
Fix: ClickHouse Not Working — MergeTree ORDER BY, INSERT Batching, LowCardinality, and Replication
How to fix ClickHouse errors — table engine choice, ORDER BY for primary key, INSERT too many small parts, LowCardinality types, Nullable performance, ZooKeeper for Replicated tables, and HTTP vs Native client.