Skip to content

feat: replace mattn/go-sqlite3 with modernc.org/sqlite (pure Go, no CGO)#15

Merged
aeneasr merged 8 commits intomainfrom
modernc-sqlite
Mar 19, 2026
Merged

feat: replace mattn/go-sqlite3 with modernc.org/sqlite (pure Go, no CGO)#15
aeneasr merged 8 commits intomainfrom
modernc-sqlite

Conversation

@aeneasr
Copy link
Member

@aeneasr aeneasr commented Mar 17, 2026

Summary

Replaces github.com/mattn/go-sqlite3 (CGO-based) with modernc.org/sqlite (pure Go). The modernc driver is registered under the "sqlite3" name, so all existing DSN schemes (sqlite3://, sqlite://) and dialect configuration continue to work without any further changes.

Why this is safe

SQLite's on-disk file format — including WAL mode — is a public specification at https://sqlite.org/fileformat.html and is implementation-independent. Both drivers implement the same spec:

  • mattn/go-sqlite3 wraps the official SQLite C amalgamation via CGO
  • modernc.org/sqlite is a mechanical C→Go translation of the same amalgamation

They are not different databases. They are different build-time approaches to the same library. Any conforming SQLite implementation reads any other's files — existing production databases written by mattn are fully readable/writable by modernc.

Additional confirmation:

  • modernc's own test suite imports mattn as a test-only dependency to perform exactly this cross-driver verification
  • No interoperability bugs are reported in the open-source community; projects like Gogs have migrated live production databases from mattn to modernc without file-format issues
  • The .shm shared memory index file is transient and regenerated on open by both drivers — not a concern for migration

Why this is needed

mattn/go-sqlite3 via CGO:

  • Requires a C compiler at build time
  • Blocks cross-compilation
  • Complicates Docker builds (must set CGO_ENABLED=1 explicitly)

Switching to modernc enables pure-Go builds and is a prerequisite for removing CGO requirements across the Ory monorepo (Kratos, Hydra, Keto, Oathkeeper).

DSN compatibility

This change also resolves the open risk item for the monorepo migration: pop previously required sqlite3:// to select the mattn driver by name. By registering modernc under the "sqlite3" name, the sqlite3:// scheme continues to work — no DSN changes are needed in consuming services.

Test plan

  • go build -tags sqlite ./... — clean
  • go test -tags sqlite -run TestSqlite ./... — all pass

🤖 Generated with Claude Code

Switches the SQLite driver from the CGO-based mattn/go-sqlite3 to the
pure-Go modernc.org/sqlite. The modernc driver is registered under the
"sqlite3" name so all existing DSN schemes (sqlite3://, sqlite://) and
dialect configuration continue to work without any changes.

This eliminates the CGO requirement for SQLite builds, enabling
cross-compilation and simpler Docker/CI builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aeneasr aeneasr requested a review from a team as a code owner March 17, 2026 21:58
Copilot AI review requested due to automatic review settings March 17, 2026 21:58
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR switches the SQLite backend used by pop from the CGO-based github.com/mattn/go-sqlite3 to the pure-Go modernc.org/sqlite, while preserving existing sqlite3/sqlite dialect and URL scheme behavior via explicit driver registration.

Changes:

  • Replace the SQLite driver dependency with modernc.org/sqlite and update transitive module dependencies.
  • Register the modernc SQLite driver under the "sqlite3" driver name behind the sqlite build tag.
  • Update golang.org/x/* dependency versions as part of the module graph changes.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 1 comment.

File Description
dialect_sqlite_tag.go Switches driver loading/registration from mattn’s init-based registration to an explicit sql.Register("sqlite3", ...) using modernc.
go.mod Drops direct mattn/go-sqlite3 requirement, adds modernc.org/sqlite, and bumps several dependencies.
go.sum Updates checksums to reflect the new dependency graph (modernc + related transitive modules).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

aeneasr added 2 commits March 17, 2026 23:36
modernc.org/sqlite only recognizes _pragma=name(value) DSN params.
Legacy mattn-style params (_fk, _journal_mode, _busy_timeout) are
silently ignored.

finalizerSQLite now:
- translates _fk, _journal_mode, _busy_timeout to _pragma= equivalents
- adds busy_timeout(5000) and foreign_keys(1) defaults via _pragma
- sets _time_format=sqlite for correct time round-tripping

The fix works by building url.Values (which supports duplicate keys)
from RawOptions or the Options map, then re-encoding to RawOptions.
…ound-trip

The _time_format=sqlite option writes times with +00:00 timezone which
time.Parse creates as FixedZone("", 0) rather than time.UTC (nil).
This causes reflect.DeepEqual failures when comparing times written as
UTC to times read back from the database.

Without _time_format, modernc uses t.String() format which includes the
"UTC" timezone name. time.Parse correctly identifies this as time.UTC
(nil), making round-trips preserve the location for reflect.DeepEqual.

The scan errors originally attributed to missing _time_format were
caused by DEFAULT 'CURRENT_TIMESTAMP' (string literal) in migrations,
not by the format itself. That bug is fixed separately in the migrations.
@gaultier
Copy link

gaultier commented Mar 18, 2026

WDYT about these findings:

I have a branch (pgaultier-modernc-sqlite) where I am attempting to replace mattn/go-sqlite3 with modernc.org/sqlite (pure Go library, no CGO).
Unfortunately I think this library is buggy and drops some writes.

The tests fail with various errors about data not found in the database that should be there, or duplicate data, but the canary in the coal mine is the up/down migration test TestMigrations_SQLite, it applies all up migrations and then all down migrations. This test successfully applies all up migrations (ostensibly), then starts reverting each migration from newest to oldest, and then fails with an error saying it tried to revert the migration 20251105000000000002 but could not find it. The migration 20251105000000000002 exists on disk, but not in the db:

[...]
20250708190000000001|0
20250710085000000000|0
20251104000000000000|0
20251215000000000000|0
20260114175904000000|0
Somehow the insert for it was reported successfully by the driver but the write did not actually take place? So, yikes. For now we should stick with the existing especially if we ship that to customers, and also no one wants more flakiness in the tests 😉

I also found some issues:

Don't get me wrong, I would love for it to work.

@aeneasr
Copy link
Member Author

aeneasr commented Mar 18, 2026

WDYT about these findings:

I have a branch (pgaultier-modernc-sqlite) where I am attempting to replace mattn/go-sqlite3 with modernc.org/sqlite (pure Go library, no CGO).
Unfortunately I think this library is buggy and drops some writes.
The tests fail with various errors about data not found in the database that should be there, or duplicate data, but the canary in the coal mine is the up/down migration test TestMigrations_SQLite, it applies all up migrations and then all down migrations. This test successfully applies all up migrations (ostensibly), then starts reverting each migration from newest to oldest, and then fails with an error saying it tried to revert the migration 20251105000000000002 but could not find it. The migration 20251105000000000002 exists on disk, but not in the db:
[...]
20250708190000000001|0
20250710085000000000|0
20251104000000000000|0
20251215000000000000|0
20260114175904000000|0
Somehow the insert for it was reported successfully by the driver but the write did not actually take place? So, yikes. For now we should stick with the existing especially if we ship that to customers, and also no one wants more flakiness in the tests 😉

I also found some issues:

Don't get me wrong, I would love for it to work.

I find the conclusion hard to believe to be honest! SQLite is not a toy database

aeneasr and others added 3 commits March 19, 2026 10:52
Tests and client code check cd.Options for legacy keys (_fk, _busy_timeout)
but finalizerSQLite only wrote translated pragmas to cd.RawOptions. Add two
setOptionWithDefault calls to reflect applied defaults back into cd.Options.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pragma detection

- Remove sqlite build tag: modernc.org/sqlite is pure-Go, no build constraint needed
- Delete dialect_nosqlite_test.go: its !sqlite complement is obsolete
- urlParserSQLite3: set cd.RawOptions from raw query string so finalizerSQLite
  sees all duplicate _pragma keys (map[string]string drops duplicates silently)
- sqlitePragmaSet: require '(' after pragma name to prevent false prefix match
  (e.g. foreign_keys_per_table would have matched foreign_keys check)
- Expand legacy param translation from 3 to 23 entries covering the full mattn
  DSN surface: _foreign_keys/_fk, _journal_mode/_journal, _busy_timeout/_timeout,
  _synchronous/_sync, _auto_vacuum/_vacuum, _case_sensitive_like/_cslike,
  _defer_foreign_keys/_defer_fk, _locking_mode/_locking, _recursive_triggers/_rt,
  _cache_size, _ignore_check_constraints, _query_only, _secure_delete, _writable_schema
- Warn and drop _loc/tz/timezone (mattn-only, no modernc equivalent)
- Generic pragma echo-back: all applied _pragma values reflected into cd.Options
  via sqliteOptionKey map (foreign_keys -> _fk for backward compat)
- Replace wrong-polarity _time_format=sqlite test with negative guard
- Add comprehensive DSN-level tests: RawOptions assertions, legacy param table,
  programmatic path, direct _pragma round-trip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, fix double TrimSpace

- Extract sqliteInternalKeys to package-level var (avoid per-call allocation)
- Add moderncSQLiteParams allowlist: warn and strip any DSN param not supported
  by modernc.org/sqlite (vfs, _pragma, _time_format, _txlock,
  _time_integer_format, _inttotime, _texttotime)
- Remove spurious _loc warning for tz/timezone (neither was ever a mattn param)
- Remove redundant outer TrimSpace in pragma value extraction
- Update tests: unsupported pass-through params (foo=bar) are now stripped from
  both RawOptions and cd.Options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -1,15 +0,0 @@
//go:build !sqlite
// +build !sqlite
Copy link
Member Author

Choose a reason for hiding this comment

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

No more -tags sqlite!!

if err := genericSelectOne(c, model, query); err != nil {
return fmt.Errorf("sqlite select one: %w", err)
}
normalizeTimesToUTC(model.Value)
Copy link
Member Author

Choose a reason for hiding this comment

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

The problem is that modernc and mattn parse timezones differently, and to ensure we return UTC codes consistently, we normalize this here.

aeneasr and others added 2 commits March 19, 2026 13:46
…race

The parallel subtests both call finalizerSQLite which writes to the
package-level log var, racing with setNewTestLogger in migration tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gaultier
Copy link

LGTM, terrific

@aeneasr aeneasr merged commit b19b822 into main Mar 19, 2026
8 checks passed
@aeneasr aeneasr deleted the modernc-sqlite branch March 19, 2026 13:03
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.

3 participants