Library / SDK
cockroachdb/copyist avatar
cockroachdb/copyist

copyist: recording SQL calls at the Go driver layer

Mocking your SQL database in Go tests has never been easier.

850 stars21 forksGoApache-2.0

At a glance

What is it?
copyist intercepts the low-level SQL calls a Go test makes, stores them as files next to the test, and replays them later so the suite no longer needs a live database. It is fast and it keeps test packages from fighting over shared state, but it only works where the SQL path is deterministic.
Who is it for?
copyist suits Go teams whose tests speak SQL through the database/sql driver layer and whose query sequences are stable enough to replay, and it is a poor fit for anyone whose tests depend on non-deterministic query ordering or on database behaviour the recording cannot capture.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

copyist hooks the sql package instead of your test code

The design choice that separates copyist from a mocking library is where it intercepts traffic. copyist ships a driver for Go's database/sql package, so it sees the same calls any other driver would: connection setup, prepared statements, queries, rows iteration and transaction boundaries. Because the interception happens below your application code, the README states that copyist imposes no overhead on production code and requires almost no changes to your application or testing code, as long as that code directly or indirectly uses Go's sql package. Go ORMs and the widely used sqlx package fall into that category since they all route through database/sql.

That placement also explains what copyist is not. It is not a canned-response framework where a developer hand-writes the expected results. The comparison the project draws is with mocking libraries that intercept calls at some layer of the application or data access stack and return canned responses without needing to touch the database. The criticism aimed at those libraries is that the developer has to construct the canned responses by hand, which the README calls time-consuming and fragile when the application changes. copyist derives its responses from a real database run instead, so the recorded values are whatever the database actually returned that day.

The payoff is a test suite whose database state is known. Instead of resetting a database between cases, the recording replays a fixed sequence, so a test that removes a customer row cannot break a later test that expects the row to exist.

Registering a driver and opening a recording session

The recommended pattern registers the real driver once and then opens a session around each test. The registration call happens in init, in TestMain, or anywhere that runs before the tests, and it creates a new driver name with Go's sql package of the form copyist_<driverName>. The session is opened with a deferred Close so the recording file is written when the test finishes.

go
func init() {
	copyist.Register("postgres")
}

func TestQueryName(t *testing.T) {
	defer copyist.Open(t).Close()

	db, _ := sql.Open("copyist_postgres", "postgresql://root@localhost")
	defer db.Close()

	name := QueryName(db)
	if name != "Andy" {
		t.Error("failed test")
	}
}

The connection string still points at a real database during recording, which is why this only works where a Postgres instance is reachable. The important detail is that the application code under test does not change. Only the driver name passed to sql.Open changes, from postgres to copyist_postgres, and that is the whole visible difference between the recording run and the playback run.

The -record flag writes a .copyist file into testdata

Recording is triggered by a command-line flag, and playback happens by omitting it. Run a single test in record mode and copyist captures every low-level SQL call the code makes.

code
go test -run TestQueryName -record

The output lands in a testdata subdirectory with the same name as the test file and a .copyist extension, so a file called app_test.go produces testdata/app_test.copyist holding the recording for that test. Running the test again without the flag replays the recorded calls and never opens a connection to the database:

code
go test -run TestQueryName

The expectation stated in the project documentation is that the second run is significantly faster, because no database connection or query round trip is involved. Note what the reader has to keep track of: the flag is per invocation, so a package that does not link the library at all simply has no record flag to pass. That detail is what motivates the environment variable form.

COPYIST_RECORD covers packages that lack the flag

When a repository contains many test packages and only some of them import copyist, passing -record to a package that never registered the driver is a problem, since the flag does not exist there. Setting the COPYIST_RECORD environment variable to any value switches copyist into recording mode without touching the command line, so a single invocation can drive every package at once.

bash
COPYIST_RECORD=1 go test ./...

The project's own Makefile uses exactly that pattern for its test target. It first deletes every .copyist file to start from a clean slate, then re-records everything with the environment variable set and the parallelism pinned to a single package at a time:

code
	@find . -type f -name '*.copyist' -delete
	@COPYIST_RECORD=1 go test ./... -p=1 -count=1
	@cd drivertest/pqtestold && COPYIST_RECORD=1 go test ./... -p=1 -count=1
	@go test ./... -count=1
	@cd drivertest/pqtestold && go test ./... -count=1

The final pair of lines runs the same tests through playback. Reading the Makefile is instructive because it separates the two phases explicitly, and the comment on the test target also explains that the drivertest/pqtestold package is run on its own because it has its own go.mod file.

Resetting the database is delegated to a session init function

The problem copyist is built around is state leaking between test cases. The usual fix is to reset the database before each test, and the usual cost is that dropping and recreating tables, clearing rows and reinserting fixtures is slow. copyist does not remove that need during recording, it just relocates it. A function registered with SetSessionInit is called by copyist each time Open is called in a test, but only while copyist is in recording mode.

go
func init() {
    copyist.Register("postgres")
    copyist.SetSessionInit(resetDB)
}

The reset function can do anything it likes, and the documentation says it usually runs a SQL script against the database to drop and create tables, delete data from tables, and insert fixture data that makes testing more convenient. During playback the function is not the thing being replayed, the recorded calls are, which is why the playback phase can run different test packages in parallel without them conflicting at the database level. That parallelism is a real gain, but it also means a fixture change is invisible until someone re-records.

Unexpected call panics expose non-deterministic SQL

A playback run that does not match the recording fails with an unexpected call panic telling you to regenerate the recording. The common cause is simple: the application or the test now issues a different sequence or different content of calls, so the recording is stale and the fix is to re-run with the record flag.

The harder case described in the project documentation is a recording that was just regenerated, with no code changes, that still fails depending on the order tests run in. The cause is non-determinism in the application or in the ORM, and the example given is an ORM that sends a setup query when the first connection is opened in order to determine the database version. Whichever test runs first records that extra Query call, so running a different test first produces the unexpected call error. This is the sharpest limit of the approach. copyist can only replay a fixed sequence, so any dependency on ordering, on wall-clock time, or on an ORM that varies its bootstrap queries will need either a stable setup or a per-package recording.

A small Apache-2.0 library with a recent commit but older tags

The maintenance picture has two halves that should not be conflated. The last push to the repository was on 2026-10-14, so the code is being changed. The most recent tagged release, v1.7.0, is dated 2024-04-10, with v1.6.0 from 2022-08-24 and v1.5.0 from 2022-07-12. The gap between the newest commit and the newest tag means you are likely to depend on untagged work if you track the default branch, so pinning to a release is a reasonable precaution.

The module targets go 1.16 and its dependencies are small and Postgres centric: pgx v4, pgconn, pgproto3, lib/pq, sqlx, testify, leaktest and pkg/errors. The top-level layout is flat and readable, with conn.go, driver.go, session.go, stmt.go, tx.go, rows.go, result.go and values.go alongside copyist.go, recording_source.go and recordtype.go, plus a vendor directory and a drivertest folder. The license is Apache-2.0, the permissive license commonly used for infrastructure libraries, so embedding it in a proprietary product carries the usual notice and patent conditions rather than a copyleft obligation. That is a licensing note, not legal advice.

Editorial conclusion

copyist suits Go teams whose tests speak SQL through the database/sql driver layer and whose query sequences are stable enough to replay, and it is a poor fit for anyone whose tests depend on non-deterministic query ordering or on database behaviour the recording cannot capture. Check first that your application and ORM produce the same call sequence on every run, that your test files can live in a testdata directory as committed fixtures, and that the license obligations of the Apache-2.0 project match your distribution model.

Frequently asked questions

How do I install copyist for Go tests?

The module path is github.com/cockroachdb/copyist and it is added to your Go project like any other module dependency. The project documentation gives no separate install command, so use the standard Go module tooling to fetch it.

How do I record and then replay SQL calls with copyist?

Register the driver with copyist.Register, open a session with copyist.Open(t).Close() around the test, then run the test with the -record flag to write a .copyist file into testdata. Running the same test without the flag replays that file instead of querying a database.

What happens when a copyist playback does not match the recording?

You get an unexpected call panic asking you to regenerate the recording, which usually means the application or test changed the sequence of SQL calls. If you just re-recorded and still see it, the cause is non-determinism, for example an ORM that sends a version detection query on the first connection.

Can copyist run test packages in parallel against a database?

During playback there is no database access, so different test packages do not conflict at the database level and can run in parallel. During recording the session init function still resets the database, so parallel recording needs coordination.

Which database drivers does copyist support?

The example registers the postgres driver and the go.mod dependencies are Postgres oriented, including pgx, pgconn and lib/pq. The documentation only shows the postgres case, so treat other drivers as unconfirmed.

Official sources

  1. cockroachdb/copyist on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases