Documentation
¶
Overview ¶
Package sqltest is sqlkit's toolkit for testing SQL code. It brings together two complementary halves: helpers that seed and manage a real database, and a spy-and-mock pair built on querytrace for asserting on the queries code issues. Import the one package whether a test needs a seeded database, a query budget, or both.
Seeding a database ¶
The seeding helpers cut the boilerplate of managing the schema and inserting fixtures, leaving the connection (and any test container) to the caller. Every helper takes a testing.TB and fails the test with t.Fatalf on error, so callers write linear setup code with no error handling.
A DB is the entry point. New wraps a *sqlkit.Database the caller opened against a disposable test database, returning a handle whose methods reset, isolate, and seed it. New itself changes nothing; an explicit SetupSchema applies the schema and drops it again on cleanup:
tdb := sqltest.New(database, md)
tdb.SetupSchema(t) // schema applied now, dropped on t.Cleanup
id := tdb.InsertID(t, appdb.Users, UserDTO{Email: "a@example.com"})
tdb.InsertRows(t, appdb.Posts, PostDTO{UserID: id, Title: "hello"})
Constructing a DB is the safety bar: the destructive operations (applying or dropping the schema, truncating tables) live only as DB methods, so a database the test did not deliberately wrap as throwaway is never dropped.
- Schema lifecycle: DB.SetupSchema (apply now, drop on t.Cleanup), and the methods DB.ApplySchema (apply, no teardown) and DB.Truncate for resetting within a test. Their drop and truncate steps are foreign-key-safe regardless of how the tables were declared — children before or after parents, cross-schema, or in a cycle.
- Isolation: DB.Tx hands each case its own transaction, rolled back on t.Cleanup, so cases that share one schema stay independent and may run in parallel. Snapshot (and Fixture.LoadSnapshot) instead capture the seeded state of a single session as a SAVEPOINT and return a restore function, so many serial cases reuse one seed by rolling back between them.
- Data insertion: the DB.InsertRows / DB.InsertID / DB.Load methods seed through auto-commit; the InsertRows, InsertID, InsertReturning, and Fixture.Load functions take any sqlkit.Querier, so they seed into a Tx session (isolated, rolled back with the case) just as well.
- Fixtures: Fixture, a named, ordered set of rows across tables, with Load.
The insert helpers smooth over the main cross-dialect wrinkle in seeding: reading back a database-generated key. InsertID uses RETURNING on PostgreSQL and Result.LastInsertId on MySQL, so fixture code that wires up foreign keys works on both without branching. Foreign-key ordering of the rows is the caller's responsibility: insert parent rows before the children that reference them, exactly as in hand-written seed code.
Asserting on queries ¶
The spy turns a querytrace.Trace into a record of the SQL one operation actually issued, and asserts on it after the fact. Where a mock library makes you declare the queries up front and fails if they do not run, the spy works the other way around — run the code under a Trace, then ask what it did: a query budget, no writes, no N+1, a SELECT on a given table that ran exactly once, an INSERT before the COMMIT.
The entry point is Expect, a fluent assertion bound to a testing.TB:
trace := querytrace.New("GetUserPage")
ctx := querytrace.WithTrace(context.Background(), trace)
service.RenderUserPage(ctx, id)
sqltest.Expect(t, trace).
MaxQueries(5).
NoWrites().
NoNPlusOne().
Query(sqltest.Select(), sqltest.Table("users")).Times(1)
Match values (Select, Table, Fingerprint, SqlContains, Caller, HasTopLevelWhere, …) select the statements an assertion applies to; pass several and they must all hold. All, Any, and Not compose them. Count and Events expose the same matching without a testing.TB, for inspecting a Trace directly.
Mocking a database ¶
For unit tests that should not touch a database at all, NewDB returns a *sql.DB backed by a programmable Mock and traced by querytrace, so the mock provides the inputs (canned rows, results, or errors registered with ExpectQuery and ExpectExec) and the spy assertions verify the calls.
Index ¶
- func AnyColumn() sqlpkg.Selection
- func AnyExpr() sqlpkg.Expression
- func AnyTable() sqlpkg.Source
- func AssertCount(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, want int)
- func AssertCountWhere(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, ...)
- func AssertEmpty(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string)
- func AssertExists(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, ...)
- func AssertGoldenRows(t testing.TB, path string, rows *stdsql.Rows)
- func AssertGoldenTrace(t testing.TB, path string, trace *querytrace.Trace)
- func AssertNotExists(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, ...)
- func AssertTableContents(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, ...)
- func Count(trace *querytrace.Trace, ms ...Match) int
- func Events(trace *querytrace.Trace, ms ...Match) []querytrace.Event
- func Golden(t testing.TB, path, got string)
- func InsertID(t testing.TB, ex sqlkit.Querier, table sqlkit.Table, row any) int64
- func InsertMap(t testing.TB, ex Execer, dialect sqlkit.Dialect, table string, ...)
- func InsertMapID(t testing.TB, ex ExecQueryer, dialect sqlkit.Dialect, table, idColumn string, ...) int64
- func InsertReturning[Row any](t testing.TB, ex sqlkit.Querier, table sqlkit.Table, rows ...Row) []Row
- func InsertRows(t testing.TB, ex sqlkit.Querier, table sqlkit.Table, rows ...any)
- func LoadFixtureFile(t testing.TB, ex Execer, dialect sqlkit.Dialect, table, path string)
- func LoadFixtures(t testing.TB, ex Execer, dialect sqlkit.Dialect, dir string)
- func RenderRows(rows *stdsql.Rows) (string, error)
- func Snapshot(t testing.TB, s *sqlkit.Session) (restore func(t testing.TB))
- type ArgMatcher
- type Assertion
- func (a *Assertion) AllRowsClosed() *Assertion
- func (a *Assertion) Committed() *Assertion
- func (a *Assertion) InOrder(ms ...Match) *Assertion
- func (a *Assertion) MaxQueries(n int) *Assertion
- func (a *Assertion) MinQueries(n int) *Assertion
- func (a *Assertion) NoNPlusOne() *Assertion
- func (a *Assertion) NoQueries() *Assertion
- func (a *Assertion) NoTransaction() *Assertion
- func (a *Assertion) NoWarn(code querytrace.WarningCode) *Assertion
- func (a *Assertion) NoWarnings() *Assertion
- func (a *Assertion) NoWrites() *Assertion
- func (a *Assertion) Queries(n int) *Assertion
- func (a *Assertion) Query(ms ...Match) *QueryAssertion
- func (a *Assertion) Reads(n int) *Assertion
- func (a *Assertion) RolledBack() *Assertion
- func (a *Assertion) Transactions(n int) *Assertion
- func (a *Assertion) UniqueQueries(n int) *Assertion
- func (a *Assertion) Warns(code querytrace.WarningCode) *Assertion
- func (a *Assertion) Writes(n int) *Assertion
- type CallStub
- type DB
- func (d *DB) ApplySchema(t testing.TB)
- func (d *DB) ApplySchemaSQL(t testing.TB, ddl ...string)
- func (d *DB) AssertCount(t testing.TB, table string, want int)
- func (d *DB) AssertEmpty(t testing.TB, table string)
- func (d *DB) AssertExists(t testing.TB, table string, where map[string]any)
- func (d *DB) AssertGoldenQuery(t testing.TB, path, query string, args ...any)
- func (d *DB) AssertTableContents(t testing.TB, table string, want []map[string]any)
- func (d *DB) Database() *sqlkit.Database
- func (d *DB) ExplainQuery(t testing.TB, query string, args ...any) *Plan
- func (d *DB) InsertID(t testing.TB, table sqlkit.Table, row any) int64
- func (d *DB) InsertMap(t testing.TB, table string, rows ...map[string]any)
- func (d *DB) InsertMapID(t testing.TB, table, idColumn string, row map[string]any) int64
- func (d *DB) InsertRows(t testing.TB, table sqlkit.Table, rows ...any)
- func (d *DB) Load(t testing.TB, f *Fixture)
- func (d *DB) SetupSchema(t testing.TB)
- func (d *DB) SetupSchemaSQL(t testing.TB, ddl ...string)
- func (d *DB) Truncate(t testing.TB, tables ...sqlkit.Table)
- func (d *DB) TruncateTables(t testing.TB, names ...string)
- func (d *DB) Tx(t testing.TB, opts ...sqlkit.SessionOption) *sqlkit.Session
- type ExecQueryer
- type Execer
- type Fixture
- type Match
- func All(ms ...Match) Match
- func Any(ms ...Match) Match
- func Begin() Match
- func Caller(sub string) Match
- func Commit() Match
- func Ddl() Match
- func Delete() Match
- func Fingerprint(fp string) Match
- func HasTopLevelJoin() Match
- func HasTopLevelWhere() Match
- func Insert() Match
- func Label(key, value string) Match
- func Named(name string) Match
- func Not(m Match) Match
- func Op(k querytrace.StmtKind) Match
- func Params(n int) Match
- func ParamsAtLeast(n int) Match
- func Read() Match
- func Rollback() Match
- func Select() Match
- func Sql(text string) Match
- func SqlContains(sub string) Match
- func SqlMatches(re string) Match
- func Table(name string) Match
- func Update() Match
- func Write() Match
- type Matcher
- type Mock
- type Option
- type Plan
- func (p *Plan) AssertContains(t testing.TB, sub string)
- func (p *Plan) AssertNoSeqScan(t testing.TB)
- func (p *Plan) AssertNotContains(t testing.TB, sub string)
- func (p *Plan) AssertUsesIndex(t testing.TB, name string)
- func (p *Plan) Contains(sub string) bool
- func (p *Plan) HasSeqScan() bool
- func (p *Plan) Text() string
- func (p *Plan) UsesIndex(name string) bool
- type QueryAssertion
- type Queryer
- type RowSet
- type Stub
- func (s *Stub) AnyTimes() *Stub
- func (s *Stub) MaxTimes(n int) *Stub
- func (s *Stub) MinTimes(n int) *Stub
- func (s *Stub) Required() *Stub
- func (s *Stub) Return(rows *RowSet) *Stub
- func (s *Stub) ReturnError(err error) *Stub
- func (s *Stub) ReturnResult(lastInsertID, rowsAffected int64) *Stub
- func (s *Stub) Times(n int) *Stub
- func (s *Stub) WillDelayFor(d time.Duration) *Stub
- func (s *Stub) WithArgs(args ...any) *Stub
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AnyColumn ¶
AnyColumn is a Select selection that matches any column or expression:
mock.ExpectQuery(sql.Select(sqltest.AnyColumn()).From(sql.Tbl("users")))
func AnyExpr ¶
func AnyExpr() sqlpkg.Expression
AnyExpr is an expression that matches any expression, for Where and other clauses:
...Where(sqltest.AnyExpr())
func AnyTable ¶
AnyTable is a From source that matches any table in a sql.Query expectation:
mock.ExpectQuery(sql.Select(sql.Col("id")).From(sqltest.AnyTable()))
func AssertCount ¶
AssertCount asserts that table holds exactly want rows. It is the after-the-act counterpart of the query spy: where the spy checks which statements ran, AssertCount checks the data they left behind.
func AssertCountWhere ¶
func AssertCountWhere(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, where map[string]any, want int)
AssertCountWhere asserts that table holds exactly want rows matching where, a column→value map joined with AND (a nil value becomes IS NULL).
func AssertEmpty ¶
AssertEmpty asserts that table holds no rows. It is AssertCount(t, …, 0).
func AssertExists ¶
func AssertExists(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, where map[string]any)
AssertExists asserts that at least one row of table matches where.
func AssertGoldenRows ¶
AssertGoldenRows renders rows and compares them to the golden file at path. Add an ORDER BY to the query so the row order — and therefore the golden file — is stable. rows is consumed and closed.
func AssertGoldenTrace ¶
func AssertGoldenTrace(t testing.TB, path string, trace *querytrace.Trace)
AssertGoldenTrace renders trace's query log in a timing-free form and compares it to the golden file at path. Pinning the query log this way catches an accidental change in the SQL an operation issues — a query that turns into an N+1, a new statement, a dropped index hint — that a count-based budget would miss.
Unlike TextReport, the rendered form leaves out everything that varies from run to run: durations, the total DB time, and the warnings (some of which are derived from timing). It keeps the operation name, the query/unique/write counts, and the ordered timeline of statements (kind, name, fingerprint), transaction control points, and marks.
func AssertNotExists ¶
func AssertNotExists(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, where map[string]any)
AssertNotExists asserts that no row of table matches where.
func AssertTableContents ¶
func AssertTableContents(t testing.TB, q Queryer, dialect sqlkit.Dialect, table string, want []map[string]any)
AssertTableContents asserts that table's rows equal want, comparing only the columns want names (a subset — columns not listed, such as autogenerated keys or timestamps, are ignored) and ignoring row order. want is a slice of column→value maps, all sharing the same columns; an empty want asserts the table is empty.
Values compare by their textual form, so a want of 1 matches whether the driver returns int64(1), "1", or []byte("1") — the comparison is tolerant of the type a column scans back as. A row in want with no match in the table, or an extra row in the table, fails the assertion with a readable diff.
func Count ¶
func Count(trace *querytrace.Trace, ms ...Match) int
Count returns how many statements in trace match every Match in ms. It is the spy query underneath Query(...).Times, usable without a testing.TB.
func Events ¶
func Events(trace *querytrace.Trace, ms ...Match) []querytrace.Event
Events returns the statements in trace matching every Match in ms, in record order.
func Golden ¶
Golden compares got against the contents of the golden file at path and fails the test on any difference. With the SQLKIT_UPDATE_GOLDEN environment variable set it rewrites the file (creating parent directories) instead of comparing, so a deliberate change is recorded by re-running the test rather than editing fixtures by hand. It is the text-golden primitive the result helpers build on, and works for any string a test wants to pin (rendered rows, a query log, a report).
func InsertID ¶
InsertID inserts a single row and returns its database-generated primary key, smoothing over the dialect difference: on dialects with RETURNING (PostgreSQL) it reads the key back with RETURNING <pk>; otherwise (MySQL) it falls back to Result.LastInsertId. It assumes a single auto-generated integer primary-key column, which is the common test fixture; tables with a composite or non-integer key are not supported.
func InsertMap ¶
func InsertMap(t testing.TB, ex Execer, dialect sqlkit.Dialect, table string, rows ...map[string]any)
InsertMap seeds rows into table through ex, addressing columns by name with no sqlkit.Table binding or row struct — the catalog-free counterpart of InsertRows. Each row is a column→value map; every row must carry the same set of columns. dialect selects identifier quoting and the bind-placeholder style ($1 on PostgreSQL, ? on MySQL), so the same call works on either engine.
ex is any Execer (*sql.DB for auto-commit, or a *sql.Tx the case rolls back for isolation). All rows insert in one multi-row statement; an empty rows list is a no-op. Foreign-key ordering across tables is the caller's responsibility, the same as InsertRows.
func InsertMapID ¶
func InsertMapID(t testing.TB, ex ExecQueryer, dialect sqlkit.Dialect, table, idColumn string, row map[string]any) int64
InsertMapID inserts a single row addressed by column name and returns the value the database generated for idColumn, smoothing over the same cross-dialect wrinkle as InsertID: it reads the key back with RETURNING on PostgreSQL and falls back to Result.LastInsertId on MySQL. It is the catalog-free counterpart of InsertID for a single auto-generated integer key.
func InsertReturning ¶
func InsertReturning[Row any](t testing.TB, ex sqlkit.Querier, table sqlkit.Table, rows ...Row) []Row
InsertReturning inserts rows and scans the inserted rows — with database-generated columns filled in — back into a slice of Row. It relies on RETURNING, so it is PostgreSQL-only; on a dialect without RETURNING (MySQL) it skips the test rather than silently returning unpopulated rows. Use InsertRows or InsertID on those dialects.
func InsertRows ¶
InsertRows inserts rows into table and fails the test on error. Each row may be a struct, a pointer to a struct, or a slice of either — the same shapes InsertQuery.Values accepts — so generated row structs and caller-defined DTOs both work. It does not read anything back; use InsertID when you need a generated key, or InsertReturning to scan the inserted rows.
ex is any sqlkit.Querier, satisfied by both *sqlkit.Database (auto-commit) and *sqlkit.Session (inside a transaction).
func LoadFixtureFile ¶
LoadFixtureFile loads a single .json or .csv file into table through ex, for when the table name is decoupled from the filename or only one file is needed. The format is chosen by the file's extension. An empty file (or empty JSON array) inserts nothing.
func LoadFixtures ¶
LoadFixtures loads every .json and .csv file in dir into the table named after the file, seeding through ex with no sqlkit.Table binding — the file-based, language-agnostic counterpart of Fixture (which declares rows as Go structs). It is the fixtures style of Rails and go-testfixtures: a directory of per-table data files a test shares as its starting state.
sqltest.LoadFixtures(t, db, sqlkit.PostgreSQLDialect{}, "testdata/blog")
// testdata/blog/01_users.json, testdata/blog/02_posts.csv → tables users, posts
Files load in lexical filename order, so a numeric prefix controls foreign-key order (parents before children); the prefix is stripped from the table name, so 01_users.json loads into users (and 2024_sales.csv into sales: a table whose real name starts with digits and an underscore cannot be addressed). ex is any Execer (a *sql.DB, or a *sql.Tx a case rolls back), and dialect selects quoting and placeholders as in InsertMap. Subdirectories and files with other extensions are ignored.
func RenderRows ¶
RenderRows renders a result set as the canonical text the golden helpers compare: a tab-separated header line of column names, then one tab-separated line per row. A NULL renders as the literal NULL and byte/string columns as their text, so the form is stable across drivers. A backslash, tab, newline, or carriage return inside a value is escaped (\\, \t, \n, \r) so distinct results never render to the same text. It consumes and closes rows.
func Snapshot ¶
Snapshot records the session's current state as a reusable restore point and returns a function that rolls back to it. Seed your fixtures into the session once, snapshot, then run each case against the same session and call the returned restore between cases to discard that case's writes and return to the seeded state — far cheaper than reloading the fixtures every time:
tdb := sqltest.New(db, md)
tdb.SetupSchema(t)
session := tdb.Tx(t) // one session, rolled back at the end
blog.Load(t, session) // seed once
restore := sqltest.Snapshot(t, session)
t.Run("case1", func(t *testing.T) {
defer restore(t) // undo case1's writes
// ... mutate and assert against session ...
})
t.Run("case2", func(t *testing.T) {
defer restore(t) // case2 sees the seeded state again
// ...
})
It uses a SAVEPOINT (Session.Savepoint / RollbackTo), so it works on PostgreSQL and MySQL alike and the restore point survives repeated restores. The catch is that every case must run its statements through the same session and the session must stay open and uncommitted: committing or closing it ends the transaction and invalidates the restore point. Cases therefore cannot run in parallel against the snapshot.
Types ¶
type ArgMatcher ¶
type ArgMatcher interface {
// MatchArg reports whether the executed bind value matches.
MatchArg(v any) bool
// String describes the matcher.
String() string
}
ArgMatcher decides whether the bind argument at a placeholder position matches. Place one in a sql.Query expectation with Arg, AnyArg, or ArgMatch — for example Where(Eq(Col("id"), sqltest.Arg(5))) — and the mock checks the executed statement's argument there. Placeholders without an ArgMatcher (plain values) are left unconstrained, so a query matches any value unless you say otherwise.
func AnyArg ¶
func AnyArg() ArgMatcher
AnyArg matches any bind argument. It is explicit documentation that a value is deliberately unconstrained.
func Arg ¶
func Arg(v any) ArgMatcher
Arg matches a bind argument equal to v, comparing by database/sql driver value (so an int and the int64 the driver sees compare equal).
func ArgMatch ¶
func ArgMatch(fn func(v any) bool) ArgMatcher
ArgMatch matches a bind argument for which fn returns true.
type Assertion ¶
type Assertion struct {
// contains filtered or unexported fields
}
Assertion is a fluent set of checks against a single Trace. Each method reports a failure through the testing.TB and returns the Assertion so checks chain. Build one with Expect.
func Expect ¶
func Expect(t testing.TB, trace *querytrace.Trace) *Assertion
Expect snapshots trace and returns an Assertion over it. Call it once the code under test has run; the snapshot is stable, so later checks see the same statements and warnings even if the Trace keeps recording.
func (*Assertion) AllRowsClosed ¶
AllRowsClosed asserts no read left its row cursor open (no rows_not_closed warning).
func (*Assertion) InOrder ¶
InOrder asserts that statements matching ms appear in the trace in the given relative order: an event matching ms[0] before one matching ms[1], and so on. The matched events need not be adjacent, and matchers may be transaction matchers (Commit, Rollback, Begin), so Insert() before Commit() is expressible. Compose multi-condition steps with All.
func (*Assertion) MaxQueries ¶
MaxQueries asserts the trace recorded at most n statements, a query budget.
func (*Assertion) MinQueries ¶
MinQueries asserts the trace recorded at least n statements.
func (*Assertion) NoNPlusOne ¶
NoNPlusOne asserts the trace produced neither a possible-N+1 nor a query-in-loop warning.
func (*Assertion) NoQueries ¶
NoQueries asserts the trace touched the database not at all, the check for a path that should be served without a query (a cache hit).
func (*Assertion) NoTransaction ¶
NoTransaction asserts no transaction began within the trace.
func (*Assertion) NoWarn ¶
func (a *Assertion) NoWarn(code querytrace.WarningCode) *Assertion
NoWarn asserts the trace produced no warning with code.
func (*Assertion) NoWarnings ¶
NoWarnings asserts the trace produced no warning at all.
func (*Assertion) NoWrites ¶
NoWrites asserts the trace recorded no write statement, the check for a read-only code path.
func (*Assertion) Query ¶
func (a *Assertion) Query(ms ...Match) *QueryAssertion
Query narrows the assertion to the statements matching every Match in ms and returns a QueryAssertion to check how many ran. With no matchers it selects every statement.
func (*Assertion) RolledBack ¶
RolledBack asserts at least one transaction rolled back.
func (*Assertion) Transactions ¶
Transactions asserts exactly n transactions began within the trace.
func (*Assertion) UniqueQueries ¶
UniqueQueries asserts the trace recorded exactly n distinct query shapes (fingerprints).
func (*Assertion) Warns ¶
func (a *Assertion) Warns(code querytrace.WarningCode) *Assertion
Warns asserts the trace produced a warning with code.
type CallStub ¶
type CallStub struct {
// contains filtered or unexported fields
}
CallStub is a programmed expectation for a non-statement database call — a transaction Begin, Commit, or Rollback, or a Ping. Build it with ExpectBegin, ExpectCommit, ExpectRollback, or ExpectPing and, optionally, make the call fail with ReturnError or require it with Required / Times. Unlike a statement Stub, a CallStub is not required by default: a transaction the code under test opens and commits without any expectation still succeeds, so a CallStub serves only to inject an error or to assert the call happened.
Like a statement Stub, a CallStub answers every matching call by default (it has no call limit): ExpectCommit().ReturnError(err) makes every Commit fail. Limit it with Times or MaxTimes to have it answer only the next call(s) and let a later stub, or the default success, answer the rest.
func (*CallStub) MaxTimes ¶
MaxTimes caps how many calls the stub answers; further calls fall through to a later stub or the default success. Zero (the default) is unlimited.
func (*CallStub) MinTimes ¶
MinTimes sets the fewest calls Verify requires. Zero (the default) requires none.
func (*CallStub) Required ¶
Required marks the call as one Verify must see happen at least once. It is shorthand for MinTimes(1).
func (*CallStub) ReturnError ¶
ReturnError makes the call fail with err (BeginTx, Commit, Rollback, or Ping returns it).
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a throwaway test database: a thin handle over a *sqlkit.Database that owns the destructive test operations — applying and dropping the schema, truncating tables, and opening isolated per-test transactions — alongside auto-commit seeding sugar.
Constructing a DB is itself the safety bar. The drop and truncate operations live only as methods, reachable only through a DB a test deliberately built over a disposable database, so a *sqlkit.Database wired to a staging or production DSN — a stray environment variable, the wrong config profile — can never be wiped by a test that did not wrap it here. Construction touches nothing: SetupSchema and ApplySchema are the explicit steps that change the database, never New.
Do not confuse DB with NewDB: NewDB builds a programmable mock *sql.DB for unit tests that must not touch a database at all, whereas a DB drives a real one. The two halves of the package are independent.
func New ¶
New wraps db as a throwaway test database. It only builds the handle — it issues no SQL and changes nothing — so the schema is applied by an explicit SetupSchema (or ApplySchema), never as a side effect of construction:
tdb := sqltest.New(database, md)
tdb.SetupSchema(t) // drop + create now, dropped again on t.Cleanup
id := tdb.InsertID(t, appdb.Users, UserDTO{Email: "a@example.com"})
tdb.InsertRows(t, appdb.Posts, PostDTO{UserID: id, Title: "hello"})
md is the meta.Metadata a schema catalog produces (decl.Catalog.Build). db is a *sqlkit.Database the caller opened against a disposable test database — a container, a CI service database, or a scratch schema — never production.
func NewRaw ¶
NewRaw wraps a standard *sql.DB as a throwaway test database for code that does not use a sqlkit catalog (meta.Metadata) — a project on database/sql, sqlx, or pgx that only wants sqltest's schema lifecycle, isolation, and seeding helpers. The schema is applied from raw DDL with SetupSchemaSQL or ApplySchemaSQL rather than from a rendered catalog; everything else (Tx, Snapshot, the table-name seeding helpers) works the same.
tdb := sqltest.NewRaw(openTestDB(t), sqlkit.PostgreSQLDialect{})
tdb.SetupSchemaSQL(t, `
CREATE TABLE users (id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL);
CREATE TABLE posts (id BIGSERIAL PRIMARY KEY, user_id BIGINT REFERENCES users(id));
`)
// ... seed and exercise the code under test ...
dialect is sqlkit.PostgreSQLDialect{} or sqlkit.MySQLDialect{}; it selects the foreign-key-safe teardown strategy (PostgreSQL CASCADE, MySQL foreign-key checks toggled) and the identifier quoting used to drop the tables. db must be opened against a disposable test database — a container, a CI service database, or a scratch schema — never production: constructing a DB is the safety bar exactly as with New.
The metadata-driven methods (SetupSchema, ApplySchema, Truncate with no arguments) have no schema to act on a raw DB; use SetupSchemaSQL/ApplySchemaSQL and TruncateTables instead.
func (*DB) ApplySchema ¶
ApplySchema applies md's schema — a foreign-key-safe drop of the existing tables followed by the CREATE DDL — registering no teardown. It is SetupSchema without the automatic drop on cleanup: reach for it to reset the schema again within a test, or when something else owns the teardown.
func (*DB) ApplySchemaSQL ¶
ApplySchemaSQL applies the given DDL with no teardown registered — the raw-DDL counterpart of ApplySchema. It is SetupSchemaSQL without the automatic drop on cleanup: reach for it to reset the schema again within a test, or when something else owns the teardown. The names of the created tables are recorded so a later SetupSchemaSQL, TruncateTables, or manual drop can find them.
func (*DB) AssertCount ¶
AssertCount asserts table holds exactly want rows, reading through the DB's connection. See the package-level AssertCount.
func (*DB) AssertEmpty ¶
AssertEmpty asserts table holds no rows, reading through the DB's connection.
func (*DB) AssertExists ¶
AssertExists asserts at least one row of table matches where, reading through the DB's connection.
func (*DB) AssertGoldenQuery ¶
AssertGoldenQuery runs query (with args) through the DB's connection and compares the rendered rows to the golden file at path — the DB-method convenience over AssertGoldenRows. Add an ORDER BY so the row order, and so the golden file, is stable.
func (*DB) AssertTableContents ¶
AssertTableContents asserts table's rows equal want, reading through the DB's connection. See the package-level AssertTableContents.
func (*DB) Database ¶
Database returns the underlying *sqlkit.Database, for running queries, seeding through a sqlkit.Querier helper (InsertRows, Fixture.Load, …), or any sqlkit operation the DB does not wrap.
func (*DB) ExplainQuery ¶
ExplainQuery runs EXPLAIN through the DB's connection, supplying its dialect — the DB-method convenience over the package-level ExplainQuery.
func (*DB) InsertID ¶
InsertID inserts a single row through auto-commit and returns its database-generated primary key. It is shorthand for InsertID(t, d.Database(), table, row); see that function for the cross-dialect key read-back.
func (*DB) InsertMap ¶
InsertMap seeds rows into table through the DB's auto-commit handle, addressing columns by name. It is shorthand for InsertMap(t, d.Database().SQL(), d.Database().Dialect(), table, rows...); seed into a *sql.Tx instead when cases run in parallel, so the rows roll back with the case.
func (*DB) InsertMapID ¶
InsertMapID inserts a single row through the DB's auto-commit handle and returns its database-generated key. It is shorthand for InsertMapID(t, d.Database().SQL(), d.Database().Dialect(), table, idColumn, row).
func (*DB) InsertRows ¶
InsertRows seeds rows into table through auto-commit (the underlying database), failing the test on error. It is shorthand for InsertRows(t, d.Database(), table, rows...); seed into a Tx session instead when cases run in parallel, so the rows roll back with the case rather than persisting.
func (*DB) Load ¶
Load seeds a fixture through auto-commit. It is shorthand for f.Load(t, d.Database()); load into a Tx session instead when cases run in parallel.
func (*DB) SetupSchema ¶
SetupSchema gives the test a clean schema and tears it down automatically: it applies md's schema now — a foreign-key-safe drop of any existing tables followed by the CREATE DDL md renders for the dialect — then registers a t.Cleanup that drops the tables again, leaving the database empty. Call it once at the top of a test, or of a parent test whose subtests share the schema.
func (*DB) SetupSchemaSQL ¶
SetupSchemaSQL applies the given DDL now and drops the tables it creates on t.Cleanup, leaving the database empty — the raw-DDL counterpart of SetupSchema. Each argument may hold several semicolon-separated statements, and several arguments may be passed (e.g. one embedded .sql file per argument); they run in order. Before creating, it drops any table the DDL declares (foreign-key-safely), so a leftover table from a crashed run does not fail the CREATE.
Only the names of tables created by CREATE TABLE statements are tracked for teardown; other objects (types, sequences, functions, tables created through constructs this does not recognise) are not dropped automatically. Add their teardown with a t.Cleanup of your own when needed.
func (*DB) Truncate ¶
Truncate deletes every row from the given tables foreign-key-safely, whatever order they were declared in. Called with no tables it clears every table in the schema. It is a faster between-cases reset than re-applying the schema when the tables already exist and only their data needs clearing.
func (*DB) TruncateTables ¶
TruncateTables deletes every row from the named tables foreign-key-safely, whatever order they are given in — the raw-DDL counterpart of Truncate, which needs a meta.Metadata to resolve sqlkit.Table bindings. Called with no names it clears every table the raw schema created (the tables ApplySchemaSQL or SetupSchemaSQL recorded).
func (*DB) Tx ¶
Tx gives the test its own transaction and rolls it back automatically when the test finishes. Each call opens a separate sqlkit.Session — a transaction on its own pooled connection — so cases that share the one schema stay isolated from each other's writes and can run in parallel: nothing a case inserts, updates, or deletes is visible to another, and the rollback leaves the database exactly as the next case (or the next run) finds it.
Apply the schema once for the group, then hand each case its own transaction:
func TestBlog(t *testing.T) {
tdb := sqltest.New(database, md)
tdb.SetupSchema(t) // schema created once, dropped after all subtests
t.Run("publish", func(t *testing.T) {
t.Parallel()
s := tdb.Tx(t) // isolated transaction, rolled back on return
blog.Load(t, s) // seed inside the transaction
// ... mutate and assert against s ...
})
t.Run("archive", func(t *testing.T) {
t.Parallel()
s := tdb.Tx(t) // sees none of "publish"'s writes
// ...
})
}
SetupSchema's teardown runs on the parent test's cleanup, which Go schedules after every parallel subtest has finished, so the shared schema outlives the cases.
The returned *sqlkit.Session is a sqlkit.Querier, so it drops into the seeding helpers (InsertRows, InsertID, Fixture.Load, …) and starts query chains (s.Select()…). The transaction begins lazily on the first statement, so a case that runs nothing costs nothing. Pass sqlkit.SessionOption values (WithTxOptions, …) to control isolation level or read-only intent.
Tx isolates through a real transaction per connection, which scales to parallel cases; Snapshot isolates through savepoints on one shared, serial session. Reach for Tx when cases run in parallel or each wants a clean slate, and for Snapshot when many serial cases reuse one expensive seed.
type ExecQueryer ¶
ExecQueryer is both surfaces at once — the handle InsertMapID needs to insert and then read a generated key back with RETURNING. *sql.DB and *sql.Tx satisfy it.
type Execer ¶
type Execer interface {
ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error)
}
Execer is the write surface the table-name seeding helpers need, satisfied by *sql.DB, *sql.Tx, and *sql.Conn. It lets InsertMap seed through a raw database/sql handle — including a *sql.Tx a test rolls back for isolation — without the sqlkit query builder.
type Fixture ¶
type Fixture struct {
// contains filtered or unexported fields
}
Fixture is a named, ordered set of rows spanning one or more tables. Declare it once and Load it into a database in any test that needs the same starting data:
var blog = sqltest.NewFixture("blog").
Add(blogdb.Users, userRow{Email: "alice@example.com"}, userRow{Email: "bob@example.com"}).
Add(blogdb.Posts, postRow{Title: "Hello"})
blog.Load(t, db)
Groups load in declaration order, so add parent tables before the children that reference them — foreign-key ordering is the caller's responsibility, the same as hand-written seed code. Fixture is for static rows whose values are known up front; when later rows must reference a generated key (e.g. a post needs its author's id), capture it with InsertID in a plain seed function instead.
func NewFixture ¶
NewFixture creates an empty fixture. name appears in failure messages.
func (*Fixture) Add ¶
Add appends a group of rows for table, preserving call order. It returns the fixture so calls chain. Each row follows the same shapes InsertRows accepts (struct, pointer to struct, or slice of either).
func (*Fixture) Load ¶
Load inserts every group in declaration order, failing the test on the first error. ex is any sqlkit.Querier, so a fixture loads through a *sqlkit.Database or within a *sqlkit.Session transaction alike.
func (*Fixture) LoadSnapshot ¶
LoadSnapshot loads the fixture into the session and snapshots immediately, returning the restore function. It is Fixture.Load followed by Snapshot, for the common case of seeding a fixture and reusing it across cases.
type Match ¶
type Match struct {
// contains filtered or unexported fields
}
Match is a predicate over a recorded Event. Matchers select the statements an assertion or a spy query applies to; pass several to Query, Count, or Events and they must all hold (logical AND). All, Any, and Not compose them explicitly.
Most matchers look at the statement: Select/Insert/Update/Delete, Table, Fingerprint, HasTopLevelWhere, Params, SqlContains. A few look at the surrounding Event (Named, Caller, Label, InTransaction) or the transaction timeline (Begin, Commit, Rollback). Matchers that depend on structural analysis (Table, HasTopLevelWhere, Fingerprint) need a parser wired into the Config; without one they do not match, while operation and SQL-text matchers still do.
func Caller ¶
Caller matches a statement issued from application code whose function name or file path contains sub. It needs caller capture (WithCaller).
func Fingerprint ¶
Fingerprint matches a statement with the given normalized fingerprint.
func HasTopLevelJoin ¶
func HasTopLevelJoin() Match
HasTopLevelJoin matches a statement whose top-level node carries a JOIN clause (from analysis). A JOIN inside a subquery, CTE body, or UNION arm does not count.
func HasTopLevelWhere ¶
func HasTopLevelWhere() Match
HasTopLevelWhere matches a statement whose top-level node carries a WHERE clause (from analysis). A WHERE inside a subquery, CTE body, or UNION arm does not count.
func ParamsAtLeast ¶
ParamsAtLeast matches a statement carrying at least n bind arguments, the signature of a large IN list or batch.
func Read ¶
func Read() Match
Read matches any pure read statement. A locking read (SELECT ... FOR UPDATE) or a data-modifying CTE is a write (see Write), so it is not matched here — Read and Write partition consistently with the Reads/Writes assertions and WriteCount.
func Sql ¶
Sql matches a statement whose raw SQL equals text, ignoring surrounding whitespace. It needs raw SQL capture (WithRawSQL).
func SqlContains ¶
SqlContains matches a statement whose raw SQL contains sub, case-insensitively. It needs raw SQL capture (WithRawSQL).
func SqlMatches ¶
SqlMatches matches a statement whose raw SQL matches the regular expression re. It panics if re does not compile. It needs raw SQL capture (WithRawSQL).
func Table ¶
Table matches a statement that references table, by name and case-insensitively. It needs structural analysis (a parser in the Config).
type Matcher ¶
type Matcher interface {
// Matches reports whether a statement with the given SQL matches.
Matches(sql string) bool
// String describes the matcher for Verify failure messages.
String() string
}
Matcher decides whether a mock stub answers a statement, by its SQL text. The built-in matchers are Contains (a partial match), Regexp, Equals, and Anything; ExpectQuery and ExpectExec also accept a plain string or a sql.Query for convenience. Implement Matcher for custom matching.
It is distinct from the spy-side Match, which selects recorded statements by their analyzed shape; a Matcher sees only the raw SQL, at the point the mock must answer it.
type Mock ¶
type Mock struct {
// contains filtered or unexported fields
}
Mock is a programmable database/sql test double. It backs the *sql.DB returned by NewDB, answering each statement with a response you registered — canned rows, a result, or an error — so code that talks to a database can run in a unit test without one. Because the same DB is traced by querytrace, the spy assertions (Expect, Count, Events) see every statement the code issued, so the mock provides the inputs and the spy verifies the calls.
Register responses with ExpectQuery and ExpectExec. Each takes a Matcher that decides which statements the stub answers — Contains for a partial match, Regexp, or Equals — a plain string (matched with Equals), or a sql.Query built with the sqlkit builder. A sql.Query matches its compiled shape, and you may leave holes in it: AnyTable, AnyColumn, and AnyExpr match any table, column, or expression, while Arg, AnyArg, and ArgMatch constrain (or wildcard) a bind argument. Stubs are tried in registration order; the first applicable one answers. A statement matching no stub gets a lenient default — empty rows or a zero result — so a mock need only program the statements a test cares about. Verify reports any stub whose MinTimes (or Required) was not met.
func NewDB ¶
NewDB returns a *sql.DB backed by a programmable Mock and traced by querytrace, so statements run under a context carrying a Trace are recorded for the spy assertions. It registers a t.Cleanup that verifies the mock and closes the DB when the test ends, so a missed Required stub — or, by default, any statement no stub answered — fails the test without a manual Verify call. Pass Lenient to allow unmatched statements.
db, mock := sqltest.NewDB(t)
mock.ExpectQuery(sqltest.Contains("FROM users")).
Return(sqltest.NewRows("id", "name").AddRow(1, "alice"))
trace := querytrace.New("LoadUser")
ctx := querytrace.WithTrace(context.Background(), trace)
// ... use db under ctx ...
sqltest.Expect(t, trace).Query(sqltest.Select(), sqltest.Table("users")).Times(1)
func (*Mock) ExpectBegin ¶
ExpectBegin programs transaction begins (every one, unless limited by Times/MaxTimes). Use ReturnError to make BeginTx fail, or Required to assert the code starts a transaction.
func (*Mock) ExpectCommit ¶
ExpectCommit programs transaction commits (every one, unless limited by Times/MaxTimes): ReturnError simulates a commit failure, Required asserts the code commits.
func (*Mock) ExpectExec ¶
ExpectExec registers a response for ExecContext statements (db.Exec). See ExpectQuery for how the expectation is matched.
func (*Mock) ExpectPing ¶
ExpectPing programs a database Ping (db.Ping / db.PingContext), so a health check can be made to fail with ReturnError or asserted with Required.
func (*Mock) ExpectQuery ¶
ExpectQuery registers a response for QueryContext statements (db.Query, db.QueryRow) whose SQL the matcher accepts. The matcher is a Matcher (Contains, Regexp, Equals, Anything), a plain string (matched with Equals), or a sql.Query (compiled with the mock's dialect, then Equals).
func (*Mock) ExpectRollback ¶
ExpectRollback programs transaction rollbacks (every one, unless limited by Times/MaxTimes), the natural way to assert that a failing unit of work rolled back rather than committed.
type Option ¶
type Option func(*config)
Option configures NewDB.
func Lenient ¶
func Lenient() Option
Lenient turns off strict verification: a statement that matches no stub is allowed (it gets the empty-rows / zero-result default) instead of failing the test at cleanup. Use it for code paths that deliberately issue statements the mock does not program.
func WithConfig ¶
func WithConfig(cfg querytrace.Config) Option
WithConfig sets the querytrace.Config the mock DB records with. The default is NewConfig(WithVerboseCapture()): the PostgreSQL dialect, no parser, raw SQL and callers captured so every matcher works; supply your own (for example with a parser wired in) to enable the structural matchers Table, HasTopLevelWhere, and Fingerprint.
type Plan ¶
type Plan struct {
// contains filtered or unexported fields
}
Plan is a captured query plan — the output of EXPLAIN — ready for assertions about how the database will run a statement: whether it falls back to a full table scan, whether it uses a particular index, or any substring of the plan. It complements the query spy (which counts statements) and the data assertions (which check rows) with the third question a test may care about: is the query served efficiently.
Build one with ExplainQuery to run EXPLAIN through a live connection, or with NewPlan from EXPLAIN text captured elsewhere. The structural checks (HasSeqScan, UsesIndex) read PostgreSQL's text plan and MySQL's tabular EXPLAIN; for any other shape fall back to Contains on Text.
func ExplainQuery ¶
ExplainQuery runs EXPLAIN for query (with args) through q and returns the captured Plan. q is any Queryer — the same *sql.DB or *sql.Tx the code under test uses — so the plan reflects the real schema and statistics. The query is not executed, only planned.
func NewPlan ¶
NewPlan builds a Plan from EXPLAIN output captured outside this package: header is the column names (["QUERY PLAN"] for PostgreSQL's text format, the EXPLAIN columns for MySQL) and rows the corresponding cells. It is the seam the structural checks are tested through and lets a caller analyze a plan it obtained another way.
func (*Plan) AssertContains ¶
AssertContains fails the test unless some plan cell contains sub.
func (*Plan) AssertNoSeqScan ¶
AssertNoSeqScan fails the test if the plan includes a full table scan.
func (*Plan) AssertNotContains ¶
AssertNotContains fails the test if some plan cell contains sub.
func (*Plan) AssertUsesIndex ¶
AssertUsesIndex fails the test unless the plan reads through the named index.
func (*Plan) Contains ¶
Contains reports whether any plan cell contains sub, case-insensitively. It is the portable check when the structural helpers do not fit a given plan shape.
func (*Plan) HasSeqScan ¶
HasSeqScan reports whether the plan includes a full table scan: a "Seq Scan" node on PostgreSQL, or an access type of ALL on MySQL (the EXPLAIN type column).
type QueryAssertion ¶
type QueryAssertion struct {
// contains filtered or unexported fields
}
QueryAssertion narrows an Assertion to the statements matching a set of matchers, ready to check how many of them ran. Build one with Assertion.Query; its terminal methods (Times, MinTimes, …) report the result and return the parent Assertion so checks keep chaining. The vocabulary mirrors the mock's Stub call-count methods.
func (*QueryAssertion) MaxTimes ¶
func (q *QueryAssertion) MaxTimes(n int) *Assertion
MaxTimes asserts at most n matching statements ran.
func (*QueryAssertion) MinTimes ¶
func (q *QueryAssertion) MinTimes(n int) *Assertion
MinTimes asserts at least n matching statements ran.
func (*QueryAssertion) Never ¶
func (q *QueryAssertion) Never() *Assertion
Never asserts no matching statement ran. It is shorthand for Times(0).
func (*QueryAssertion) Times ¶
func (q *QueryAssertion) Times(n int) *Assertion
Times asserts exactly n matching statements ran.
type Queryer ¶
type Queryer interface {
QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error)
}
Queryer is the read surface the builder-free helpers need (generated-key read-back, the DB-state assertions, EXPLAIN), satisfied by *sql.DB, *sql.Tx, and *sql.Conn.
type RowSet ¶
type RowSet struct {
// contains filtered or unexported fields
}
RowSet is a set of canned rows a query stub returns. Build it with NewRows and add rows with AddRow.
func (*RowSet) AddRow ¶
AddRow appends a row. The values are coerced to the database/sql driver value types, so plain ints, strings, and the like are accepted.
func (*RowSet) FromCSVString ¶
FromCSVString appends rows parsed from CSV text, one record per row, matching the columns NewRows was given (the CSV has no header). Each field becomes a string value; it is a convenient way to spell out a wide canned result inline, mirroring go-sqlmock's RowSet.FromCSVString. It panics on malformed CSV or a record whose field count differs from the RowSet's columns — a test-setup error.
sqltest.NewRows("id", "name").FromCSVString("1,alice\n2,bob")
type Stub ¶
type Stub struct {
// contains filtered or unexported fields
}
Stub is one programmed response. Build it with ExpectQuery or ExpectExec and set its reply with Return, ReturnResult, or ReturnError. The setters chain.
func (*Stub) AnyTimes ¶
AnyTimes lets the stub answer any number of statements, zero included, and imposes no Verify floor. It clears an earlier Times/MinTimes/MaxTimes/Required.
func (*Stub) MaxTimes ¶
MaxTimes caps how many statements the stub answers; further matching statements fall through to later stubs or the lenient default. Zero (the default) is unlimited.
func (*Stub) MinTimes ¶
MinTimes sets the fewest matching statements Verify requires. Zero (the default) requires none.
func (*Stub) Required ¶
Required marks the stub as one Verify must see matched at least once. It is shorthand for MinTimes(1).
func (*Stub) ReturnError ¶
ReturnError makes the stub answer with err.
func (*Stub) ReturnResult ¶
ReturnResult sets the result an exec stub answers with.
func (*Stub) Times ¶
Times sets the exact number of statements the stub answers and that Verify requires: shorthand for MinTimes(n) and MaxTimes(n).
func (*Stub) WillDelayFor ¶
WillDelayFor makes the stub wait d before answering, returning the context's error if it is canceled or its deadline passes first. It exercises slow-query and cancellation handling — querytrace records a mid-iteration cancellation as a warning.