Skip to content

refactor(refid)!: stop registering database/sql drivers in store subpackages - #186

Merged
sthanikan2000 merged 1 commit into
mainfrom
refactor/refid-store-driver-imports
Sep 6, 2026
Merged

refactor(refid)!: stop registering database/sql drivers in store subpackages#186
sthanikan2000 merged 1 commit into
mainfrom
refactor/refid-store-driver-imports

Conversation

@sthanikan2000

@sthanikan2000 sthanikan2000 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

A store handed a *sql.DB shouldn't decide which driver gets linked into its consumer's binary. Both subpackages did: store/sqlite blank-imported modernc.org/sqlite and store/postgres blank-imported pgx/v5/stdlib, purely as a convenience so callers could sql.Open without an import of their own. Neither needs it — New and Migrate both receive an already-open connection and only ever issue SQL against it.

For sqlite that convenience is actively breaking. modernc.org/sqlite registers the driver name "sqlite" in init(), and so does github.com/glebarez/go-sqlite — a fork of it, used by the GORM sqlite driver. Neither registration is guarded, so any binary linking both dies at startup:

panic: sql: Register called twice for driver sqlite

That made store/sqlite unusable in OpenNSW/agency, which already links glebarez through its GORM setup.

store/postgres does not panic today: pgx guards its "pgx" registration, and only pgx/v5/stdlib claims "pgx/v5". It changes anyway, because the rule above applies to it just as much — a consumer using lib/pq still compiles all of pgx for nothing and inherits its version constraints into their module graph. Fixing both leaves one contract rather than an asymmetry the docs have to keep explaining.

Type of Change

  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Changes Made

  • Removed both blank driver imports. The package docs now simply tell the caller to import one.
  • Moved those imports into the two store_test.go files, which had been relying on the package under test to register a driver for their own sql.Open.
  • Updated the README's Database Setup / SQLite / PostgreSQL sections, which described the stores as linking a driver.

Testing

  • I have tested this change locally
  • All existing tests pass

go build ./... && go vet ./... && go test ./... all pass.

Because sql.Open resolves the driver by name at runtime, a missing import is not a compile error — so two extra checks:

  • Forcing the POSTGRES_TEST_DSN-gated test to run against an unreachable host fails with connection refused, not unknown driver, confirming the test's own import registers pgx.
  • go list -deps on both stores now reports zero driver packages, where store/sqlite previously pulled in all of modernc (libc, memory, mathutil).

Related Issues

Required by the reference ID integration in OpenNSW/agency (agency#306).

Additional Notes

Breaking: callers must now import a driver themselves (e.g. _ "modernc.org/sqlite"), or sql.Open returns sql: unknown driver. Nothing outside this module's own tests imports either subpackage today, so nothing breaks in practice — which is what makes now the cheap moment to do this.

Summary by CodeRabbit

  • Documentation

    • Updated database setup guidance for PostgreSQL and SQLite.
    • Clarified that callers must import the appropriate database driver, open the connection, and provide it to the store.
    • Documented support for PostgreSQL drivers compatible with database/sql.
  • Bug Fixes

    • Prevented automatic driver registration by store packages, avoiding duplicate-registration initialization errors.
    • Updated integration tests to explicitly register their database drivers.

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 257dae87-ef2f-4331-86be-e7d40d124ffe

📥 Commits

Reviewing files that changed from the base of the PR and between a57d1de and 726cfd6.

📒 Files selected for processing (5)
  • refid/README.md
  • refid/store/postgres/store.go
  • refid/store/postgres/store_test.go
  • refid/store/sqlite/store.go
  • refid/store/sqlite/store_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The database stores no longer register drivers. Documentation now requires callers to import drivers and pass opened *sql.DB connections. Integration tests add the required driver imports.

Changes

Database driver registration

Layer / File(s) Summary
Caller-owned driver contract
refid/README.md, refid/store/postgres/store.go, refid/store/sqlite/store.go
Documentation now requires callers to import database drivers, open connections, and pass *sql.DB values to store constructors.
Integration test driver setup
refid/store/postgres/store_test.go, refid/store/sqlite/store_test.go
Integration tests now blank-import the PostgreSQL and SQLite drivers used by their sql.Open calls.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Merge Risk: ⚪ Minimal · up to 726cf

Database driver registration is now explicitly owned by callers, avoiding duplicate SQLite registration while allowing callers to choose their database driver. The updated documentation and tests align with this contract, with no remaining merge-readiness risk.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: store subpackages no longer register database/sql drivers.
Description check ✅ Passed The description explains the rationale, breaking-change impact, implementation details, testing, and related issue. It omits the Checklist and Screenshots/Demo sections, but the required change inform…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 4…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/refid-store-driver-imports

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ackages

store/sqlite blank-imported modernc.org/sqlite and store/postgres
blank-imported pgx/v5/stdlib, purely as a convenience so callers could
sql.Open without a driver import of their own. Neither store needs it:
New and Migrate both receive an already-open *sql.DB and only ever issue
SQL against it.

The sqlite one actively breaks consumers. modernc.org/sqlite registers
the driver name "sqlite" in init(), as does github.com/glebarez/go-sqlite
(a fork of it, used by the GORM sqlite driver). Neither registration is
guarded, so any binary linking both panics at startup with

    sql: Register called twice for driver sqlite

which made store/sqlite unusable in OpenNSW/agency. store/postgres does
not panic today — pgx guards its "pgx" registration, and only
pgx/v5/stdlib claims "pgx/v5" — but it is wrong for the same reason: a
store that receives a *sql.DB should not decide which driver gets linked
into its consumer's binary. Fixing both keeps the contract uniform
rather than an asymmetry the docs have to explain.

BREAKING CHANGE: callers must now import a driver themselves. A caller
relying on the old convenience will see `sql: unknown driver` from
sql.Open until it adds, e.g., _ "modernc.org/sqlite" or
_ "github.com/jackc/pgx/v5/stdlib". Nothing outside this module's own
tests imported either subpackage at the time of this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@mushrafmim mushrafmim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@sthanikan2000
sthanikan2000 merged commit 3c88324 into main Sep 6, 2026
22 checks passed
@sthanikan2000
sthanikan2000 deleted the refactor/refid-store-driver-imports branch September 6, 2026 04:48
sthanikan2000 added a commit to OpenNSW/agency that referenced this pull request Sep 6, 2026
Drops the temporary replace directive now that OpenNSW/core#186 (the
driver-registration fix refid/store/sqlite needs here) has merged, and
repoints the require at that commit.

Verified against the published module rather than the local checkout:
build, vet and all tests pass, and the server boots without the
"sql: Register called twice for driver sqlite" panic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sthanikan2000 added a commit to OpenNSW/agency that referenced this pull request Sep 6, 2026
Drops the temporary replace directive now that OpenNSW/core#186 (the
driver-registration fix refid/store/sqlite needs here) has merged, and
repoints the require at that commit.

Verified against the published module rather than the local checkout:
build, vet and all tests pass, and the server boots without the
"sql: Register called twice for driver sqlite" panic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sthanikan2000 added a commit to OpenNSW/agency that referenced this pull request Sep 7, 2026
Drops the temporary replace directive now that OpenNSW/core#186 (the
driver-registration fix refid/store/sqlite needs here) has merged, and
repoints the require at that commit.

Verified against the published module rather than the local checkout:
build, vet and all tests pass, and the server boots without the
"sql: Register called twice for driver sqlite" panic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ginaxu1 pushed a commit to OpenNSW/agency that referenced this pull request Sep 8, 2026
Drops the temporary replace directive now that OpenNSW/core#186 (the
driver-registration fix refid/store/sqlite needs here) has merged, and
repoints the require at that commit.

Verified against the published module rather than the local checkout:
build, vet and all tests pass, and the server boots without the
"sql: Register called twice for driver sqlite" panic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sthanikan2000 added a commit to OpenNSW/agency that referenced this pull request Sep 8, 2026
* feat(refid): generate agency reference IDs on application inject

An agency had no way to issue its own reference number for an
application — the only identifier was the opaque NSW task ID. Where a
number was needed it was typed by hand into the review form, with
nothing guaranteeing it unique, sequential, correctly formatted, or
scoped to the issuing office.

Adopts github.com/OpenNSW/core/refid, split across two config layers so
that what an agency can issue is a deployment decision while which tasks
get one is a task decision:

- refIDGen in config.yaml declares the formats (issuers, segments,
  lists). Optional — omit it and no task can generate a reference ID.
- A new optional refid block in a task config names an (issuer, idType)
  from there, the JSON Pointer to store the result at, and params mapped
  to JSON Pointers into the injected data. Sourcing params from the
  data is what lets one task config serve every office rather than
  needing one config per office.

Generated once, on first inject only; a re-inject keeps the number it
already has. This required carrying ReviewerResponse forward in
CreateApplication, since CreateOrUpdate does a full-row Save that would
otherwise NULL the column and destroy an issued ID — the same reason
ClaimedBy/ClaimedAt are already carried over.

Generation failure fails the inject, so an application never exists
without its reference ID. An unresolvable param maps to 400; an
unconfigured issuer/idType, counter overflow or a database error to 500.

Counters live in a new refid_sequences table (migration 000010) rather
than refid's own Migrate helpers, keeping the .sql file the single
source of truth for schema and getting down/status with it. The store
reuses the existing GORM pool: a second sql.Open would be a different
database for sqlite :memory: and a second competing writer for a file.

internal/refidstore is tested against this module's real SQLite driver
(glebarez), not modernc — refid's queries use RETURNING and ?N ordinal
placeholders, which upstream only exercises against modernc.

Requires the driver-registration fix in OpenNSW/core refid/store/*; the
go.mod replace directive is temporary and must be dropped, and the
require repointed at the merged ref, before this merges.

Closes #306

* chore(deps): point refid at the merged core module

Drops the temporary replace directive now that OpenNSW/core#186 (the
driver-registration fix refid/store/sqlite needs here) has merged, and
repoints the require at that commit.

Verified against the published module rather than the local checkout:
build, vet and all tests pass, and the server boots without the
"sql: Register called twice for driver sqlite" panic.

* fix(refid): address review feedback on reference ID generation

Honour the documented refid.params contract. Three places said params may
be declared generously because refid ignores keys a format doesn't
consume, but generateRefID resolved every declared param and rejected the
inject if any pointer missed. Resolve what's present and let refid decide
what it needs: it returns ErrInvalidParam for a param a segment requires
and for a scope key left with an unresolved placeholder, and that already
maps to a 400.

Generate before creating the consignment, so a generation failure leaves
nothing behind. CreateConsignment fetches NSW extras and inserts a row,
which previously survived a later generation failure as an orphan. The
cost is a slightly wider window in which a crash strands the counter
value just claimed, which refid tolerates by design.

Skip building the counter store and registry when no refIDGen section is
configured, rather than building an empty registry and taking a database
handle for a feature that is off. refidstore.Disabled fills the gap: a
Registry whose Generate always fails, so a task declaring refid on such a
deployment is still a loud misconfiguration rather than a silent no-op,
and application.NewService keeps its non-nil-dependency invariant. Its
error wraps ErrUnknownIssuer so the HTTP mapping is unchanged, but names
the real cause instead of reading like a task-config typo. The startup log
now says "not configured" rather than "configured issuers=0".

Pick the counter-table DDL by dialect in the end-to-end test. newTestStore
runs against PostgreSQL when AGENCY_DB_DRIVER=postgres, which has no
datetime('now'), so the test failed during setup on that path.

Drop the migration number from the docs and refidstore's comment — it goes
stale if migrations are ever collapsed.

* refactor(refid): trim redundant tests and commentary

Self-review pass over the PR, no behaviour change.

Drop three redundant tests. TestRegistry_GeneratesFullID duplicated the
application end-to-end test, which covers strictly more — same real
registry and store, plus persistence, and both dialects rather than
SQLite only. The orphan-consignment test merged into the
unconfigured-deployment one, which shares its setup and trigger. The
missing-required-param test folded into the end-to-end test, which
already had the registry built and an adjacent rejection case.

Cut commentary that states what isn't done rather than what the code
does: the task-config doc no longer carries a note about review-payload
validation being future work, keeping only the caveat a form author acts
on. generateRefID's doc comment was longer than the function; the
counter-burn trade-off in CreateApplication belongs in a commit message,
not beside the code.

Stop naming the migration by number in refidstore's test comment, for the
same reason it was dropped elsewhere — it goes stale if migrations are
ever collapsed.

Both regression checks still catch what they were written for: the old
generation ordering still leaves an orphan consignment, and removing the
ReviewerResponse carry-forward still loses the ID on re-inject.

* refactor(refid): generateRefID returns the ID, not a document

generateRefID built a fresh JSONB and returned it for the caller to
assign, which made two failures possible the moment anything changed: a
caller running it against an existing reviewer response would silently
discard that document, and the error path returned a nil map that nulls
the field if the error is ever mishandled. It now returns a string, and
CreateApplication owns the write.

Fold the three consecutive `existing` checks into one if/else while
here. They were mutually exclusive already, which is the only reason the
reference ID write could not clobber a carried-forward reviewer
response — as a single branch that safety is structural rather than
incidental, and the new-application branch provably starts with no
reviewer response, so no defensive nil check is needed.

* refactor(refid): extract initRefIDs, drop HTTP codes from service docs

Move the reference ID wiring out of main() into initRefIDs, which returns
an error rather than calling log.Fatalf so it is testable. Three tests
cover it, including that a deployment with no refIDGen section never
reaches the database — the nil *gorm.DB they pass is the assertion.

generateRefID's doc comment described its errors as a 400 and a 500. It
isn't an HTTP handler and has no business naming status codes; it now
says which sentinel it wraps and leaves the mapping to the handler.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants