Files
zeekayandHanzo Dev 2060416aba After is its own interface (Walker) and orm.After, so no Query implementation outside orm has to grow it
v0.6.43 added After to the Query interfaces, which any Query implemented elsewhere (commerce's mixin adapter) then failed to satisfy. ormdb.Walker carries it on the SQLite and ZapSQL queries; orm.After(q, id) walks an adapted query and refuses one it did not adapt.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-02 23:45:57 -07:00

152 lines
5.7 KiB
Go

// Package orm provides a generics-based ORM with auto-registration,
// auto-serialization of underscore fields, lifecycle hooks, and optional
// KV cache. It is designed to be extracted from and remain backward-compatible
// with github.com/hanzoai/commerce.
package orm
import (
"context"
"errors"
)
// DB is the minimal database interface the ORM requires.
// Implementations exist for SQLite, PostgreSQL, MongoDB, and the legacy
// Hanzo Datastore. Commerce's datastore.Datastore satisfies this via a
// thin adapter.
type DB interface {
// Get loads the entity identified by key into dst.
Get(ctx context.Context, key Key, dst any) error
// Put persists src under key, returning the (possibly updated) key.
Put(ctx context.Context, key Key, src any) (Key, error)
// CreateIfAbsent conditionally inserts src under key with first-writer-wins
// semantics. It returns created=true iff this call inserted the row (key was
// absent); created=false iff a live row of the same kind already existed under
// key, which is left untouched. Unlike Put — an unconditional upsert —
// CreateIfAbsent never overwrites a live row, so the winner's content is
// immutable: a caller that sees created=false can Get the existing row with no
// lost update and no TOCTOU window. Use it for constraint-based CAS such as
// create-org-if-absent or a claim-once row.
//
// Existence is scoped to (kind, id) and the match is exact. An id already held
// by a DIFFERENT kind returns ErrKindMismatch (never a silent created=false
// that Get cannot see), so callers must keep each kind in its own stringID
// keyspace and normalize the id (case, trim, Unicode) before building the key.
// key must be complete with a non-empty id; otherwise the call errors.
//
// It is atomic on its own — a single conditional insert, no enclosing
// transaction required — so it is race-safe under both the serialized-writer
// (SQLite) and autocommit (ZAP) storage contracts.
CreateIfAbsent(ctx context.Context, key Key, src any) (created bool, err error)
// Delete removes the entity identified by key.
Delete(ctx context.Context, key Key) error
// Query returns a new query builder for the given kind.
Query(kind string) Query
// NewKey creates a key with the given kind, string ID, int ID, and parent.
NewKey(kind string, stringID string, intID int64, parent Key) Key
// NewIncompleteKey creates an incomplete key (ID allocated on put).
NewIncompleteKey(kind string, parent Key) Key
// AllocateIDs pre-allocates n IDs for the given kind.
AllocateIDs(kind string, parent Key, n int) ([]Key, error)
// RunInTransaction executes fn inside a transaction.
RunInTransaction(ctx context.Context, fn func(tx DB) error) error
// RunInTransactionWith executes fn inside a transaction with explicit
// retry semantics. SQLite serializes every write through its reserved
// lock; on SQLITE_BUSY / SQLITE_LOCKED the callback is retried up to
// opts.MaxAttempts times. IsolationLevel is kept in the signature for
// API stability but is a compile-time hint only — SQLite has a single
// isolation model.
RunInTransactionWith(ctx context.Context, opts *TxOptions, fn func(tx DB) error) error
// GetForUpdate is Get + row-level exclusive lock for the duration of the
// enclosing transaction. Under SQLite the write mutex + BEGIN IMMEDIATE
// path already holds the reserved lock across the entire tx, so
// GetForUpdate becomes a regular Get that is strictly serialized against
// other writers. Calling GetForUpdate OUTSIDE a transaction is an error.
GetForUpdate(ctx context.Context, key Key, dst any) error
// Close releases resources.
Close() error
}
// IsolationLevel is kept for API stability. SQLite has a single isolation
// model — every writer holds the reserved lock for the full transaction —
// so the enum is a compile-time hint, not a runtime switch. Zero value
// (IsolationDefault) is the only meaningful value.
type IsolationLevel int
const (
IsolationDefault IsolationLevel = iota
// Retained for API stability. All values collapse to the SQLite default
// isolation at runtime.
IsolationReadCommitted
IsolationRepeatableRead
IsolationSerializable
)
// TxOptions configures a transaction. MaxAttempts includes the first attempt;
// set to 1 to disable retry. MaxAttempts == 0 defaults to 5.
type TxOptions struct {
Isolation IsolationLevel
ReadOnly bool
MaxAttempts int
}
// ErrSerializationFailure wraps a driver error the ORM recognized as a
// transient SQLITE_BUSY / SQLITE_LOCKED. Callers may wrap their CAS-conflict
// sentinels with this to piggyback retry, but typically they should let
// RunInTransactionWith do the retrying itself.
var ErrSerializationFailure = errors.New("orm: serialization failure")
// Key identifies an entity in the datastore.
type Key interface {
Kind() string
StringID() string
IntID() int64
Parent() Key
Namespace() string
Encode() string
}
// Query is a fluent query builder.
type Query interface {
Filter(filterStr string, value any) Query
Order(fieldPath string) Query
Limit(limit int) Query
Offset(offset int) Query
Ancestor(ancestor Key) Query
KeysOnly() Query
// GetAll executes the query, populating dst (pointer to slice).
// Returns the matching keys.
GetAll(ctx context.Context, dst any) ([]Key, error)
// First returns the first matching entity.
First(dst any) (Key, bool, error)
// Count returns the number of matching entities.
Count(ctx context.Context) (int, error)
// ById finds an entity by its encoded ID.
ById(id string, dst any) (Key, bool, error)
// IdExists checks whether the given ID exists.
IdExists(id string) (Key, bool, error)
// KeyExists checks whether the given key exists.
KeyExists(key Key) (bool, error)
}
// Iterator iterates over query results.
type Iterator interface {
Next(dst any) (Key, error)
}