grit-core 0.2.2

Embedded, bi-temporal property graph for agent memory: one SQLite file, in-process, deterministic
Documentation
//! Forward-only schema migrations via `PRAGMA user_version`
//! (Design Invariant 8).

use rusqlite::Connection;

use crate::error::{Error, Result};

/// The schema version this build reads and writes.
pub const SCHEMA_VERSION: i64 = 3;

/// Creates the LATEST schema from scratch (schema.sql tracks head).
const SCHEMA_LATEST: &str = include_str!("schema.sql");

/// v1 → v2 (grit 0.2.0): per-field LWW records backing `UpdateNode`.
/// Frozen — never edit; schema.sql carries the same DDL for fresh DBs.
const MIGRATE_1_TO_2: &str = "
CREATE TABLE node_updates (
    node_id TEXT NOT NULL,
    field   TEXT NOT NULL,
    value   TEXT NOT NULL,
    hlc     TEXT NOT NULL,
    PRIMARY KEY (node_id, field)
) STRICT, WITHOUT ROWID;
";

/// v2 → v3 (grit 0.2.2): `episodes.kind` — a free-form source-kind tag
/// (`"message"` / `"text"` / `"json"`), opaque to grit, empty for
/// pre-existing rows. Frozen — never edit.
const MIGRATE_2_TO_3: &str = "
ALTER TABLE episodes ADD COLUMN kind TEXT NOT NULL DEFAULT '';
";

/// Bring `conn`'s schema up to [`SCHEMA_VERSION`]. A brand-new database gets
/// the full latest schema; older versions are migrated step by step; a newer
/// version is refused (migrations are forward-only, never down).
pub(crate) fn migrate(conn: &mut Connection) -> Result<()> {
    loop {
        let version: i64 = conn.query_row("PRAGMA user_version", [], |r| r.get(0))?;
        match version {
            SCHEMA_VERSION => return Ok(()),
            v if v > SCHEMA_VERSION => {
                return Err(Error::SchemaTooNew {
                    found: v,
                    supported: SCHEMA_VERSION,
                });
            }
            0 => apply_step(conn, SCHEMA_LATEST, SCHEMA_VERSION)?,
            1 => apply_step(conn, MIGRATE_1_TO_2, 2)?,
            2 => apply_step(conn, MIGRATE_2_TO_3, 3)?,
            v => unreachable!("no migration path from version {v}"),
        }
    }
}

fn apply_step(conn: &mut Connection, sql: &str, target: i64) -> Result<()> {
    let tx = conn.transaction()?;
    tx.execute_batch(sql)?;
    // PRAGMA can't take bound parameters; target is a compile-time constant.
    tx.pragma_update(None, "user_version", target)?;
    tx.commit()?;
    Ok(())
}