sqlite

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Jun 6, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package sqlite is the derived-cache store described in ARCHITECTURE § 4.

The SQLite database mirrors JSONL columns and adds writer-computed columns (model, tokens, key_hash, prefix_canonical_hash, parent_id, session_root_id). Everything in here is fully rebuildable from data/**/*.jsonl{,.gz}; SQLite is a rebuildable derived cache.

v0 uses modernc.org/sqlite (pure Go, no cgo). Slightly slower than mattn/go-sqlite3 but trivial to build / cross-compile.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("trace not found")

ErrNotFound is returned by GetByID when no row matches the given id.

Functions

This section is empty.

Types

type ListFilters

type ListFilters struct {
	Since         time.Time // ts_start >= Since (zero = no bound)
	Until         time.Time // ts_start <  Until (zero = no bound)
	Status        *int      // exact match if set
	StatusBucket  int       // 0 = no constraint; 2/4/5 = match 2xx/4xx/5xx range
	Model         string    // exact match if set
	Path          string    // exact match if set
	PathPrefix    string    // matched as `LIKE <prefix>%`; mutually exclusive with Path
	KeyHashPrefix string    // accepts 8- or 16-char prefix; matches by LIKE
	SessionRootID string    // exact match if set
	Project       string    // exact match on client_project if set
	Limit         int       // 1..500; <=0 means default 100

	// Cursor is opaque base64 of "<ts_start_ms>:<id>"; rows strictly older
	// than the cursor (ts_start DESC, id DESC ordering) are returned.
	// Empty = first page.
	CursorTsStart int64
	CursorID      string
}

ListFilters bounds and filters /api/traces queries per ARCHITECTURE § 6.2. All fields are optional; zero values mean "no constraint".

type ListPage

type ListPage struct {
	Rows         []Row
	NextCursorMs int64  // 0 if no next page
	NextCursorID string // empty if no next page
}

ListPage is the paginated result.

type Row

type Row struct {
	ID       string
	TsStart  time.Time
	TsEnd    time.Time
	Client   string
	Method   string
	Path     string
	Upstream string
	Status   int

	Disconnected  bool
	TruncatedReq  bool
	TruncatedResp bool

	// Protocol field copies (may be NULL if absent). Populated from
	// trace.Trace by the writer; pass nil for unknown.
	Model            *string
	Stream           *bool
	PromptTokens     *int64
	CompletionTokens *int64
	TotalTokens      *int64
	FinishReason     *string

	// Extracted usage fields. Deterministic copies of named protocol usage
	// fields. Nil when the upstream response did not carry the field; nil is
	// distinct from a real zero.
	CachedTokens        *int64 // sum of cache-hit tokens across protocols
	CacheCreationTokens *int64 // Anthropic cache_creation_input_tokens (cache miss writes)
	ReasoningTokens     *int64 // OpenAI Responses reasoning tokens from output

	// Client-identity fields. Deterministic copies of named request-header
	// fields routed through ExtractClient's taxonomy table; no heuristic UA
	// parsing.
	ClientKind    *string // taxonomy row key (e.g. "claude-code-desktop")
	ClientVersion *string // version string from the matching header (if any)

	// Project-context field. Deterministic copy of the project name parsed from
	// the request body's system/instructions text. Nil when the trace carried
	// no project signal.
	ClientProject *string // project name (e.g. "my-repo" from `# my-repo` heading)

	// Identifiers.
	KeyHash string

	// Session fields. Populated by AppendTrace (writer side) or by
	// scanRow (read side). Callers should NOT set these on the writer
	// path — they are derived inside the transaction.
	PrefixLen           *int
	PrefixCanonicalHash *string
	ParentID            *string
	SessionRootID       string

	// JSONL location.
	JSONLPath   string
	JSONLOffset int64

	// MediaCount is the number of extracted media files for this trace. Filled
	// by the writer after extraction runs; default 0 when extraction is disabled
	// or the trace has no extractable media.
	// Deterministic count of named protocol fields only.
	MediaCount int
}

Row is the Go-side projection of one traces row. Session-inference columns (PrefixLen, PrefixCanonicalHash, ParentID, SessionRootID) are filled by AppendTrace, not by the caller — pass them empty.

func FromTrace

func FromTrace(t trace.Trace, keyHash, jsonlPath string, jsonlOffset int64) Row

FromTrace builds a Row from a trace.Trace plus the writer's known extras. Session columns are left to AppendTrace; derived columns (model, tokens, finish_reason) are left nil — caller fills them.

type SessionSummary

type SessionSummary struct {
	SessionRootID    string
	NTurns           int64
	FirstTs          time.Time
	LastTs           time.Time
	LastPath         string
	LastStatus       int
	LastModel        string
	DistinctKeyCount int64
	OkCount          int64
	ErrCount         int64
}

SessionSummary is one row of the /api/sessions response — an aggregate over a session_root_id. Useful for "what conversations are happening" without paging through every per-turn trace.

type Store

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

Store is the writer-side handle into index.sqlite. One Writer holds one Store. Read-API handlers will get their own Store handles in M4 (separate connections, WAL allows concurrent reads).

func Open

func Open(path string) (*Store, error)

Open opens or creates index.sqlite at path, runs migration, and applies the pragmas from ARCHITECTURE § 4.

func (*Store) AllMatching

func (s *Store) AllMatching(filters ListFilters, hardCap int) ([]Row, error)

AllMatching returns every row matching filters, ordered chronologically (ts_start ASC, id ASC for a deterministic tiebreak).

Unlike List, this method ignores filters.Limit (the per-page cap) and any cursor fields, and orders ASC so callers (the exporter) can stream results into a reproducible zip. The hardCap argument optionally bounds the total rows fetched; pass 0 (or any non-positive value) for unlimited — the export path uses this. The cap parameter is kept on the signature for future callers that want a guard (rate-limit, preview).

The WHERE-clause is built from the same fields as List (status, status bucket, model, path/prefix, key_hash prefix, session_root_id, since, until) — keep them in sync if List grows new filters.

func (*Store) AppendTrace

func (s *Store) AppendTrace(r Row, sessionPrefix []json.RawMessage) error

AppendTrace inserts a row and runs session inference atomically.

sessionPrefix is the result of session.Build(path, req.body). Pass nil if the trace has no session concept (embeddings, image gen, etc.); the row's session_root_id defaults to id (self-root) and parent_id stays NULL.

Everything happens in one transaction: this gives us one fsync per trace (instead of two for INSERT + UPDATE) and atomic session-inference reads. On commit failure the row is not inserted; the caller's JSONL line is still on disk and will be picked up by startup rebuild.

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying *sql.DB.

func (*Store) CountMatching added in v0.1.1

func (s *Store) CountMatching(ctx context.Context, filters ListFilters, hardCap int) (int, error)

CountMatching returns the number of rows that match filters, capped at hardCap+1. The subquery pattern (`SELECT COUNT(*) FROM (SELECT 1 ... LIMIT ?)`) lets SQLite stop counting once the cap is reached rather than scanning every matching row — important for the export pre-flight check on multi-million-row stores.

Pass hardCap = 0 (or any non-positive value) to count without a cap. Callers (api/export.go) use the result to short-circuit oversized exports with a 413 BEFORE any zip bytes hit the wire. Returns the raw count; callers compare against their own cap.

Reuses buildListConds so the predicate stays in lockstep with List and StreamMatching — adding a new ListFilters field only requires editing buildListConds.

func (*Store) CountRows

func (s *Store) CountRows() (int64, error)

CountRows is used by the startup rebuild path to detect a "fresh" index.

func (*Store) DeleteByJSONLPath added in v0.1.1

func (s *Store) DeleteByJSONLPath(ctx context.Context, canonicalPath string) (int64, error)

DeleteByJSONLPath removes every traces row whose jsonl_path column equals canonicalPath. Returns the affected row count.

Called by the storage coordinator's eviction sequence (see internal/storage/eviction.go.deleteIfIdle) after the on-disk JSONL + media subtree have been removed. Backed by idx_jsonl_path so the DELETE plan is a single index seek even with millions of rows.

Context-aware: a canceled ctx surfaces as an error from ExecContext and the caller (deleteIfIdle) returns it up the stack to the monitor tick, which logs and continues. Zero rows affected is NOT an error — it's the "already cleaned up by a previous partial-failure tick" case; deleteIfIdle's defensive-delete model assumes this can happen.

func (*Store) FindParent

func (s *Store) FindParent(keyHash string, strictHashes []string) (parentID, sessionRootID string, prefixLen int, ok bool, err error)

FindParent runs the session-inference IN-clause query described in ARCHITECTURE § 5.2. Given a trace's strict-prefix canonical hashes (in descending-length order), it returns the longest-matching prior trace's (id, session_root_id, prefix_len). If no prior trace matches, ok is false.

strictHashes MUST be ordered by length descending so the SQL's ORDER BY prefix_len DESC stably returns the longest match. The caller's responsibility — sqlite has no information about prefix semantics, only the hashes.

func (*Store) GetByID

func (s *Store) GetByID(id string) (Row, error)

GetByID returns the row with the given trace ID. Returns ErrNotFound when no such row exists.

func (*Store) List

func (s *Store) List(filters ListFilters) (ListPage, error)

List returns at most filters.Limit (default 100, max 500) rows ordered by (ts_start DESC, id DESC) that match the filters.

Always orders by ts_start DESC for cursor stability. To page, pass the previous page's NextCursorMs / NextCursorID back as filters.CursorTsStart / filters.CursorID.

func (*Store) ListDistinctJSONLPaths added in v0.1.1

func (s *Store) ListDistinctJSONLPaths(ctx context.Context) ([]string, error)

ListDistinctJSONLPaths returns every distinct jsonl_path stored in the traces table, ordered ASC for deterministic test output. With idx_jsonl_path SQLite can satisfy the DISTINCT scan from the index alone — no table-row visits.

Used by the storage coordinator's reconcileOrphans path to find rows referencing files that no longer exist on disk (post-crash gap between os.Remove + DELETE). Returned slice is safe for the caller to mutate; nothing else in the package shares it.

func (*Store) ListSessions

func (s *Store) ListSessions(since time.Time, limit int) ([]SessionSummary, error)

ListSessions returns the most recent sessions, latest-activity first. limit defaults to 100, max 500. Optional since filter applies to the session's last-activity timestamp.

func (*Store) StreamMatching added in v0.1.1

func (s *Store) StreamMatching(ctx context.Context, filters ListFilters, visit func(Row) error) error

StreamMatching iterates every row matching filters in the same order AllMatching returns them (ts_start ASC, id ASC), invoking visit on each row. If visit returns an error, iteration stops and that error is returned to the caller. Used by the streaming export path so the exporter never has to hold the full result set in memory.

One database connection is borrowed via db.Conn(ctx) for the entire cursor lifetime — the WAL snapshot stays consistent across all visit calls without blocking the writer. The conn is released on return so the standard pool can serve subsequent queries.

No internal LIMIT: callers gate oversize exports via CountMatching BEFORE calling StreamMatching. The cursor itself is unbounded so adopters opting into `?all=1` see every row.

Context propagation: a canceled ctx surfaces as the visit boundary (next QueryContext call returns ctx.Err()) — partial-write callers (the zip exporter) recover from that mid-stream.

Jump to

Keyboard shortcuts

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