Skip to main content

Module sqlite

Module sqlite 

Source
Expand description

SqliteRunStore — SQLite-backed RunStore using [rusqlite-isle].

The Connection is confined to a dedicated OS thread by AsyncIsle; every call is a typed closure dispatched over a bounded channel. step_entries, degradations, and result_ref are stored as JSON blobs — the former two are pure trace/observability artifacts (not queried relationally), the latter is caller-defined payload shape. append_step_entry/append_degradation run as a read-modify-write inside a single transaction so concurrent appenders don’t clobber each other’s entries.

§Schema

CREATE TABLE IF NOT EXISTS runs (
  id                 TEXT PRIMARY KEY,
  task_id            TEXT NOT NULL,
  status             TEXT NOT NULL,      -- JSON-encoded `RunStatus`
  step_entries_json  TEXT NOT NULL,      -- JSON-encoded `Vec<StepEntry>`
  degradations_json  TEXT NOT NULL DEFAULT '[]', -- JSON-encoded `Vec<DegradationEntry>` (GH #32)
  operator_sid       TEXT,
  current_json       TEXT,               -- JSON object: slot -> `Assignee`, NULL when no slot is held
  next_generation    INTEGER NOT NULL DEFAULT 0, -- the model's `G`
  result_ref_json    TEXT,               -- JSON-encoded `serde_json::Value`, NULL when unset
  input_json         TEXT,               -- opaque launch-input snapshot for resume, NULL when unset
  created_at         INTEGER NOT NULL,
  updated_at         INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_runs_task_id ON runs(task_id, created_at);

degradations_json (GH #32), input_json (the resume launch-input snapshot) and the assignment pair current_json / next_generation were all added after the initial release; each migration is applied idempotently on open via a PRAGMA table_info(runs) existence check followed by the matching ALTER TABLE runs ADD COLUMN … when missing, so pre-existing database files pick up the columns without a manual migration step. input_json and current_json are nullable TEXT (no default) — rows written before those features read back None (current_json NULL = no slot held); next_generation carries DEFAULT 0 so a back-filled row starts at the launch value of G.

current_json holds the whole slot -> Assignee map as one JSON object, not one row per slot: the map is read and rewritten whole on every assignment event anyway (the event has to bump the sibling next_generation in the same transaction), and nothing queries a Run by who holds one of its slots. A map that has gone empty is stored back as SQL NULL, so “no slot held” has exactly one representation on disk.

acquire_assignee/vacate_assignee bump next_generation and rewrite current_json as a read-modify-write inside one Immediate transaction — the same shape as append_step_entry, and for the same reason: the increment-and-stamp spans two columns, which a conditional UPDATE (the try_transition compare-and-set) cannot express. Two acquires naming different slots take the same path, so the map merge is serialized too and neither can drop the other’s entry.

The two events have separate bodies rather than one parameterized one, because they no longer share a shape: an Assign always writes (A8 — no precondition on the incumbent), while a Vacant first compares the seat’s generation against the one its caller observed and writes nothing when they differ. Putting the comparison inside the same transaction as the removal is the whole point of the verb — see VacateOutcome — so it cannot be hoisted into a shared prologue.

Structs§

SqliteRunStore
SQLite-backed persistent RunStore.