hazedb

package module
v0.1.29 Latest Latest
Warning

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

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

README

hazedb

sql db written in go

Documentation

Overview

Package hazedb is a memory-resident SQL store for embedded Go applications with single-process deployment and latency-sensitive reads.

Index

Constants

View Source
const (
	WALOff      = iota // 0: memory only; nothing survives a crash.
	WALPeriodic        // 1: writes fsynced by a background ticker (~1s); the caller never waits.
	WALPerWrite        // 2: every write fsynced before the caller gets a reply — slowest, safest.
)

WALLevel selects on-disk durability — the single switch for the write-ahead log. WALPath turns nothing on by itself; the level does.

Variables

View Source
var (
	ErrDuplicatePK   = errors.New("fastsql: duplicate primary key")
	ErrUnknownTable  = errors.New("fastsql: unknown table")
	ErrUnknownColumn = errors.New("fastsql: unknown column")
	ErrTypeMismatch  = errors.New("fastsql: type mismatch")
	ErrParamMismatch = errors.New("fastsql: parameter count mismatch")
	ErrPKUpdate      = errors.New("fastsql: UPDATE on PK column not supported")
	ErrParse         = errors.New("fastsql: parse error")
	ErrUnindexedJoin = errors.New("fastsql: JOIN requires an index on the join column")
	ErrWALCorrupt    = errors.New("fastsql: WAL corrupted")
	ErrTableExists   = errors.New("hazedb: table already exists")
	ErrTxUnsupported = errors.New("hazedb: operation not supported in a transaction")
)

Functions

func ArgsFromJSON added in v0.1.4

func ArgsFromJSON(raw []byte) ([]any, error)

ArgsFromJSON parses a JSON array of positional SQL args into the []any db.Query / db.Exec accept. Empty/nil input yields no args. See the file header for the type mapping (notably the canonical-UUID-string rule).

func DeregisterDBIf added in v0.1.4

func DeregisterDBIf(name string, db *DB)

DeregisterDBIf clears the slot only if it still holds db (CAS). Safe for caddymodule Cleanup during reload: preserves a newer Provision's registration.

func ErrorJSON added in v0.1.4

func ErrorJSON(msg string) []byte

ErrorJSON renders an error as {"error":"msg"}.

func ExecResultJSON added in v0.1.4

func ExecResultJSON(affected int) []byte

ExecResultJSON renders a write result as {"affected":n}.

func LookupDBSlot added in v0.1.4

func LookupDBSlot(name string) *atomic.Pointer[DB]

LookupDBSlot returns the stable atomic slot for name (lazily created). Cache it once and Load() per use:

var defaultSlot = hazedb.LookupDBSlot("default")
db := defaultSlot.Load() // nil until a caddymodule Provision registers one

func QueryArgs added in v0.1.4

func QueryArgs(s string) ([]any, error)

QueryArgs parses an adapter's args parameter in either form:

  • "" → no args
  • starts with '[' → a JSON array (ArgsFromJSON; multi-arg / typed / writes)
  • anything else → ONE positional arg passed directly: a canonical UUID string → UUID, otherwise STRING

The direct form lets a caller pass a key it already has — e.g. the UUID from a clicked link — straight into `WHERE id = ?` with no json_encode wrapping, the common single-key read. For integer args or several args, use the JSON array form so types are unambiguous.

func RegisterDB added in v0.1.4

func RegisterDB(name string, db *DB)

RegisterDB publishes db under name. Pass nil to clear unconditionally; from Cleanup-style paths use DeregisterDBIf to avoid clobbering a newer registration.

func RowsToJSON added in v0.1.4

func RowsToJSON(cols []string, rows []Row) ([]byte, error)

RowsToJSON renders a result set as {"columns":[...],"rows":[[...],...]}.

Hand-rolled: it appends each Value straight into one capacity-hinted buffer, with no [][]any intermediate, no interface boxing, and no reflection. On a hot read path (one PHP call per request) that is ~1.5–3× faster than encoding/json and turns ~4 allocations per row into a single buffer allocation. The returned []byte is the caller's to keep or copy (the PHP adapter copies it into a zend_string immediately). Never errors — the signature keeps the error for API symmetry with the rest of wire.go.

func RowsToJSONObjects added in v0.1.4

func RowsToJSONObjects(cols []string, rows []Row) ([]byte, error)

RowsToJSONObjects renders a result set as a JSON array of objects keyed by column name — [{"col":val,...},...] — the shape a fetchall-style caller forwards straight to an HTTP/JSON response. Same hand-rolled, single-buffer approach as RowsToJSON; never errors (error kept for API symmetry).

Types

type ColumnDef

type ColumnDef struct {
	Name string
	Type ColumnType
	// PK marks the single primary-key column. It must be TypeUUID and is
	// implicitly immutable.
	PK bool
	// Immutable rejects UPDATE SET on this column at plan time. Used for an
	// ordering column (e.g. seq) so a tail index can cache its order safely.
	Immutable bool
	// PartitionKey marks the column the table is sharded/ordered by (e.g.
	// thread_id on a messages table). At most one per table; must be UUID in
	// v1; implicitly immutable (a partition move is DELETE + INSERT). Storage
	// routing + the ordered tail index build on it.
	PartitionKey bool
	Nullable     bool
}

ColumnDef describes one column at table-create time.

type ColumnType

type ColumnType uint8

ColumnType is the declared type of a column. Generic v1 supports a small set; codegen (M3) expands to richer types.

const (
	TypeInt ColumnType = iota + 1
	TypeString
	TypeBytes
	TypeBool
	TypeUUID
)

func (ColumnType) String

func (t ColumnType) String() string

type DB

type DB struct {
	// contains filtered or unexported fields
}

--- Gateway contract -------------------------------------------------------

*DB is the single official entry point — the "gateway". Every consumer enters through it: Caddy calls these methods as native Go, and the FrankenPHP/PHP extension reaches them via cgo (C → exported Go → these same methods). There is no second transport — the PHP path is cgo calling the very same verbs, not a parallel API. The gateway verbs are Open/Close/FlushWAL/Exec/Query/QueryRow here, plus Transaction (txn.go).

Every verb upholds these guarantees, so all consumers inherit them for free:

  • Validation. SQL is parsed, planned, and bound to the live catalog in prepare(); args are type-coerced in toValue. Bad SQL or args fail here.
  • Boundary clone. []byte/Value args are deep-copied on the way in (storage never aliases caller memory), and returned rows are deep-cloned on the way out (callers may retain them past later writes).
  • No bypass. table/shard/catalog/wal are unexported, so no consumer can reach storage around the validated verbs.

Boundary rule: db semantics live behind the gateway (this package); cross-cutting concerns — auth, tenancy, logging, and the PHP↔Go marshalling the extension needs — live in the consumer/adapter, which then calls these same verbs. Never push consumer-specific concerns into the core.

DB is the embedded database handle. One DB per process per WAL path. Open is goroutine-safe; Exec and Query are goroutine-safe.

func LookupDB added in v0.1.4

func LookupDB(name string) *DB

LookupDB returns the *DB registered under name, or nil. Convenience wrapper; hot-path consumers should cache the slot via LookupDBSlot and Load() per use.

func Open

func Open(opts Options) (*DB, error)

Open prepares the database. If WALPath is non-empty, the file is opened and any existing records are replayed into memory before Open returns. Open is blocking until replay completes.

func (*DB) Close

func (db *DB) Close() error

Close stops the SQLite drain loop (which seals the active segment and runs a final drain so the mirror is current), then the index merger, then flushes and closes the WAL, then closes the mirror. Memory-only DBs still stop the merger. The drain loop is stopped first, while the WAL is still open.

func (*DB) Exec

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

Exec runs an INSERT, UPDATE, DELETE, CREATE TABLE, or DROP TABLE. Returns the affected row count (0 for DDL).

func (*DB) ExecValues added in v0.1.4

func (db *DB) ExecValues(sql string, args ...Value) (int, error)

ExecValues runs a write with pre-typed Value arguments, skipping the []any arg-conversion layer Exec uses (no JSON decode, no interface boxing, no per-arg type switch). It is the in-process fast path for callers that already hold typed Values — notably the PHP extension reading a native zend_array via cgo, which avoids the json_encode/json.Decode round-trip the string args form pays. Returns the affected row count (0 for DDL).

func (*DB) FlushWAL

func (db *DB) FlushWAL() error

FlushWAL forces bufio to fsync. Use before reading a record back from disk in tests, or for an explicit durability boundary in callers. Memory-only DBs are a no-op.

func (*DB) Prepare added in v0.1.4

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

Prepare compiles sql into a reusable statement bound to the current catalog.

func (*DB) Query

func (db *DB) Query(sql string, args ...any) ([]string, []Row, error)

Query runs a SELECT. Returns the column names (in projection order) and the rows. Rows are deep-cloned; callers may retain them past future Exec calls without worrying about aliasing into storage.

func (*DB) QueryEach added in v0.1.14

func (db *DB) QueryEach(sql string, args []Value, visit func(cols []string, row Row) bool) error

QueryEach runs a SELECT and calls visit(cols, row) once per result row, in order. It is the low-allocation counterpart to QueryValues for consumers that serialize each row immediately (e.g. building a PHP array). visit returns false to stop early. cols is the same slice on every call.

The row obeys the streaming memory-safety contract above: valid only during the call; copy out anything kept. Use QueryValues when you need owned rows.

func (*DB) QueryJSON added in v0.1.14

func (db *DB) QueryJSON(sql string, args ...Value) ([]string, []byte, error)

QueryJSON runs a SELECT and returns the result as a JSON array of objects ([{"col":val,...},...]) — the same shape as RowsToJSONObjects, but encoded straight from the live rows into one buffer, with no intermediate []Row and no per-row clone. The returned bytes are the caller's to keep or copy. For a read-and-forward path (an HTTP/JSON response) this avoids materializing rows only to re-encode and discard them.

func (*DB) QueryJSONInto added in v0.1.16

func (db *DB) QueryJSONInto(dst []byte, sql string, args ...Value) ([]string, []byte, error)

QueryJSONInto is QueryJSON that appends into the caller-supplied buffer dst, returning the columns and the grown slice. Pass dst[:0] to reuse a pooled scratch buffer: a hot read-and-forward caller (the PHP/HTTP fetchall_json path) that keeps one buffer per worker and copies the result out immediately pays no per-call allocation for the JSON envelope — only the amortised growth to the high-water mark. The returned slice may share dst's backing array.

func (*DB) QueryRow added in v0.1.2

func (db *DB) QueryRow(sql string, args ...any) ([]string, Row, error)

QueryRow runs a SELECT expected to yield a single row and returns the first matching row, or a nil Row if there is none — without allocating the []Row result slice that Query needs. For a PK-pinned query (WHERE id = ?) it goes straight through the point-read path (the common case); for an unpinned query it returns the first row of the scan, so constrain such queries with LIMIT 1 to avoid scanning more rows than needed. The returned row is deep-cloned, as with Query.

func (*DB) QueryRowValues added in v0.1.4

func (db *DB) QueryRowValues(sql string, args ...Value) ([]string, Row, error)

QueryRowValues is QueryRow with pre-typed args (see QueryValues).

func (*DB) QueryValues added in v0.1.4

func (db *DB) QueryValues(sql string, args ...Value) ([]string, []Row, error)

QueryValues is Query with pre-typed args — the read counterpart of ExecValues, for in-process callers (the PHP extension) that already hold typed Values and want to skip the []any/JSON conversion. Reads never store the args, so no per-arg clone is needed.

func (*DB) Transaction

func (db *DB) Transaction(fn func(*Tx) error) error

Transaction runs fn as a transaction. Returning nil from fn commits all staged statements atomically; returning an error (or any tx.Exec having failed) discards everything — nothing is written to the store or the WAL.

type IndexDef added in v0.1.5

type IndexDef struct {
	Name    string // optional; auto-derived as "idx_<col>[_<col>...]" when empty
	Columns []string
	// Ordered makes this a sorted index (serves equality + ranges + ORDER BY)
	// instead of the default hash index (equality only). A column has one or the
	// other, not both. Composite indexes are always Ordered.
	Ordered bool
}

IndexDef declares a secondary index on one or more non-PK columns. Maintained asynchronously and read through a hybrid path; see docs/secondary-indexes.md. A composite (multi-column) index must be Ordered (see parseIndexClause).

type Options

type Options struct {
	// Schema declares the tables present at startup. May be empty — tables can
	// also be created at runtime with CREATE TABLE.
	Schema Schema

	// WALLevel is the durability switch: WALOff (memory only), WALPeriodic
	// (background fsync), or WALPerWrite (fsync per write). Above WALOff, WALPath
	// is required; setting WALPath or SQLitePath with WALOff is rejected.
	WALLevel int

	// WALPath is the directory holding the write-ahead log segments, created if
	// absent. Required when WALLevel > 0.
	WALPath string

	// WALRotateInterval is how often the active segment is sealed and the next
	// opened, so a drain (or any external consumer) reads settled segments and
	// the log never grows as one unbounded file. Zero = 5s.
	WALRotateInterval time.Duration

	// SQLitePath enables the on-disk SQLite mirror: the drain feeds sealed
	// segments into it (compacted current state) and it becomes the recovery
	// source. Empty = no mirror (the WAL itself replays into memory on boot).
	// Requires WALLevel > 0.
	SQLitePath string
	// contains filtered or unexported fields
}

Options configure Open. The EXPORTED fields are the entire operator surface; the unexported fields are internal tuning, settable only from package tests (the defaults are right for every deployment). Genuine implementation constants — buffer sizes, SQLite PRAGMAs, segment naming, shard counts — are not settings and live next to the code that uses them.

type Row

type Row []Value

Row is a single tuple. Ordinals match the table's column order.

func (Row) Clone

func (r Row) Clone() Row

Clone returns a deep copy of the row. Needed when the executor returns rows that the caller may mutate while the underlying storage lock is no longer held.

type Schema

type Schema struct {
	Tables []TableDef
}

Schema is the database schema. Tables are addressed by name.

type Stmt added in v0.1.4

type Stmt struct {
	// contains filtered or unexported fields
}

Stmt is a SQL string compiled to a plan once and reused. Versus the bare DB.Query/Exec path it skips the per-call statement-cache lookup (no SQL-string hash), and it adds a typed, zero-allocation point-read fast path, QueryRowByPK. Safe for concurrent use: the held plan is rebound automatically if a CREATE/DROP changes the catalog after Prepare.

func (*Stmt) Columns added in v0.1.4

func (s *Stmt) Columns() []string

Columns returns the SELECT output column names (nil for a non-SELECT). The slice is shared and read-only — callers must not mutate it.

func (*Stmt) Exec added in v0.1.4

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

Exec runs a prepared INSERT/UPDATE/DELETE/DDL. See DB.Exec.

func (*Stmt) Query added in v0.1.4

func (s *Stmt) Query(args ...any) ([]string, []Row, error)

Query runs a prepared SELECT. See DB.Query.

func (*Stmt) QueryRow added in v0.1.4

func (s *Stmt) QueryRow(args ...any) ([]string, Row, error)

QueryRow runs a prepared single-row SELECT. See DB.QueryRow.

func (*Stmt) QueryRowByPK added in v0.1.4

func (s *Stmt) QueryRowByPK(pk UUID, dst []Value) (out []Value, found bool, err error)

QueryRowByPK is the zero-allocation point-read fast path. The statement must be a PK-pinned SELECT (WHERE <pk> = ?); the key is taken as a typed UUID (no interface boxing) and the projected cells are written into dst, which is grown only if too small — reuse it across calls. found reports whether a row matched. Non-byte cells are copied directly; BYTES cells are cloned to honour storage's no-alias guarantee, so a projection without BYTES columns allocates nothing.

type TableDef

type TableDef struct {
	Name    string
	Columns []ColumnDef
	Indexes []IndexDef
}

TableDef defines a table. Exactly one column must have PK=true. Multi-column PK is v1.1+.

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx is the transaction handle passed to the db.Transaction closure.

func (*Tx) Exec

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

Exec stages one INSERT/UPDATE/DELETE. It does not touch the live store. The returned count is the optimistic affected-row count (1); the real effect is decided at commit. A poisoned transaction no-ops and returns the sticky error.

type UUID

type UUID [16]byte

UUID is a 128-bit identifier (RFC 9562). It is a fixed array so it is comparable and usable directly as a map key with no allocation — the primary key index is map[UUID]uint64.

func NewUUIDv7

func NewUUIDv7() UUID

NewUUIDv7 returns a fresh UUIDv7 with within-millisecond monotonicity: IDs from this process sort by creation time, including inside one millisecond, via a 12-bit counter in rand_a (RFC 9562 "fixed-length dedicated counter"). Beyond 4096 IDs in a single ms the timestamp is nudged forward to keep the ordering total. Client-supplied UUIDs get no such guarantee.

func ParseUUID

func ParseUUID(s string) (UUID, error)

ParseUUID parses the canonical 36-character hyphenated hex form. It accepts any well-formed UUID (version is not enforced here — callers that require v7 check IsV7). Used at the API boundary to turn a string PK into the internal [16]byte; storage never sees the string form.

func (UUID) IsV7

func (u UUID) IsV7() bool

IsV7 reports whether u has the v7 version nibble and the RFC 4122/9562 variant bits (10xx). Used to validate client-supplied IDs.

func (UUID) IsZero

func (u UUID) IsZero() bool

IsZero reports whether u is the zero UUID.

func (UUID) String

func (u UUID) String() string

String renders the canonical 8-4-4-4-12 lowercase hex form.

func (UUID) Version

func (u UUID) Version() byte

Version returns the 4-bit version field (7 for a UUIDv7).

type Value

type Value struct {
	Kind ValueKind
	// contains filtered or unexported fields
}

Value is the runtime cell type: a packed 32-byte tagged union (versus the 72 bytes a struct with separate int/string/bytes/uuid fields would take). Kind is the public tag; the payload is overlapped across two words and a pointer, since a cell is only ever one kind at a time:

  • KindInt / KindBool : the value lives in w0
  • KindUUID : the 16 bytes live inline in (w0, w1), big-endian so comparing the words equals byte-lexicographic order
  • KindString/KindBytes: ptr is the backing-data pointer, w0 the length
  • KindNull : all zero

ptr is ALWAYS nil or a real Go pointer (a string/[]byte backing) — never a reinterpreted non-pointer — so the garbage collector scans it correctly and keeps the backing alive. Read payloads through the typed accessors below; never touch the private fields.

func Bool

func Bool(v bool) Value

func Bytes

func Bytes(v []byte) Value

func Int

func Int(v int64) Value

func Null

func Null() Value

func Str

func Str(v string) Value

func UUIDVal

func UUIDVal(u UUID) Value

func (Value) AsInt

func (v Value) AsInt() (int64, error)

AsInt returns the value as int64, coercing strings via strconv. Used for ORDER BY / range comparisons.

func (Value) AsString

func (v Value) AsString() string

AsString returns the value formatted as text. Used for PK keys in the map index and for display.

func (Value) Bool added in v0.1.2

func (v Value) Bool() bool

func (Value) Bytes added in v0.1.2

func (v Value) Bytes() []byte

func (Value) Compare

func (v Value) Compare(o Value) (int, bool)

Compare returns -1/0/1 for v < o, v == o, v > o. Coerces along the most-precise route: int-int → numeric, otherwise string-string. Null comparisons return 0 with comparable=false to let callers decide.

func (Value) Equal

func (v Value) Equal(o Value) bool

Equal returns true when v and o represent the same value, including null-equals-null (SQL semantics differ — null != null in WHERE — but the executor checks IsNull explicitly).

func (Value) Int added in v0.1.2

func (v Value) Int() int64

func (Value) IsNull

func (v Value) IsNull() bool

func (Value) Str added in v0.1.2

func (v Value) Str() string

func (Value) UUID added in v0.1.2

func (v Value) UUID() UUID

type ValueKind

type ValueKind uint8

ValueKind discriminates the union below.

const (
	KindNull ValueKind = iota
	KindInt
	KindString
	KindBytes
	KindBool
	KindUUID
)

Directories

Path Synopsis
addons
caddymodule module
Package GO-SQLDB is a deliberately minimal spike to validate the central thesis of the GO-SQLDB design: that an in-memory Go store with a canonical WAL can beat SQLite-in-memory on the hot OLTP path.
Package GO-SQLDB is a deliberately minimal spike to validate the central thesis of the GO-SQLDB design: that an in-memory Go store with a canonical WAL can beat SQLite-in-memory on the hot OLTP path.

Jump to

Keyboard shortcuts

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