API Reference
Complete documentation for grub's public API.
Package grub
import "github.com/zoobz-io/grub"
Errors
Semantic errors returned by all providers.
| Error | Description |
|---|---|
ErrNotFound | Record does not exist |
ErrDuplicate | Record with same key already exists |
ErrConflict | Concurrent modification conflict |
ErrConstraint | Constraint violation (FK, check, etc.) |
ErrInvalidKey | Key is malformed or empty |
ErrReadOnly | Write attempted on read-only connection |
ErrTableExists | Table name already registered |
ErrTableNotFound | Table not registered |
ErrTTLNotSupported | Provider doesn't support TTL |
ErrDimensionMismatch | Vector dimension doesn't match index |
ErrInvalidVector | Vector is malformed (nil, empty, NaN) |
ErrIndexNotReady | Index not loaded or initialized |
ErrInvalidQuery | Filter contains validation errors |
ErrOperatorNotSupported | Provider doesn't support filter operator |
ErrNoPrimaryKey | No field has constraints:"primarykey" tag |
ErrMultiplePrimaryKeys | Multiple fields have primarykey constraint |
if errors.Is(err, grub.ErrNotFound) {
// Handle missing record
}
StoreT
Type-safe key-value store wrapper.
NewStore
func NewStore[T any](provider StoreProvider) *Store[T]
Creates a new Store with JSON codec.
store := grub.NewStore[Session](redis.New(client))
NewStoreWithCodec
func NewStoreWithCodec[T any](provider StoreProvider, codec Codec) *Store[T]
Creates a new Store with custom codec.
store := grub.NewStoreWithCodec[Config](provider, grub.GobCodec{})
Methods
Get
func (s *Store[T]) Get(ctx context.Context, key string) (*T, error)
Retrieves value by key. Returns ErrNotFound if key doesn't exist.
session, err := store.Get(ctx, "session:abc123")
Set
func (s *Store[T]) Set(ctx context.Context, key string, value *T, ttl time.Duration) error
Stores value with optional TTL. TTL=0 means no expiration.
store.Set(ctx, "session:abc123", &session, 24*time.Hour)
store.Set(ctx, "config:app", &config, 0) // No expiration
Delete
func (s *Store[T]) Delete(ctx context.Context, key string) error
Removes key. Returns ErrNotFound if key doesn't exist.
err := store.Delete(ctx, "session:abc123")
Exists
func (s *Store[T]) Exists(ctx context.Context, key string) (bool, error)
Checks if key exists without loading value.
exists, err := store.Exists(ctx, "session:abc123")
List
func (s *Store[T]) List(ctx context.Context, prefix string, limit int) ([]string, error)
Lists keys matching prefix. Limit=0 means no limit.
keys, err := store.List(ctx, "session:", 100)
keys, err := store.List(ctx, "", 0) // All keys
GetBatch
func (s *Store[T]) GetBatch(ctx context.Context, keys []string) (map[string]*T, error)
Retrieves multiple keys. Missing keys are omitted from result (no error).
users, err := store.GetBatch(ctx, []string{"user:1", "user:2"})
SetBatch
func (s *Store[T]) SetBatch(ctx context.Context, items map[string]*T, ttl time.Duration) error
Stores multiple values with same TTL.
items := map[string]*User{"user:1": &alice, "user:2": &bob}
err := store.SetBatch(ctx, items, time.Hour)
Atomic
func (s *Store[T]) Atomic() *atomic.Store[T]
Returns atomic view for field-level access. Lazily initialized, cached. Panics if T is not atomizable.
atomicStore := store.Atomic()
atom, err := atomicStore.Get(ctx, "key")
BucketT
Type-safe blob storage wrapper.
NewBucket
func NewBucket[T any](provider BucketProvider) *Bucket[T]
Creates a new Bucket with JSON codec.
bucket := grub.NewBucket[Document](s3.New(client, "my-bucket"))
NewBucketWithCodec
func NewBucketWithCodec[T any](provider BucketProvider, codec Codec) *Bucket[T]
Creates a new Bucket with custom codec.
Methods
Get
func (b *Bucket[T]) Get(ctx context.Context, key string) (*Object[T], error)
Retrieves object with metadata and payload. Returns ErrNotFound if missing.
obj, err := bucket.Get(ctx, "docs/report.json")
fmt.Println(obj.Data.Title)
fmt.Println(obj.ContentType)
Put
func (b *Bucket[T]) Put(ctx context.Context, obj *Object[T]) error
Stores object with metadata.
err := bucket.Put(ctx, &grub.Object[Document]{
Key: "docs/report.json",
ContentType: "application/json",
Metadata: map[string]string{"author": "alice"},
Data: Document{Title: "Report"},
})
Delete
func (b *Bucket[T]) Delete(ctx context.Context, key string) error
Removes object. Returns ErrNotFound if missing.
Exists
func (b *Bucket[T]) Exists(ctx context.Context, key string) (bool, error)
Checks if object exists.
List
func (b *Bucket[T]) List(ctx context.Context, prefix string, limit int) ([]ObjectInfo, error)
Lists objects matching prefix. Returns metadata only (no payload).
infos, err := bucket.List(ctx, "docs/", 100)
for _, info := range infos {
fmt.Printf("%s (%d bytes)\n", info.Key, info.Size)
}
Atomic
func (b *Bucket[T]) Atomic() *atomic.Bucket[T]
Returns atomic view. Panics if T is not atomizable.
DatabaseT
Type-safe SQL database wrapper.
NewDatabase
func NewDatabase[T any](
db *sqlx.DB,
table string,
renderer astql.Renderer,
) (*Database[T], error)
Creates a new Database wrapper.
db: Database connectiontable: Table namerenderer: SQL dialect renderer (postgres.New(), mariadb.New(), etc.)
The primary key column is automatically derived from struct tags. Mark the primary key field with constraints:"primarykey":
type User struct {
ID int `db:"id" constraints:"primarykey"`
Email string `db:"email" constraints:"notnull,unique"`
Name string `db:"name"`
}
db := grub.NewDatabase[User](sqlxDB, "users", sqlite.New())
Panics if no field has the primarykey constraint (ErrNoPrimaryKey) or if multiple fields have the constraint (ErrMultiplePrimaryKeys). These are programmer errors — same category as the existing panic in Atomic().
Use the *Tx method variants (GetTx, SetTx, etc.) for transaction support.
Methods
Get
func (d *Database[T]) Get(ctx context.Context, key string) (*T, error)
Retrieves record by primary key. Returns ErrNotFound if missing.
user, err := db.Get(ctx, "123")
Set
func (d *Database[T]) Set(ctx context.Context, key string, value *T) error
Upserts record (insert or update on conflict).
err := db.Set(ctx, "123", &User{ID: "123", Name: "Alice"})
Delete
func (d *Database[T]) Delete(ctx context.Context, key string) error
Removes record. Returns ErrNotFound if missing.
Exists
func (d *Database[T]) Exists(ctx context.Context, key string) (bool, error)
Checks if record exists.
Query Builders
Direct access to soy query builders for ad-hoc queries.
Query
func (d *Database[T]) Query() *soy.Query[T]
Returns a query builder for fetching multiple records.
users, err := db.Query().
Where("age", ">=", "min_age").
OrderBy("name", "ASC").
Limit(10).
Exec(ctx, map[string]any{"min_age": 18})
Select
func (d *Database[T]) Select() *soy.Select[T]
Returns a select builder for fetching a single record.
user, err := db.Select().
Where("email", "=", "email").
Exec(ctx, map[string]any{"email": "alice@example.com"})
Insert
func (d *Database[T]) Insert() *soy.Create[T]
Returns an insert builder (auto-generates PK).
user, err := db.Insert().Exec(ctx, &User{Name: "Alice", Email: "alice@example.com"})
InsertFull
func (d *Database[T]) InsertFull() *soy.Create[T]
Returns an insert builder that includes the PK field.
user, err := db.InsertFull().Exec(ctx, &User{ID: 123, Name: "Alice"})
Modify
func (d *Database[T]) Modify() *soy.Update[T]
Returns an update builder.
updated, err := db.Modify().
Set("name", "new_name").
Where("id", "=", "user_id").
Exec(ctx, map[string]any{"new_name": "Bob", "user_id": 123})
Remove
func (d *Database[T]) Remove() *soy.Delete[T]
Returns a delete builder.
affected, err := db.Remove().
Where("status", "=", "inactive").
Exec(ctx, map[string]any{"inactive": "deleted"})
Count
func (d *Database[T]) Count() *soy.Aggregate[T]
Returns an aggregate builder for counting records.
// Count all records
count, err := db.Count().Exec(ctx, nil)
// Count with condition
count, err := db.Count().
Where("status", "=", "active").
Exec(ctx, map[string]any{"active": "enabled"})
// Count in transaction
count, err := db.Count().ExecTx(ctx, tx, nil)
Statement Execution
Execute pre-defined edamame statements.
ExecQuery
func (d *Database[T]) ExecQuery(ctx context.Context, stmt edamame.QueryStatement, params map[string]any) ([]*T, error)
Executes a query statement returning multiple records.
users, err := db.ExecQuery(ctx, grub.QueryAll, nil)
users, err := db.ExecQuery(ctx, byRoleStmt, map[string]any{"role": "admin"})
ExecSelect
func (d *Database[T]) ExecSelect(ctx context.Context, stmt edamame.SelectStatement, params map[string]any) (*T, error)
Executes a select statement returning a single record.
user, err := db.ExecSelect(ctx, byEmailStmt, map[string]any{"email": "alice@example.com"})
ExecUpdate
func (d *Database[T]) ExecUpdate(ctx context.Context, stmt edamame.UpdateStatement, params map[string]any) (*T, error)
Executes an update statement returning the modified record.
ExecAggregate
func (d *Database[T]) ExecAggregate(ctx context.Context, stmt edamame.AggregateStatement, params map[string]any) (float64, error)
Executes an aggregate statement.
count, err := db.ExecAggregate(ctx, grub.CountAll, nil)
Transaction Methods
All operations have *Tx variants that accept a transaction as the second parameter.
GetTx
func (d *Database[T]) GetTx(ctx context.Context, tx *sqlx.Tx, key string) (*T, error)
SetTx
func (d *Database[T]) SetTx(ctx context.Context, tx *sqlx.Tx, key string, value *T) error
DeleteTx
func (d *Database[T]) DeleteTx(ctx context.Context, tx *sqlx.Tx, key string) error
ExistsTx
func (d *Database[T]) ExistsTx(ctx context.Context, tx *sqlx.Tx, key string) (bool, error)
ExecQueryTx
func (d *Database[T]) ExecQueryTx(ctx context.Context, tx *sqlx.Tx, stmt edamame.QueryStatement, params map[string]any) ([]*T, error)
ExecSelectTx
func (d *Database[T]) ExecSelectTx(ctx context.Context, tx *sqlx.Tx, stmt edamame.SelectStatement, params map[string]any) (*T, error)
ExecUpdateTx
func (d *Database[T]) ExecUpdateTx(ctx context.Context, tx *sqlx.Tx, stmt edamame.UpdateStatement, params map[string]any) (*T, error)
ExecAggregateTx
func (d *Database[T]) ExecAggregateTx(ctx context.Context, tx *sqlx.Tx, stmt edamame.AggregateStatement, params map[string]any) (float64, error)
Usage Example
tx, err := sqlxDB.BeginTxx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
user, err := db.GetTx(ctx, tx, "123")
if err != nil {
return err
}
user.Name = "Updated"
err = db.SetTx(ctx, tx, "123", user)
if err != nil {
return err
}
return tx.Commit()
Executor
func (d *Database[T]) Executor() *edamame.Executor[T]
Returns the underlying edamame Executor for advanced query operations.
Atomic
func (d *Database[T]) Atomic() AtomicDatabase
Returns atomic view. Panics if T is not atomizable.
IndexT
Type-safe vector storage wrapper.
NewIndex
func NewIndex[T any](provider VectorProvider) *Index[T]
Creates a new Index with JSON codec.
index := grub.NewIndex[Embedding](qdrant.New(client, qdrant.Config{
Collection: "documents",
}))
NewIndexWithCodec
func NewIndexWithCodec[T any](provider VectorProvider, codec Codec) *Index[T]
Creates a new Index with custom codec.
Methods
Upsert
func (i *Index[T]) Upsert(ctx context.Context, id string, vector []float32, metadata *T) error
Stores or updates a vector with associated metadata. If the ID exists, the vector and metadata are replaced.
index.Upsert(ctx, "doc:1", embedding, &Embedding{Category: "tech"})
UpsertBatch
func (i *Index[T]) UpsertBatch(ctx context.Context, vectors []Vector[T]) error
Stores or updates multiple vectors.
vectors := []grub.Vector[Embedding]{
{ID: "doc:1", Vector: vec1, Metadata: Embedding{Category: "tech"}},
{ID: "doc:2", Vector: vec2, Metadata: Embedding{Category: "science"}},
}
err := index.UpsertBatch(ctx, vectors)
Get
func (i *Index[T]) Get(ctx context.Context, id string) (*Vector[T], error)
Retrieves a vector by ID. Returns ErrNotFound if the ID does not exist.
result, err := index.Get(ctx, "doc:1")
Delete
func (i *Index[T]) Delete(ctx context.Context, id string) error
Removes a vector by ID. Returns ErrNotFound if the ID does not exist.
DeleteBatch
func (i *Index[T]) DeleteBatch(ctx context.Context, ids []string) error
Removes multiple vectors by ID. Non-existent IDs are silently ignored.
Search
func (i *Index[T]) Search(ctx context.Context, vector []float32, k int, filter *T) ([]*Vector[T], error)
Performs similarity search and returns the k nearest neighbors. Filter is optional metadata filtering (nil means no filter).
results, err := index.Search(ctx, queryVector, 10, nil)
results, err := index.Search(ctx, queryVector, 10, &Embedding{Category: "tech"})
Query
func (i *Index[T]) Query(ctx context.Context, vector []float32, k int, filter *vecna.Filter) ([]*Vector[T], error)
Performs similarity search with vecna filter support. Returns ErrInvalidQuery if the filter contains validation errors. Returns ErrOperatorNotSupported if the provider doesn't support an operator.
filter := vecna.And(
vecna.Eq("category", "tech"),
vecna.Gte("score", 0.8),
)
results, err := index.Query(ctx, queryVector, 10, filter)
Filter
func (i *Index[T]) Filter(ctx context.Context, filter *vecna.Filter, limit int) ([]*Vector[T], error)
Returns vectors matching the metadata filter without similarity search. Result ordering is provider-dependent and not guaranteed by the interface. Limit of 0 returns all matching vectors. Returns ErrFilterNotSupported if the provider cannot perform metadata-only filtering (e.g., Pinecone).
filter := vecna.Eq("category", "tech")
results, err := index.Filter(ctx, filter, 100)
// Nil filter returns all vectors
all, err := index.Filter(ctx, nil, 0)
List
func (i *Index[T]) List(ctx context.Context, prefix string, limit int) ([]string, error)
Returns vector IDs matching the optional prefix. Limit of 0 means no limit.
Exists
func (i *Index[T]) Exists(ctx context.Context, id string) (bool, error)
Checks whether a vector ID exists.
Atomic
func (i *Index[T]) Atomic() *atomic.Index[T]
Returns atomic view for field-level access. Lazily initialized, cached. Panics if T is not atomizable.
Types
ObjectT
Blob object with metadata and typed payload.
type Object[T any] struct {
Key string `json:"key"`
ContentType string `json:"content_type"`
Size int64 `json:"size"`
ETag string `json:"etag,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Data T `json:"data"`
}
ObjectInfo
Blob metadata without payload (returned by List).
type ObjectInfo struct {
Key string
ContentType string
Size int64
ETag string
Metadata map[string]string
}
VectorT
Vector with typed metadata payload.
type Vector[T any] struct {
ID string `json:"id"`
Vector []float32 `json:"vector"`
Score float32 `json:"score,omitempty"`
Metadata T `json:"metadata"`
}
VectorInfo
Vector metadata returned by providers.
type VectorInfo struct {
ID string
Dimension int
Score float32
Metadata map[string]any
}
VectorRecord
Batch operation format for vectors.
type VectorRecord struct {
ID string
Vector []float32
Metadata map[string]any
}
VectorResult
Search result returned by providers.
type VectorResult struct {
ID string
Vector []float32
Metadata map[string]any
Score float32
}
AtomicVector
Vector with atomized metadata payload.
type AtomicVector struct {
ID string
Vector []float32
Score float32
Metadata *atom.Atom
}
Interfaces
StoreProvider
Raw key-value storage interface.
type StoreProvider interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)
List(ctx context.Context, prefix string, limit int) ([]string, error)
GetBatch(ctx context.Context, keys []string) (map[string][]byte, error)
SetBatch(ctx context.Context, items map[string][]byte, ttl time.Duration) error
}
BucketProvider
Raw blob storage interface.
type BucketProvider interface {
Get(ctx context.Context, key string) ([]byte, *ObjectInfo, error)
Put(ctx context.Context, key string, data []byte, info *ObjectInfo) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)
List(ctx context.Context, prefix string, limit int) ([]ObjectInfo, error)
}
VectorProvider
Raw vector storage interface.
type VectorProvider interface {
Upsert(ctx context.Context, id string, vector []float32, metadata map[string]any) error
UpsertBatch(ctx context.Context, vectors []VectorRecord) error
Get(ctx context.Context, id string) ([]float32, *VectorInfo, error)
Delete(ctx context.Context, id string) error
DeleteBatch(ctx context.Context, ids []string) error
Search(ctx context.Context, vector []float32, k int, filter map[string]any) ([]VectorResult, error)
Query(ctx context.Context, vector []float32, k int, filter *vecna.Filter) ([]VectorResult, error)
Filter(ctx context.Context, filter *vecna.Filter, limit int) ([]VectorResult, error)
List(ctx context.Context, prefix string, limit int) ([]string, error)
Exists(ctx context.Context, id string) (bool, error)
}
BeforeSave
Called before persisting T. Return an error to abort the write.
type BeforeSave interface {
BeforeSave(ctx context.Context) error
}
AfterSave
Called after T has been successfully persisted.
type AfterSave interface {
AfterSave(ctx context.Context) error
}
AfterLoad
Called after T has been loaded and decoded.
type AfterLoad interface {
AfterLoad(ctx context.Context) error
}
BeforeDelete
Called before deleting a record. Invoked on a zero-value T (no loaded state).
type BeforeDelete interface {
BeforeDelete(ctx context.Context) error
}
AfterDelete
Called after a record has been successfully deleted. Invoked on a zero-value T (no loaded state).
type AfterDelete interface {
AfterDelete(ctx context.Context) error
}
Codec
Serialization interface.
type Codec interface {
Encode(v any) ([]byte, error)
Decode(data []byte, v any) error
}
AtomicStore
Type-agnostic key-value access.
type AtomicStore interface {
Spec() atom.Spec
Get(ctx context.Context, key string) (*atom.Atom, error)
Set(ctx context.Context, key string, a *atom.Atom, ttl time.Duration) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)
}
AtomicBucket
Type-agnostic blob access.
type AtomicBucket interface {
Spec() atom.Spec
Get(ctx context.Context, key string) (*AtomicObject, error)
Put(ctx context.Context, key string, obj *AtomicObject) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)
}
AtomicDatabase
Type-agnostic SQL access.
type AtomicDatabase interface {
Table() string
Spec() atom.Spec
Get(ctx context.Context, key string) (*atom.Atom, error)
Set(ctx context.Context, key string, a *atom.Atom) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)
ExecQuery(ctx context.Context, stmt edamame.QueryStatement, params map[string]any) ([]*atom.Atom, error)
ExecSelect(ctx context.Context, stmt edamame.SelectStatement, params map[string]any) (*atom.Atom, error)
}
AtomicIndex
Type-agnostic vector access.
type AtomicIndex interface {
Spec() atom.Spec
Get(ctx context.Context, id string) (*AtomicVector, error)
Upsert(ctx context.Context, id string, vector []float32, metadata *atom.Atom) error
Delete(ctx context.Context, id string) error
Exists(ctx context.Context, id string) (bool, error)
Search(ctx context.Context, vector []float32, k int, filter *atom.Atom) ([]AtomicVector, error)
Query(ctx context.Context, vector []float32, k int, filter *vecna.Filter) ([]AtomicVector, error)
Filter(ctx context.Context, filter *vecna.Filter, limit int) ([]AtomicVector, error)
}
Codecs
JSONCodec
Default codec using encoding/json.
type JSONCodec struct{}
GobCodec
Binary codec using encoding/gob.
type GobCodec struct{}
Usage
// Default (JSON)
store := grub.NewStore[Config](provider)
// Custom codec
store := grub.NewStoreWithCodec[Config](provider, grub.GobCodec{})