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.

← All articles