Phase 2.1: move the store to sqlc
What happened
Phase 2.1 moved every query in internal/store/pg into queries/*.sql and let sqlc v1.31.1 generate the Go that runs them. The hand-written files now only build parameters from the tenant scope, call the generated method and convert the result. sqlc runs through go run at a pinned version, so it is not in go.mod or the binary. make generate regenerates and make sqlc-check fails on stale code. No escape hatch was needed. The phase 2 tests pass unchanged, apart from the append-only audit check, which now reads the query files.
Type overrides (stdlib uuid.UUID, time.Time, json.RawMessage, store.Role, store.TenantKind) and two renames made most generated row structs field-identical to the store types, so the mappers are Go struct conversions such as store.User(u). A schema change that the store type does not follow now fails to compile.
Surprises
- Hugo weights are whole numbers. With phases 2 and 2.1 both at weight 2, Hugo sorted by title and put 2.1 first. Every phase file and learning doc now has weight = phase x 10.
- The plan page generator and template parsed phase numbers as integers and needed decimal support.
What review found
No defects. Three low items were fixed: the append-only test now checks every query file, since a purge could land in any of them (a probe delete from audit_events made it fail); make generate deletes old *.sql.go first, because sqlc never removes output; and the plan generator trims a trailing period after a decimal phase number. Left as known: the ($n is null or col = $n) filter shape can stop an index being used once Postgres switches to a generic plan, and the generated API is exported inside package pg.
A second review of the whole PR found the code sound and the course material wrong in places, all fixed:
- The lesson told readers to search Tempo for
db.statement. otelpgx v0.13 follows newer semantic conventions and recordsdb.query.text, and it names the span by the SQL verb, skipping the sqlc comment. The lesson now gives the TraceQL query. - The “break the tenant rule” exercises deleted the predicate, which removes the
@tenant_idparameter, so the build failed before the isolation test could run. The exercises now keep the parameter (@tenant_id::uuid is not null) and show the compile error as a second lesson. - Phase 4’s contract still said
pgx.CollectRowsand a string-builtAND created_by = $n. Its steps now use query files, thesqlc.nargmember predicate, and anunnestinsert for the message batch, because sqlc cannot express a variable-lengthVALUESlist. - Smaller: sqlc puts a file’s top comment on the first method’s Go doc, so the query files lost their file headers;
make generaterunssqlc compilebefore deleting old output; the architecture diagram, the phase count and the cgo prerequisite were updated.
Next steps
Push and open the PR, merge, tag phase-02.1, then phase 3.
Historical work record — not durable authority. Prefer docs/specs/ADRs for current decisions.