Documentation
¶
Overview ¶
Package sqlbatch sends several SQL statements per network round trip through a database/sql handle.
database/sql has no batch verb, so N statements normally cost N round trips. That is expensive everywhere and worst under TinyGo, where each round trip is a blocking native socket read. This package queues the statements, hands them to whichever transport the driver actually offers, and delivers the results in queue order.
b := &sqlbatch.Batch{}
b.Queue("INSERT INTO t(a) VALUES ($1)", 1)
b.Queue("INSERT INTO t(a) VALUES ($1)", 2)
if err := sqlbatch.Send(ctx, db, b); err != nil { ... }
Results are read through callbacks registered on the queued statement, so nothing derived from the connection escapes the batch:
var n int
b.Queue("SELECT count(*) FROM t").QueryRow(func(r sqlbatch.Row) error {
return r.Scan(&n)
})
Semantics ¶
A batch stops at the first failing statement, and nothing it did survives. Later statements do not run, even independent ones. That is PostgreSQL's native pipeline behavior, and the other drivers reproduce it with an explicit transaction. WithoutTransaction gives it up deliberately.
The number of round trips a batch costs depends on the driver and is allowed to differ. The results and errors a caller observes are not.
Drivers ¶
A driver package registers its own adapter, so importing the driver you already open the database with is enough.
postgres pgx/stdlib exec and query, pipelined into one round trip mysql mysql exec in one round trip, through multiStatements sqlite sqlite one statement at a time, in one transaction
SQLite is not a lesser case, just a different cost. There is no network, so there are no round trips to save; what costs is the fsync each autocommit statement pays, and one transaction removes it. Measured here, 200 inserts go from ~50ms to ~1ms on all three SQLite backends.
The MySQL path needs multiStatements=true, and interpolateParams=true whenever a statement carries arguments. Both are DSN settings negotiated when the connection is made, so they cannot be turned on per batch.
Unsupported drivers ¶
Send does not silently fall back to running the statements one at a time. A driver that cannot serve the batch, and did not register for the sequential path the way SQLite does, makes it fail. The alternative is worse: a silent fallback would report success while costing N round trips instead of one, and would quietly drop the all-or-nothing guarantee. Refusing keeps the cost of a batch something a caller can reason about.
Every refusal is an *UnsupportedError naming the driver, what was missing, and where possible what would fix it:
- No adapter registered, which today means any third-party driver. Capability is "batch".
- A capability the adapter lacks, such as a queued Query on MySQL. Capability names it, and the batch is refused as a whole rather than running the statements it could.
- A DSN the adapter cannot work with, such as MySQL without interpolateParams. Hint names the setting to change.
In all of these the batch executes nothing at all: the refusal happens before any statement reaches the server, so there is no partial effect to undo.
WithFallback turns a refusal into sequential execution instead, for portable code that would rather be slow on one database than not run there. It is opt-in because the cost changes from one round trip to one per statement, and a caller who batched for speed should not get that silently. The semantics do not change: same order, same stop at the first error, same rollback.
err := sqlbatch.Send(ctx, db, b, sqlbatch.WithFallback())
Or detect it and decide for yourself:
var unsupported *sqlbatch.UnsupportedError
if errors.As(sqlbatch.Send(ctx, db, b), &unsupported) { ... }
What a batch does not promise ¶
Reads in one batch do not necessarily see the same snapshot. PostgreSQL takes a fresh one per statement at READ COMMITTED, while SQLite and MySQL InnoDB share one across the transaction the batch runs in. Nothing here can reconcile that, so a batch promises only that the statements run in order, stop at the first error, and roll back together.
Index ¶
- func ConvertArgs(dc any, args []any, ordinalBase int) ([]driver.NamedValue, error)
- func Register(drv driver.Driver, a Adapter)
- func RegisterSequential(drv driver.Driver)
- func Send(ctx context.Context, db *sql.DB, b *Batch, opts ...Option) error
- type Adapter
- type Batch
- type CommandTag
- type Option
- type Options
- type QueuedQuery
- type Results
- type Row
- type Rows
- type StatementError
- type UnsupportedError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ConvertArgs ¶
ConvertArgs turns a queued statement's arguments into driver values, for adapters that reach a driver.Conn directly and therefore do not get database/sql's own conversion.
It honors the driver's driver.NamedValueChecker when there is one, since a driver may accept types the default converter rejects.
func Register ¶
Register associates an adapter with a driver, and is meant to be called from a driver package's init. The key is the driver's type, so every handle opened through that driver finds the adapter.
Registering the same driver twice panics, matching sql.Register.
func RegisterSequential ¶ added in v1.1.5
RegisterSequential declares that a driver has no batch transport and that running the statements one at a time is the right answer for it anyway.
SQLite is the case this exists for. There is no network, so there are no round trips to save, but the statements still run in one transaction rather than paying an fsync each, which is the cost that actually matters there. Such a driver therefore needs no WithFallback: batching it is already the best available shape.
func Send ¶
Send runs the batch on a pooled connection and reports the first error.
The connection is leased for the duration and returned afterwards. Every callback registered on a queued statement runs before Send returns; a statement with no callback still participates, and its result is discarded.
An empty batch is a no-op.
Types ¶
type Adapter ¶
An Adapter runs a batch on one driver's raw connection. dc is the value sql.Conn.Raw yields, valid only until the returned Results is closed.
An adapter that cannot serve a batch must return *UnsupportedError without having executed anything, so the caller is never left with a partial effect and WithFallback can retry safely.
type Batch ¶
type Batch struct {
// contains filtered or unexported fields
}
Batch is a set of statements to send together. Queue them, then pass the batch to Send. A Batch may be sent once.
func (*Batch) Queue ¶
func (b *Batch) Queue(sql string, args ...any) *QueuedQuery
Queue appends a statement and returns it, so a result callback can be attached. Placeholder syntax is the driver's own: $1 for PostgreSQL, ? for MySQL. This package does not translate between them.
func (*Batch) Queued ¶
func (b *Batch) Queued() []*QueuedQuery
Queued returns the queued statements, for adapters.
type CommandTag ¶
type CommandTag struct {
// RowsAffected is the number of rows the statement changed. It is 0 for a
// statement that changes nothing, including a SELECT.
RowsAffected int64
// LastInsertID is meaningful only when HasLastInsertID is set. PostgreSQL
// never reports one; use a RETURNING clause instead.
LastInsertID int64
HasLastInsertID bool
}
CommandTag is what one statement reports about its effect.
type Option ¶
type Option func(*settings)
An Option adjusts how a batch is sent.
func WithFallback ¶ added in v1.1.5
func WithFallback() Option
WithFallback runs the statements one at a time when the driver refuses the batch, instead of failing. Use it for portable code that would rather be slow on one database than not run there.
The batch keeps its semantics either way: the statements run in order, stop at the first error, and roll back together. What changes is the cost, from one round trip to one per statement, which is why this is not the default.
It is safe because a refusal happens before any statement reaches the server, so there is nothing to have run twice. Adapters must keep that true.
func WithoutTransaction ¶
func WithoutTransaction() Option
WithoutTransaction lets the statements commit as they go, giving up the all-or-nothing guarantee in exchange for one less round trip on drivers that need an explicit transaction.
It has no effect on PostgreSQL, whose pipeline is atomic by construction: there is no way to ask for less.
type Options ¶
type Options struct {
// Transaction asks the adapter to make the batch atomic. It is true unless
// the caller passed WithoutTransaction.
Transaction bool
}
Options are the settings Send resolved, passed to the adapter.
type QueuedQuery ¶
QueuedQuery is one statement in a Batch.
func (*QueuedQuery) Exec ¶
func (q *QueuedQuery) Exec(fn func(CommandTag) error) *QueuedQuery
Exec registers fn to receive this statement's result. Leaving it unset is fine for a write-only batch: Send reports any error either way.
func (*QueuedQuery) Query ¶
func (q *QueuedQuery) Query(fn func(Rows) error) *QueuedQuery
Query registers fn to receive this statement's rows. The rows are closed after fn returns and must not outlive it.
func (*QueuedQuery) QueryRow ¶
func (q *QueuedQuery) QueryRow(fn func(Row) error) *QueuedQuery
QueryRow registers fn to receive this statement's first row. As with database/sql, Scan reports sql.ErrNoRows when there is none.
func (*QueuedQuery) WantsRows ¶
func (q *QueuedQuery) WantsRows() bool
Kind reports whether the statement was queued for its rows, for adapters that need to know before sending.
type Results ¶
type Results interface {
// Exec reads the next statement's result.
Exec() (CommandTag, error)
// Query reads the next statement's rows.
Query() (Rows, error)
// QueryRow reads the next statement's first row.
QueryRow() Row
// Close reads and discards anything unread, releasing the connection. It
// reports the batch's error.
Close() error
}
Results delivers one batch's results in queue order. It is implemented by adapters and driven by Send; a caller reads results through the callbacks on QueuedQuery instead.
type Rows ¶
type Rows interface {
Next() bool
Scan(dest ...any) error
Columns() ([]string, error)
Err() error
Close() error
}
Rows is one statement's result set. It is the portable subset of sql.Rows and pgx.Rows.
type StatementError ¶
StatementError identifies the queued statement a batch failed on.
Index is -1 when the driver cannot attribute the failure, which happens on MySQL: the server reports one error for the whole batch. An index of -1 means the position is unknown, never that the first statement failed.
func (*StatementError) Error ¶
func (e *StatementError) Error() string
func (*StatementError) Unwrap ¶
func (e *StatementError) Unwrap() error
type UnsupportedError ¶
type UnsupportedError struct {
// Driver names the driver that could not serve the request.
Driver string
// Capability is what was missing, such as "batch" or "query".
Capability string
// Hint, when set, says what would make it work.
Hint string
}
UnsupportedError says a driver cannot serve part of a batch. It is returned rather than silently falling back, so a caller never mistakes N round trips for one, or an unbatched read for a batched one.
func (*UnsupportedError) Error ¶
func (e *UnsupportedError) Error() string