Files
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

277 lines
9.5 KiB
Go

package db
import (
"context"
"fmt"
ormdb "github.com/hanzoai/orm/db"
)
// tenantDB is a DB for one tenant that holds no database open.
//
// The registry hands a handle to a callback and takes it back when the callback
// returns (ormdb.Namespaces.With), deliberately: a handle you can keep is a handle
// someone must remember to give back, and the forgotten give-back IS the leak
// this replaced — Manager's old userDBs/orgDBs maps opened a store per tenant
// ever touched and closed them only at shutdown.
//
// Commerce's call sites want a db.DB they can keep: Store.DB() returns one per
// request, datastore's orgDBResolver hands one to every namespaced Datastore,
// models/user holds one per service. tenantDB bridges those to Do without
// reintroducing the leak by being a VALUE — a registry pointer, a tenant name,
// and the namespace its keys carry. Each operation borrows the tenant's handle
// and gives it back before returning, so between operations nothing is pinned:
// the bound and the idle sweep still apply, and a caller may hold a tenantDB
// for the life of the process at a cost of four words.
//
// The one thing a caller must not assume is that two consecutive operations run
// on the same open handle. Nothing needs that — the file is the state, and
// anything that must be atomic across statements is already inside
// RunInTransaction, which runs entirely within one borrow.
type tenantDB struct {
// Keys are minted from the tenant id alone, so building one never opens
// the store — see tenantKeys in sqlite.go.
tenantKeys
reg *ormdb.Namespaces[DB]
t ormdb.Namespace
}
func newTenantDB(reg *ormdb.Namespaces[DB], t ormdb.Namespace) tenantDB {
_, id, _ := splitTenant(t)
return tenantDB{tenantKeys: tenantKeys(id), reg: reg, t: t}
}
// do borrows this tenant's handle for exactly one operation.
func (d tenantDB) do(ctx context.Context, fn func(DB) error) error {
return d.reg.With(ctx, d.t, fn)
}
func (d tenantDB) TenantID() string { _, id, _ := splitTenant(d.t); return id }
func (d tenantDB) TenantType() string { typ, _, _ := splitTenant(d.t); return typ }
// Close is a no-op. The registry owns handle lifetime — it opens, bounds and
// closes them — so a caller closing its view of a tenant must not close the
// store other callers are sharing. Manager.Close closes the registry.
func (d tenantDB) Close() error { return nil }
func (d tenantDB) Get(ctx context.Context, key Key, dst interface{}) error {
return d.do(ctx, func(db DB) error { return db.Get(ctx, key, dst) })
}
func (d tenantDB) Put(ctx context.Context, key Key, src interface{}) (Key, error) {
var out Key
err := d.do(ctx, func(db DB) error {
var e error
out, e = db.Put(ctx, key, src)
return e
})
return out, err
}
func (d tenantDB) Delete(ctx context.Context, key Key) error {
return d.do(ctx, func(db DB) error { return db.Delete(ctx, key) })
}
func (d tenantDB) GetMulti(ctx context.Context, keys []Key, dst interface{}) error {
return d.do(ctx, func(db DB) error { return db.GetMulti(ctx, keys, dst) })
}
func (d tenantDB) PutMulti(ctx context.Context, keys []Key, src interface{}) ([]Key, error) {
var out []Key
err := d.do(ctx, func(db DB) error {
var e error
out, e = db.PutMulti(ctx, keys, src)
return e
})
return out, err
}
func (d tenantDB) DeleteMulti(ctx context.Context, keys []Key) error {
return d.do(ctx, func(db DB) error { return db.DeleteMulti(ctx, keys) })
}
func (d tenantDB) VectorSearch(ctx context.Context, opts *VectorSearchOptions) ([]VectorResult, error) {
var out []VectorResult
err := d.do(ctx, func(db DB) error {
var e error
out, e = db.VectorSearch(ctx, opts)
return e
})
return out, err
}
func (d tenantDB) PutVector(ctx context.Context, kind string, id string, vector []float32, metadata map[string]interface{}) error {
return d.do(ctx, func(db DB) error { return db.PutVector(ctx, kind, id, vector, metadata) })
}
func (d tenantDB) RunInTransaction(ctx context.Context, fn func(tx Transaction) error, opts *TransactionOptions) error {
return d.do(ctx, func(db DB) error { return db.RunInTransaction(ctx, fn, opts) })
}
// NextSequence forwards the allocation to the tenant's own store, so a
// tenantDB satisfies Sequencer exactly when the store behind it does.
//
// The single borrow is enough, and this is the one place in this file where
// that is worth stating: the guarantee is a property of ONE statement (see
// SQLiteDB.NextSequence), not of holding a handle across several, so it does
// not need the borrow to span calls any more than a Put does.
func (d tenantDB) NextSequence(ctx context.Context, name string) (uint64, error) {
var out uint64
err := d.do(ctx, func(db DB) error {
s, ok := db.(Sequencer)
if !ok {
// Refuse rather than imitate. A caller that needs an allocated
// number needs it to be unique, and there is no safe fallback to
// offer here — see the Sequencer doc.
return fmt.Errorf("db: tenant store %T cannot allocate sequences", db)
}
var err error
out, err = s.NextSequence(ctx, name)
return err
})
return out, err
}
func (d tenantDB) Query(kind string) Query { return tenantQuery{d: d, kind: kind} }
// tenantQuery defers a query to the operation that runs it.
//
// A query is built across arbitrarily many calls and may be parked, branched or
// passed around before anything executes, so there is no borrow that could span
// construction. tenantQuery therefore records the builder calls and replays them
// onto a real query INSIDE the borrow that runs the terminal operation — the
// borrow lasts one statement, not one query object.
type tenantQuery struct {
d tenantDB
kind string
steps []func(Query) Query
}
// with returns a query with one more builder step. It copies rather than
// appending in place because db.Query builders return a new query and leave the
// receiver untouched — callers branch off a partially built query relying on it.
func (q tenantQuery) with(step func(Query) Query) Query {
steps := make([]func(Query) Query, len(q.steps)+1)
copy(steps, q.steps)
steps[len(q.steps)] = step
return tenantQuery{d: q.d, kind: q.kind, steps: steps}
}
// build replays the recorded steps onto a real query from a borrowed handle.
func (q tenantQuery) build(db DB) Query {
real := db.Query(q.kind)
for _, step := range q.steps {
real = step(real)
}
return real
}
func (q tenantQuery) Filter(filterStr string, value interface{}) Query {
return q.with(func(r Query) Query { return r.Filter(filterStr, value) })
}
func (q tenantQuery) FilterField(fieldPath string, op string, value interface{}) Query {
return q.with(func(r Query) Query { return r.FilterField(fieldPath, op, value) })
}
func (q tenantQuery) Order(fieldPath string) Query {
return q.with(func(r Query) Query { return r.Order(fieldPath) })
}
func (q tenantQuery) OrderDesc(fieldPath string) Query {
return q.with(func(r Query) Query { return r.OrderDesc(fieldPath) })
}
func (q tenantQuery) Limit(limit int) Query {
return q.with(func(r Query) Query { return r.Limit(limit) })
}
func (q tenantQuery) Offset(offset int) Query {
return q.with(func(r Query) Query { return r.Offset(offset) })
}
func (q tenantQuery) Project(fieldNames ...string) Query {
return q.with(func(r Query) Query { return r.Project(fieldNames...) })
}
func (q tenantQuery) Distinct() Query {
return q.with(func(r Query) Query { return r.Distinct() })
}
func (q tenantQuery) Ancestor(ancestor Key) Query {
return q.with(func(r Query) Query { return r.Ancestor(ancestor) })
}
func (q tenantQuery) Start(cursor Cursor) Query {
return q.with(func(r Query) Query { return r.Start(cursor) })
}
func (q tenantQuery) End(cursor Cursor) Query {
return q.with(func(r Query) Query { return r.End(cursor) })
}
func (q tenantQuery) GetAll(ctx context.Context, dst interface{}) ([]Key, error) {
var keys []Key
err := q.d.do(ctx, func(db DB) error {
var e error
keys, e = q.build(db).GetAll(ctx, dst)
return e
})
return keys, err
}
func (q tenantQuery) First(ctx context.Context, dst interface{}) (Key, error) {
var key Key
err := q.d.do(ctx, func(db DB) error {
var e error
key, e = q.build(db).First(ctx, dst)
return e
})
return key, err
}
func (q tenantQuery) Count(ctx context.Context) (int, error) {
var n int
err := q.d.do(ctx, func(db DB) error {
var e error
n, e = q.build(db).Count(ctx)
return e
})
return n, err
}
func (q tenantQuery) Keys(ctx context.Context) ([]Key, error) {
var keys []Key
err := q.d.do(ctx, func(db DB) error {
var e error
keys, e = q.build(db).Keys(ctx)
return e
})
return keys, err
}
// Run returns the store's own iterator, not a wrapper, for two reasons.
//
// It must stay the real iterator: callers close it through an
// `interface{ Close() error }` assertion (datastore/query.Query.First) to hand
// the pooled connection back, and a wrapper that did not forward Close would
// reinstate the connection leak that starved the pool in production.
//
// And it is safe for it to outlive the borrow: the iterator holds a *sql.Rows,
// which keeps its own connection alive until the rows are done even if the
// pool it came from is closed underneath it. So a mid-iteration eviction cannot
// truncate a result set — TestIteratorSurvivesHandleEviction is the guard.
func (q tenantQuery) Run(ctx context.Context) Iterator {
var it Iterator
if err := q.d.do(ctx, func(db DB) error {
it = q.build(db).Run(ctx)
return nil
}); err != nil {
// Same shape the store itself uses for a query that never ran: an
// iterator carrying the error, surfaced on the first Next.
return &sqliteIterator{err: err, kind: q.kind}
}
return it
}