Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
92 lines
3.3 KiB
Rust
92 lines
3.3 KiB
Rust
//! SQLite store — schema + open + migrate.
|
|
//!
|
|
//! Constructor Pattern: this cube owns the DDL, the schema-version pragma,
|
|
//! and `open_db`. CRUD lives in `registry.rs`. Schema changes MUST bump
|
|
//! `SCHEMA_VERSION` and append to `MIGRATIONS`; never reorder.
|
|
|
|
use anyhow::{Context, Result};
|
|
use rusqlite::Connection;
|
|
use std::path::Path;
|
|
|
|
/// v1 — initial schema. Tracks one row per (path, body_sha) tuple. The DNA
|
|
/// is the UNIQUE wire-format key. `superseded_by` points at a NEWER row's
|
|
/// DNA when this row is no longer active. `created` and `modified` are
|
|
/// Unix epoch seconds; they bracket the row's life from first registration
|
|
/// to its most recent re-touch.
|
|
pub const SCHEMA_V1: &str = "CREATE TABLE IF NOT EXISTS blocks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
dna TEXT NOT NULL UNIQUE,
|
|
block_type TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
caps TEXT NOT NULL,
|
|
scope_sha TEXT NOT NULL,
|
|
body_sha TEXT NOT NULL,
|
|
nonce TEXT NOT NULL,
|
|
created INTEGER NOT NULL,
|
|
modified INTEGER NOT NULL,
|
|
superseded_by TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_blocks_type ON blocks(block_type);
|
|
CREATE INDEX IF NOT EXISTS idx_blocks_path ON blocks(path);
|
|
CREATE INDEX IF NOT EXISTS idx_blocks_body ON blocks(body_sha);";
|
|
|
|
/// Schema version. Compared against `PRAGMA user_version`. Bumped together
|
|
/// with `MIGRATIONS`. Mismatch (DB is newer than this binary) → exit 3.
|
|
pub const SCHEMA_VERSION: u32 = 1;
|
|
|
|
/// Ordered migrations. Index = target version (1-based). Append only.
|
|
pub const MIGRATIONS: &[&str] = &[SCHEMA_V1];
|
|
|
|
/// Open or create the SQLite store at `path`. Runs all pending migrations
|
|
/// transactionally. Returns the connection ready for CRUD use. Schema
|
|
/// version mismatch (DB ahead of binary) returns an Err, NOT a silent
|
|
/// downgrade — callers should exit 3.
|
|
pub fn open_db<P: AsRef<Path>>(path: P) -> Result<Connection> {
|
|
let conn = Connection::open(&path)
|
|
.with_context(|| format!("open registry sqlite at {}", path.as_ref().display()))?;
|
|
migrate(&conn)?;
|
|
Ok(conn)
|
|
}
|
|
|
|
/// Apply pending migrations atomically — DDL + user_version bump in one
|
|
/// transaction per version. Mirrors the kei-ledger schema.rs idiom so a
|
|
/// crash mid-migration leaves a consistent file.
|
|
pub fn migrate(conn: &Connection) -> Result<()> {
|
|
let current: i64 = conn
|
|
.query_row("PRAGMA user_version", [], |r| r.get(0))
|
|
.unwrap_or(0);
|
|
if current as u32 > SCHEMA_VERSION {
|
|
anyhow::bail!(
|
|
"registry schema v{} is newer than binary v{}; upgrade kei-registry",
|
|
current,
|
|
SCHEMA_VERSION
|
|
);
|
|
}
|
|
for (i, sql) in MIGRATIONS.iter().enumerate() {
|
|
let target = (i + 1) as i64;
|
|
if current < target {
|
|
apply_one(conn, sql, target)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_one(conn: &Connection, sql: &str, target: i64) -> Result<()> {
|
|
conn.execute_batch("BEGIN IMMEDIATE")?;
|
|
let step: Result<()> = (|| {
|
|
conn.execute_batch(sql)?;
|
|
conn.pragma_update(None, "user_version", target)?;
|
|
Ok(())
|
|
})();
|
|
match step {
|
|
Ok(()) => {
|
|
conn.execute_batch("COMMIT")?;
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
let _ = conn.execute_batch("ROLLBACK");
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|