Documentation
¶
Overview ¶
Package offthread is v5's client for a SQLite database living in another thread (plan item P3.5).
This is the plan's OpenOffThread. It lives in its own package rather than beside sqlite.Open for one concrete reason: criterion (b) requires an app using the off-thread model to contain no wazero symbol at all, and any package importing db/sqlite links the whole engine — a ~1MB wasm interpreter — into app.wasm. Nothing in this package imports db/sqlite, ncruces, or wazero, and TestOffThreadClientDoesNotLinkTheEngine fails the build if that ever changes.
The engine-side half is db/offthread/server, which does import db/sqlite and is meant to be compiled into domain.wasm.
The two halves speak this protocol and nothing else.
Index ¶
- Constants
- func IsKnownOp(parseOp Op) bool
- type DB
- func (parseDB *DB) Close(parseCtx context.Context) error
- func (parseDB *DB) Exec(parseCtx context.Context, parseSQL string, parseArgs ...any) (Result, error)
- func (parseDB *DB) Flush(parseCtx context.Context) error
- func (parseDB *DB) Query(parseCtx context.Context, parseSQL string, parseArgs ...any) (Rows, error)
- func (parseDB *DB) Tx(parseCtx context.Context, parseFn func(*Tx) error) (parseErr error)
- type Op
- type Options
- type RemoteError
- type Request
- type Response
- type Result
- type Rows
- type Transport
- type Tx
- type Value
- type ValueKind
Constants ¶
const DefaultMaxRows = 10000
DefaultMaxRows is the result-set cap applied when Options.MaxRows is unset.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a handle to a database running on another thread.
It deliberately does NOT expose database/sql types. *sql.Rows is a cursor over a live connection, and there is no live connection here — pretending otherwise would mean a thread round-trip per row, which is precisely the pattern this package exists to prevent. Results are materialized, capped, and returned whole.
func Open ¶
Open connects to a database on another thread.
The transport is assumed to already address a database; opening the underlying file is the server's job, on its own thread. That split is what keeps the engine out of this binary.
func (*DB) Exec ¶
func (parseDB *DB) Exec(parseCtx context.Context, parseSQL string, parseArgs ...any) (Result, error)
Exec runs a statement that returns no rows.
func (*DB) Tx ¶
Tx runs fn inside a transaction, committing on success and rolling back on error or panic.
The rollback on panic matters more here than in an in-process driver: an abandoned transaction holds a write lock on the database thread, so every later write from every caller blocks behind it. A panicking callback that left the transaction open would look like the whole database had hung.
type Op ¶
type Op string
Op names one database operation.
const ( // OpExec runs a statement that returns no rows. OpExec Op = "exec" // OpQuery runs a statement that returns rows. OpQuery Op = "query" // OpBegin opens a transaction and returns its id. OpBegin Op = "begin" // OpCommit commits an open transaction. OpCommit Op = "commit" // OpRollback discards an open transaction. OpRollback Op = "rollback" // OpFlush forces the database image to its durable backend. OpFlush Op = "flush" // OpClose closes the database. OpClose Op = "close" )
type Options ¶
type Options struct {
// MaxRows caps every query's result set unless the query overrides it.
//
// It defaults to DefaultMaxRows rather than to unlimited, and that default is
// the point: a query returning every row of a large table has to marshal the
// whole set across the thread boundary, which is the exact stall the
// off-thread model exists to remove. A cap turns that mistake into a visible
// Truncated flag instead of a frozen UI.
MaxRows int
}
Options configure an off-thread database.
type RemoteError ¶
RemoteError is a failure that happened on the database thread.
A distinct type so a caller can tell "the query was rejected" from "the worker never answered" — the first is a bug in the query, the second is a transport or liveness problem, and they want different handling.
func (*RemoteError) Error ¶
func (parseErr *RemoteError) Error() string
type Request ¶
type Request struct {
Op Op `json:"op"`
SQL string `json:"sql,omitempty"`
// Args are bind parameters. They are never interpolated into SQL, which is
// what keeps the boundary from becoming an injection surface.
Args []Value `json:"args,omitempty"`
// TxID scopes the operation to an open transaction. Empty means autocommit.
TxID string `json:"txId,omitempty"`
// MaxRows caps a query's result set. Zero means the server's default.
MaxRows int `json:"maxRows,omitempty"`
}
Request is one operation sent to the database thread.
type Response ¶
type Response struct {
// Err is the failure message, empty on success. A string rather than an
// error because the failure has to cross a thread boundary; RemoteError
// wraps it back into an error on arrival.
Err string `json:"err,omitempty"`
RowsAffected int64 `json:"rowsAffected,omitempty"`
LastInsertID int64 `json:"lastInsertId,omitempty"`
Columns []string `json:"columns,omitempty"`
Rows [][]Value `json:"rows,omitempty"`
TxID string `json:"txId,omitempty"`
// Truncated reports that the result set hit MaxRows and is incomplete.
//
// Reported rather than silently cut: a caller that paginates on a short
// result would stop early and believe it had everything.
Truncated bool `json:"truncated,omitempty"`
}
Response is one operation's result.
type Rows ¶
type Rows struct {
Columns []string
Values [][]Value
// Truncated reports that the result hit the row cap and is incomplete.
Truncated bool
}
Rows is a materialized result set.
type Transport ¶
Transport carries one request to the database thread and returns its response.
An interface rather than a concrete worker binding so the client is testable without a browser, and so the same client works over a Web Worker, a MessagePort, or an in-process server during development. A transport is responsible for delivery and for surfacing worker death as an error; it is not responsible for interpreting Response.Err, which is the database's own answer rather than a transport failure.
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx is an open transaction on the database thread.
type Value ¶
type Value struct {
Kind ValueKind `json:"kind"`
Int int64 `json:"int,omitempty"`
Float float64 `json:"float,omitempty"`
Text string `json:"text,omitempty"`
Blob []byte `json:"blob,omitempty"`
}
Value is one SQLite value in transit.
A struct with a kind rather than an interface: the wire form has to survive a structured clone or a JSON encode without the receiver inferring types from what the numbers happen to look like.
func NewValue ¶
NewValue converts a Go value to its wire form.
The accepted set is deliberately narrow — what SQLite actually stores, plus the Go integer and float widths that convert without loss. Anything else is an error at the call site rather than a surprise at the far end, where the only available response is a string.
type ValueKind ¶
type ValueKind uint8
ValueKind names one SQLite storage class.
SQLite has exactly five, and modelling them explicitly rather than shipping `any` across the boundary is what keeps the encoding total: every value has exactly one representation, and a decoder never has to guess whether a number was an integer or a float. That guess is how JSON round-trips silently turn an int64 id into a float64 and lose precision above 2^53.