Documentation
¶
Overview ¶
Package db is the relational plane: entities in a database, one database per namespace.
DB is the entity contract, implemented here by SQLite (with sqlite-vec for vector search) and by ZAP for the backends that speak it — hanzo/sql over PostgreSQL, document storage, KV. Namespaces owns the other half: which database a namespace resolves to, how many stay open, and when a cold one is closed.
Datastore is declared here and implemented in orm/datastore. It is the analytics plane — measurements in one shared columnar store, where a tenant is a column rather than a database — and it deliberately shares no connection with this one.
ZAP protocol driver for the ORM.
ZAP (Zero-Copy App Proto) uses binary encoding over RPC, communicating directly with ZAP-native backends (hanzo/sql, hanzo/kv, hanzo/datastore, hanzo/documentdb). Each backend speaks ZAP natively — no sidecar needed.
Transport is github.com/zap-proto/http: a fasthttp-style request/response exchange carried over ZAP length-prefixed frames (encoded by the pure-Go zap-proto/go runtime). This is the same ZAP-HTTP transport the gateway, ingress, and luxd use — one and only one internal transport. The driver speaks it as a client: each backend op is a POST to a path (/query, /get, /set, /find, …) with a JSON body; the response carries a status and a JSON body. Routing is by address (each backend on its own port; see DefaultPorts) and by path — there is no peer-discovery layer, so the ORM takes no mDNS dependency.
Index ¶
- Variables
- func LowercaseFirst(s string) string
- func NormalizeOp(op string) string
- func ParseFilterString(s string) (field, op string)
- func ToJSONFieldName(field string) string
- type AfterCreateHook
- type AfterDeleteHook
- type AfterUpdateHook
- type BeforeCreateHook
- type BeforeDeleteHook
- type BeforeUpdateHook
- type Cursor
- type DB
- type Datastore
- type Entity
- type IsolationLevel
- type Iterator
- type Key
- type Kind
- type Model
- func (m *Model) Create(ctx context.Context) error
- func (m *Model) DB() DB
- func (m *Model) Delete(ctx context.Context) error
- func (m *Model) Entity() Kind
- func (m *Model) Exists(ctx context.Context) (bool, error)
- func (m *Model) Get(ctx context.Context) error
- func (m *Model) GetByID(ctx context.Context, id string) error
- func (m *Model) GetID() string
- func (m *Model) GetKind() string
- func (m *Model) GetNamespace() string
- func (m *Model) Init(database DB, entity Kind)
- func (m *Model) IsCreated() bool
- func (m *Model) IsLoaded() bool
- func (m *Model) JSON() ([]byte, error)
- func (m *Model) JSONString() string
- func (m *Model) Key() Key
- func (m *Model) MarkLoaded()
- func (m *Model) ModelQuery() Query
- func (m *Model) MustGet(ctx context.Context)
- func (m *Model) MustGetByID(ctx context.Context, id string)
- func (m *Model) MustPut(ctx context.Context)
- func (m *Model) Put(ctx context.Context) error
- func (m *Model) RunInTransaction(ctx context.Context, fn func(tx Transaction) error) error
- func (m *Model) SetDB(database DB)
- func (m *Model) SetEntity(entity Kind)
- func (m *Model) SetID(id string)
- func (m *Model) SetKey(key Key) error
- func (m *Model) SetKeyFromString(id string) error
- func (m *Model) SetNamespace(ns string)
- func (m *Model) SoftDelete(ctx context.Context) error
- func (m *Model) Update(ctx context.Context) error
- type Namespace
- type Namespaces
- type NamespacesConfig
- type Query
- type QueryFilter
- type QueryOrder
- type SQLiteConfig
- type SQLiteDB
- func (db *SQLiteDB) AllocateIDs(kind string, parent Key, n int) ([]Key, error)
- func (db *SQLiteDB) Close() error
- func (db *SQLiteDB) CreateIfAbsent(ctx context.Context, key Key, src interface{}) (bool, error)
- func (db *SQLiteDB) Delete(ctx context.Context, key Key) error
- func (db *SQLiteDB) DeleteMulti(ctx context.Context, keys []Key) error
- func (db *SQLiteDB) Get(ctx context.Context, key Key, dst interface{}) error
- func (db *SQLiteDB) GetMulti(ctx context.Context, keys []Key, dst interface{}) error
- func (db *SQLiteDB) NewIncompleteKey(kind string, parent Key) Key
- func (db *SQLiteDB) NewKey(kind string, stringID string, intID int64, parent Key) Key
- func (db *SQLiteDB) Put(ctx context.Context, key Key, src interface{}) (Key, error)
- func (db *SQLiteDB) PutMulti(ctx context.Context, keys []Key, src interface{}) ([]Key, error)
- func (db *SQLiteDB) PutVector(ctx context.Context, kind string, id string, vector []float32, ...) error
- func (db *SQLiteDB) Query(kind string) Query
- func (db *SQLiteDB) RunInTransaction(ctx context.Context, fn func(tx Transaction) error, opts *TransactionOptions) error
- func (db *SQLiteDB) VectorSearch(ctx context.Context, opts *VectorSearchOptions) ([]VectorResult, error)
- type SQLiteDBConfig
- type SimpleCursor
- type Syncable
- type Transaction
- type TransactionOptions
- type Validator
- type VectorResult
- type VectorSearchOptions
- type ZapBackend
- type ZapConfig
- type ZapDB
- func (z *ZapDB) AllocateIDs(kind string, parent Key, n int) ([]Key, error)
- func (z *ZapDB) Close() error
- func (z *ZapDB) CreateIfAbsent(ctx context.Context, key Key, src interface{}) (bool, error)
- func (z *ZapDB) Delete(ctx context.Context, key Key) error
- func (z *ZapDB) DeleteMulti(ctx context.Context, keys []Key) error
- func (z *ZapDB) Get(ctx context.Context, key Key, dst interface{}) error
- func (z *ZapDB) GetMulti(ctx context.Context, keys []Key, dst interface{}) error
- func (z *ZapDB) NewIncompleteKey(kind string, parent Key) Key
- func (z *ZapDB) NewKey(kind string, stringID string, intID int64, parent Key) Key
- func (z *ZapDB) Put(ctx context.Context, key Key, src interface{}) (Key, error)
- func (z *ZapDB) PutMulti(ctx context.Context, keys []Key, src interface{}) ([]Key, error)
- func (z *ZapDB) PutVector(ctx context.Context, kind string, id string, vector []float32, ...) error
- func (z *ZapDB) Query(kind string) Query
- func (z *ZapDB) RunInTransaction(ctx context.Context, fn func(tx Transaction) error, opts *TransactionOptions) error
- func (z *ZapDB) VectorSearch(ctx context.Context, opts *VectorSearchOptions) ([]VectorResult, error)
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoSuchEntity is returned when an entity is not found. ErrNoSuchEntity = errors.New("db: no such entity") // ErrInvalidKey is returned when a key is invalid. ErrInvalidKey = errors.New("db: invalid key") // ErrInvalidEntityType is returned when an entity type is invalid. ErrInvalidEntityType = errors.New("db: invalid entity type") // ErrConcurrentModification is returned when optimistic locking fails. ErrConcurrentModification = errors.New("db: concurrent modification") // ErrDatabaseClosed is returned when operating on a closed database. ErrDatabaseClosed = errors.New("db: database closed") // ErrValidationFailed is returned when entity validation fails. ErrValidationFailed = errors.New("db: validation failed") // ErrEntityNotFound aliases ErrNoSuchEntity. ErrEntityNotFound = ErrNoSuchEntity // ErrKindMismatch is returned by CreateIfAbsent when the id is already held // by a row of a DIFFERENT kind. Entity identity is (kind, id); on the SQL // backends the id column is a bare primary key, so two kinds cannot share an // id. That squatting row is invisible to Get (which filters by kind), so // reporting created=false would strand the caller — CreateIfAbsent surfaces // the collision loudly instead. Keep each kind in its own stringID keyspace. ErrKindMismatch = errors.New("db: id held by a different kind") )
var DefaultPorts = map[ZapBackend]int{ ZapSQL: 9651, ZapKV: 9653, ZapDocumentDB: 9654, ZapDatastore: 9655, }
DefaultPorts for each ZAP-native backend.
var ErrClosed = errors.New("db: namespaces closed")
ErrClosed is returned once the namespace set is closed.
Functions ¶
func LowercaseFirst ¶
LowercaseFirst lowercases the first character of a string.
func ParseFilterString ¶
ParseFilterString parses "Field=" into field and operator.
func ToJSONFieldName ¶
ToJSONFieldName converts a Go struct field name (PascalCase) to its JSON equivalent (camelCase) by lowercasing the first letter of each path segment. Handles nested paths like "Account.TransactionHash" → "account.transactionHash".
It also drops every character a JSON field path cannot contain, because the result is interpolated into a SQL string literal — json_extract(data, '$.X') — where a quote ENDS the literal and the rest is executed. Callers pass this straight from a query string: commerce's generic REST list helper does .Order(c.Query("sort")) for every entity it serves. Without the filter, ?sort=x') UNION SELECT name FROM sqlite_master -- closed the literal and appended a working UNION.
Filtering rather than rejecting keeps the signature and fails CLOSED: a name that had illegal characters becomes a field that does not exist, so a filter on it matches no rows and an ORDER BY on it sorts every row equally. A field path is identifier segments joined by dots and never needed anything else.
Types ¶
type AfterCreateHook ¶
type AfterCreateHook interface {
AfterCreate() error
}
AfterCreateHook is called after entity creation.
type AfterDeleteHook ¶
type AfterDeleteHook interface {
AfterDelete() error
}
AfterDeleteHook is called after entity deletion.
type AfterUpdateHook ¶
type AfterUpdateHook interface {
AfterUpdate(prev interface{}) error
}
AfterUpdateHook is called after entity update.
type BeforeCreateHook ¶
type BeforeCreateHook interface {
BeforeCreate() error
}
BeforeCreateHook is called before entity creation.
type BeforeDeleteHook ¶
type BeforeDeleteHook interface {
BeforeDelete() error
}
BeforeDeleteHook is called before entity deletion.
type BeforeUpdateHook ¶
type BeforeUpdateHook interface {
BeforeUpdate(prev interface{}) error
}
BeforeUpdateHook is called before entity update.
type Cursor ¶
type Cursor interface {
String() string
}
Cursor represents a position in a result set.
func DecodeCursor ¶
DecodeCursor parses a cursor string.
type DB ¶
type DB interface {
// Core operations
Get(ctx context.Context, key Key, dst interface{}) error
Put(ctx context.Context, key Key, src interface{}) (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 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.
//
// "Absent" means no live row of the SAME kind. A soft-deleted row (see
// Delete) of the same kind is resurrected as the new content and reported
// created=true, so CreateIfAbsent and Get share one definition of existence.
// Resurrection never changes an existing row's kind.
//
// Existence is scoped to (kind, id). Because the id column is a bare primary
// key on the SQL backends, an id already held by a DIFFERENT kind is a
// keyspace collision: CreateIfAbsent returns ErrKindMismatch rather than a
// silent created=false that Get could not see. Callers must therefore keep
// each kind in its own stringID keyspace. CreateIfAbsent is also exact-match
// on the stringID: "Acme", "acme" and "acme " are distinct ids, so callers
// must normalize (case, trim, Unicode) BEFORE constructing the key.
//
// The write is atomic at the storage layer — SQLite serializes writers and
// the SQL backend applies INSERT ... ON CONFLICT at the row — so for N
// concurrent callers on the same absent key exactly one observes created=true.
// key must be complete with a non-empty id; otherwise ErrInvalidKey.
CreateIfAbsent(ctx context.Context, key Key, src interface{}) (created bool, err error)
Delete(ctx context.Context, key Key) error
// Batch operations
GetMulti(ctx context.Context, keys []Key, dst interface{}) error
PutMulti(ctx context.Context, keys []Key, src interface{}) ([]Key, error)
DeleteMulti(ctx context.Context, keys []Key) error
// Query
Query(kind string) Query
// Vector search
VectorSearch(ctx context.Context, opts *VectorSearchOptions) ([]VectorResult, error)
PutVector(ctx context.Context, kind string, id string, vector []float32, metadata map[string]interface{}) error
// Key management
NewKey(kind string, stringID string, intID int64, parent Key) Key
NewIncompleteKey(kind string, parent Key) Key
AllocateIDs(kind string, parent Key, n int) ([]Key, error)
// Transactions
RunInTransaction(ctx context.Context, fn func(tx Transaction) error, opts *TransactionOptions) error
// Lifecycle
Close() error
}
DB is the main database interface for entity storage.
type Datastore ¶ added in v0.6.12
type Datastore interface {
Ready() bool
Exec(ctx context.Context, stmt string, args ...any) error
Query(ctx context.Context, query string, args ...any) ([]map[string]any, error)
Close() error
}
Datastore is the analytics plane: one shared columnar warehouse holding measurements, where a tenant is a column rather than a database. It is the counterpart of DB, which holds entities and where a tenant is the whole database. Analytics SQL is written by hand, so this carries statements and rows and maps no records.
Ready reports whether the warehouse is reachable; Exec and Query return an error rather than fabricating a result when it is not. Implemented by orm/datastore.Conn — declared here so the relational plane can name the analytics plane without importing its driver.
type Entity ¶
type Entity interface {
Kind() string
}
Entity is the interface that all model entities should implement.
type IsolationLevel ¶
type IsolationLevel int
IsolationLevel represents transaction isolation levels.
const ( IsolationDefault IsolationLevel = iota IsolationReadUncommitted IsolationReadCommitted IsolationRepeatableRead IsolationSerializable )
type Key ¶
type Key interface {
Kind() string
StringID() string
IntID() int64
Parent() Key
Namespace() string
Incomplete() bool
Encode() string
Equal(other Key) bool
}
Key represents a unique identifier for an entity.
type Kind ¶
type Kind interface {
Kind() string
}
Kind interface for entities with a kind/table name.
type Model ¶
type Model struct {
Parent Key `json:"-"`
ID string `json:"id,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Version int64 `json:"version,omitempty"`
Namespace_ string `json:"-"`
Mock bool `json:"-"`
UseStringKey bool `json:"-"`
// contains filtered or unexported fields
}
Model is a base type that provides common functionality for entities. Embed this in your entity structs for non-generic model usage.
func (*Model) GetNamespace ¶
GetNamespace returns the namespace for this entity.
func (*Model) JSONString ¶
JSONString returns the JSON string representation.
func (*Model) ModelQuery ¶
ModelQuery returns a new query for this entity's kind.
func (*Model) MustGetByID ¶
MustGetByID retrieves by ID or panics.
func (*Model) RunInTransaction ¶
RunInTransaction executes a function within a transaction.
func (*Model) SetKeyFromString ¶
SetKeyFromString sets the key from a string ID.
func (*Model) SetNamespace ¶
SetNamespace sets the namespace.
func (*Model) SoftDelete ¶
SoftDelete marks the entity as deleted without removing it.
type Namespace ¶ added in v0.6.14
type Namespace string
Namespace names one database, e.g. "org/acme" or "user/123/notes". It is opaque here: a path component and an eviction key, nothing more. What qualifies a namespace is hanzoai/iam's business, and this package never branches on what one means. One tenant owns many namespaces; a namespace is what maps to a single file.
type Namespaces ¶ added in v0.6.14
Namespaces resolves a namespace to its database.
The model is one SQLite file per namespace, remote storage as the source of truth, and local disk as a cache. A node holds a bounded number of databases open, materialising a file from remote storage when it is not on disk and closing the coldest handles when the bound is reached. That is what lets any node serve any namespace, and lets a node stay small while their number grows.
Before this existed the capability was split and neither half was complete: this package declared a per-tenant database contract with no lifecycle, while hanzoai/commerce carried the lifecycle in unbounded userDBs/orgDBs maps whose handles were only closed at shutdown — so file descriptors and memory grew with the number of tenants ever touched, and nothing replicated. Open-per-name without a bound leaks by construction, which is why MaxOpen has no default.
T is the handle type. This type calls exactly one method on it — Close — because releasing a handle is the whole of its job; what a handle *is* stays the caller's business. That separation is load-bearing: pinning T to this package's DB would force every owner of per-namespace files to adopt this package's entity API as well, which is exactly the toll that made commerce write its own lifecycle instead of reusing this one. Use Namespaces[DB] here, Namespaces[yourDB] there, one implementation either way.
Layering, one job each:
transport carries WHICH namespace (request context) Namespaces resolves namespace -> handle: open, cache, evict <- here replicate makes each file durable (WAL -> object storage) caller asks for a namespace's database and thinks about none of it
func NewNamespaces ¶ added in v0.6.14
func NewNamespaces[T io.Closer](cfg NamespacesConfig[T]) (*Namespaces[T], error)
NewNamespaces builds a Namespaces. Dir, a positive MaxOpen and Open are required — an unbounded registry is the leak this type exists to prevent.
func (*Namespaces[T]) Close ¶ added in v0.6.14
func (n *Namespaces[T]) Close() error
Close shuts every open database. Further calls to With fail with ErrClosed.
It DRAINS: a database still inside a With is not closed until that With returns. evict has always refused to touch an entry with in-flight users — "a slow request is never yanked out from under its caller" — and shutdown is the one moment every in-flight request meets that promise at once, so it is the last place to break it. Closing under a live query does not merely fail that query; the handle it is reading through goes away beneath it.
The wait is unbounded on purpose. With releases its reference with defer, so this drains unless a caller's fn never returns — and a deadline here would be this type inventing a shutdown policy it cannot know. A caller that wants a bounded drain owns that decision and can impose it from outside.
func (*Namespaces[T]) Held ¶ added in v0.6.17
func (n *Namespaces[T]) Held() int
Held reports how many databases are currently held open. Named for the state it reports, not the verb that produced it: Open on this type is the opener in NamespacesConfig, and one word cannot be both a count and an action.
func (*Namespaces[T]) With ¶ added in v0.6.14
With runs fn while the namespace's database is held open.
This is the whole API on purpose. Handing callers a raw handle means handing them the job of returning it, and a forgotten return pins a database open forever — reintroducing exactly the unbounded growth this type prevents. Within fn the handle cannot be evicted; after fn returns it becomes evictable. Do not retain the handle beyond fn.
func (*Namespaces[T]) WithAll ¶ added in v0.6.19
func (n *Namespaces[T]) WithAll(ctx context.Context, nss []Namespace, fn func(map[Namespace]T) error) error
WithAll runs fn with several namespaces held open at once, acquired in parallel.
It exists because entities relate across namespaces even though each one lives in exactly one, and because the files are meant to be small: a request that renders an issue list touches the repository's issues, its labels, and a handful of users, and every one of those is a different database. Serving that by nesting With per namespace builds a pyramid as deep as the number of namespaces involved, and makes the wall clock their SUM -- which is the wrong cost model entirely when each open may be a restore from object storage.
So they are acquired concurrently and the wall clock is the slowest one, not the total. There is no lock-ordering hazard to design around: acquire holds no lock while it waits, it takes a reference and waits on that entry alone, so two callers asking for the same namespaces in opposite orders cannot hold each other's next handle. Sorting would buy nothing here and cost the parallelism.
Namespaces are canonicalised and de-duplicated first, so asking for the same one twice is one handle rather than a second reference that must also be returned.
Held handles cannot be evicted, so fn holds len(ns) of them at once. That may exceed MaxOpen -- the bound governs how many stay open while IDLE, not how many one caller may use -- but a request naming more namespaces than the entire bound describes a query that does not fit on this node, and is refused here rather than left to evict every other tenant to make room for itself.
The handles follow the same rule as With: valid for the life of fn, and not beyond it.
type NamespacesConfig ¶ added in v0.6.14
type NamespacesConfig[T io.Closer] struct { // Dir is the local cache directory holding the files. It is a CACHE: // anything here must be reconstructible from remote storage, because // eviction deletes handles and a node may be replaced at any time. Dir string // MaxOpen bounds how many databases stay open at once. Reaching it evicts // the least recently used handle that nobody is currently using. // // Zero means unbounded, which is the shape that leaks; NewNamespaces rejects // it rather than letting it be the accidental default. MaxOpen int // IdleTTL closes handles unused for this long, even when below MaxOpen, so // a node that goes quiet gives its file descriptors back. Zero disables it. // // Finding idle handles means looking at every open one, so it runs as a // sweep paced at IdleTTL/2 on registry activity, not on every request: a // handle closes between IdleTTL and 1.5*IdleTTL after its last use. Per // request the sweep made serving a hit O(open), which is backwards for the // type whose job is holding many handles. IdleTTL time.Duration // Open opens the database for a namespace at path. Required — there is no // default, because a default could only ever be right for one T, and a // silently-wrong handle type is worse than a missing one. OpenNamespace // is the ready-made opener for Namespaces[DB]. Open func(ns Namespace, path string) (T, error) // Materialize is called when path does not exist locally, to restore it // from remote storage before Open. Returning nil without creating the file // is valid and means "new namespace, start empty". // // Nil skips the step entirely — local-only, which is correct for tests and // single-node development but is NOT the production shape. Materialize func(ctx context.Context, ns Namespace, path string) error // OnOpen runs after a database is opened, for setup that must // track the handle's lifetime — starting WAL replication for this file is // the reason it exists. Its error fails the open. OnOpen func(ns Namespace, path string, db T) error // OnClose runs before a database is closed, to undo OnOpen. Its error is // returned by Close but does not prevent the handle being released. OnClose func(ns Namespace, path string, db T) error // OnEvictError reports a handle that failed to shut down during eviction. // // Close returns its shutdown error to the caller; eviction has nobody to // return to, so without this the error is discarded. That matters more here // than anywhere else in the type: eviction IS the durability checkpoint. When // OnClose is a final WAL flush to object storage, a failure means that // namespace's writes are gone — and until now it happened with no error, no log // and no signal of any kind, on the one path the whole "disk is a cache, S3 // is the truth" model depends on. // // Nil means those failures stay silent. That is a deliberate choice a caller // has to make, not a default they back into: set it to log, alert, or refuse // to evict further. OnEvictError func(ns Namespace, path string, err error) // PathFor maps a namespace to its file path under Dir. It returns an error // when the result would escape Dir. Defaults to // <Dir>/<type>/<id>.db. PathFor func(dir string, ns Namespace) (string, error) }
NamespacesConfig configures how namespace databases are located, opened and bounded.
type Query ¶
type Query interface {
Filter(filterStr string, value interface{}) Query
FilterField(fieldPath string, op string, value interface{}) Query
Order(fieldPath string) Query
OrderDesc(fieldPath string) Query
Limit(limit int) Query
Offset(offset int) Query
Project(fieldNames ...string) Query
Distinct() Query
Ancestor(ancestor Key) Query
GetAll(ctx context.Context, dst interface{}) ([]Key, error)
First(ctx context.Context, dst interface{}) (Key, error)
Count(ctx context.Context) (int, error)
Keys(ctx context.Context) ([]Key, error)
Run(ctx context.Context) Iterator
Start(cursor Cursor) Query
End(cursor Cursor) Query
}
Query provides a fluent interface for querying entities.
type QueryFilter ¶
QueryFilter holds a filter condition.
type QueryOrder ¶
QueryOrder holds an order directive.
type SQLiteConfig ¶
type SQLiteConfig struct {
MaxOpenConns int
MaxIdleConns int
BusyTimeout int
JournalMode string
Synchronous string
CacheSize int
QueryTimeout time.Duration
}
SQLiteConfig holds SQLite-specific configuration.
type SQLiteDB ¶
type SQLiteDB struct {
// contains filtered or unexported fields
}
SQLiteDB implements the DB interface using SQLite.
func AdaptSQLDB ¶ added in v0.6.7
AdaptSQLDB layers the ORM's typed-record model (the `_entities` table) over an already-open *sql.DB the CALLER owns. Use it when a store's file is opened elsewhere — cloud's per-org SQLite, for instance, is opened through one seam (cek-encrypted at rest, single-writer, WAL pragmas) and handed to subsystems as a *sql.DB. AdaptSQLDB lets the ORM manage records IN that file without owning the file: the caller keeps the connection's pragmas, encryption, durability, and Close; the ORM only ensures its schema and marshals records.
The one connection serves both reads and writes (the caller's pool, typically MaxOpenConns(1) for a serialized single writer), serialized by writeMu exactly as NewSQLiteDB serializes against its dedicated write connection. initSchema runs so `_entities` exists; any table the caller created is untouched. Close is a no-op — the connection belongs to the caller, who closes it.
func NewSQLiteDB ¶
func NewSQLiteDB(cfg *SQLiteDBConfig) (*SQLiteDB, error)
NewSQLiteDB creates a new SQLite database connection.
func (*SQLiteDB) AllocateIDs ¶
func (*SQLiteDB) CreateIfAbsent ¶ added in v0.6.6
func (*SQLiteDB) DeleteMulti ¶
func (*SQLiteDB) NewIncompleteKey ¶
func (*SQLiteDB) RunInTransaction ¶
func (db *SQLiteDB) RunInTransaction(ctx context.Context, fn func(tx Transaction) error, opts *TransactionOptions) error
func (*SQLiteDB) VectorSearch ¶
func (db *SQLiteDB) VectorSearch(ctx context.Context, opts *VectorSearchOptions) ([]VectorResult, error)
type SQLiteDBConfig ¶
type SQLiteDBConfig struct {
Path string
Config SQLiteConfig
EnableVectorSearch bool
VectorDimensions int
Namespace string
}
SQLiteDBConfig holds configuration for a SQLite database.
type SimpleCursor ¶
SimpleCursor is a basic cursor implementation.
func (*SimpleCursor) String ¶
func (c *SimpleCursor) String() string
type Transaction ¶
type Transaction interface {
Get(key Key, dst interface{}) error
Put(key Key, src interface{}) (Key, error)
// CreateIfAbsent is the transaction-scoped conditional insert: the same
// first-writer-wins semantics as DB.CreateIfAbsent, participating in the
// enclosing transaction.
CreateIfAbsent(key Key, src interface{}) (created bool, err error)
Delete(key Key) error
Query(kind string) Query
// GetForUpdate reads the row into dst AND acquires a row-level exclusive
// lock for the duration of the transaction. Concurrent txs that also call
// GetForUpdate on the same key block until this tx commits or rolls back.
// Required for compare-and-swap patterns against a shared row, where SSI
// alone is insufficient because ON CONFLICT DO UPDATE can miss the
// rw-dependency cycle. SQLite honors this via the write mutex it already
// holds; drivers without row-locking treat it as a regular Get.
GetForUpdate(key Key, dst interface{}) error
}
Transaction represents a database transaction.
type TransactionOptions ¶
type TransactionOptions struct {
ReadOnly bool
MaxAttempts int
Isolation IsolationLevel
}
TransactionOptions configures transaction behavior.
type Validator ¶
type Validator interface {
Validate() error
}
Validator interface for entities that support validation.
type VectorResult ¶
VectorResult represents a vector search result.
type VectorSearchOptions ¶
type VectorSearchOptions struct {
Kind string
Vector []float32
Limit int
MinScore float32
Filters map[string]interface{}
}
VectorSearchOptions configures vector similarity search.
type ZapBackend ¶ added in v0.2.0
type ZapBackend int
ZapBackend selects which ZAP-native backend to connect to.
const ( // ZapSQL connects to hanzo/sql, the relational transactional backend, on // port 9651. ZapSQL ZapBackend = iota // ZapDocumentDB connects to hanzo/documentdb on port 9654. It serves // document semantics over relational storage, for clients that model data // as documents; the rows live in hanzo/sql. ZapDocumentDB // ZapKV connects to hanzo/kv, the key-value cache and session backend, on // port 9653. ZapKV // ZapDatastore connects to hanzo/datastore, the columnar analytics // backend, on port 9655. ZapDatastore )
type ZapConfig ¶ added in v0.2.0
type ZapConfig struct {
// Addr is the backend address (e.g., "localhost:9651").
// If empty, uses DefaultPorts[Backend] on localhost.
Addr string
// Backend selects which ZAP-native backend to connect to.
Backend ZapBackend
// Database is the target database name (for SQL/DocumentDB backends).
Database string
// Collection is the default collection/table for entity storage.
// Defaults to "_entities" for SQL, "entities" for DocumentDB.
Collection string
// QueryTimeout is the per-query timeout (default 30s).
QueryTimeout time.Duration
}
ZapConfig configures a ZAP database connection.
type ZapDB ¶ added in v0.2.0
type ZapDB struct {
// contains filtered or unexported fields
}
ZapDB implements db.DB over the ZAP-HTTP binary protocol.
func NewZapDB ¶ added in v0.2.0
NewZapDB dials a ZAP-native backend and returns a DB implementation. The transport connects lazily on the first operation, so this does not fail when the backend is momentarily unreachable — the first Get/Put surfaces a clear dial error instead.
func (*ZapDB) AllocateIDs ¶ added in v0.2.0
func (*ZapDB) CreateIfAbsent ¶ added in v0.6.6
CreateIfAbsent conditionally inserts src under key, first-writer-wins. See db.DB.CreateIfAbsent for the contract. Dispatch mirrors Put: each ZAP-native backend uses its own conditional-insert primitive (SQL ON CONFLICT, Valkey SET NX, document unique _id). The reply decides created; an unrecognized or error reply returns an error rather than a guessed created value, so a caller never mistakes an upsert or a transport failure for a first-writer win.
Status: the hanzo ZAP backends do not yet expose a zap-proto/http listener (see LLM.md), so these paths are wire-complete but exercised only by the env-gated live integration test, not unit CI. The SQLite backend is the fully-tested reference implementation of the identical contract.
func (*ZapDB) DeleteMulti ¶ added in v0.2.0
func (*ZapDB) NewIncompleteKey ¶ added in v0.2.0
func (*ZapDB) RunInTransaction ¶ added in v0.2.0
func (z *ZapDB) RunInTransaction(ctx context.Context, fn func(tx Transaction) error, opts *TransactionOptions) error
func (*ZapDB) VectorSearch ¶ added in v0.2.0
func (z *ZapDB) VectorSearch(ctx context.Context, opts *VectorSearchOptions) ([]VectorResult, error)