cmd / nullscan
nullscan is a static check that flags pgx scans where a SQL column
is nullable but the Go destination is a non-nullable type (int64,
string, time.Time).
It pairs with the package-local SQL layout in go / postgres.
Why
A pgx scan into a non-nullable Go type panics on the first row that
returns NULL. Each such bug used to be a production incident.
nullscan makes it a failing check on the change.
The rule is decidable from db/schema.sql and the SQL text. If the
schema says a column can be NULL and the query does not COALESCE it or
JOIN it to a NOT NULL column, the Go destination must accept NULL.
How it works
nullscan loads db/schema.sql for column nullability, then walks
every package in the repo for pgx.RowToStructByName[T] and
pgx.RowToStructByPos[T] call sites. For each site, it resolves the
query through the q<Name> = sqlFile("<file>") pattern and derives
the nullability of each SELECT projection. It flags a struct field
scanned into a non-nullable type when its column is nullable.
Struct fields map to columns by db:"col" tag for by-name scans and
by index for positional scans.
Scope
Inference handles bare table.col, coalesce(...), count(...),
literal projections, and LEFT JOIN propagation. nullscan treats
anything harder (subqueries in the projection, UNION, CTEs, other
expressions) as unknown and leaves it alone. It reports a mismatch only
when it is sure.
There is no per-field ignore directive. If nullscan flags a column,
COALESCE it or project from a NOT NULL column through a JOIN. If the
analyzer is wrong, fix the analyzer.
In CI
nullscan runs as one line in the cibot Checkfile:
nullscan: go run ./cmd/nullscan
Output is plain text, one line per finding:
<query>.sql: field PersonRow.Email (string) scans nullable column "email" from people/queries/fetch_person.sql
A failed run pastes into an agent prompt.
Compared to sqlc
The package-local SQL technique
keeps hand-written structs and embedded .sql files with no codegen
step. Nothing checks the Go types against the schema the way
sqlc does when it generates code.
nullscan checks the one property that caused incidents:
NULL into a non-nullable type. sqlc does more. It generates typed
methods and validates whole result shapes.