Skip to content

Add support for the cgo-free modernc.org/sqlite driver - #37

Open
peczenyj wants to merge 8 commits into
cockroachdb:masterfrom
peczenyj:sqlite-support
Open

Add support for the cgo-free modernc.org/sqlite driver#37
peczenyj wants to merge 8 commits into
cockroachdb:masterfrom
peczenyj:sqlite-support

Conversation

@peczenyj

@peczenyj peczenyj commented Jun 4, 2026

Copy link
Copy Markdown

Summary

Closes #40.

This PR adds support for the cgo-free SQLite driver modernc.org/sqlite, addressing the README's invitation to extend copyist beyond the Postgres pq/pgx drivers. It consists of one small, dependency-free core change plus a fully self-contained test module — the heavy SQLite dependency never touches the root module.

The core change: generic error-code round-tripping (values.go)

SQLite values themselves (int64, float64, string, []byte, time.Time) already round-trip through the existing valueTypes, so the proxy layer needed no changes at all. The only gap was error fidelity: modernc.org/sqlite's Error type has unexported fields and no exported constructor, so copyist cannot reconstruct it during playback the way it reconstructs *pq.Error / *pgconn.PgError via the wire protocol.

Instead of importing the driver (see "Why a separate module" below), this PR adds one generic, dependency-free mechanism:

  • A new errorWithCodeType (valueType = 12): at record time, any error implementing interface{ Code() int } is captured as 12:<code> <quoted message> — no import of the driver needed.

  • At playback time, copyist returns an unexported errorWithCode type preserving the original message and numeric code.

  • Test code asserts codes through an interface, which works identically in recording and playback modes:

    var coder interface{ Code() int }
    require.True(t, errors.As(err, &coder))
    require.Equal(t, sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY, coder.Code())

The new type-switch case is placed after the concrete *pq.Error/*pgconn.PgError cases (which expose codes as fields, not methods, so they cannot match it) and before the generic case error:. Any future driver whose errors expose Code() int gets this behavior for free. What is not preserved is the concrete driver error type — errors.As(err, &sqliteErr) will not match during playback; this limitation is documented in the README.

A real recorded line from the test suite, produced by a PRIMARY KEY violation:

25=ConnExec	2:"INSERT INTO customers VALUES (1, 'Dup')"	12:1555 "constraint failed: UNIQUE constraint failed: customers.id (1555)"

Why a separate test module (drivertest/sqlitetest)

modernc.org/sqlite is SQLite transpiled to Go — a very large dependency that requires a much newer Go than the root module's go 1.16. Following the existing drivertest/pqtestold precedent, the test package has its own go.mod (go 1.23, replace github.com/cockroachdb/copyist => ./../..), so:

  • the root module gains zero new dependencies and keeps its Go 1.16 floor;
  • users who don't use SQLite are completely unaffected.

Unlike the Postgres packages, no Docker is needed: recording mode runs against a local file database (file:copyist_test.db, gitignored via *.db). A file rather than :memory: is deliberate — copyist re-opens connections per session for determinism, and each new connection to :memory: would see a fresh empty database.

The suite mirrors the commontest coverage: TestQuery, TestInsert, TestMultiStatement, TestTxns, TestDataTypes (all four SQLite storage classes + time.Time), TestSqlx, and TestError (the end-to-end proof that error codes survive playback). The committed testdata/sqlitetest_test.copyist recording allows playback-only runs with no setup.

Supporting changes

  • copyist.go — comment-only: documents that sqlx doesn't know the "sqlite" driver name (only "sqlite3"), so BindType returns UNKNOWN, which sqlx treats like the default ? — exactly SQLite's native placeholder style. Verified against sqlx's bind.go; TestSqlx exercises the path.
  • Makefilemake test now records + plays back drivertest/sqlitetest alongside pqtestold (no Docker required for this step).
  • CI (go.yml) — new test-sqlite job on Go 1.23 running playback-only; the existing Go 1.18 job is untouched, so the root module's compatibility floor remains verified.
  • README.md — supported-drivers update, a short SQLite usage paragraph, and the error-fidelity limitation with the errors.As pattern.
  • .gitignore*.db scratch files (and a local docs/ working directory).

Test Plan

  • go test ./... at root: all packages pass on Go 1.18-compatible code (core change is backward-compatible with all existing committed Postgres recordings)
  • Unit tests for the new valueType: round-trip case in TestRoundtrip + TestErrorWithCode (foreign coder type → playback type)
  • drivertest/sqlitetest: 7 tests pass in recording mode (real driver, local file DB) and playback mode (recordings only, no DB file present)
  • Recording verified to contain the new 12: valueType for the constraint-violation error
  • gofmt/go vet clean on both modules

Notes for reviewers

  • The exact numeric constant asserted in TestError is SQLITE_CONSTRAINT_PRIMARYKEY (1555); SQLite reports the message as "UNIQUE constraint failed" because INTEGER PRIMARY KEY is enforced via a unique index — the code, not the message, identifies the constraint kind.
  • Possible follow-ups, intentionally out of scope: MySQL support (its *mysql.MySQLError has exported fields and fits the existing in-core reconstruction pattern), and stable ordering of recordings in WriteRecording (file ordering currently varies between from-scratch re-records due to map iteration; playback is unaffected).

🤖 Generated with Claude Code

peczenyj and others added 8 commits June 4, 2026 17:44
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Driver-specific error types whose fields are unexported (such as
modernc.org/sqlite's Error type) cannot be reconstructed during playback.
Preserve their message and numeric code via a new errorWithCodeType
valueType, so tests can assert error codes identically in recording and
playback modes using errors.As with an interface{ Code() int } target.
The package lives in its own Go module (like pqtestold) so that the large
modernc.org/sqlite dependency stays out of the root module. No docker is
required: recording mode runs against a local temporary database file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TestError which inserts a duplicate primary key and checks that the
resulting constraint-violation error code (SQLITE_CONSTRAINT_PRIMARYKEY=1555)
is preserved through copyist's errorWithCodeType (valueType 12) in both
recording and playback modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cockroach-teamcity

Copy link
Copy Markdown
Member

This change is Reviewable

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.

Support the cgo-free SQLite driver (modernc.org/sqlite)

2 participants