Documentation

Data Model

SQLite schema v10: tables, migrations, indexes and invariants.

3. DATA MODEL

Verified against crates/blazar-core/src/store.rs (SCHEMA_VERSION = 10) in Blazar v0.19.0.

Database type(s) and justification

SQLite (embedded via rusqlite 0.40, bundled feature — no external SQLite dependency), file <data_dir>/blazar.db.

Justification: single-writer local metadata workload (engine roster, model catalog, per-(model,engine) profiles, key usage counters, bench history, durable jobs and response chains). Zero-ops, ACID, no server process to clash with the local-first product model.

Connection pragmas (set at open): journal_mode = WAL, synchronous = NORMAL, foreign_keys = ON, busy_timeout = 5000 (ms).

Migrations: driven by PRAGMA user_version against SCHEMA_VERSION = 10. The ladder:

  • v4 — engines.kind added (ALTER TABLE for pre-existing databases; rows created before v4 default to 'llamacpp').
  • v5→v6 — the three fixed diffusion columns (vae_path, llm_path, llm_vision_path) folded into one flag-keyed components JSON column (families differ in dialect: Qwen pairs --vae/--llm, FLUX pairs --t5xxl/--clip_l). Legacy values are collected up front, folded, and the old columns dropped.
  • v6→v7 — bench_history grew the HTTP-lane columns (kind + probe metrics). NULL metrics mark rows the llama-bench lane wrote.
  • v7→v8 — four new tables: jobs, job_events, responses, model_caps (the durable jobs plane).
  • v8→v9 — models.last_used_at added, backfilled from pulled_at so no existing model is instantly “unused” (drives blazar prune --unused).
  • v9→v10 — responses.conversation (listing handle for /v1/conversations) and responses.body_json (the exact completed body, so background-mode polls return what the foreground call would have received). Both nullable-or-default — no backfill owed.

Snapshot: snapshot_db_to uses VACUUM INTO — transactionally consistent, fails if destination already exists.

Tables

engines

Field Type Nullable Default Notes
tag TEXT no — PRIMARY KEY
asset TEXT no — download/build provenance
sha256 TEXT no — integrity digest of the engine artifact
installed_at INTEGER no — unix timestamp
active INTEGER no 0 exactly one row should have 1
manifest TEXT no — JSON manifest
kind TEXT no 'llamacpp' v4 column; one of llamacpp, mistralrs, sglang, sdcpp, whisper, mlx

Key operations (store.rs):

  • upsert_engine — ON CONFLICT(tag) updates asset/sha/installed_at/manifest/kind but never touches active.
  • set_active_engine — single transaction: exists-check → UPDATE active=0 (all) → UPDATE active=1 WHERE tag=?. Unknown tag → Err, state untouched. Invariant: never leaves zero active engines (failure path rolls back).
  • active_engine() — WHERE active = 1.
  • list_engines() — ORDER BY installed_at DESC, rowid DESC (newest first).
  • delete_engine.

Lane-relevant manifest keys (engines.manifest JSON): source ("fork" | "upstream" — drives lane_class() routing preference and fork-tree provenance re-stamping for --source-dir builds), repo/ref_pin (immutable fork pin, full SHA verified after fetch), architectures (mined from the build’s llama-arch.cpp; the routing advertiser set), trust (curated/user tier), and supersede state superseded_by + superseded_at_epoch (stamped only when mainstream covers every lane architecture; there is no supersede column). The shipped registry file registry/capability-lanes.json mirrors one row per curated lane: id, repo, ref_sha, upstream_pr, architectures, backends, status, added_at.

models

Field Type Nullable Default Notes
name TEXT no — PRIMARY KEY; flat name
repo TEXT no — source repository
quant TEXT no — quant label; unreliable for lane detection (see invariants)
path TEXT no — local file path (first shard for multi-shard GGUF)
bytes INTEGER no — size
sha256 TEXT yes NULL content digest when known
mmproj_path TEXT yes NULL multimodal projector file
components TEXT no '[]' v6 column; JSON array of {flag, path} diffusion companions (--vae, --llm, --t5xxl…) — the sdcpp lane’s component set; empty on text rows
shards INTEGER no 1 GGUF shard count (-0000N-of-0000M)
arch TEXT yes NULL architecture from GGUF metadata
params REAL yes NULL parameter count
ctx_train INTEGER yes NULL trained context length
pulled_at INTEGER no — unix timestamp
last_used_at INTEGER no 0 v9 column; last spawn activity, backfilled from pulled_at — drives blazar prune --unused

Key operations: model upsert rejects # in name (reserved for replica keys model#N; the store returns a teaching CoreError::Catalog error). resolve_model_name: for ollama migrants, a colon→dash swap is applied only if it lands on an existing row (exact match tried first; on miss the input is returned verbatim). Pull never uses this resolver — in pull targets the colon is the owner/repo:QUANT separator.

profiles

Field Type Nullable Default Notes
model_name TEXT no — composite PK part 1
engine_tag TEXT no — composite PK part 2
args_hash TEXT no — digest of argv
args_json TEXT no — compiled launch argv
benchmark_json TEXT yes NULL recorded bench result
updated_at INTEGER no — unix timestamp

PRIMARY KEY (model_name, engine_tag). One launch profile per (model, engine); upsert replaces. Produced by blazar tune / automatic profile compilation; consumed by the supervisor at spawn.

loras

Field Type Nullable Default Notes
id INTEGER no — PRIMARY KEY AUTOINCREMENT
model_name TEXT no — owning model
path TEXT no — LoRA adapter file
scale REAL no 1.0 adapter scale

key_usage

Field Type Nullable Default Notes
day TEXT no — UTC YYYY-MM-DD; PK part 1
name TEXT no — key name; PK part 2
requests INTEGER no 0  
tokens INTEGER no 0  

PRIMARY KEY (day, name). set_key_usage_day writes the absolute whole-day state per batch inside one transaction (not increments); key_usage(day) reads a day.

bench_history

Field Type Nullable Default Notes
id INTEGER no — PRIMARY KEY AUTOINCREMENT
ts INTEGER no — unix seconds (unixepoch())
engine_tag TEXT no —  
model TEXT no —  
tg_tokens_per_sec REAL no — decode (token generation) throughput
pp_tokens_per_sec REAL no 0 prefill throughput
ctx INTEGER no 0 context size used
kind TEXT no 'llamacpp' v7 column; lane that produced the row
ttft_ms REAL yes NULL v7 column; HTTP lane first-token latency
gen_tps REAL yes NULL v7 column; HTTP lane generation t/s (usage)
prompt_tps REAL yes NULL v7 column; HTTP lane prompt t/s (usage)
images_per_sec REAL yes NULL v7 column; HTTP image lane images/s
detail TEXT yes NULL v7 column; HTTP lane probe params JSON

Operations: record_bench (insert), latest_bench_by_time (single newest row), latest_benches (newest per model).

jobs

Durable asynchronous work units (/v1/jobs plane) — audio, image, video, and batch requests that outlive the HTTP call that submitted them.

Field Type Nullable Default Notes
id TEXT no — PRIMARY KEY
kind TEXT no — audio | image | video | batch
model TEXT yes NULL serving model, when one is named
state TEXT no — queued | running | completed | failed | cancelled | abandoned
request_json TEXT no '{}' kind-specific submit descriptor
result_json TEXT yes NULL terminal payload (small, inline)
error TEXT yes NULL  
artifact_path TEXT yes NULL larger result/input file, relative to data_dir
created_at INTEGER no — unix timestamp
updated_at INTEGER no — unix timestamp

Indexes: idx_jobs_state (state), idx_jobs_updated (updated_at).

Key operations: insert_job, set_job_state, set_job_artifact, get_job, list_jobs (state + limit filters), plus retention: sweep_jobs_updated_before and prune_jobs.

job_events

Append-only lifecycle log per job.

Field Type Nullable Default Notes
seq INTEGER no — PRIMARY KEY AUTOINCREMENT
job_id TEXT no — owning job
ts INTEGER no — unix timestamp
kind TEXT no — created | started | completed | failed | cancelled | abandoned
data_json TEXT yes NULL optional event detail

Index: idx_job_events_job (job_id, seq).

Key operations: append_job_event, job_events(job_id, limit) (limit clamped to 1..=1000).

responses

Durable response chains — completed chat/completions records with client-chainable ids.

Field Type Nullable Default Notes
id TEXT no — PRIMARY KEY; resp_... client-chainable id
model TEXT no —  
input_json TEXT no —  
output_json TEXT no —  
input_tokens INTEGER yes NULL  
output_tokens INTEGER yes NULL  
ts INTEGER no — unix timestamp
conversation TEXT no '' v10 column; grouping key for /v1/conversations
body_json TEXT yes NULL v10 column; exact completed body (background mode fidelity)

Index: idx_responses_conversation (conversation, ts) — created in the migration (not SCHEMA_SQL) because on an upgrade database SCHEMA_SQL runs before the ALTERs and the index would reference a not-yet-added column.

Key operations: put_response, get_response, prune_responses.

model_caps

Per-(model, engine) capability probe results — what a given engine actually served for a given model (feature discovery by measurement).

Field Type Nullable Default Notes
model TEXT no — composite PK part 1
engine_tag TEXT no — composite PK part 2
tested_at INTEGER no — unix timestamp
caps_json TEXT no — measured capability set

PRIMARY KEY (model, engine_tag). Key operations: put_model_caps, get_model_caps.

Entity relationships

engines 1 ──── * profiles * ──── 1 models        (profiles = join on (engine, model))
models 1 ──── * loras                            (loras.model_name -> models.name)
[[keys]].name ──── * key_usage(name, day)        (usage rows keyed by config-defined key name)
engines/models ──── * bench_history              (denormalized tag/model strings)
models.mmproj_path -> mmproj file on disk        (not an FK; path reference)
jobs 1 ──── * job_events                         (job_events.job_id -> jobs.id)
jobs/responses.model -> models.name (by string, not FK)
(model, engine_tag) ──── model_caps              (probe result per pair; engine_tag -> engines.tag)

No SQL foreign keys are declared between these tables (foreign_keys pragma ON applies to any FKs; these tables rely on application-level integrity). INFERRED: relationships are enforced by store API usage, not DDL constraints.

Indexes and constraints

  • PRIMARY KEYs as listed (engines.tag, models.name, profiles(model_name,engine_tag), loras.id, key_usage(day,name), bench_history.id, jobs.id, job_events.seq, responses.id, model_caps(model,engine_tag)).
  • Secondary indexes: idx_jobs_state, idx_jobs_updated, idx_job_events_job(job_id, seq), idx_responses_conversation(conversation, ts).
  • engines.active has no unique partial index — the “exactly one active” rule is enforced transactionally by set_active_engine, not by DDL.
  • Application-level constraints: models name may not contain #; engine kind restricted by EngineKind::from_str at the application layer (unknown kind → teaching error unknown engine kind ... (supported: llamacpp, mistralrs, sglang, sdcpp, whisper, mlx)).
  • WAL checkpointing is SQLite-default.

Business invariants that must always hold

  1. Exactly one active engine (or the intended pre-existing state) after any set_active_engine call — the store never transitions to zero active engines, even on unknown-tag errors.
  2. models.name never contains # — upsert rejects it; # is reserved for replica instance keys (model#N).
  3. profiles rows are unique per (model_name, engine_tag) — upsert replaces atomically.
  4. Quantized-safetensors detection never trusts models.quant — the signal is QUANTIZED_SAFETENSORS_TOKENS = [awq, gptq, fp8]: true iff the path is NOT .gguf AND any word-boundary token of name/repo/path (lowercased, split on non-alphanumeric) is one of those tokens. Known mislabels: AWQ pulls record quant 4BIT, fp8-dynamic records BF16. Word-boundary matters: a name containing hawk stays clean. GGUF paths never match.
  5. key_usage rows are whole-day absolute snapshots — writers must never increment; they compute the day total and write it in one transaction.
  6. engines.kind legacy default — rows predating v4 read as llamacpp (serde default), so kind-sensitive logic must treat missing kind as llamacpp.
  7. Colon resolution only onto existing rows — resolve_model_name never creates a mapping; a miss returns the input verbatim.
  8. job_events is append-only — lifecycle transitions are logged (created → started → terminal), never rewritten; jobs.state is the mutable projection of the same machine.
  9. Pre-v10 responses rows keep the reconstructed-shape GET contract — conversation/body_json are nullable-or-default with no backfill; readers must tolerate their absence.
Improve this page on GitHub