use rusqlite::Connection;
use kimetsu_core::KimetsuResult;
pub fn apply_pragmas(conn: &Connection) -> KimetsuResult<()> {
conn.pragma_update(None, "cache_size", -65536_i64)?;
conn.pragma_update(None, "temp_store", "MEMORY")?;
let _ = conn.pragma_update(None, "mmap_size", 268_435_456_i64);
let _ = conn.pragma_update(None, "synchronous", "NORMAL");
Ok(())
}
pub fn initialize(conn: &Connection) -> KimetsuResult<()> {
apply_pragmas(conn)?;
create_baseline(conn)?;
crate::migrate::run_migrations(conn)?;
let _ = conn.execute_batch("DROP TABLE IF EXISTS memory_vec;");
Ok(())
}
#[cfg(test)]
pub fn create_baseline_for_test(conn: &Connection) -> KimetsuResult<()> {
create_baseline(conn)
}
fn create_baseline(conn: &Connection) -> KimetsuResult<()> {
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "busy_timeout", 15_000)?;
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS schema_info (
key TEXT PRIMARY KEY,
value INTEGER NOT NULL
);
INSERT OR IGNORE INTO schema_info (key, value)
VALUES ('kimetsu_schema_version', 1);
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
task TEXT NOT NULL,
started_at TEXT NOT NULL,
ended_at TEXT,
terminal_kind TEXT,
model TEXT,
total_cost_usd REAL NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
ts TEXT NOT NULL,
kind TEXT NOT NULL,
schema_version INTEGER NOT NULL,
payload_json TEXT NOT NULL,
origin TEXT,
hlc TEXT
);
CREATE INDEX IF NOT EXISTS idx_events_run_ts ON events (run_id, ts);
CREATE INDEX IF NOT EXISTS idx_events_kind_ts ON events (kind, ts);
CREATE TABLE IF NOT EXISTS sources (
source_id TEXT PRIMARY KEY,
kind TEXT NOT NULL,
ref TEXT NOT NULL,
hash TEXT,
added_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS memories (
memory_id TEXT PRIMARY KEY,
scope TEXT NOT NULL,
kind TEXT NOT NULL,
text TEXT NOT NULL,
normalized_text TEXT NOT NULL,
confidence REAL NOT NULL,
source_event_id TEXT,
provenance_snapshot_json TEXT NOT NULL,
created_at TEXT NOT NULL,
last_used_at TEXT,
use_count INTEGER NOT NULL DEFAULT 0,
usefulness_score REAL NOT NULL DEFAULT 0.0,
invalidated_at TEXT,
invalidated_reason TEXT
);
CREATE INDEX IF NOT EXISTS idx_memories_scope_kind_norm
ON memories (scope, kind, normalized_text);
CREATE TABLE IF NOT EXISTS memory_proposals (
proposal_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
scope TEXT NOT NULL,
kind TEXT NOT NULL,
text TEXT NOT NULL,
rationale TEXT NOT NULL,
proposed_confidence REAL NOT NULL,
source_event_ids_json TEXT NOT NULL,
status TEXT NOT NULL,
decided_at TEXT,
decided_by TEXT,
decided_reason TEXT
);
CREATE INDEX IF NOT EXISTS idx_memory_proposals_status_run
ON memory_proposals (status, run_id);
CREATE TABLE IF NOT EXISTS repo_files (
repo_root TEXT NOT NULL,
path TEXT NOT NULL,
hash TEXT NOT NULL,
size INTEGER NOT NULL,
mtime TEXT NOT NULL,
language_guess TEXT NOT NULL,
snippet TEXT NOT NULL,
PRIMARY KEY (repo_root, path)
);
CREATE INDEX IF NOT EXISTS idx_repo_files_language
ON repo_files (repo_root, language_guess);
CREATE TABLE IF NOT EXISTS repo_manifests (
repo_root TEXT NOT NULL,
manifest_path TEXT NOT NULL,
manifest_kind TEXT NOT NULL,
parsed_summary_json TEXT NOT NULL,
hash TEXT NOT NULL,
mtime TEXT NOT NULL,
PRIMARY KEY (repo_root, manifest_path)
);
CREATE VIRTUAL TABLE IF NOT EXISTS repo_files_fts
USING fts5(repo_root, path, snippet, language_guess);
CREATE VIRTUAL TABLE IF NOT EXISTS repo_manifests_fts
USING fts5(repo_root UNINDEXED, manifest_path, manifest_kind, parsed_summary_json);
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
USING fts5(memory_id UNINDEXED, text, kind, scope);
",
)?;
Ok(())
}
pub(crate) fn migrate_v1_to_v2(conn: &Connection) -> KimetsuResult<()> {
add_column_if_missing(conn, "memory_proposals", "decided_reason TEXT")?;
add_column_if_missing(
conn,
"memories",
"usefulness_score REAL NOT NULL DEFAULT 0.0",
)?;
add_column_if_missing(conn, "memories", "invalidated_at TEXT")?;
add_column_if_missing(conn, "memories", "invalidated_reason TEXT")?;
add_column_if_missing(conn, "memories", "embedding BLOB")?;
add_column_if_missing(conn, "memories", "embedding_model TEXT")?;
add_column_if_missing(conn, "memories", "last_useful_at TEXT")?;
conn.execute_batch(
"
CREATE INDEX IF NOT EXISTS idx_memories_active_created
ON memories (invalidated_at, created_at);
",
)?;
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS memory_citations (
run_id TEXT NOT NULL,
memory_id TEXT NOT NULL,
turn INTEGER NOT NULL,
cited_at TEXT NOT NULL,
rationale TEXT,
PRIMARY KEY (run_id, memory_id, turn)
);
CREATE INDEX IF NOT EXISTS idx_citations_run
ON memory_citations (run_id);
CREATE INDEX IF NOT EXISTS idx_citations_memory
ON memory_citations (memory_id);
",
)?;
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS memory_conflicts (
conflict_id TEXT PRIMARY KEY,
new_memory_id TEXT NOT NULL,
existing_memory_id TEXT NOT NULL,
scope TEXT NOT NULL,
kind TEXT NOT NULL,
similarity REAL NOT NULL,
detected_at TEXT NOT NULL,
resolved_at TEXT,
resolution TEXT,
UNIQUE (new_memory_id, existing_memory_id)
);
CREATE INDEX IF NOT EXISTS idx_conflicts_unresolved
ON memory_conflicts (resolved_at, detected_at);
CREATE INDEX IF NOT EXISTS idx_conflicts_new_memory
ON memory_conflicts (new_memory_id);
-- v3.0 #3 Slice B: concurrent-supersede conflicts surfaced during team
-- sync (a member superseded to two DIFFERENT survivors by concurrent
-- edits). HLC replay still picks a deterministic winner; this records the
-- collision for human review. A PROJECTION — cleared + repopulated by
-- rebuild. survivor_a < survivor_b (canonicalized) so it records once.
CREATE TABLE IF NOT EXISTS sync_conflicts (
member_id TEXT NOT NULL,
survivor_a TEXT NOT NULL,
survivor_b TEXT NOT NULL,
detected_at TEXT NOT NULL,
PRIMARY KEY (member_id, survivor_a, survivor_b)
);
",
)?;
ensure_memories_fts_shape(conn)?;
ensure_repo_manifests_fts_shape(conn)?;
conn.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_memories_scope_model_active
ON memories (scope, embedding_model, invalidated_at);",
)?;
Ok(())
}
pub(crate) fn migrate_v2_to_v3(conn: &Connection) -> KimetsuResult<()> {
add_column_if_missing(conn, "memories", "superseded_by TEXT")?;
conn.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_memories_superseded
ON memories (superseded_by);",
)?;
Ok(())
}
pub(crate) fn migrate_v3_to_v4(conn: &Connection) -> KimetsuResult<()> {
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS memory_edges (
src_id TEXT NOT NULL,
dst_id TEXT NOT NULL,
edge_type TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (src_id, dst_id, edge_type)
);
CREATE INDEX IF NOT EXISTS idx_memory_edges_src
ON memory_edges (src_id, edge_type);
CREATE INDEX IF NOT EXISTS idx_memory_edges_dst
ON memory_edges (dst_id, edge_type);
",
)?;
Ok(())
}
pub(crate) fn migrate_v4_to_v5(conn: &Connection) -> KimetsuResult<()> {
crate::episode::create_work_episodes_table(conn)
}
pub(crate) fn migrate_v5_to_v6(conn: &Connection) -> KimetsuResult<()> {
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS skill_proposals (
proposal_id TEXT PRIMARY KEY,
skill_name TEXT NOT NULL,
description TEXT NOT NULL,
draft_content TEXT,
source_memory_ids_json TEXT NOT NULL DEFAULT '[]',
trigger_kind TEXT NOT NULL,
trigger_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
decided_at TEXT,
installed_path TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_skill_proposals_status
ON skill_proposals (status, created_at);
",
)?;
Ok(())
}
pub(crate) fn migrate_v6_to_v7(conn: &Connection) -> KimetsuResult<()> {
add_column_if_missing(conn, "memories", "valid_from TEXT")?;
add_column_if_missing(conn, "memories", "valid_to TEXT")?;
conn.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_memories_valid_to
ON memories (valid_to);",
)?;
Ok(())
}
pub(crate) fn migrate_v7_to_v8(conn: &Connection) -> KimetsuResult<()> {
add_column_if_missing(conn, "events", "origin TEXT")?;
Ok(())
}
pub(crate) fn migrate_v8_to_v9(conn: &Connection) -> KimetsuResult<()> {
add_column_if_missing(conn, "events", "hlc TEXT")?;
conn.execute_batch(
"UPDATE events
SET hlc = printf('%013d.%010d.local', 0, rowid)
WHERE hlc IS NULL;",
)?;
Ok(())
}
pub fn validate(conn: &Connection) -> KimetsuResult<()> {
apply_pragmas(conn)?;
use kimetsu_core::KIMETSU_SCHEMA_VERSION;
let current: i64 = conn.query_row(
"SELECT value FROM schema_info WHERE key = 'kimetsu_schema_version'",
[],
|row| row.get(0),
)?;
let target = KIMETSU_SCHEMA_VERSION;
if current > target {
return Err(format!(
"brain.db schema version {current} was written by a newer Kimetsu (this binary expects {target}); upgrade Kimetsu"
)
.into());
}
if current < target {
return Err(Box::new(crate::migrate::SchemaNeedsMigration {
from: current,
to: target,
}));
}
Ok(())
}
fn add_column_if_missing(conn: &Connection, table: &str, column_def: &str) -> KimetsuResult<()> {
let column_name = column_def
.split_whitespace()
.next()
.ok_or("empty column definition")?;
let exists: bool = {
let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?;
let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
let mut found = false;
for row in rows {
if row? == column_name {
found = true;
break;
}
}
found
};
if !exists {
conn.execute_batch(&format!("ALTER TABLE {table} ADD COLUMN {column_def};"))?;
}
Ok(())
}
fn ensure_memories_fts_shape(conn: &Connection) -> KimetsuResult<()> {
if table_has_column(conn, "memories_fts", "memory_id")? {
return Ok(());
}
conn.execute_batch(
"
DROP TABLE IF EXISTS memories_fts;
CREATE VIRTUAL TABLE memories_fts
USING fts5(memory_id UNINDEXED, text, kind, scope);
INSERT INTO memories_fts (memory_id, text, kind, scope)
SELECT memory_id, text, kind, scope FROM memories;
",
)?;
Ok(())
}
fn ensure_repo_manifests_fts_shape(conn: &Connection) -> KimetsuResult<()> {
if table_has_column(conn, "repo_manifests_fts", "parsed_summary_json")? {
return Ok(());
}
conn.execute_batch(
"
DROP TABLE IF EXISTS repo_manifests_fts;
CREATE VIRTUAL TABLE repo_manifests_fts
USING fts5(repo_root UNINDEXED, manifest_path, manifest_kind, parsed_summary_json);
INSERT INTO repo_manifests_fts (
repo_root, manifest_path, manifest_kind, parsed_summary_json
)
SELECT repo_root, manifest_path, manifest_kind, parsed_summary_json
FROM repo_manifests;
",
)?;
Ok(())
}
fn table_has_column(conn: &Connection, table: &str, column: &str) -> KimetsuResult<bool> {
let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?;
let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
for row in rows {
if row? == column {
return Ok(true);
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::migrate;
use rusqlite::Connection;
fn column_names(conn: &Connection, table: &str) -> Vec<String> {
let mut stmt = conn
.prepare(&format!("PRAGMA table_info({table})"))
.expect("prepare table_info");
stmt.query_map([], |row| row.get::<_, String>(1))
.expect("query_map")
.map(|r| r.expect("row"))
.collect()
}
fn table_exists(conn: &Connection, name: &str) -> bool {
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
[name],
|r| r.get(0),
)
.unwrap_or(0);
count > 0
}
#[test]
fn fresh_init_reaches_current_version_with_full_shape() {
use kimetsu_core::KIMETSU_SCHEMA_VERSION;
let conn = Connection::open_in_memory().expect("open_in_memory");
initialize(&conn).expect("initialize");
assert_eq!(
migrate::current_version(&conn).expect("current_version"),
KIMETSU_SCHEMA_VERSION,
"fresh DB must be at current schema version after initialize"
);
let mem_cols = column_names(&conn, "memories");
assert!(
mem_cols.contains(&"embedding".to_string()),
"memories must have `embedding` column"
);
assert!(
mem_cols.contains(&"embedding_model".to_string()),
"memories must have `embedding_model` column"
);
assert!(
mem_cols.contains(&"last_useful_at".to_string()),
"memories must have `last_useful_at` column"
);
assert!(
mem_cols.contains(&"superseded_by".to_string()),
"memories must have `superseded_by` column after v3 migration"
);
assert!(
mem_cols.contains(&"valid_from".to_string()),
"memories must have `valid_from` column after v7 migration"
);
assert!(
mem_cols.contains(&"valid_to".to_string()),
"memories must have `valid_to` column after v7 migration"
);
assert!(
table_exists(&conn, "memory_citations"),
"memory_citations table must exist"
);
assert!(
table_exists(&conn, "memory_conflicts"),
"memory_conflicts table must exist"
);
assert!(
table_exists(&conn, "memory_edges"),
"memory_edges table must exist after v4 migration"
);
assert!(
table_exists(&conn, "work_episodes"),
"work_episodes table must exist after v5 migration"
);
assert!(
table_exists(&conn, "skill_proposals"),
"skill_proposals table must exist after v6 migration"
);
}
#[test]
fn idempotent_rerun_preserves_data() {
let conn = Connection::open_in_memory().expect("open_in_memory");
initialize(&conn).expect("initialize");
conn.execute_batch(
"INSERT INTO memories (
memory_id, scope, kind, text, normalized_text,
confidence, provenance_snapshot_json, created_at,
use_count, usefulness_score
) VALUES (
'mem-1', 'test', 'fact', 'hello world', 'hello world',
0.9, '{}', '2024-01-01T00:00:00Z',
0, 0.0
);",
)
.expect("insert row");
let outcome = migrate::run_migrations(&conn).expect("second run_migrations");
assert_eq!(
outcome.applied,
Vec::<i64>::new(),
"second run_migrations must apply nothing"
);
assert_eq!(
migrate::current_version(&conn).expect("current_version"),
kimetsu_core::KIMETSU_SCHEMA_VERSION,
"version must still be at target"
);
let text: String = conn
.query_row(
"SELECT text FROM memories WHERE memory_id = 'mem-1'",
[],
|r| r.get(0),
)
.expect("row must survive");
assert_eq!(text, "hello world");
}
#[test]
fn idempotent_initialize_twice() {
use kimetsu_core::KIMETSU_SCHEMA_VERSION;
let conn = Connection::open_in_memory().expect("open_in_memory");
initialize(&conn).expect("first initialize");
initialize(&conn).expect("second initialize must not error");
assert_eq!(
migrate::current_version(&conn).expect("current_version"),
KIMETSU_SCHEMA_VERSION,
"version must still be at target after double initialize"
);
}
#[test]
fn apply_pragmas_sets_cache_size_on_rw_connection() {
let conn = Connection::open_in_memory().expect("open_in_memory");
initialize(&conn).expect("initialize");
let cache_size: i64 = conn
.pragma_query_value(None, "cache_size", |row| row.get(0))
.expect("cache_size query");
assert_ne!(
cache_size, -2000,
"cache_size must have been updated from the 2 MiB default, got {cache_size}"
);
assert!(
!(-2000..=2000).contains(&cache_size),
"cache_size should reflect the 64 MiB tuning (not default -2000), got {cache_size}"
);
}
#[test]
fn apply_pragmas_does_not_error_on_in_memory_conn() {
let conn = Connection::open_in_memory().expect("open_in_memory");
apply_pragmas(&conn).expect("apply_pragmas must not error on a fresh in-memory conn");
let cache_size: i64 = conn
.pragma_query_value(None, "cache_size", |row| row.get(0))
.expect("cache_size");
assert!(
!(-2000..=2000).contains(&cache_size),
"apply_pragmas must update cache_size from the default, got {cache_size}"
);
}
fn seed_schema_info(version: i64) -> Connection {
let conn = Connection::open_in_memory().expect("open_in_memory");
conn.execute_batch(&format!(
"CREATE TABLE schema_info (key TEXT PRIMARY KEY, value INTEGER NOT NULL);
INSERT INTO schema_info VALUES ('kimetsu_schema_version', {version});"
))
.expect("seed schema_info");
conn
}
#[test]
fn validate_ok_at_target() {
use kimetsu_core::KIMETSU_SCHEMA_VERSION;
let conn = seed_schema_info(KIMETSU_SCHEMA_VERSION);
validate(&conn).expect("validate at target must return Ok(())");
}
#[test]
fn validate_returns_needs_migration_for_older_db() {
use kimetsu_core::KIMETSU_SCHEMA_VERSION;
let conn = seed_schema_info(1);
let err = validate(&conn).expect_err("validate on v1 DB must return Err");
let snm = err
.downcast_ref::<migrate::SchemaNeedsMigration>()
.expect("error must downcast to SchemaNeedsMigration");
assert_eq!(
snm,
&migrate::SchemaNeedsMigration {
from: 1,
to: KIMETSU_SCHEMA_VERSION,
},
"SchemaNeedsMigration must carry the correct from/to versions"
);
}
#[test]
fn v2_to_v3_migration_adds_superseded_by() {
let conn = Connection::open_in_memory().expect("open_in_memory");
create_baseline(&conn).expect("create_baseline");
migrate_v1_to_v2(&conn).expect("migrate_v1_to_v2");
conn.execute(
"UPDATE schema_info SET value = 2 WHERE key = 'kimetsu_schema_version'",
[],
)
.expect("set v2");
let cols_before = column_names(&conn, "memories");
assert!(
!cols_before.contains(&"superseded_by".to_string()),
"superseded_by must not exist before v3 migration"
);
migrate_v2_to_v3(&conn).expect("migrate_v2_to_v3");
let cols_after = column_names(&conn, "memories");
assert!(
cols_after.contains(&"superseded_by".to_string()),
"superseded_by must exist after v3 migration"
);
let idx_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_memories_superseded'",
[],
|r| r.get(0),
)
.expect("query index");
assert_eq!(
idx_count, 1,
"idx_memories_superseded must exist after v3 migration"
);
}
#[test]
fn validate_hard_errors_for_newer_db() {
let conn = seed_schema_info(999);
let err = validate(&conn).expect_err("validate on v999 DB must return Err");
assert!(
err.downcast_ref::<migrate::SchemaNeedsMigration>()
.is_none(),
"error for a newer DB must NOT downcast to SchemaNeedsMigration"
);
let msg = err.to_string();
assert!(
msg.contains("newer"),
"error message must contain 'newer', got: {msg}"
);
}
#[test]
fn v3_to_v4_migration_adds_memory_edges() {
let conn = Connection::open_in_memory().expect("open_in_memory");
create_baseline(&conn).expect("create_baseline");
migrate_v1_to_v2(&conn).expect("migrate_v1_to_v2");
migrate_v2_to_v3(&conn).expect("migrate_v2_to_v3");
conn.execute(
"UPDATE schema_info SET value = 3 WHERE key = 'kimetsu_schema_version'",
[],
)
.expect("set v3");
assert!(
!table_exists(&conn, "memory_edges"),
"memory_edges must not exist before v4 migration"
);
migrate_v3_to_v4(&conn).expect("migrate_v3_to_v4");
assert!(
table_exists(&conn, "memory_edges"),
"memory_edges must exist after v4 migration"
);
let src_idx: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_memory_edges_src'",
[],
|r| r.get(0),
)
.expect("query idx_memory_edges_src");
assert_eq!(src_idx, 1, "idx_memory_edges_src must exist");
let dst_idx: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_memory_edges_dst'",
[],
|r| r.get(0),
)
.expect("query idx_memory_edges_dst");
assert_eq!(dst_idx, 1, "idx_memory_edges_dst must exist");
}
#[test]
fn v4_to_v5_migration_adds_work_episodes() {
let conn = Connection::open_in_memory().expect("open_in_memory");
create_baseline(&conn).expect("create_baseline");
migrate_v1_to_v2(&conn).expect("migrate_v1_to_v2");
migrate_v2_to_v3(&conn).expect("migrate_v2_to_v3");
migrate_v3_to_v4(&conn).expect("migrate_v3_to_v4");
conn.execute(
"UPDATE schema_info SET value = 4 WHERE key = 'kimetsu_schema_version'",
[],
)
.expect("set v4");
assert!(
!table_exists(&conn, "work_episodes"),
"work_episodes must not exist before v5 migration"
);
migrate_v4_to_v5(&conn).expect("migrate_v4_to_v5");
assert!(
table_exists(&conn, "work_episodes"),
"work_episodes must exist after v5 migration"
);
let idx: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_episodes_repo_live'",
[],
|r| r.get(0),
)
.expect("query idx_episodes_repo_live");
assert_eq!(idx, 1, "idx_episodes_repo_live must exist");
}
#[test]
fn v5_to_v6_migration_adds_skill_proposals() {
let conn = Connection::open_in_memory().expect("open_in_memory");
create_baseline(&conn).expect("create_baseline");
migrate_v1_to_v2(&conn).expect("migrate_v1_to_v2");
migrate_v2_to_v3(&conn).expect("migrate_v2_to_v3");
migrate_v3_to_v4(&conn).expect("migrate_v3_to_v4");
migrate_v4_to_v5(&conn).expect("migrate_v4_to_v5");
conn.execute(
"UPDATE schema_info SET value = 5 WHERE key = 'kimetsu_schema_version'",
[],
)
.expect("set v5");
assert!(
!table_exists(&conn, "skill_proposals"),
"skill_proposals must not exist before v6 migration"
);
migrate_v5_to_v6(&conn).expect("migrate_v5_to_v6");
assert!(
table_exists(&conn, "skill_proposals"),
"skill_proposals must exist after v6 migration"
);
let idx: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_skill_proposals_status'",
[],
|r| r.get(0),
)
.expect("query idx_skill_proposals_status");
assert_eq!(
idx, 1,
"idx_skill_proposals_status must exist after v6 migration"
);
}
#[test]
fn v6_to_v7_migration_adds_temporal_validity_columns() {
let conn = Connection::open_in_memory().expect("open_in_memory");
create_baseline(&conn).expect("create_baseline");
migrate_v1_to_v2(&conn).expect("migrate_v1_to_v2");
migrate_v2_to_v3(&conn).expect("migrate_v2_to_v3");
migrate_v3_to_v4(&conn).expect("migrate_v3_to_v4");
migrate_v4_to_v5(&conn).expect("migrate_v4_to_v5");
migrate_v5_to_v6(&conn).expect("migrate_v5_to_v6");
conn.execute(
"UPDATE schema_info SET value = 6 WHERE key = 'kimetsu_schema_version'",
[],
)
.expect("set v6");
let cols_before = column_names(&conn, "memories");
assert!(
!cols_before.contains(&"valid_from".to_string()),
"valid_from must not exist before v7 migration"
);
assert!(
!cols_before.contains(&"valid_to".to_string()),
"valid_to must not exist before v7 migration"
);
migrate_v6_to_v7(&conn).expect("migrate_v6_to_v7");
let cols_after = column_names(&conn, "memories");
assert!(
cols_after.contains(&"valid_from".to_string()),
"valid_from must exist after v7 migration"
);
assert!(
cols_after.contains(&"valid_to".to_string()),
"valid_to must exist after v7 migration"
);
let idx: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_memories_valid_to'",
[],
|r| r.get(0),
)
.expect("query idx_memories_valid_to");
assert_eq!(
idx, 1,
"idx_memories_valid_to must exist after v7 migration"
);
}
}