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.kindadded (ALTER TABLEfor 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-keyedcomponentsJSON 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_historygrew 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_atadded, backfilled frompulled_atso no existing model is instantly “unused” (drivesblazar prune --unused). - v9→v10 —
responses.conversation(listing handle for/v1/conversations) andresponses.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 touchesactive.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.activehas no unique partial index — the “exactly one active” rule is enforced transactionally byset_active_engine, not by DDL.- Application-level constraints: models name may not contain
#; enginekindrestricted byEngineKind::from_strat the application layer (unknown kind → teaching errorunknown engine kind ... (supported: llamacpp, mistralrs, sglang, sdcpp, whisper, mlx)). - WAL checkpointing is SQLite-default.
Business invariants that must always hold
- Exactly one active engine (or the intended pre-existing state) after any
set_active_enginecall — the store never transitions to zero active engines, even on unknown-tag errors. models.namenever contains#— upsert rejects it;#is reserved for replica instance keys (model#N).profilesrows are unique per (model_name, engine_tag) — upsert replaces atomically.- Quantized-safetensors detection never trusts
models.quant— the signal isQUANTIZED_SAFETENSORS_TOKENS = [awq, gptq, fp8]: true iff the path is NOT.ggufAND any word-boundary token of name/repo/path (lowercased, split on non-alphanumeric) is one of those tokens. Known mislabels: AWQ pulls record quant4BIT, fp8-dynamic recordsBF16. Word-boundary matters: a name containinghawkstays clean. GGUF paths never match. key_usagerows are whole-day absolute snapshots — writers must never increment; they compute the day total and write it in one transaction.engines.kindlegacy default — rows predating v4 read asllamacpp(serde default), so kind-sensitive logic must treat missing kind as llamacpp.- Colon resolution only onto existing rows —
resolve_model_namenever creates a mapping; a miss returns the input verbatim. job_eventsis append-only — lifecycle transitions are logged (created→started→ terminal), never rewritten;jobs.stateis the mutable projection of the same machine.- Pre-v10
responsesrows keep the reconstructed-shape GET contract —conversation/body_jsonare nullable-or-default with no backfill; readers must tolerate their absence.