sequel

package module
v1.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 26, 2026 License: Apache-2.0 Imports: 32 Imported by: 1

README

Sequel

License Apache 2 Go Reference Test Discord

A Go library that enhances database/sql with cross-driver SQL, schema migration, ephemeral test databases, and adaptive connection pooling.

Features at a Glance

  • Connection pool management - Prevents database exhaustion when many consumers in one process share a DSN
  • Schema migration - Concurrency-safe, incremental database migrations
  • Cross-driver support - MySQL, PostgreSQL, CockroachDB, SQL Server, and SQLite with unified API
  • Retrying transactions - Transact runs a closure in a transaction, retries on deadlock/lock contention, and never commits partial work
  • Ephemeral test databases - Isolated databases per test with automatic cleanup, with optional simulated network latency

Quick Start

import "github.com/microbus-io/sequel"

// Open a database connection with its own pool
db, err := sequel.Open("", "root:root@tcp(127.0.0.1:3306)/mydb")

// Run migrations
err = db.Migrate("myservice@v1", migrationFilesFS)

// Use db.DB for standard sql.DB operations
rows, err := db.Query("SELECT * FROM users WHERE tenant_id=?", tenantID)

Connection Pool Management

Sequel exposes two constructors so the connection-pool strategy is self-documenting at the call site:

  • Open(driver, dsn) returns a fresh *DB with its own pool. Each call returns a distinct instance; sequel does not coalesce by DSN and does not size the pool automatically. The standard database/sql defaults apply (unlimited open, 2 idle) until the caller adjusts them with SetMaxOpenConns / SetMaxIdleConns. Use this for a single heavy consumer (e.g. a long-running worker pool) where you want to size the pool to the workload.

  • OpenSingleton(driver, dsn) returns a coalesced *DB: multiple calls with the same (driver, dsn) share one *sql.DB and one connection pool. Sequel automatically sizes that pool based on the number of openers using a sqrt-based formula:

    • maxIdle ≈ sqrt(N) where N is the number of openers
    • maxOpen ≈ (sqrt(N) * 2) + 2

    This is the right choice when many parts of the same process each open the same DSN occasionally — the pool grows gently with the number of openers and no caller has to think about pool sizing.

// Single heavy consumer — caller manages the pool.
db, err := sequel.Open("", dsn)
db.SetMaxOpenConns(32)
db.SetMaxIdleConns(8)

// Multiple consumers sharing a DSN — sequel manages one pool across them.
db, err := sequel.OpenSingleton("", dsn)

Schema Migration

Sequel performs incremental schema migration using numbered SQL files (1.sql, 2.sql, etc.). Migrations are:

  • Concurrency-safe - Distributed locking ensures only one replica executes each migration
  • Tracked - A sequel_migrations table records completed migrations
  • Driver-aware - Use -- DRIVER: drivername comments for driver-specific SQL (list multiple, space-separated, to share a statement across drivers)
// Embed migration files
//go:embed sql/*.sql
var migrationFS embed.FS

// Run migrations (safe to call from multiple replicas)
err := db.Migrate("unique-sequence-name", migrationFS)

Example migration file with driver-specific syntax:

-- DRIVER: mysql
ALTER TABLE users MODIFY COLUMN email VARCHAR(384) NOT NULL;

-- DRIVER: pgx cockroachdb
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(384);

-- DRIVER: mssql
ALTER TABLE users ALTER COLUMN email NVARCHAR(384) NOT NULL;

-- DRIVER: sqlite
-- SQLite does not support ALTER COLUMN; a table rebuild would be needed

Cross-Driver Support

Sequel supports MySQL, PostgreSQL, CockroachDB, SQL Server, and SQLite through a unified API. Write your SQL once using MySQL-style ? placeholders and virtual functions, and Sequel automatically adapts queries for the active driver.

CockroachDB speaks the PostgreSQL wire protocol and shares the pgx driver, but it is exposed as a distinct driver name (cockroachdb) because callers may need to branch on Cockroach-specific behavior — retry semantics and async schema changes in particular. Internally, every PostgreSQL expansion (placeholders, virtual functions, DSN parsing) applies identically to cockroachdb.

Automatic Placeholder Conversion

All query methods (Exec, Query, QueryRow, Prepare, and their Context variants) automatically convert ? placeholders to the driver's native syntax. For PostgreSQL, ? becomes $1, $2, etc. For MySQL, SQL Server, and SQLite, ? is left as-is. Placeholders inside quoted strings are left untouched.

// Works on all drivers - placeholders are converted automatically
rows, err := db.Query("SELECT * FROM users WHERE tenant_id = ? AND active = ?", tenantID, true)
// PostgreSQL receives: SELECT * FROM users WHERE tenant_id = $1 AND active = $2
Virtual Functions

Virtual functions are driver-agnostic function calls in your SQL that Sequel expands into driver-specific expressions before execution. They are matched case-insensitively and support nesting. Quoted strings inside arguments are handled correctly.

Built-in Virtual Functions

NOW_UTC() returns the current UTC timestamp with millisecond precision.

Driver NOW_UTC() expands to
MySQL (UTC_TIMESTAMP(3))
PostgreSQL (NOW() AT TIME ZONE 'UTC')
SQL Server (CONVERT(DATETIME2(3), SYSUTCDATETIME()))
SQLite STRFTIME('%Y-%m-%d %H:%M:%f', 'now')

On SQL Server the value is rounded to millisecond precision so it matches the other drivers and the precision of a DATETIME2(3) column. SYSUTCDATETIME() alone is 100-nanosecond precision, which rounds up when stored into a millisecond column and can leave a just-written "now" timestamp slightly in the future relative to a later NOW_UTC() comparison.

REGEXP_TEXT_SEARCH(expr IN col1, col2, ...) performs a case-insensitive regular expression search across one or more columns.

Driver REGEXP_TEXT_SEARCH(? IN name, email) expands to
MySQL CONCAT_WS(' ',name,email) REGEXP ?
PostgreSQL REGEXP_LIKE(CONCAT_WS(' ',name,email), ?, 'i')
SQL Server REGEXP_LIKE(CONCAT_WS(' ',name,email), ?, 'i')
SQLite CONCAT_WS(' ',name,email) LIKE '%' || ? || '%'

DATE_ADD_MILLIS(baseExpr, milliseconds) adds milliseconds to a timestamp expression.

Driver DATE_ADD_MILLIS(created_at, ?) expands to
MySQL DATE_ADD(created_at, INTERVAL (?) * 1000 MICROSECOND)
PostgreSQL created_at + MAKE_INTERVAL(secs => (?) / 1000.0)
SQL Server DATEADD(MILLISECOND, ?, created_at)
SQLite STRFTIME('%Y-%m-%d %H:%M:%f', created_at, '+' || ((?) / 1000.0) || ' seconds')

DATE_DIFF_MILLIS(a, b) returns the difference (a - b) in milliseconds.

Driver DATE_DIFF_MILLIS(updated_at, created_at) expands to
MySQL TIMESTAMPDIFF(MICROSECOND, created_at, updated_at) / 1000.0
PostgreSQL EXTRACT(EPOCH FROM (updated_at - created_at)) * 1000.0
SQL Server DATEDIFF_BIG(MILLISECOND, created_at, updated_at)
SQLite (JULIANDAY(updated_at) - JULIANDAY(created_at)) * 86400000.0

LIMIT_OFFSET(limit, offset) provides cross-driver pagination. Note that SQL Server requires an ORDER BY clause.

Driver LIMIT_OFFSET(10, 0) expands to
MySQL LIMIT 10 OFFSET 0
PostgreSQL LIMIT 10 OFFSET 0
SQL Server OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY
SQLite LIMIT 10 OFFSET 0
db.Query("SELECT * FROM users ORDER BY id LIMIT_OFFSET(?, ?)", limit, offset)

JSON_FIELD(column, '$.path') extracts one field from a JSON column and returns it as text.

Driver JSON_FIELD(doc, '$.name') expands to
MySQL (CASE WHEN JSON_TYPE(JSON_EXTRACT(doc, '$.name')) = 'NULL' THEN NULL ELSE JSON_UNQUOTE(JSON_EXTRACT(doc, '$.name')) END)
PostgreSQL ((doc)::jsonb #>> '{"name"}')
SQL Server (COALESCE(JSON_QUERY(doc, '$.name'), JSON_VALUE(doc, '$.name')))
SQLite (JSON_EXTRACT(doc, '$.name'))

The return contract is the same on every driver:

  • a JSON string comes back unquoted,
  • an object or array comes back as its JSON text,
  • a number or boolean comes back as its text form,
  • a JSON null, or a path that does not exist, comes back as SQL NULL.
db.Query("SELECT JSON_FIELD(doc, '$.name') FROM users WHERE JSON_FIELD(doc, '$.address.city') = ?", city)

The path supports member access and array indexes ($.a.b, $.tags[0]). The JSONPath $ root is optional'$.name' and 'name' are the same path — so a path copied from the MySQL, SQLite or SQL Server documentation works as-is, and you can leave the $ off when writing one yourself.

The path must be a literal, not a ? placeholder — PostgreSQL needs it as an array of keys and SQL Server needs it split across two functions, so it has to be known before the query is bound. Member names are restricted to [A-Za-z_][A-Za-z0-9_]*. The column expression is referenced twice on MySQL and SQL Server, so it must not itself contain a ?.

SQL Server caps scalars at 4000 characters. JSON_VALUE returns NVARCHAR(4000) and yields NULL for anything longer, so on SQL Server a JSON string over 4000 characters reads back as NULL. Objects and arrays are unaffected (they go through JSON_QUERY, which is NVARCHAR(MAX)), as is every other driver. Lifting the cap requires an OPENJSON ... WITH rowset, which is a different statement shape than a virtual function can expand into; select the whole column and extract in Go if you need large scalars there.

Nesting

Virtual functions can be nested. Inner functions are expanded first across multiple passes:

db.Exec("UPDATE t SET expires_at = DATE_ADD_MILLIS(NOW_UTC(), ?) WHERE id = ?", ttlMs, id)
// MySQL:      UPDATE t SET expires_at = DATE_ADD(UTC_TIMESTAMP(3), INTERVAL (?) * 1000 MICROSECOND) WHERE id = ?
// PostgreSQL: UPDATE t SET expires_at = (NOW() AT TIME ZONE 'UTC') + MAKE_INTERVAL(secs => ($1) / 1000.0) WHERE id = $2
Custom Virtual Functions

Register your own virtual functions with RegisterVirtualFunc:

sequel.RegisterVirtualFunc("BOOL", func(driverName string, args string) (string, error) {
    switch driverName {
    case "mysql", "pgx", "sqlite":
        return args, nil
    case "mssql":
        // SQL Server uses BIT, not BOOL
        return "CAST(" + args + " AS BIT)", nil
    default:
        return "", errors.New("unsupported driver: %s", driverName)
    }
})
UnpackQuery

UnpackQuery is the public method that expands virtual functions and conforms placeholders. It is called automatically by the query shadow methods, but can be used directly if needed:

expanded, err := db.UnpackQuery("SELECT * FROM t WHERE updated_at > DATE_ADD_MILLIS(NOW_UTC(), ?) AND active = ?")
InsertReturnID

InsertReturnID executes an INSERT statement and returns the auto-generated ID for the named ID column. Each driver uses its native mechanism:

Driver Mechanism
MySQL LastInsertId() from the result
PostgreSQL Appends RETURNING <idColumn> to the query
SQL Server Injects OUTPUT INSERTED.<idColumn> before VALUES
SQLite LastInsertId() from the result
id, err := db.InsertReturnID(ctx, "id", "INSERT INTO users (name, email) VALUES (?, ?)", name, email)

The ID column must be a plain identifier matching [A-Za-z_][A-Za-z0-9_]*. Because it is spliced into the statement on PostgreSQL, CockroachDB, and SQL Server, quoted or exotic column names are rejected up front — on every driver, so the contract is uniform.

DriverName()

DriverName() returns the active driver name ("mysql", "pgx", "mssql", or "sqlite") for cases where you need driver-specific logic in Go code.

Transactions

db.BeginTx returns a sequel.Tx that shadows sql.Tx with virtual-function expansion and placeholder conforming — use it exactly like sql.Tx.

For transactions that must survive contention, db.Transact runs a closure in a transaction, commits on success, and retries the whole closure on a deadlock or lock-contention error with a short jittered backoff:

err := db.Transact(ctx, func(tx *sequel.Tx) error {
    if _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amt, from); err != nil {
        return err
    }
    _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + ? WHERE id = ?", amt, to)
    return err
})
  • Retry-safe by re-running. A retried attempt re-executes the closure from the start in a new transaction (the previous attempt is rolled back), so the closure must be safe to run more than once — any non-transactional side effects it performs may repeat. Because retries re-run the Go code rather than replay recorded statements, a transaction whose control flow depends on data committed by another transaction between attempts stays correct.
  • No partial commits. The Tx passed to the closure records the first error and short-circuits every statement after it, so the transaction never commits half its work even if the closure forgets to check an error. This covers every way an error surfaces: a failed Exec/Query/InsertReturnID statement, an error while iterating a result set (a rows.Scan failure or a streaming error from rows.Err()), a failed QueryRow(...).Scan — with sql.ErrNoRows exempt, since a missing row is normal control flow rather than a failure — and executions of a prepared statement, whether prepared inside the transaction (tx.Prepare) or bound to it (tx.Stmt).
  • SQL Server XACT_ABORT ON. Applied automatically inside Transact so any statement error aborts the whole transaction server-side.

A Tx from BeginTx does neither error-recording nor retry — it behaves exactly like sql.Tx.

Ephemeral Test Databases

Provisioning a per-test database is a separate step from opening a connection. CreateTestingDatabase(driver, baseDSN, uniqueTestID) creates (or reuses) a uniquely-named database and returns its DSN; pass that DSN to Open or OpenSingleton to connect.

// Test fixture
func TestUserService(t *testing.T) {
    dsn, err := sequel.CreateTestingDatabase("", "root:root@tcp(127.0.0.1:3306)/mydb", t.Name())
    if err != nil { t.Fatal(err) }
    db, err := sequel.OpenSingleton("", dsn)
    if err != nil { t.Fatal(err) }
    defer db.Close()  // also drops the testing database
}

The same helper can be invoked from production startup paths that want to swap in a per-test database without rewriting the rest of the wiring:

func startup(cfg Config) (*sequel.DB, error) {
    dsn := cfg.DSN
    if cfg.Testing {
        var err error
        dsn, err = sequel.CreateTestingDatabase("", cfg.DSN, cfg.TestID)
        if err != nil { return nil, err }
    }
    return sequel.OpenSingleton("", dsn)
}

Repeated calls within the same process with the same (driver, baseDSN, uniqueTestID) reuse the same testing database — the DROP+CREATE runs only once, for as long as a handle on it remains open. The returned DSN points at a database whose name has the testing_NN_ prefix; sequel inspects this on Close and drops the database automatically when the last referencing *DB is closed — last across every handle, since Open returns a distinct *DB per call. There is no separate cleanup call to remember. A call after that point provisions the database again rather than returning a DSN for one that no longer exists. If a process exits before Close runs, the leftover-cleanup sweep on the next CreateTestingDatabase call removes stale databases older than 1–2 hours.

Choosing the server with SEQUEL_TESTING_DSN

When CreateTestingDatabase is called with neither a driver nor a base DSN, it falls back to the SEQUEL_TESTING_DSN environment variable. This lets you run the same test suite against any supported server without touching test code — leave the variable unset to use in-memory SQLite (the default, no server required), or set it to a base DSN to run against that server instead, with the driver inferred from the DSN:

func TestUserService(t *testing.T) {
    // "" driver + "" DSN → SEQUEL_TESTING_DSN, or in-memory SQLite if it is unset.
    dsn, err := sequel.CreateTestingDatabase("", "", t.Name())
    if err != nil { t.Fatal(err) }
    db, err := sequel.OpenSingleton("", dsn)
    if err != nil { t.Fatal(err) }
    defer db.Close()
}
# Same tests, different engine — no code change.
go test ./...                                                          # SQLite (default)
SEQUEL_TESTING_DSN='postgres://user:pw@127.0.0.1:5432/' go test ./...  # PostgreSQL
SEQUEL_TESTING_DSN='root:pw@tcp(127.0.0.1:3306)/'       go test ./...  # MySQL

Passing an explicit driver — even with an empty DSN, which just selects that driver's localhost default — opts out of the fallback, so a test that deliberately targets a specific engine keeps using it regardless of the environment. Because the variable is read inside CreateTestingDatabase, any project that provisions its test databases through sequel inherits this behavior with no additional wiring.

Simulating network latency with SimulateRTT

Tests run against in-memory SQLite or a server on localhost, where a round trip costs microseconds. Code that is needlessly chatty — a loop that issues one query per element, a transaction that could have batched — therefore performs indistinguishably from code that is not, and the timeout paths never fire. SimulateRTT makes every operation sequel sends over the wire pause first, so the cost of a real network shows up in the test:

db, _ := sequel.Open("", dsn)
db.SimulateRTT(20 * time.Millisecond) // testing only — this is deliberate latency injection

start := time.Now()
err := db.Transact(ctx, func(tx *sequel.Tx) error {
    for _, u := range users {
        if _, err := tx.ExecContext(ctx, "INSERT INTO users (name) VALUES (?)", u.Name); err != nil {
            return err
        }
    }
    return nil
})
// 100 users → 102 round trips (BEGIN + 100 statements + COMMIT) → over 2 seconds.
// The same loop takes microseconds against localhost, which is what hides the problem.

db.SimulateRTT(0) // off again

The delay is charged per round trip, not per call: the statement methods on DB and Tx, each execution of a prepared Stmt, Begin/BeginTx, Commit, Rollback and Ping each pay it once. Operations sequel is not in the path for are not delayed — a *sql.Conn talks to the driver directly, and fetching successive rows from an open Rows is batched by the driver, so charging a full round trip per Next() would model the wire worse than charging nothing.

The Context variants honor their context: a deadline shorter than the simulated latency fails the operation with the context's error and never reaches the database, which is what a real round trip that outlives its deadline does. That makes cancellation and timeout handling testable without an unreliable server to provoke it.

A Tx captures the setting when the transaction begins, so a transaction runs at one consistent latency even if the setting changes underneath it. The default is zero (off), and a negative duration is treated as zero. For a *DB shared by OpenSingleton the setting is process-wide for that pool, so set it from the owning caller.

Observability

Sequel emits OpenTelemetry traces and metrics, and slog logs. A freshly opened *DB is not uninstrumented: it starts on the process-wide otel.GetTracerProvider() / otel.GetMeterProvider(), so a program that configures OpenTelemetry globally gets sequel's spans and metrics with no further setup. Those globals are delegating no-ops until real providers are installed, and they start working the moment they are — no re-open needed. Logging defaults to a discard logger. To genuinely disable a signal, install an explicit no-op provider; "unset" does not mean "off".

Providers are attached after Open/OpenSingleton (which keep the standard database/sql signature) rather than at construction. Nothing is lost by this: sql.Open does no I/O — it only prepares a lazy pool — so there is no work inside Open worth instrumenting; every operation that does real work happens later on the returned *DB.

db, _ := sequel.Open("", dsn)
db.SetTracerProvider(tracerProvider) // trace.TracerProvider — client spans per query/transaction/migration
db.SetMeterProvider(meterProvider)   // metric.MeterProvider — sequel_* metrics
db.SetLogger(logger)                 // *slog.Logger — migration events; per-query when enabled at Debug

Configure once, before the *DB is used concurrently. For an OpenSingleton-shared *DB, the providers are process-wide for that pool; set them from the owning caller (last writer wins). Pass nil to any setter to disable that signal.

Spans

Each query, Transact, and Migrate gets a client span following OpenTelemetry database semantic conventions:

  • db.system.name — the driver (mysql, pgx, cockroachdb, mssql, sqlite)
  • db.operation.name — the SQL verb (SELECT, INSERT, …), whatever the dialect: common verbs are reported immediately and rarer ones are picked up on first use, so nothing needs to be on a list. The number of distinct verbs a process reports is capped (at 128) and only verb-shaped tokens count toward it, so the attribute stays low-cardinality even if an application interpolates uncontrolled input into its SQL; past the cap, further unrecognized verbs report as OTHER and sequel logs that once at Info
  • db.collection.name — the table, only when it can be determined unambiguously (omitted for joins, multi-table FROM lists, and subqueries, so a present value is trustworthy)

The span name is "{operation} {table}" (e.g. SELECT users), or just the operation when no table is captured. The statement text is never attached to a span; operation, table, and the caller's own parent span identify it, and the full parameterized statement is available in the per-query Debug log instead.

BEGIN, COMMIT and ROLLBACK are round trips too, so each gets its own span, nested under the transaction it belongs to. This matters beyond bookkeeping: a serialization failure surfaces at commit time rather than at a statement on CockroachDB and on PostgreSQL under SERIALIZABLE, so a commit span is what puts that failure into sequel_query_duration and sequel_lock_contention.

A call on an already-finalized transaction reports sql.ErrTxDone without reaching the database, and correspondingly emits nothing — no span, no duration sample. That covers the ubiquitous defer tx.Rollback() next to a successful Commit, and equally the transaction that database/sql finalized itself when its context was cancelled, so a cancelled request does not show up as a failed rollback.

Metrics

All metric names carry the sequel_ prefix. Counter instrument names carry no _total suffix; a Prometheus exporter appends it at the scrape boundary, so sequel_lock_contention is queried in PromQL as sequel_lock_contention_total (and sequel_migration_runs as sequel_migration_runs_total):

Metric Type Notes
sequel_query_duration histogram (s) attrs: db.system.name, db.operation.name, status (ok/error)
sequel_transaction_duration histogram (s) attrs: db.system.name, outcome (committed/rolledback)
sequel_lock_contention counter incremented once per surfaced lock-contention/deadlock error (PromQL: sequel_lock_contention_total)
sequel_migration_runs counter counts migrations that actually ran (skipped ones excluded); attrs include status (PromQL: sequel_migration_runs_total)
sequel_pool_open_connections gauge from sql.DBStats, attr database (never the raw DSN)
sequel_pool_in_use_connections gauge
sequel_pool_idle_connections gauge
sequel_pool_wait_count gauge
sequel_pool_wait_duration_seconds gauge
Logs

The library does not log operation errors — every error is returned to the caller, who is best placed to log it. Logging is reserved for:

  • Info — one-off events: each schema migration as it is attempted (regardless of outcome).
  • Debug — every query, including the full parameterized statement text. There is no separate sequel switch: the lines are gated on your own logger's level, so they cost nothing when Debug is disabled. (Statement text is never a privacy risk — sequel always parameterizes, so the text carries ?/$1 placeholders, never argument values.)
Query, QueryRow and Prepare return *sequel.Rows / *sequel.Row / *sequel.Stmt

Query methods return sequel's own types rather than database/sql's: Query/QueryContext return a *sequel.Rows (embedding *sql.Rows), QueryRow/QueryRowContext return a *sequel.Row (embedding *sql.Row), and Prepare/PrepareContext return a *sequel.Stmt (embedding *sql.Stmt). All embed, so ordinary call sites are unchanged:

rows, err := db.Query("SELECT id, name FROM users")   // type inference — no change needed
for rows.Next() { rows.Scan(&id, &name) }
err = db.QueryRow("SELECT name FROM users WHERE id=?", id).Scan(&name)
stmt, err := db.Prepare("INSERT INTO users (name) VALUES (?)")
_, err = stmt.Exec("Rivka")

Only code that explicitly types a result as *sql.Rows / *sql.Row / *sql.Stmt, or that implements the Executor interface itself, needs adjustment.

These types exist for two reasons. Instrumentation: database/sql defers a QueryRow error to Scan and a streaming error to rows.Err(), so the shadows capture the error where it actually becomes available; executions of a prepared Stmt get spans, duration samples and the simulated round-trip delay like any other statement. Transaction safety: inside a Transact closure they latch errors into the transaction, so a closure that ignores a failed scan, a truncated read, or a failed prepared-statement execution cannot commit state built on it — see Transactions. Outside Transact — a *DB query, or a Tx from BeginTx — they are passthroughs with no latching.

Errors

Errors returned by sequel wrap the driver's error with a stack trace (via github.com/microbus-io/errors), recording where in your code the failure surfaced. The wrapping preserves Unwrap, so errors.Is and errors.As see through it — compare and unwrap exactly as you would any wrapped Go error:

if errors.Is(err, context.DeadlineExceeded) { ... }
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) { ... }

The two database/sql sentinels are deliberately not wrapped: sql.ErrNoRows and sql.ErrTxDone are routine control flow rather than failures, and they pass through exactly as database/sql returns them, so existing err == sql.ErrNoRows comparisons keep working. errors.Is(err, sql.ErrNoRows) works too, and is the more robust habit.

Every other error — a driver error, a context cancellation — is wrapped, so never compare those with ==; use errors.Is/errors.As. (Code that compares a driver error with == is broken with plain database/sql as well — drivers return distinct error values per occurrence — so in practice this asks nothing new.)

Sequel is the copyrighted work of various contributors. It is licensed to you free of charge by Microbus LLC - a Delaware limited liability company formed to hold rights to the combined intellectual property of all contributors - under the Apache License 2.0.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyBindings

func ApplyBindings(args ...any) (err error)

ApplyBindings should be called after scanning values from the result set to perform all late binding.

func CreateTestingDatabase added in v1.6.1

func CreateTestingDatabase(driverName string, baseDataSourceName string, uniqueTestID string) (dsn string, err error)

CreateTestingDatabase provisions a uniquely-named database (or returns a SQLite in-memory DSN) for testing and returns the resolved data source name. Pass the result to Open or OpenSingleton to open a connection.

The returned DSN points at a database whose name has the testing_NN_ prefix. When the last *DB referencing that database is Closed, sequel drops it automatically — no separate cleanup call is required.

uniqueTestID scopes the database so that independent tests don't collide. Pass t.Name() from a test, or an equivalent identifier from production startup code that wants a per-run database:

dsn := cfg.DSN
if cfg.Testing {
    dsn, err = sequel.CreateTestingDatabase("", cfg.DSN, cfg.TestID)
    if err != nil { return err }
}
db, err := sequel.OpenSingleton("", dsn)

Within a single process, repeated calls with the same (driverName, baseDataSourceName, uniqueTestID) reuse the same testing database — the underlying DROP+CREATE only happens on the first call. Once every handle on that database has been closed it is dropped, and a later call with the same triple provisions it again.

If a driver name is not provided, it is inferred from the data source name on a best-effort basis. Drivers currently supported: "mysql" (MySQL), "pgx" (Postgres), "cockroachdb" (CockroachDB), "mssql" (SQL Server) or "sqlite" (SQLite).

If neither a driver name nor a base data source name is provided, it falls back to the SEQUEL_TESTING_DSN environment variable. This lets any consumer that builds ephemeral test databases through sequel redirect its entire suite at a real server without changing test code: leave SEQUEL_TESTING_DSN unset to keep the SQLite default, or set it to a base DSN to run against that server instead, with the driver inferred from it. Naming a driver — even with an empty DSN — opts out of the fallback, so a test that explicitly asks for SQLite keeps running on SQLite regardless of the environment.

If neither the arguments nor SEQUEL_TESTING_DSN select a server, the following localhost defaults are used based on the driver name:

  • (empty): SQLite in-memory database
  • sqlite: SQLite in-memory database
  • mysql: root:root@tcp(127.0.0.1:3306)/
  • pgx: postgres://postgres:postgres@127.0.0.1:5432/
  • cockroachdb: postgres://root@127.0.0.1:26257/?sslmode=disable
  • mssql: sqlserver://sa:Password123@127.0.0.1:1433

func IsLockContentionError added in v1.5.7

func IsLockContentionError(err error) bool

IsLockContentionError returns true if the error indicates database lock contention or a deadlock. Such errors are transient and the operation can typically be retried. Recognizes lock errors from SQLite, MySQL, PostgreSQL, SQL Server, and CockroachDB.

Classification prefers the driver's native error code (immune to message wording, localization, and user data appearing in error messages); a substring match is used as a fallback for errors whose driver type is not present in the chain (e.g. some wrapped or text-only CockroachDB retry errors).

func Nullify

func Nullify[T comparable](value T) any

Nullify returns nil if the value equals to the zero value of its Go data type, else it returns the value. Use this construct to convert zero values to nil when writing to a nullable database column.

Example:

db.Exec(
	"INSERT INTO my_table (id, desc, modified_time) VALUES (?,?,?)",
	obj.ID,
	sequel.Nullify(obj.Description),
	sequel.Nullify(obj.ModifiedTime),
)

func RegisterVirtualFunc added in v1.2.0

func RegisterVirtualFunc(name string, handler func(driverName string, args string) (string, error))

RegisterVirtualFunc registers a virtual SQL function that will be replaced in queries before execution. The name is matched case-insensitively, e.g. registering "NOW_UTC" matches NOW_UTC(), now_utc(), Now_Utc(), etc. The handler receives the driver name and the string found between the parentheses, and returns the replacement SQL expression, or an error.

Types

type Binder

type Binder[T any] struct {
	sql.Null[T]
	// contains filtered or unexported fields
}

Binder is a thin wrapper over sql.Null that allows for late-binding of its value.

func Bind

func Bind[T any](binder func(value T) (err error)) *Binder[T]

Bind applies a binding function to the scanned value.

Example:

var obj Object
args := []any{
	&obj.ID,
	sequel.Bind(func(tags string) {
		return json.Unmarshal([]byte(tags), &obj.Tags)
	}),
	sequel.Bind(func(modifiedTime time.Time) {
		obj.Year, obj.Month, obj.Day = modifiedTime.Date()
		return nil
	}),
}
db.QueryRow("SELECT id, tags, modified_time FROM my_table WHERE id=?", id).Scan(args...)
sequel.ApplyBindings(args...)

func (*Binder[T]) Apply

func (n *Binder[T]) Apply() (err error)

Apply should be called after scanning the columns from the result set.

type DB

type DB struct {
	*sql.DB
	// contains filtered or unexported fields
}

DB is an enhanced database connection that

  • Limits the size of the connection pool to each server to approx the sqrt of the number of clients
  • Performs schema migration
  • Automatically creates and connects to a localhost database while testing

func Open

func Open(driverName string, dataSourceName string) (db *DB, err error)

Open returns a database connection to the named data source with a dedicated connection pool. Each call returns a distinct *DB; sequel does not coalesce by DSN. The caller is responsible for sizing the pool via SetMaxOpenConns / SetMaxIdleConns if the database/sql defaults (unlimited open, 2 idle) don't fit.

Use OpenSingleton when multiple consumers in the same process share a DSN and you want sequel to manage one pool across all of them.

If a driver name is not provided, it is inferred from the data source name on a best-effort basis. Drivers currently supported: "mysql" (MySQL), "pgx" (Postgres), "cockroachdb" (CockroachDB), "mssql" (SQL Server) or "sqlite" (SQLite).

Example data source name for each of the supported drivers:

  • mysql: username:password@tcp(hostname:3306)/
  • pgx: postgres://username:password@hostname:5432/
  • cockroachdb: postgres://username:password@hostname:26257/
  • mssql: sqlserver://username:password@hostname:1433
  • sqlite: file:path/to/database.sqlite

func OpenSingleton added in v1.6.1

func OpenSingleton(driverName string, dataSourceName string) (db *DB, err error)

OpenSingleton returns a per-DSN coalesced *DB whose connection pool sequel manages automatically based on the number of openers (sqrt-based growth, see [DB.adjustConnectionLimits]). Multiple OpenSingleton calls with the same (driverName, dataSourceName) return the same *DB and share its connection pool. This is the right choice when many parts of the same process each access the database occasionally.

Use Open when you want a dedicated pool with explicit caller-managed sizing.

Driver inference, DSN defaults, and supported drivers are the same as Open.

func (*DB) Begin added in v1.4.0

func (db *DB) Begin() (*Tx, error)

Begin starts a transaction and returns a sequel.Tx that applies virtual function expansion and placeholder conforming.

func (*DB) BeginTx added in v1.4.0

func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)

BeginTx starts a transaction with the given options and returns a sequel.Tx that applies virtual function expansion and placeholder conforming.

func (*DB) Close

func (db *DB) Close() (err error)

Close closes the database connection.

When the underlying database name matches the testing pattern (testing_NN_…), the last handle to close drops the database from the server as a best-effort cleanup, making CreateTestingDatabase-provisioned databases self-cleaning on test teardown. Last across every handle on that database, not just this one: Open hands out a *DB per call, so several may share one testing database.

func (*DB) ConformArgPlaceholders deprecated

func (db *DB) ConformArgPlaceholders(stmt string) string

Deprecated: ConformArgPlaceholders is applied automatically by the query shadow methods. Use ? placeholders directly in queries passed to Exec, Query, QueryRow, and Prepare.

func (*DB) DriverName

func (db *DB) DriverName() string

DriverName is the name of the driver: "mysql", "pgx", "cockroachdb", "mssql" or "sqlite".

func (*DB) Exec added in v1.2.0

func (db *DB) Exec(query string, args ...any) (sql.Result, error)

Exec shadows sql.DB.Exec and conforms arg placeholders for the driver.

func (*DB) ExecContext added in v1.2.0

func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)

ExecContext shadows sql.DB.ExecContext and conforms arg placeholders for the driver.

func (*DB) InsertReturnID added in v1.3.0

func (db *DB) InsertReturnID(ctx context.Context, idColumn string, stmt string, args ...any) (int64, error)

InsertReturnID executes an INSERT statement and returns the auto-generated ID for the named ID column. idColumn must be a plain identifier matching [A-Za-z_][A-Za-z0-9_]* — it is spliced into the statement on some drivers, so quoted or exotic column names are rejected rather than escaped.

func (*DB) Migrate

func (db *DB) Migrate(sequenceName string, fileSys fs.FS) (err error)

Migrate reads all #.sql files from the FS, and executes any new migrations in order of their file name. The order of execution is guaranteed only within the context of a sequence name.

func (*DB) NowUTC deprecated

func (db *DB) NowUTC() string

Deprecated: Use the NOW_UTC() virtual function directly in queries instead.

func (*DB) Ping added in v1.11.0

func (db *DB) Ping() error

Ping shadows sql.DB.Ping so a simulated round-trip delay (DB.SimulateRTT) applies to it: a ping is a wire operation like any statement. It is not otherwise instrumented — sequel has never traced a ping.

func (*DB) PingContext added in v1.11.0

func (db *DB) PingContext(ctx context.Context) error

PingContext shadows sql.DB.PingContext for the same reason as DB.Ping. A context that expires during the simulated latency fails the ping without reaching the database.

func (*DB) Prepare added in v1.2.0

func (db *DB) Prepare(query string) (*Stmt, error)

Prepare shadows sql.DB.Prepare and conforms arg placeholders for the driver. It returns a Stmt, which embeds *sql.Stmt so existing stmt.Exec(...)/stmt.Close() call sites are unchanged; executions of the returned statement stay on sequel's instrumented path.

func (*DB) PrepareContext added in v1.2.0

func (db *DB) PrepareContext(ctx context.Context, query string) (*Stmt, error)

PrepareContext shadows sql.DB.PrepareContext and conforms arg placeholders for the driver. It returns a Stmt (see Prepare).

func (*DB) Query added in v1.2.0

func (db *DB) Query(query string, args ...any) (*Rows, error)

Query shadows sql.DB.Query and conforms arg placeholders for the driver. It returns a Rows, which embeds *sql.Rows so existing rows.Next()/rows.Scan()/rows.Err() call sites are unchanged. A *DB query is not transactional, so Rows here is a pure passthrough (no error latching).

func (*DB) QueryContext added in v1.2.0

func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)

QueryContext shadows sql.DB.QueryContext and conforms arg placeholders for the driver. It returns a Rows (see Query); a *DB query does not latch errors into any transaction.

func (*DB) QueryRow added in v1.2.0

func (db *DB) QueryRow(query string, args ...any) *Row

QueryRow shadows sql.DB.QueryRow and conforms arg placeholders for the driver. It returns a Row, which embeds *sql.Row so existing QueryRow(...).Scan(...) call sites are unchanged.

func (*DB) QueryRowContext added in v1.2.0

func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row

QueryRowContext shadows sql.DB.QueryRowContext and conforms arg placeholders for the driver. It returns a Row, which embeds *sql.Row so existing QueryRowContext(...).Scan(...) call sites are unchanged.

func (*DB) RegexpTextSearch deprecated

func (db *DB) RegexpTextSearch(searchableColumns ...string) string

Deprecated: Use the REGEXP_TEXT_SEARCH() virtual function directly in queries instead.

func (*DB) SetLogger added in v1.9.0

func (db *DB) SetLogger(logger *slog.Logger)

SetLogger attaches an slog.Logger. The library does not log operation errors (they are returned to the caller, who logs them); it logs one-off events such as schema migrations at Info, and — when the logger is enabled at Debug level — each query at Debug. Per-query logging is therefore controlled by the logger's own level, not a separate switch. A freshly opened *DB uses a discard logger; pass nil here to revert to that discard logger (disabling logging).

func (*DB) SetMeterProvider added in v1.9.0

func (db *DB) SetMeterProvider(mp metric.MeterProvider)

SetMeterProvider attaches an OpenTelemetry MeterProvider so sequel emits sequel_ metrics (query and transaction duration, lock-contention count, migration count, and connection-pool gauges). A freshly opened *DB already uses the process-wide otel.GetMeterProvider(); call this to override it, or pass nil to revert to that global provider (whose default is a no-op). See DB.SetTracerProvider for when to call.

func (*DB) SetTracerProvider added in v1.9.0

func (db *DB) SetTracerProvider(tp trace.TracerProvider)

SetTracerProvider attaches an OpenTelemetry TracerProvider so sequel emits a client span around each query, transaction, and migration. A freshly opened *DB already uses the process-wide otel.GetTracerProvider(); call this to override it, or pass nil to revert to that global provider (whose default is a no-op).

Observability is configured after Open/OpenSingleton (which keep the standard database/sql signature) rather than at construction. This loses nothing: sql.Open does no I/O — it only prepares a lazy pool — so there is no work inside Open worth a span; every operation that does real work happens later on the returned *DB.

Configure before the *DB is used concurrently. For an OpenSingleton-shared *DB the providers are process- wide for that pool; the last setter wins, so configure once from the owning caller.

func (*DB) SimulateRTT added in v1.11.0

func (db *DB) SimulateRTT(delay time.Duration)

SimulateRTT makes every operation sequel sends over the wire pause for the given duration first, simulating the round-trip latency of a remote server. It is a testing aid: against in-memory SQLite or a server on localhost a round trip costs microseconds, so needlessly chatty code performs the same as code that batches, and timeout paths never fire. Raising the round trip to what a real network costs makes both visible in a test.

The delay is charged per round trip, not per call: a DB.Transact that begins a transaction, runs three statements and commits pays it five times (six on SQL Server, which adds a SET XACT_ABORT ON preamble). It applies to the statement methods (Exec, Query, QueryRow, Prepare, and the Context variants) on DB and Tx, to each execution of a prepared Stmt, to Begin/BeginTx, Commit and Rollback, and to Ping. DB.InsertReturnID is one statement on every driver, so it pays once.

Three things are not charged. A sql.Conn talks to the driver directly, with sequel out of the path. Fetching successive rows from an open Rows is batched by the driver, so a full round trip per Next would model the wire worse than nothing. Lifecycle — Close on a pool or a Stmt, and the DROP that retires a testing database — is not caller-facing work.

The Context variants honor their context: a deadline shorter than the simulated latency fails the operation with the context's error and never reaches the database, as a real round trip outliving its deadline does.

Zero turns the simulation off and is the default; a negative duration is treated as zero. The setting is safe to change while the pool is in use and applies to operations begun after it. A Tx captures it at begin, so one transaction runs at one latency. For a *DB shared by OpenSingleton it is process-wide for that pool — last writer wins — so set it from the owning caller.

This is deliberate latency injection and slows real work exactly as advertised. Keep it behind the same switch that selects your test database.

func (*DB) Transact added in v1.8.0

func (db *DB) Transact(ctx context.Context, fn func(tx *Tx) error) (err error)

Transact runs fn inside a transaction, committing on success and rolling back on error. If the transaction fails on lock contention or a deadlock, it is retried with a short jittered backoff. Because a retry re-executes fn from the start in a new transaction, fn must be safe to run more than once; any non-transactional side effects it performs (in-memory changes, channel sends) may repeat.

The Tx passed to fn records the first statement error and short-circuits the remaining statements, so fn cannot commit partial work even if it does not check every statement's error. For SQL Server, SET XACT_ABORT ON is applied so that any statement error aborts the whole transaction.

func (*DB) UnpackQuery added in v1.2.0

func (db *DB) UnpackQuery(query string) (string, error)

UnpackQuery expands virtual functions (e.g. NOW_UTC(), REGEXP_TEXT_SEARCH()) into driver-specific SQL expressions, and conforms arg placeholders to the syntax expected by the driver (e.g. ? to $1, $2 for PostgreSQL).

type Executor added in v1.4.1

type Executor interface {
	Exec(query string, args ...any) (sql.Result, error)
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
	Query(query string, args ...any) (*Rows, error)
	QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
	QueryRow(query string, args ...any) *Row
	QueryRowContext(ctx context.Context, query string, args ...any) *Row
	Prepare(query string) (*Stmt, error)
	PrepareContext(ctx context.Context, query string) (*Stmt, error)
	InsertReturnID(ctx context.Context, idColumn string, stmt string, args ...any) (int64, error)
	DriverName() string
	UnpackQuery(query string) (string, error)
}

Executor is the interface satisfied by both DB and Tx.

type Null

type Null[T any] struct {
	*Binder[T]
}

Null is a thin wrapper over sql.Null that allows for reading NULL values.

func Nullable

func Nullable[T any](ptr *T) *Null[T]

Nullable is a simple binder that interprets NULL values to be the zero value of their Go data type.

Example:

var obj Object
args := []any{
	&obj.ID,
	sequel.Nullable(&obj.Description),
	sequel.Nullable(&obj.ModifiedTime),
}
db.QueryRow("SELECT id, desc, modified_time FROM my_table WHERE id=?", id).Scan(args...)
sequel.ApplyBindings(args...)

type Row added in v1.9.0

type Row struct {
	*sql.Row
	// contains filtered or unexported fields
}

Row shadows *sql.Row so sequel can observe a single-row query and latch its error into a DB.Transact-managed transaction. database/sql does not surface a QueryRow error until Scan, so Row records the operation's duration, classifies lock contention, and ends the span when the caller calls Scan (or Err). It embeds *sql.Row, so the common QueryRow(...).Scan(...) call site is unchanged; only code that explicitly stores the result as *sql.Row needs adjustment.

In Transact (autoErr) mode Scan/Err also latch the error into the transaction, exactly as Rows does for a streamed read, so a closure that ignores a QueryRow error cannot commit work built on a row it never read. sql.ErrNoRows is deliberately exempt: unlike a Rows iteration, where an empty result set is simply Next returning false, "no row" reaches a QueryRow caller as an error and is routine control flow (`if err == sql.ErrNoRows { ...default... }`). Latching it would doom every transaction that legitimately handles a missing row. Every other error — deadlock, type-conversion failure, connection drop — is latched. Outside a Transact-managed Tx, recordErr is nil and no latching occurs.

As with *sql.Row, a Row whose Scan/Err is never called holds resources open — and here, leaves its span unended. Call Scan (or Err) exactly as you would with *sql.Row.

func (*Row) Err added in v1.9.0

func (r *Row) Err() error

Err shadows sql.Row.Err, finishes instrumentation with the query error, and latches it, so a caller that inspects Err instead of scanning still closes the span and aborts the transaction.

func (*Row) Scan added in v1.9.0

func (r *Row) Scan(dest ...any) error

Scan shadows sql.Row.Scan, finishes instrumentation with the scan error, and latches it into the transaction (autoErr mode), so a closure that ignores the returned error cannot commit work built on a row it never read.

type Rows added in v1.10.7

type Rows struct {
	*sql.Rows
	// contains filtered or unexported fields
}

Rows shadows *sql.Rows so sequel can latch a row-iteration or Scan error into a DB.Transact-managed transaction, completing the "no partial commit" guarantee for streamed reads.

Transact already records the first Exec/Query *statement* error and short-circuits the rest, so a closure that ignores a statement's error still cannot commit half its work. The gap that remained was the errors that surface *while iterating a result set* — a mid-stream Scan failure or a streaming error reported by rows.Err(). Those were invisible to Transact, so a closure that read rows in a loop and forgot to check rows.Err() could build state from a truncated read and commit it. Rows closes that gap: Scan and the end-of-iteration Err are latched exactly like a statement error, so such a closure can no longer commit partial work.

It embeds *sql.Rows, so the usual `for rows.Next() { rows.Scan(...) }`, `rows.Err()`, and `rows.Close()` call sites are unchanged; only code that explicitly stores the result as *sql.Rows needs adjustment (the same source-compat caveat as Row). Outside a Transact-managed Tx — a *DB query, or a Tx obtained from DB.BeginTx — recordErr is nil, so Rows is a pure passthrough and behaves exactly like *sql.Rows.

func (*Rows) Err added in v1.10.7

func (r *Rows) Err() error

Err shadows sql.Rows.Err and latches the streaming error, so an explicit rows.Err() check aborts the transaction as well as surfacing the error to the caller.

func (*Rows) Next added in v1.10.7

func (r *Rows) Next() bool

Next shadows sql.Rows.Next. When iteration ends (Next returns false) it latches any streaming error (rows.Err()), so a `for rows.Next()` loop that never checks rows.Err() still aborts the transaction on a mid-stream failure. An early break (Next still returning true) latches nothing — the caller stopped deliberately, and a streaming error would itself have made Next return false.

func (*Rows) NextResultSet added in v1.11.0

func (r *Rows) NextResultSet() bool

NextResultSet shadows sql.Rows.NextResultSet. Like Next, a false return latches the streaming error, so a multi-result-set loop that never checks rows.Err() still aborts the transaction when advancing to the next result set failed rather than ran out.

func (*Rows) Scan added in v1.10.7

func (r *Rows) Scan(dest ...any) error

Scan shadows sql.Rows.Scan and latches a scan error into the transaction (autoErr mode), so a closure that ignores the returned error still cannot commit state read from a failed scan.

type Stmt added in v1.11.0

type Stmt struct {
	*sql.Stmt
	// contains filtered or unexported fields
}

Stmt is a prepared statement that shadows sql.Stmt so that executing it stays on sequel's path: each execution emits a span and a duration sample, is classified for lock contention, and pays the simulated round-trip delay (DB.SimulateRTT). Inside a DB.Transact transaction, an execution error is recorded into the transaction and subsequent statements short-circuit, exactly as for a statement issued through Tx directly — a closure that ignores a prepared statement's error cannot commit partial work.

It embeds *sql.Stmt, so stmt.Exec(...)/stmt.Query(...)/stmt.Close() call sites are unchanged; only code that explicitly stores the result of Prepare as *sql.Stmt needs adjustment (the same source-compat shape as Rows and Row).

A Stmt prepared on a DB reads the pool's telemetry and simulated delay at each execution; a Stmt bound to a Tx (from Tx.Prepare or Tx.Stmt) uses the snapshots the transaction captured when it began, so one transaction runs at one consistent latency. Close is a passthrough: releasing the statement handle is lifecycle, not caller-facing work.

func (*Stmt) Exec added in v1.11.0

func (s *Stmt) Exec(args ...any) (sql.Result, error)

Exec shadows sql.Stmt.Exec.

func (*Stmt) ExecContext added in v1.11.0

func (s *Stmt) ExecContext(ctx context.Context, args ...any) (sql.Result, error)

ExecContext shadows sql.Stmt.ExecContext.

func (*Stmt) Query added in v1.11.0

func (s *Stmt) Query(args ...any) (*Rows, error)

Query shadows sql.Stmt.Query. It returns a Rows, which embeds *sql.Rows so existing call sites are unchanged; inside a Transact transaction it latches row-read errors like any Tx query.

func (*Stmt) QueryContext added in v1.11.0

func (s *Stmt) QueryContext(ctx context.Context, args ...any) (*Rows, error)

QueryContext shadows sql.Stmt.QueryContext. It returns a Rows (see Query).

func (*Stmt) QueryRow added in v1.11.0

func (s *Stmt) QueryRow(args ...any) *Row

QueryRow shadows sql.Stmt.QueryRow. It returns a Row, which embeds *sql.Row so existing QueryRow(...).Scan(...) call sites are unchanged.

func (*Stmt) QueryRowContext added in v1.11.0

func (s *Stmt) QueryRowContext(ctx context.Context, args ...any) *Row

QueryRowContext shadows sql.Stmt.QueryRowContext. It returns a Row (see QueryRow).

type Tx added in v1.4.0

type Tx struct {
	*sql.Tx
	// contains filtered or unexported fields
}

Tx is an in-progress database transaction that shadows sql.Tx methods to apply virtual function expansion and placeholder conforming.

When created by DB.Transact, a Tx records the first statement error and short-circuits subsequent statements (returning that error without touching the database). This guarantees a transaction cannot commit partial state when a caller forgets to check a statement's error, and it surfaces a deadlock (rather than masking it as a later "COMMIT has no corresponding BEGIN" on some drivers) so Transact can retry. A Tx obtained from DB.BeginTx does not do this — its statement methods behave exactly like the underlying sql.Tx.

func (*Tx) Commit added in v1.11.0

func (tx *Tx) Commit() error

Commit shadows sql.Tx.Commit so the COMMIT round trip is instrumented like any statement: it emits its own span (nested under the transaction), records into sequel_query_duration as operation COMMIT, is classified for sequel_lock_contention, and pays any simulated round-trip delay (DB.SimulateRTT). Classification matters here because a serialization failure most often surfaces at commit on CockroachDB and on PostgreSQL under SERIALIZABLE.

A call that answers sql.ErrTxDone never reaches the database, so it emits no span, records no duration, and pays no delay. Return values are identical to sql.Tx throughout.

Behavior is otherwise unchanged, including in Transact mode: Transact decides whether to commit before calling this, and a commit error is not recorded into Tx.Err.

func (*Tx) DriverName added in v1.4.0

func (tx *Tx) DriverName() string

DriverName is the name of the driver: "mysql", "pgx", "cockroachdb", "mssql" or "sqlite".

func (*Tx) Err added in v1.8.0

func (tx *Tx) Err() error

Err returns the first statement error recorded in Transact mode, or nil. Always nil for a Tx obtained from BeginTx.

func (*Tx) Exec added in v1.4.0

func (tx *Tx) Exec(query string, args ...any) (sql.Result, error)

Exec shadows sql.Tx.Exec and conforms arg placeholders for the driver.

func (*Tx) ExecContext added in v1.4.0

func (tx *Tx) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)

ExecContext shadows sql.Tx.ExecContext and conforms arg placeholders for the driver.

func (*Tx) InsertReturnID added in v1.4.0

func (tx *Tx) InsertReturnID(ctx context.Context, idColumn string, stmt string, args ...any) (int64, error)

InsertReturnID executes an INSERT statement and returns the auto-generated ID for the named ID column. idColumn must be a plain identifier matching [A-Za-z_][A-Za-z0-9_]* — it is spliced into the statement on some drivers, so quoted or exotic column names are rejected rather than escaped.

func (*Tx) Prepare added in v1.4.0

func (tx *Tx) Prepare(query string) (*Stmt, error)

Prepare shadows sql.Tx.Prepare and conforms arg placeholders for the driver. It returns a Stmt bound to this transaction: in Transact mode an execution error is recorded and short-circuits later statements, exactly as for a statement issued through the Tx directly.

func (*Tx) PrepareContext added in v1.4.0

func (tx *Tx) PrepareContext(ctx context.Context, query string) (*Stmt, error)

PrepareContext shadows sql.Tx.PrepareContext and conforms arg placeholders for the driver. It returns a Stmt bound to this transaction (see Prepare).

func (*Tx) Query added in v1.4.0

func (tx *Tx) Query(query string, args ...any) (*Rows, error)

Query shadows sql.Tx.Query and conforms arg placeholders for the driver. It returns a Rows, which embeds *sql.Rows so existing rows.Next()/rows.Scan()/rows.Err() call sites are unchanged. In Transact (autoErr) mode the returned Rows latches a mid-iteration Scan or streaming error into the transaction, so a closure that forgets rows.Err() cannot commit state read from a truncated result set.

func (*Tx) QueryContext added in v1.4.0

func (tx *Tx) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)

QueryContext shadows sql.Tx.QueryContext and conforms arg placeholders for the driver. It returns a Rows (see Query) that latches row-iteration errors into the transaction in Transact (autoErr) mode.

func (*Tx) QueryRow added in v1.4.0

func (tx *Tx) QueryRow(query string, args ...any) *Row

QueryRow shadows sql.Tx.QueryRow and conforms arg placeholders for the driver. It returns a Row, which embeds *sql.Row so existing QueryRow(...).Scan(...) call sites are unchanged. In Transact (autoErr) mode the Row latches its Scan/Err error into the transaction (except sql.ErrNoRows — see Row).

func (*Tx) QueryRowContext added in v1.4.0

func (tx *Tx) QueryRowContext(ctx context.Context, query string, args ...any) *Row

QueryRowContext shadows sql.Tx.QueryRowContext and conforms arg placeholders for the driver. It returns a Row (see QueryRow) that latches its error into the transaction in Transact (autoErr) mode.

func (*Tx) Rollback added in v1.11.0

func (tx *Tx) Rollback() error

Rollback shadows sql.Tx.Rollback for the same reasons as Tx.Commit, and handles an already-finalized transaction the same way. A rollback is a round trip whether or not anything went right, so it is instrumented while the transaction unwinds after a failure too.

func (*Tx) Stmt added in v1.11.0

func (tx *Tx) Stmt(stmt *Stmt) *Stmt

Stmt shadows sql.Tx.Stmt: it binds a statement prepared on the DB to this transaction. The returned Stmt is transaction-bound, so in Transact mode its execution errors are recorded and short-circuit later statements — a prepared statement is not an escape hatch from the no-partial-commit guarantee.

func (*Tx) StmtContext added in v1.11.0

func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt

StmtContext shadows sql.Tx.StmtContext (see Stmt).

func (*Tx) UnpackQuery added in v1.4.0

func (tx *Tx) UnpackQuery(query string) (string, error)

UnpackQuery expands virtual functions (e.g. NOW_UTC(), REGEXP_TEXT_SEARCH()) into driver-specific SQL expressions, and conforms arg placeholders to the syntax expected by the driver (e.g. ? to $1, $2 for PostgreSQL).

type UnsafeSQL

type UnsafeSQL string

UnsafeSQL wraps a string to indicate not to use an argument placeholder when inserting it into a SQL statement. It should be used to insert values such as NOW() or calculation of other fields. Use with caution to avoid SQL injection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL