96 lines
3.0 KiB
Go
96 lines
3.0 KiB
Go
// Package store is maven's persistent state layer.
|
|
//
|
|
// The layer is append-only: a wrong fact is superseded, not overwritten.
|
|
// Current value for a key = the latest non-voided fact row.
|
|
//
|
|
// Schema lives in schema.sql and is applied idempotently on Open.
|
|
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
_ "embed" // schema.sql
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
//go:embed schema.sql
|
|
var schemaSQL string
|
|
|
|
// FactKind — self | env | config. The loop only evaluates predicates against
|
|
// `self` and `config` rows; `env` feeds env predicates (calendar/weather/health).
|
|
type FactKind string
|
|
|
|
const (
|
|
KindSelf FactKind = "self"
|
|
KindEnv FactKind = "env"
|
|
KindConfig FactKind = "config"
|
|
)
|
|
|
|
// Fact — one observation. ts is valid-time (true-as-of), not insert-time.
|
|
type Fact struct {
|
|
ID int64
|
|
Ts time.Time
|
|
Kind FactKind
|
|
Key string
|
|
Value string // raw json if structured
|
|
Source string // tap:* | infer:* | poll:* | ambient | promote | feedback
|
|
Confidence float64
|
|
VoidsID sql.NullInt64
|
|
}
|
|
|
|
// Bucket — presence hysteresis state.
|
|
type Bucket string
|
|
|
|
const (
|
|
Present Bucket = "present"
|
|
Away Bucket = "away"
|
|
)
|
|
|
|
// Store is the persistent state layer. All writes are append-only; nothing
|
|
// here performs an UPDATE of a fact value (status-flips on reminders/nudges
|
|
// are the documented exceptions — they mutate small state-machine columns).
|
|
type Store struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// Open opens or creates the sqlite database at path and applies the schema.
|
|
// Pragmas (WAL, NORMAL, FK on, busy_timeout) are set in schema.sql and re-applied
|
|
// per connection on open via the modernc driver DSN.
|
|
func Open(ctx context.Context, path string) (*Store, error) {
|
|
// `_pragma=busy_timeout(5000)` etc. embed cleanly; schema.sql sets them too.
|
|
dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)", path)
|
|
db, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open %s: %w", path, err)
|
|
}
|
|
// single writer expected; the daemon is the only process touching the db.
|
|
db.SetMaxOpenConns(1)
|
|
if _, err := db.ExecContext(ctx, schemaSQL); err != nil {
|
|
_ = db.Close()
|
|
return nil, fmt.Errorf("apply schema: %w", err)
|
|
}
|
|
return &Store{db: db}, nil
|
|
}
|
|
|
|
// Close releases the database handle.
|
|
func (s *Store) Close() error { return s.db.Close() }
|
|
|
|
// DB exposes the underlying handle for internal read-only snapshots.
|
|
// Used by the loop to take a consistent read under a single transaction.
|
|
// Modules never receive this handle — core mediates.
|
|
func (s *Store) DB(ctx context.Context) (*sql.Tx, error) {
|
|
return s.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
|
}
|
|
|
|
var (
|
|
// ErrNoFact — no non-voided row exists for this key.
|
|
ErrNoFact = errors.New("store: no fact for key")
|
|
// ErrConfidence — a write attempted to use a confidence outside (0,1.0].
|
|
ErrConfidence = errors.New("store: confidence must be in (0.0, 1.0]")
|
|
// ErrVoidsMissing — a correction pointed at a nonexistent fact.
|
|
ErrVoidsMissing = errors.New("store: voids_id does not reference an existing fact")
|
|
) |