store

package
v1.7.3 Latest Latest
Warning

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

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

Documentation

Overview

Package store is the Dockyard persistence seam (RFC §13).

All durable state — the future TaskStore (RFC §8.5), obs/v1 history (RFC §11), and inspector state — flows through the Store interface. The seam follows the interface + factory + driver pattern mandated by AGENTS.md §4.4: a driver registers a factory in its init block, and store.Open constructs a Store by driver name. V1 ships two drivers — the in-memory driver (runtime/store/inmem) for single-user stdio apps and the modernc.org/sqlite driver (runtime/store/sqlitestore), pure-Go and CGo-free.

The seam is deliberately generic. Rather than expose Tasks() / Obs() accessors directly (which would force this package to define out-of-scope sub-store types), Store exposes a namespaced, transactional key-value primitive. Future sub-stores — the TaskStore and ObsStore — are thin typed facades constructed over a Store, each owning its own forward-only migrations, which the application composes into a MigrationSet and passes to Store.Migrate. See decisions D-025 and D-073.

Every driver must pass the shared conformance suite in runtime/store/storetest; a new persistence concern adds a migration and is proven by that suite, never bolted onto one driver (AGENTS.md §9).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound is returned by Tx.Get when the (namespace, key) pair has no
	// value.
	ErrNotFound = errors.New("store: key not found")

	// ErrUnknownDriver is returned by Open when no driver is registered under
	// the requested name.
	ErrUnknownDriver = errors.New("store: unknown driver")

	// ErrClosed is returned by any operation on a Store after Close.
	ErrClosed = errors.New("store: store is closed")

	// ErrMigrationOutOfOrder is returned by Migrate when the registered
	// migration sequence does not extend the already-applied sequence as a
	// prefix — an applied migration was reordered or removed. Migrations are
	// append-only and forward-only.
	//
	// Note: editing a migration's Up body in place (same ID, same ordinal) is
	// also forbidden (CLAUDE.md §9) but is NOT detected here — a Go func cannot
	// be content-hashed, so that rule is review-enforced, not runtime-enforced
	// (D-111).
	ErrMigrationOutOfOrder = errors.New("store: migration registered out of order")

	// ErrReadOnly is returned by Tx.Put and Tx.Delete when they are called on
	// a read-only transaction (one produced by View). A write inside a View
	// callback is a programming error; the transaction is not mutated.
	ErrReadOnly = errors.New("store: write attempted on a read-only transaction")

	// ErrDuplicateDriver is the panic value when Register is called with a
	// driver name that is already registered.
	ErrDuplicateDriver = errors.New("store: driver registered twice")

	// ErrDuplicateMigration is returned by MigrationSet.Add (and MigrationSet.
	// Extend) when a migration ID is already present in the set. MustAdd panics
	// with it.
	ErrDuplicateMigration = errors.New("store: migration registered twice")
)

Sentinel errors returned across the Store seam. Callers match with errors.Is; drivers wrap these with %w to add context.

Functions

func Drivers

func Drivers() []string

Drivers returns the names of all registered drivers, sorted.

func Register

func Register(name string, factory Factory)

Register adds a driver factory under name. It is called from a driver package's init block. Registering the same name twice panics — a duplicate registration is a programming error, caught at process start.

func RunMigrations

func RunMigrations(ctx context.Context, s Store, set *MigrationSet) error

RunMigrations applies every migration of set not yet recorded in s, inside s's own transactions. It is the shared implementation every driver's Migrate method delegates to, so migration semantics are identical across drivers (CLAUDE.md §9). A nil set is a valid no-op. It is forward-only and idempotent:

  • A migration already recorded at its current ordinal is skipped.
  • A recorded migration now registered at a different ordinal, or an applied migration no longer registered at all, yields ErrMigrationOutOfOrder — a migration was reordered or removed after merge.

It does NOT detect an edit to a migration's Up body that keeps the same ID and ordinal: a Go func cannot be content-hashed (see Migration), so that rule is review-enforced, not runtime-enforced (D-111).

Types

type Factory

type Factory func(ctx context.Context, dsn string) (Store, error)

Factory constructs a Store for a given data-source name. The dsn is driver-specific: a filesystem path for the sqlite driver, ignored by the in-memory driver.

type KeyValue

type KeyValue struct {
	Key   string
	Value []byte
}

KeyValue is one entry returned by Tx.Scan.

type Migration

type Migration struct {
	// ID uniquely identifies the migration and fixes its order. Use a
	// zero-padded numeric prefix, e.g. "0001_init", "0002_add_obs".
	ID string

	// Up performs the migration inside a read-write transaction. It must be
	// idempotent-safe in the sense that it only runs once per store, but it
	// need not itself guard against re-runs — the runner records completion.
	Up func(ctx context.Context, tx Tx) error
}

Migration is one forward-only schema or data step. Migrations are append-only and forward-only: the runner detects a reorder or removal of an already-applied migration and refuses to proceed (ErrMigrationOutOfOrder).

"Never edit a migration's Up body after it merges" (CLAUDE.md §9) is a REVIEW-ENFORCED rule, NOT a runtime-enforced one — and deliberately so. A Migration's effect is a Go func (Up), and a func value cannot be reliably content-hashed: Go offers no stable hash of a closure's body, and two closures over different source share an entry point. The runner therefore cannot detect an edit to an Up body that keeps the same ID and ordinal. It does not pretend to: it enforces ordering and non-removal at runtime, and the no-post-merge-edit rule is enforced in code review and CI diff (D-111, which supersedes D-027's runtime-enforcement claim).

type MigrationSet

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

MigrationSet is an explicit, caller-owned, ordered collection of migrations (D-073). It REPLACES the former process-global migration registry: a MigrationSet is a plain value a caller constructs, populates, and passes to Store.Migrate, so there is no mutable shared state and no panic-on-duplicate global. Two goroutines — or two t.Parallel() test fixtures — each build their own MigrationSet and migrate independent stores with no cross-talk and no external locking.

Registration order is application order. A MigrationSet is NOT safe for concurrent mutation (it is built once, by its owner, before use); it is safe to pass a fully-built set to many concurrent Store.Migrate calls because Migrate only reads it.

func NewMigrationSet

func NewMigrationSet() *MigrationSet

NewMigrationSet returns an empty MigrationSet ready for MigrationSet.Add.

func (*MigrationSet) Add

func (s *MigrationSet) Add(m Migration) (*MigrationSet, error)

Add appends a migration. It returns ErrDuplicateMigration for a repeated ID, and a descriptive error for an empty ID or a nil Up — a duplicate or malformed migration is a programming error, but Add returns it rather than panicking so a caller (a sub-store assembling its set, a test) handles it cleanly (CLAUDE.md §5: never panic for control flow). Add returns the receiver so calls chain.

func (*MigrationSet) Extend

func (s *MigrationSet) Extend(other *MigrationSet) (*MigrationSet, error)

Extend appends every migration of other into s, preserving order. It is how a caller composes the migration sets of several sub-stores (e.g. the TaskStore set plus a future ObsStore set) into the one set it hands Store.Migrate. A duplicate ID across the sets yields ErrDuplicateMigration.

func (*MigrationSet) Len

func (s *MigrationSet) Len() int

Len reports the number of migrations in the set.

func (*MigrationSet) MustAdd

func (s *MigrationSet) MustAdd(m Migration) *MigrationSet

MustAdd is MigrationSet.Add for a caller that treats a duplicate/malformed migration as a build-time invariant — typically a sub-store assembling its own fixed set in a constructor. It panics on error. It is not used on any request path, so a panic here never crosses the MCP boundary (CLAUDE.md §13).

type Store

type Store interface {
	// Migrate applies every migration of set that has not yet been applied, in
	// the set's order. Migrations are forward-only and idempotent: a clean run
	// and any later re-run are both safe and leave identical schema state. A
	// nil set is a valid no-op. Migrate returns ErrMigrationOutOfOrder if the
	// set diverges from what was previously applied — an applied migration was
	// reordered or removed. (An in-place edit to a migration's Up body is also
	// forbidden but cannot be detected at runtime — D-111.)
	//
	// set is supplied explicitly — not read from a process global — so two
	// stores can migrate concurrently from independent sets with no shared
	// state and no external locking (D-073).
	Migrate(ctx context.Context, set *MigrationSet) error

	// View runs fn inside a read-only transaction. A non-nil error from fn is
	// returned to the caller; no writes are possible.
	View(ctx context.Context, fn func(Tx) error) error

	// Update runs fn inside a read-write transaction. If fn returns a non-nil
	// error the transaction is rolled back and the error is returned;
	// otherwise it is committed.
	Update(ctx context.Context, fn func(Tx) error) error

	// Ping verifies the store is reachable and usable.
	Ping(ctx context.Context) error

	// Close releases all resources held by the Store. Operations after Close
	// return ErrClosed. Close is idempotent.
	Close() error
}

Store is the Dockyard persistence seam. A Store is a reusable artifact: a single value must be safe for concurrent use by multiple goroutines (AGENTS.md §5).

func Open

func Open(ctx context.Context, driver, dsn string) (Store, error)

Open constructs a Store using the named driver. The driver package must be imported (typically a blank import) so its init block has registered the factory. Open returns ErrUnknownDriver if no such driver is registered.

Open does not run migrations; the caller invokes Store.Migrate explicitly so migration timing is under application control.

type Tx

type Tx interface {
	// Get returns the value stored under (ns, key), or ErrNotFound if absent.
	// The returned slice is owned by the caller.
	Get(ns, key string) ([]byte, error)

	// Put stores value under (ns, key), overwriting any existing value. The
	// value slice is copied; the caller may reuse it after Put returns.
	Put(ns, key string, value []byte) error

	// Delete removes (ns, key). It is a no-op if the key is absent.
	Delete(ns, key string) error

	// Scan returns every key/value in ns whose key has the given prefix,
	// ordered lexicographically by key. An empty prefix scans the whole
	// namespace.
	Scan(ns, prefix string) ([]KeyValue, error)
}

Tx is a namespaced key-value transaction handle. It is the primitive every future sub-store (TaskStore §8.5, ObsStore §11) builds on. A Tx is not safe for concurrent use and must not be retained beyond the View/Update callback that produced it.

Directories

Path Synopsis
Package inmem is the in-memory Store driver (RFC §13).
Package inmem is the in-memory Store driver (RFC §13).
Package sqlitestore is the modernc.org/sqlite Store driver (RFC §13).
Package sqlitestore is the modernc.org/sqlite Store driver (RFC §13).
Package storetest holds the shared Store-driver conformance suite (RFC §13, AGENTS.md §9).
Package storetest holds the shared Store-driver conformance suite (RFC §13, AGENTS.md §9).

Jump to

Keyboard shortcuts

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