offthread

package
v5.0.1 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 5 Imported by: 0

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

View Source
const DefaultMaxRows = 10000

DefaultMaxRows is the result-set cap applied when Options.MaxRows is unset.

Variables

This section is empty.

Functions

func IsKnownOp

func IsKnownOp(parseOp Op) bool

IsKnownOp reports whether an op is one the server understands.

Unknown ops are refused rather than ignored: a request that silently did nothing would look identical to one that succeeded and wrote no rows.

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

func Open(parseTransport Transport, parseOptions Options) (*DB, error)

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) Close

func (parseDB *DB) Close(parseCtx context.Context) error

Close flushes and closes the database on its thread.

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) Flush

func (parseDB *DB) Flush(parseCtx context.Context) error

Flush forces the database image to its durable backend.

func (*DB) Query

func (parseDB *DB) Query(parseCtx context.Context, parseSQL string, parseArgs ...any) (Rows, error)

Query runs a statement and returns its rows, capped at the handle's row limit.

func (*DB) Tx

func (parseDB *DB) Tx(parseCtx context.Context, parseFn func(*Tx) error) (parseErr error)

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

type RemoteError struct {
	Op      Op
	Message string
}

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 Result

type Result struct {
	RowsAffected int64
	LastInsertID int64
}

Result reports what a statement changed.

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.

func (Rows) Len

func (parseRows Rows) Len() int

Len reports the row count.

type Transport

type Transport interface {
	Call(parseCtx context.Context, parseRequest Request) (Response, error)
}

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.

func (*Tx) Exec

func (parseTx *Tx) Exec(parseCtx context.Context, parseSQL string, parseArgs ...any) (Result, error)

Exec runs a statement inside the transaction.

func (*Tx) Query

func (parseTx *Tx) Query(parseCtx context.Context, parseSQL string, parseArgs ...any) (Rows, error)

Query runs a query inside the transaction.

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

func NewValue(parseGoValue any) (Value, error)

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.

func NewValues

func NewValues(parseGoValues []any) ([]Value, error)

NewValues converts a Go argument list to its wire form.

func (Value) Go

func (parseValue Value) Go() any

Go converts a wire value back to a Go value.

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.

const (
	// ValueNull is SQL NULL.
	ValueNull ValueKind = iota
	// ValueInt is INTEGER.
	ValueInt
	// ValueFloat is REAL.
	ValueFloat
	// ValueText is TEXT.
	ValueText
	// ValueBlob is BLOB.
	ValueBlob
)

func (ValueKind) String

func (parseKind ValueKind) String() string

Directories

Path Synopsis
Package server executes off-thread database requests against a real SQLite database (plan item P3.5).
Package server executes off-thread database requests against a real SQLite database (plan item P3.5).

Jump to

Keyboard shortcuts

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