use super::*;
#[test]
fn migration_replay_does_not_trip_on_already_present_column() {
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
let path = tmp.path().to_str().unwrap();
{
let _db = YantrikDB::new(path, 8).unwrap();
}
{
let conn = rusqlite::Connection::open(path).unwrap();
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '23')",
[],
)
.unwrap();
}
let db = YantrikDB::new(path, 8)
.expect("v0.7.3 idempotent migration runner must heal rewound-meta deployments");
db.record(
"post-heal smoke",
"episodic",
0.5,
0.0,
604800.0,
&empty_meta(),
&vec_seed(1.0, 8),
"default",
0.8,
"general",
"user",
None,
)
.unwrap();
}
#[test]
fn migration_replay_does_not_trip_on_alter_table_against_view() {
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
let path = tmp.path().to_str().unwrap();
{
let _db = YantrikDB::new(path, 8).unwrap();
}
{
let conn = rusqlite::Connection::open(path).unwrap();
let kind: String = conn
.query_row(
"SELECT type FROM sqlite_master WHERE name = 'edges'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
kind, "view",
"fixture precondition: at current schema, edges should be a backward-compat view"
);
}
{
let conn = rusqlite::Connection::open(path).unwrap();
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '14')",
[],
)
.unwrap();
}
let db = YantrikDB::new(path, 8)
.expect("v0.7.8 idempotent runner must heal rewound-meta DBs that hit ALTER-on-view");
db.record(
"post-heal smoke (issue 10)",
"episodic",
0.5,
0.0,
604800.0,
&empty_meta(),
&vec_seed(1.0, 8),
"default",
0.8,
"general",
"user",
None,
)
.unwrap();
}
#[test]
fn migration_meta_stamp_does_not_downgrade() {
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
let path = tmp.path().to_str().unwrap();
{
let conn = rusqlite::Connection::open(path).unwrap();
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '999');",
)
.unwrap();
}
let _db = YantrikDB::new(path, 8).unwrap();
let stamped: String = {
let conn = rusqlite::Connection::open(path).unwrap();
conn.query_row(
"SELECT value FROM meta WHERE key = 'schema_version'",
[],
|row| row.get(0),
)
.unwrap()
};
assert_eq!(
stamped, "999",
"MAX-stamp invariant: open() must never rewind meta.schema_version below the on-disk value"
);
}
#[test]
fn schema_v26_fresh_install_has_provenance_columns_and_indexes() {
let db = YantrikDB::new(":memory:", 8).unwrap();
let conn = db.conn();
let cols = table_columns(&conn, "memories");
for required in [
"prior_rid",
"resolution_kind",
"dismissal_reason",
"confidence_at_write",
] {
assert!(
cols.iter().any(|c| c == required),
"v26: fresh-install memories table missing column {required}, got: {cols:?}"
);
}
assert!(
index_exists(&conn, "idx_memories_prior_rid"),
"v26: fresh-install missing partial index idx_memories_prior_rid"
);
assert!(
index_exists(&conn, "idx_memories_resolution_kind"),
"v26: fresh-install missing partial index idx_memories_resolution_kind"
);
}
#[test]
fn schema_v26_migration_from_v25_adds_columns_and_normalizes_source() {
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
let path = tmp.path().to_str().unwrap();
{
let db = YantrikDB::new(path, 8).unwrap();
let conn = db.conn();
for (rid, src) in [
("01900000-0000-7000-8000-000000000001", "user"),
("01900000-0000-7000-8000-000000000002", "inference"),
("01900000-0000-7000-8000-000000000003", "legacy-freetext"),
] {
conn.execute(
"INSERT INTO memories (rid, type, text, created_at, updated_at, last_access, source) \
VALUES (?1, 'episodic', 'test', 0.0, 0.0, 0.0, ?2)",
params![rid, src],
)
.unwrap();
}
}
{
let conn = rusqlite::Connection::open(path).unwrap();
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '25')",
[],
)
.unwrap();
conn.execute("DROP INDEX IF EXISTS idx_memories_prior_rid", [])
.unwrap();
conn.execute("DROP INDEX IF EXISTS idx_memories_resolution_kind", [])
.unwrap();
}
let db = YantrikDB::new(path, 8)
.expect("v26 migration must run cleanly against a rewound-meta v25 DB");
let conn = db.conn();
let cols = table_columns(&conn, "memories");
for required in [
"prior_rid",
"resolution_kind",
"dismissal_reason",
"confidence_at_write",
] {
assert!(
cols.iter().any(|c| c == required),
"v26 migration: missing column {required} after re-open"
);
}
assert!(
index_exists(&conn, "idx_memories_prior_rid"),
"v26 migration: missing partial index idx_memories_prior_rid"
);
assert!(
index_exists(&conn, "idx_memories_resolution_kind"),
"v26 migration: missing partial index idx_memories_resolution_kind"
);
let normalized: String = conn
.query_row(
"SELECT source FROM memories WHERE rid = '01900000-0000-7000-8000-000000000003'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
normalized, "user",
"v26 migration must coerce non-enum source to 'user'"
);
let preserved: String = conn
.query_row(
"SELECT source FROM memories WHERE rid = '01900000-0000-7000-8000-000000000002'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
preserved, "inference",
"v26 migration must preserve enum-valid source values"
);
let log: String = conn
.query_row(
"SELECT value FROM meta WHERE key = 'source_normalization_log_v26'",
[],
|row| row.get(0),
)
.unwrap();
assert!(
log.contains("normalized 1 rows"),
"v26 migration must log normalization count, got: {log}"
);
}
#[test]
fn schema_v26_migration_replay_is_idempotent() {
use tempfile::NamedTempFile;
let tmp = NamedTempFile::new().unwrap();
let path = tmp.path().to_str().unwrap();
{
let _db = YantrikDB::new(path, 8).unwrap();
}
{
let conn = rusqlite::Connection::open(path).unwrap();
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '25')",
[],
)
.unwrap();
}
let db = YantrikDB::new(path, 8)
.expect("v26 migration runner must heal rewound-meta deployments on a v26-schema DB");
db.record(
"post-v26-heal smoke",
"episodic",
0.5,
0.0,
604800.0,
&empty_meta(),
&vec_seed(1.0, 8),
"default",
0.8,
"general",
"user",
None,
)
.unwrap();
}
#[test]
fn schema_v43_fresh_install_has_typed_synthesis_lifecycle() {
let db = YantrikDB::new(":memory:", 8).unwrap();
let conn = db.conn();
let cols = table_columns(&conn, "memories");
for required in [
"synthesis_axis",
"synthesis_granularity",
"synthesis_logical_key",
"synthesis_evidence_version",
"synthesis_generation_hlc",
"synthesis_state",
] {
assert!(
cols.iter().any(|col| col == required),
"v43 fresh schema missing memories.{required}"
);
}
assert_eq!(
conn.query_row(
"SELECT COUNT(*) FROM sqlite_master \
WHERE type = 'table' AND name = 'synthesis_dependencies'",
[],
|row| row.get::<_, i64>(0),
)
.unwrap(),
1
);
for index in [
"idx_synthesis_dependencies_source",
"idx_synthesis_dependencies_synthesis",
] {
assert!(
index_exists(&conn, index),
"v42 fresh schema missing {index}"
);
}
}
#[test]
fn schema_v46_fresh_install_has_rollup_outcome_ledger() {
let db = YantrikDB::new(":memory:", 8).unwrap();
let conn = db.conn();
for table in [
"rollup_impressions",
"rollup_impression_children",
"rollup_impression_outcomes",
"rollup_impression_additions",
] {
assert_eq!(
conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
[table],
|row| row.get::<_, i64>(0),
)
.unwrap(),
1,
"v46 fresh schema missing {table}"
);
}
for index in [
"idx_rollup_impressions_rollup",
"idx_rollup_impressions_query",
"idx_rollup_impression_children_child",
"idx_rollup_impression_outcomes_created",
"idx_rollup_impression_additions_child",
] {
assert!(
index_exists(&conn, index),
"v46 fresh schema missing {index}"
);
}
for expected in ["requested_count", "query_shape"] {
assert!(
table_columns(&conn, "rollup_impressions")
.iter()
.any(|column| column == expected),
"v46 fresh schema missing rollup_impressions.{expected}"
);
}
assert!(
table_columns(&conn, "rollup_impression_children")
.iter()
.any(|column| column == "score"),
"v46 fresh schema missing rollup_impression_children.score"
);
}
#[test]
fn schema_v46_migration_adds_omission_features() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE rollup_impressions (impression_id TEXT PRIMARY KEY); \
CREATE TABLE rollup_impression_children ( \
impression_id TEXT NOT NULL, child_rid TEXT NOT NULL, rank INTEGER NOT NULL, \
PRIMARY KEY (impression_id, child_rid) \
);",
)
.unwrap();
conn.execute_batch(crate::base::schema::MIGRATE_V45_TO_V46)
.unwrap();
let impression_cols = table_columns(&conn, "rollup_impressions");
assert!(impression_cols.iter().any(|col| col == "requested_count"));
assert!(impression_cols.iter().any(|col| col == "query_shape"));
assert!(table_columns(&conn, "rollup_impression_children")
.iter()
.any(|col| col == "score"));
assert_eq!(
conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
["rollup_impression_additions"],
|row| row.get::<_, i64>(0),
)
.unwrap(),
1
);
assert!(index_exists(&conn, "idx_rollup_impression_additions_child"));
}
#[test]
fn schema_v46_migration_bootstraps_child_ledger_skipped_by_v43() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute_batch(crate::base::schema::MIGRATE_V44_TO_V45)
.unwrap();
conn.execute_batch(crate::base::schema::MIGRATE_V45_TO_V46)
.unwrap();
assert!(table_columns(&conn, "rollup_impression_children")
.iter()
.any(|column| column == "score"));
assert_eq!(
conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
["rollup_impression_additions"],
|row| row.get::<_, i64>(0),
)
.unwrap(),
1
);
}
#[test]
fn schema_v45_migration_adds_rollup_outcome_finalization() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE rollup_impressions (impression_id TEXT PRIMARY KEY)",
[],
)
.unwrap();
conn.execute_batch(crate::base::schema::MIGRATE_V44_TO_V45)
.unwrap();
let cols = table_columns(&conn, "rollup_impressions");
assert!(cols.iter().any(|col| col == "outcome_payload_hash"));
assert!(cols.iter().any(|col| col == "outcome_finalized_at"));
}
#[test]
fn schema_v45_migration_bootstraps_ledger_skipped_by_v43() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute_batch(crate::base::schema::MIGRATE_V44_TO_V45)
.unwrap();
let cols = table_columns(&conn, "rollup_impressions");
for expected in [
"rollup_rid",
"expansion_payload_hash",
"outcome_payload_hash",
"outcome_finalized_at",
] {
assert!(cols.iter().any(|col| col == expected), "missing {expected}");
}
}
#[test]
fn schema_v43_migration_adds_synthesis_generation_clock() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE memories (rid TEXT PRIMARY KEY)", [])
.unwrap();
conn.execute_batch(crate::base::schema::MIGRATE_V42_TO_V43)
.unwrap();
let cols = table_columns(&conn, "memories");
assert!(cols.iter().any(|col| col == "synthesis_generation_hlc"));
}
#[test]
fn schema_v42_migration_adds_the_same_synthesis_surface() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE memories (rid TEXT PRIMARY KEY)", [])
.unwrap();
conn.execute_batch(crate::base::schema::MIGRATE_V41_TO_V42)
.unwrap();
let cols = table_columns(&conn, "memories");
for required in [
"synthesis_axis",
"synthesis_granularity",
"synthesis_logical_key",
"synthesis_evidence_version",
"synthesis_state",
] {
assert!(cols.iter().any(|col| col == required));
}
assert!(index_exists(&conn, "idx_synthesis_dependencies_source"));
assert!(index_exists(&conn, "idx_synthesis_dependencies_synthesis"));
conn.execute(
"INSERT INTO memories (rid, synthesis_granularity, synthesis_state) \
VALUES ('ordinary', NULL, NULL), ('synth', 'atomic', 'verified')",
[],
)
.unwrap();
assert!(conn
.execute(
"INSERT INTO memories (rid, synthesis_granularity) VALUES ('bad-g', 'session')",
[],
)
.is_err());
assert!(conn
.execute(
"INSERT INTO memories (rid, synthesis_state) VALUES ('bad-s', 'active')",
[],
)
.is_err());
}
#[test]
fn failing_migration_statement_names_itself_in_the_error() {
use rusqlite::Connection;
let conn = Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE memories (rid TEXT PRIMARY KEY)", [])
.unwrap();
let err = YantrikDB::run_migration_idempotent(&conn, "ALTER TABLE memories ADD")
.expect_err("a truncated statement must not succeed");
let msg = err.to_string();
assert!(
msg.contains("migration statement"),
"error must be stage-tagged, got: {msg}"
);
assert!(
msg.contains("ALTER TABLE memories ADD"),
"error must carry the statement it choked on, got: {msg}"
);
}
#[test]
fn swallowed_replay_errors_still_do_not_leak_a_stage_error() {
use rusqlite::Connection;
let conn = Connection::open_in_memory().unwrap();
YantrikDB::run_migration_idempotent(&conn, "DROP TABLE definitely_not_a_table;")
.expect("swallowed replay errors must not become failures");
}