sqlstore

package
v1.801.360 Latest Latest
Warning

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

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

Documentation

Overview

manager.go is the PER-TENANT selector over the treasury Store: it resolves each request to its OWN Hanzo Base (SQLite) file instead of a process-wide singleton, so one tenant's finance/ledger writes can NEVER appear in another tenant's reads. This is the storage side of the standing Hanzo rule — Postgres stays a supported option (Formance, one layer up), but every tenant's books run live on its own Base file.

TWO file classes, one opener:

  • the HOUSE ledger — the platform's OWN reserve fund (fund:reserve, revenue:*, payout:*): a SINGLE single-writer, overdraw-guarded file. It CANNOT be split per tenant (the reserve overdraw guard is one atomic balance), so it is one fixed file — the pre-existing {DataDir}/treasury.db, kept verbatim so live reserve capital is never orphaned by a path change.
  • a CUSTOMER ledger — one isolated file per tenant at {DataDir}/finance/{slug}.db, opened on first use and cached.

It CONSUMES the treasury's canonical Base opener (Open, over github.com/hanzoai/ sqlite — the ONE Hanzo Base driver) rather than importing the ledger's per-tenant opener: that opener is a test-only helper deliberately kept unexported because it registers modernc's database/sql "sqlite" driver, which would COLLIDE with the hanzoai/sqlite registration this store already carries. the ledger's own contract is "production callers supply their own *bun.DB to New" — which Open does — so the per-tenant file selection lives HERE, over Open, and the slug guard matches the ledger's per-tenant guard exactly.

Package sqlstore is the Hanzo Base (HIP-0105 per-tenant SQLite) adapter for the ledger core: it implements ledger.Store + ledger.Tx over one SQLite file, and nothing more. It carries the storage concern the core deliberately does not — the SAME single-connection + WAL pattern every clients/* store uses (referrals, crm, prompts), so it is Base-compatible and drops into the unified binary unchanged.

It imports the core (ledger) and the one Hanzo SQLite driver — never cloud, zip, or IAM. When the core is lifted to hanzoai/finance this adapter travels with it as the default backend; the driver import is the only thing a different Base backend would swap.

MONEY IS EXACT AND BIG. Amounts are 18-decimal USD (1e-18, the EVM/uint256 unit) held as big.Int money.Amount — a value exceeds SQLite's 64-bit INTEGER past ~$9.20, so amount columns are TEXT (the signed 18-decimal integer string) and an account's balance is a maintained running total (treasury_accounts.balance), NOT a SQL SUM (you cannot SUM a decimal-string column, and a busy wallet's million usage postings must not be re-summed on every gate read). The running balance is updated inside the same transaction as each posting, so it can never drift from the journal.

Index

Constants

View Source
const HouseSlug = "house"

HouseSlug names the platform's OWN reserve/house ledger — the single (single-writer, overdraw-guarded) book behind every backed payout. It is RESERVED: tenantSlug routes a literal "house" tenant to a hashed slug, so a caller can never open the house fund by naming its org "house".

Variables

This section is empty.

Functions

This section is empty.

Types

type Manager

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

Manager opens and caches one *Store per tenant. It is safe for concurrent use. Each distinct tenant maps to a distinct file; the mapping is INJECTIVE (it never folds "acme" and "ACME" into one bucket — that would itself be a cross-tenant break) and can never traverse the path or collide with the reserved house file.

func NewManager

func NewManager(dataDir string) (*Manager, error)

NewManager roots the per-tenant treasury stores under dataDir: the house ledger at {dataDir}/treasury.db (preserved) and customer ledgers under {dataDir}/finance/. The finance dir (and thus dataDir) is created if absent.

func (*Manager) Close

func (m *Manager) Close() error

Close closes every open store (house + tenants). Idempotent; returns the first close error, if any.

func (*Manager) Get

func (m *Manager) Get(tenant string) (*Store, error)

Get resolves the caller's OWN per-tenant store from the validated tenant key. The tenant is turned into a safe, injective, path-guarded slug FIRST, so a caller can only ever open its own file — never another tenant's, never the house fund, never a path outside the finance dir.

func (*Manager) House

func (m *Manager) House() (*Store, error)

House opens (once, then cached) the platform's reserve/house ledger — the single file the ledger-of-record binds to.

type Store

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

Store is the SQLite-backed ledger persistence. ONE file holds the whole chart of accounts, journal and policy for a deployment (the platform's own books — not per-org, unlike a tenant product store). Serialized on a single connection so the engine's balance-guard read-then-write is atomic under load.

func Open

func Open(p cek.Principal, path string) (*Store, error)

Open opens (creating + migrating) the ledger database at path, under the principal its key is bound to.

The principal is a PARAMETER because these stores do not share one. A per-org ledger (orgs/<org>/finance.db) is bound to that org; the platform's own treasury.db is bound to Global. This function used to hard-code Global for both — and cek-rewrap had already moved every per-org sidecar to its owner ("a store's key names its owner", 2026-07-31 01:25). So the data was migrated correctly and the opener was never updated: every per-org ledger failed to unwrap with "wrong key, wrong principal, or corrupt blob".

That read as data loss and was not: 86 finance ledgers were intact and correctly keyed the whole time. Because the AI balance gate is fail-closed and could not read a balance, it refused EVERY completion — chat, copilot and documents — fleet-wide. Naming the principal at the call site is what stops an opener and a migration from disagreeing again, because now they cannot both be silent about it.

func (*Store) Balance

func (s *Store) Balance(ctx context.Context, account string) (money.Amount, error)

func (*Store) BalancesWithPrefix

func (s *Store) BalancesWithPrefix(ctx context.Context, prefix string) (map[string]money.Amount, error)

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database.

func (*Store) Entries

func (s *Store) Entries(ctx context.Context, limit int) ([]ledger.JournalEntry, error)

func (*Store) Policy

func (s *Store) Policy(ctx context.Context) (ledger.SharePolicy, error)

func (*Store) SetPolicy

func (s *Store) SetPolicy(ctx context.Context, p ledger.SharePolicy) error

func (*Store) SumByKindSince

func (s *Store) SumByKindSince(ctx context.Context, kind string, since int64) (money.Amount, error)

SumByKindSince sums the entry amounts of ONE kind created at/after `since` (unix seconds) — a read-only aggregate over the indexed created_at, no schema change. Amounts are 18-decimal TEXT (SQLite INTEGER overflows past ~$9.20), so the fold is in Go. This is the usage-cap's period-spend source: sum kind "finance.usage" for the org store since the start of the UTC month, so a cap enforces on real ledger spend rather than a bounded, truncatable journal scan.

func (*Store) Tx

func (s *Store) Tx(ctx context.Context, fn func(ledger.Tx) error) error

Jump to

Keyboard shortcuts

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