feat(store): migrate remaining store domains to sqlc + vet in CI + gate identifier-passed SQL (RIG-3034) - #812
Merged
Conversation
This was referenced Aug 31, 2026
Closed
|
Compass engineering docs preview: https://compass-repo-rig-3034-sqlc-c.compass-eng-docs.pages.dev Deployed from Changed pages: |
…te identifier-passed SQL (RIG-3034) Collapses the remaining sqlc-adoption slices (T5, T6, T7) into one change atop the merged T4 (#795). Every store domain now uses sqlc-generated typed queries, the inline-sql-gate allowlist is down to its two permanent escape hatches, `sqlc vet` (db-prepare) runs against a live schema in CI, and the gate now catches identifier-passed SQL, not just string literals. Method signatures are unchanged throughout; only bodies now call generated queries via s.q (pool) or s.q.WithTx(tx) / db.New(tx). Error wrapping, domain-type mapping, RowsAffected branching, uint64<->int64 narrowing, flush orchestration, and tx isolation stay hand-written. ## Agent domains (was T5) Five agent-domain files migrated (queries/<domain>.sql): - agent_sessions.go → 3 queries - agent_transcripts.go → 13 distinct queries backing 16 call sites (SessionTranscript + SafetyValveSegments each serve a pool method and the resume-snapshot tx; InsertTranscriptEntry is :execrows for ON CONFLICT dedup). - agent_activity.go → 2 queries - agent_config.go → 3 queries - agent_placements.go → 5 queries COALESCE(MAX/SUM(...),0) reads carry an explicit ::BIGINT cast so sqlc types them int64 (mirrors messages.sql MessagesHeadSeq). Bare-column references visible in both outer and MAX-subquery scope are table-aliased (te/cp/e) — pure disambiguation, semantically identical. Removed dead scanTranscriptRows/ scanSegmentRows helpers and the remarkSafetyValveSQL const. ## Remaining domains (was T6) Every remaining domain file migrated, leaving the allowlist at only the two permanent entries (store.go migration runner, pgshare.go test harness): - authz.go: 3 new queries; dead querier interface removed from store.go; helpers take db.DBTX (pool and pgx.Tx both satisfy it). - tokens.go: 4 (RevokeToken :execrows; revoked_at IS NOT NULL cast ::boolean). - secrets.go: 3 (DeleteSecret :execrows). - issues.go: 4 (UpsertIssueForgeFields CTE, SetIssueState :execrows); scanIssue/ scanRow → issueFromColumns. - forge_authored.go: 4. - forge_cursors.go: 7 (StoreForgeRepoWatermark, SetForgeRepoSubscriptionEnabled :execrows). - forge_subscriptions.go: 9 (AdvanceForgeDeliveredRevision :execrows). derefString removed. - tenant.go: 2. - linear_sessions.go: 2 (UpsertLinearAgentSession :execrows). scanLinearAgentSession removed. - dm.go: residue file (added post-record); 8 queries covering get-or-create, the channel resolution/insert loop, the advisory lock, the R3 verify-reconcile belt, and the reserved-group discriminator. Non-obvious mappings: interface{} columns forced concrete via casts (tokens revoked ::boolean; forge_subscriptions coord_number ::BIGINT, has_cursor ::boolean; SubscribersForArtifact openedEvent ::boolean); GC subquery columns table-qualified to resolve an ambiguous-column analyzer failure; constant enum→int16 conversions carry no nolint (compile-time checked), only variable narrowings do. ## Vet in CI + gate promotion (was T7) sqlc.yaml: add `database.uri: ${SQLC_DATABASE_URL}` + `rules: [sqlc/db-prepare]` so `sqlc vet` PREPAREs every generated query against a live Postgres. Set `analyzer.database: false` so `sqlc generate` and the DB-free sqlc-drift gate keep working without a database — only db-prepare (vet) uses database.uri. Without this, the database stanza would force generate to require a live DB and break the 'sqlc-drift is fully local, no DB' invariant. moon.yml: add a `sqlc-vet` task (modeled on sqlc-drift) — DSN-gated skip when neither COMPASS_TEST_DATABASE_DSN nor SQLC_DATABASE_URL is set; otherwise it provisions a uniquely-named throwaway DB (sqlcvet_$$) on the service, applies internal/store/migrations/*.sql via psql, points SQLC_DATABASE_URL at it, runs `sqlc vet`, and drops the DB on a cleanup trap. NOT added to the moon `ci` deps: that battery realizes no service. ci.yml: run `moon run compass-go:sqlc-vet --force` in the existing `pgtest` job, which already carries the postgres:16-alpine service + COMPASS_TEST_DATABASE_DSN. This is where db-prepare runs live — the same peel-into-a-service-job pattern as pgtest/microvm/forge/gtk4 (Matt's Option-B ruling on the CI fork: the spec assumed the ci-aggregate job had the service, but the service lives only on the pgtest job). A phase-two nixpkgs-tools step puts psql + sqlc on PATH. `--force` because the pgtest job checks out shallow (no fetch-depth: 0); a bare `moon run` resolves the PR base branch for affected/cache-hash and dies git-128 in a shallow clone, and sqlc-vet must always run here regardless of what the diff touched. inline-sql-gate: promote identifier-passed SQL from advisory to GATED. Beyond string-literal SQL at any receiver, a bare-identifier SQL slot (`q`, `ddl`, `m.sql`) is now flagged when the call's receiver is a pgx pool/tx/conn handle (last segment in {pool, tx, conn, c}). The receiver scope is the load-bearing guard against runtime/compute false positives (r.runtime.Exec(ctx,id,spec) / g.client.Exec(ctx,req) are not queries). Only the SQL slot (arg after ctx) is tested; a call/composite/concat expression is left alone. Stays GREEN on the migrated tree: the only remaining identifier-passed sites are conn.Exec(ctx,ddl) / tx.Exec(ctx,m.sql) in the permanently-allowlisted store.go (allowlist=2). Fail-closed stale-entry ratchet intact; +7 detection tests. RIG-3034 Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka
force-pushed
the
compass-repo/rig-3034-sqlc-collapse
branch
from
August 31, 2026 21:52
287abbd to
057e8d2
Compare
mattwilkinsonn
approved these changes
Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Collapses the remaining sqlc-adoption slices (T5, T6, T7) into one change atop
the merged T4 (#795). Every store domain now uses sqlc-generated typed queries,
the inline-sql-gate allowlist is down to its two permanent escape hatches,
sqlc vet(db-prepare) runs against a live schema in CI, and the gate now catchesidentifier-passed SQL, not just string literals. Method signatures are unchanged
throughout; only bodies now call generated queries via s.q (pool) or
s.q.WithTx(tx) / db.New(tx). Error wrapping, domain-type mapping, RowsAffected
branching, uint64<->int64 narrowing, flush orchestration, and tx isolation stay
hand-written.
Agent domains (was T5)
Five agent-domain files migrated (queries/.sql):
(SessionTranscript + SafetyValveSegments each serve a pool method and the
resume-snapshot tx; InsertTranscriptEntry is :execrows for ON CONFLICT dedup).
COALESCE(MAX/SUM(...),0) reads carry an explicit ::BIGINT cast so sqlc types them
int64 (mirrors messages.sql MessagesHeadSeq). Bare-column references visible in
both outer and MAX-subquery scope are table-aliased (te/cp/e) — pure
disambiguation, semantically identical. Removed dead scanTranscriptRows/
scanSegmentRows helpers and the remarkSafetyValveSQL const.
Remaining domains (was T6)
Every remaining domain file migrated, leaving the allowlist at only the two
permanent entries (store.go migration runner, pgshare.go test harness):
take db.DBTX (pool and pgx.Tx both satisfy it).
scanRow → issueFromColumns.
:execrows).
removed.
removed.
channel resolution/insert loop, the advisory lock, the R3 verify-reconcile belt,
and the reserved-group discriminator.
Non-obvious mappings: interface{} columns forced concrete via casts (tokens
revoked ::boolean; forge_subscriptions coord_number ::BIGINT, has_cursor
::boolean; SubscribersForArtifact openedEvent ::boolean); GC subquery columns
table-qualified to resolve an ambiguous-column analyzer failure; constant
enum→int16 conversions carry no nolint (compile-time checked), only variable
narrowings do.
Vet in CI + gate promotion (was T7)
sqlc.yaml: add
database.uri: ${SQLC_DATABASE_URL}+rules: [sqlc/db-prepare]so
sqlc vetPREPAREs every generated query against a live Postgres. Setanalyzer.database: falsesosqlc generateand the DB-free sqlc-drift gate keepworking without a database — only db-prepare (vet) uses database.uri. Without
this, the database stanza would force generate to require a live DB and break the
'sqlc-drift is fully local, no DB' invariant.
moon.yml: add a
sqlc-vettask (modeled on sqlc-drift) — DSN-gated skip whenneither COMPASS_TEST_DATABASE_DSN nor SQLC_DATABASE_URL is set; otherwise it
provisions a uniquely-named throwaway DB (sqlcvet_$$) on the service, applies
internal/store/migrations/*.sql via psql, points SQLC_DATABASE_URL at it, runs
sqlc vet, and drops the DB on a cleanup trap. NOT added to the mooncideps:that battery realizes no service.
ci.yml: run
moon run compass-go:sqlc-vet --forcein the existingpgtestjob,which already carries the postgres:16-alpine service + COMPASS_TEST_DATABASE_DSN.
This is where db-prepare runs live — the same peel-into-a-service-job pattern as
pgtest/microvm/forge/gtk4 (Matt's Option-B ruling on the CI fork: the spec
assumed the ci-aggregate job had the service, but the service lives only on the
pgtest job). A phase-two nixpkgs-tools step puts psql + sqlc on PATH.
--forcebecause the pgtest job checks out shallow (no fetch-depth: 0); a bare
moon runresolves the PR base branch for affected/cache-hash and dies git-128 in a shallow
clone, and sqlc-vet must always run here regardless of what the diff touched.
inline-sql-gate: promote identifier-passed SQL from advisory to GATED. Beyond
string-literal SQL at any receiver, a bare-identifier SQL slot (
q,ddl,m.sql) is now flagged when the call's receiver is a pgx pool/tx/conn handle(last segment in {pool, tx, conn, c}). The receiver scope is the load-bearing
guard against runtime/compute false positives (r.runtime.Exec(ctx,id,spec) /
g.client.Exec(ctx,req) are not queries). Only the SQL slot (arg after ctx) is
tested; a call/composite/concat expression is left alone. Stays GREEN on the
migrated tree: the only remaining identifier-passed sites are conn.Exec(ctx,ddl)
/ tx.Exec(ctx,m.sql) in the permanently-allowlisted store.go (allowlist=2).
Fail-closed stale-entry ratchet intact; +7 detection tests.
RIG-3034
Co-authored-by: Matt Wilkinson matt@rigel.build