Documentation
¶
Overview ¶
Package db persists parsed budgetclaw events and their per-day rollups in a local SQLite database.
Two tables:
events one row per billable assistant API response.
Claude Code writes the same response on multiple JSONL
lines (one per tool-result roundtrip), each with its own
line uuid but a shared (message_id, request_id) pair and
the same response usage. We dedupe on that pair so the
response is counted once, matching ccusage. Lines with no
message_id (older Claude Code schemas) fall back to uuid
dedupe. A later line for the same response REPLACEs the
stored row, so the most complete usage wins and
re-processing stays idempotent.
rollups one row per (project, git_branch, day) aggregate.
Updated atomically with the event insert so the budget
evaluator can do O(1) reads for cap checks. A replace
updates the rollup by the delta (new minus old) so a
redundant line never double-counts.
SQLite is opened with WAL journal mode on file-backed databases, enabling the CLI to read while the watcher writes from a separate process. The driver is modernc.org/sqlite (pure Go, no cgo) so the static-binary pledge holds.
Costs are passed in from the caller at insert time and stored as historical fact. The db package has no dependency on the pricing table. A future Anthropic rate change will not retroactively re-price old events.
Day boundaries in rollups are UTC. Budget evaluators that need local-timezone semantics should use RollupSum over a UTC time range computed from the user's tz, not the day string directly.
Index ¶
- type DB
- func (d *DB) Close() error
- func (d *DB) Insert(ctx context.Context, e *parser.Event, costUSD float64) error
- func (d *DB) Reset(ctx context.Context) error
- func (d *DB) RollupForDay(ctx context.Context, project, branch string, day time.Time) (*Rollup, error)
- func (d *DB) RollupSum(ctx context.Context, project, branch string, start, end time.Time) (*Rollup, error)
- func (d *DB) StatusByProject(ctx context.Context, start, end time.Time) ([]Rollup, error)
- func (d *DB) SyncAggregates(ctx context.Context, since time.Time) ([]SyncAggregate, error)
- type Rollup
- type SyncAggregate
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB wraps a *sql.DB with budgetclaw-specific methods.
func Open ¶
Open opens or creates the state database.
If path is empty, Open resolves the default location via paths.StateDir() (honoring XDG_STATE_HOME) and creates any missing parent directories. If path is the literal string ":memory:", Open returns an in-memory database suitable for tests.
The schema is applied on every call. Existing tables are not recreated thanks to IF NOT EXISTS.
func OpenMemory ¶
OpenMemory returns an in-memory database for tests. Equivalent to Open(":memory:") but avoids forcing test code to know the magic string.
func (*DB) Insert ¶
Insert stores a billable event and updates its rollup row inside a single transaction.
Dedupe key:
- When e.MessageID is non-empty, uniqueness is (message_id, request_id). Claude Code writes the same API response across several JSONL lines (each with its own uuid), so this is the key that counts one response once and matches ccusage.
- When e.MessageID is empty (older Claude Code schemas), the key falls back to e.UUID, preserving the original behavior.
A second line for an already-stored response REPLACEs the row: later lines of a streaming response can carry more complete usage, and replacing makes re-processing idempotent. The rollup is updated by the delta (new contribution minus the old row's), so a redundant or growing line never double-counts. When the redundant line is identical the delta is zero and the rollup is untouched.
costUSD is passed in so the db package stays independent of the pricing table. Callers should compute it via pricing.CostForModel before calling Insert.
func (*DB) Reset ¶ added in v0.1.4
Reset truncates the events and rollups tables so a subsequent backfill can re-attribute every historical event from scratch. Used by `budgetclaw backfill --rebuild` after a pricing-table correction lands: existing rollups are stuck at the old (wrong) rate because Insert is idempotent on uuid, so the only way to recompute them is to wipe and replay.
The tables are truncated inside a single transaction so a crash mid-reset cannot leave half-empty state behind.
func (*DB) RollupForDay ¶
func (d *DB) RollupForDay(ctx context.Context, project, branch string, day time.Time) (*Rollup, error)
RollupForDay returns the rollup row for a specific (project, branch, day). Returns (nil, nil) if the row does not exist — "nothing spent today" is a valid state, not an error.
func (*DB) RollupSum ¶
func (d *DB) RollupSum(ctx context.Context, project, branch string, start, end time.Time) (*Rollup, error)
RollupSum returns the sum across a date range for a single (project, branch). Range is inclusive on both ends. Returned Rollup has empty Day and (Project, GitBranch) copied from args so callers don't need to re-thread them.
Used by the budget evaluator for weekly/monthly caps, and by the CLI for "status --period=week" output.
func (*DB) StatusByProject ¶
StatusByProject returns rollup totals grouped by (project, branch) across a date range. Ordered by project then branch for deterministic CLI output.
Empty result is not an error — a user with nothing tracked yet gets a nil slice and no rows.
func (*DB) SyncAggregates ¶ added in v0.1.9
SyncAggregates returns per-(project, branch, model, day) aggregates for every event at or after since.
Days are UTC calendar days, bucketed in Go from each event's timestamp rather than in SQL. The events.ts column is stored in Go's native time format ("2006-01-02 15:04:05.999 -0700 MST"), which SQLite's strftime/date functions cannot parse, so any SQL day bucketing would silently return NULL. Scanning into time.Time and formatting in Go is the only correct path here.
The result is ordered by day, then project, branch, model for deterministic output and stable chunking downstream.
type Rollup ¶
type Rollup struct {
Project string
GitBranch string
Day string
EventCount int
InputTokens int
OutputTokens int
CacheReadTokens int
CacheWrite5mTokens int
CacheWrite1hTokens int
CostUSD float64
}
Rollup is one (project, branch, day) aggregate. For range-sum queries, Day is empty because the result spans multiple days.
type SyncAggregate ¶ added in v0.1.9
type SyncAggregate struct {
Project string
GitBranch string
Model string
Day string // YYYY-MM-DD (UTC)
CostUSD float64
InputTokens int
OutputTokens int
CacheReadTokens int
CacheWrite5mTokens int
CacheWrite1hTokens int
}
SyncAggregate is one (project, branch, model, day) cost-and-token aggregate derived from the events table, shaped for pushing to an external dashboard. Unlike the rollups table it carries the model dimension, which dashboards use for per-model breakdowns.