use fathomdb_schema::{
check_migration_accretion, migrate_with_steps, Migration, MIGRATIONS, SCHEMA_VERSION,
};
use rusqlite::Connection;
use std::sync::Once;
fn register_sqlite_vec_once() {
static REGISTER: Once = Once::new();
REGISTER.call_once(|| unsafe {
let entrypoint: unsafe extern "C" fn(
*mut rusqlite::ffi::sqlite3,
*mut *mut std::os::raw::c_char,
*const rusqlite::ffi::sqlite3_api_routines,
) -> std::os::raw::c_int = std::mem::transmute(sqlite_vec::sqlite3_vec_init as *const ());
rusqlite::ffi::sqlite3_auto_extension(Some(entrypoint));
});
}
fn user_version(conn: &Connection) -> u32 {
conn.query_row("PRAGMA user_version", [], |row| row.get::<_, u32>(0)).unwrap()
}
fn set_user_version(conn: &Connection, version: u32) {
conn.pragma_update(None, "user_version", version).unwrap();
}
fn steps_through(limit: u32) -> Vec<Migration> {
MIGRATIONS.iter().filter(|m| m.step_id <= limit).cloned().collect()
}
fn columns(conn: &Connection, table: &str) -> Vec<String> {
let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})")).unwrap();
stmt.query_map([], |row| row.get::<_, String>(1)).unwrap().map(|r| r.unwrap()).collect()
}
fn object_exists(conn: &Connection, name: &str) -> bool {
let n: i64 = conn
.query_row("SELECT COUNT(*) FROM sqlite_master WHERE name = ?1", [name], |r| r.get(0))
.unwrap();
n > 0
}
fn seed_v23() -> Connection {
register_sqlite_vec_once();
let conn = Connection::open_in_memory().unwrap();
set_user_version(&conn, 1);
migrate_with_steps(&conn, &steps_through(23)).expect("migrate to v23");
assert_eq!(user_version(&conn), 23, "precondition: DB at the pre-slice head 23");
conn
}
#[test]
fn s24_registry_eav_and_property_fts_objects_exist() {
let conn = seed_v23();
assert!(!object_exists(&conn, "canonical_attributes"), "pre-slice: no EAV store");
assert!(!object_exists(&conn, "property_search_index"), "pre-slice: no property-FTS");
assert!(!object_exists(&conn, "_fathomdb_projection_registry"), "pre-slice: no registry");
migrate_with_steps(&conn, &steps_through(24)).expect("migrate to v24");
assert!(object_exists(&conn, "_fathomdb_projection_registry"));
assert!(object_exists(&conn, "canonical_attributes"));
assert!(object_exists(&conn, "property_search_index"));
let reg = columns(&conn, "_fathomdb_projection_registry");
for c in ["name", "roles", "fts_tokenizer", "vector_embedder", "vector_declared"] {
assert!(reg.contains(&c.to_string()), "_fathomdb_projection_registry.{c} must exist");
}
let eav = columns(&conn, "canonical_attributes");
for c in ["write_cursor", "attr_name", "attr_value"] {
assert!(eav.contains(&c.to_string()), "canonical_attributes.{c} must exist");
}
let pfts = columns(&conn, "property_search_index");
for c in ["attr_value", "attr_name", "write_cursor"] {
assert!(pfts.contains(&c.to_string()), "property_search_index.{c} must exist");
}
}
#[test]
fn s24_no_data_migration_tables_come_up_empty() {
let conn = seed_v23();
conn.execute_batch(
"INSERT INTO canonical_nodes(write_cursor, kind, body, source_id, logical_id, row_kind, state)
VALUES(1, 'doc', '{\"status\":\"open\"}', 'src:1', 'lid-a', 'leaf', 'active');",
)
.expect("seed a v23 node");
migrate_with_steps(&conn, &steps_through(24)).expect("migrate to v24");
for table in ["canonical_attributes", "property_search_index", "_fathomdb_projection_registry"]
{
let n: i64 =
conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |r| r.get(0)).unwrap();
assert_eq!(n, 0, "{table} must come up EMPTY — step 24 is shape-only, no backfill");
}
}
#[test]
fn s24_is_idempotent_across_repeated_migrate_calls() {
let conn = seed_v23();
migrate_with_steps(&conn, MIGRATIONS).expect("first migrate to head");
assert_eq!(user_version(&conn), SCHEMA_VERSION);
let report = migrate_with_steps(&conn, MIGRATIONS).expect("re-running migrate must be a no-op");
assert!(
report.migration_steps.is_empty(),
"no step may re-run once user_version is at head; ran {:?}",
report.migration_steps
);
assert_eq!(user_version(&conn), SCHEMA_VERSION);
}
#[test]
fn s24_failed_step_rolls_back_objects_and_version_together() {
let conn = seed_v23();
let poisoned = vec![Migration {
step_id: 24,
sql: "-- MIGRATION-ACCRETION-EXEMPTION: poisoned step-24 stand-in
CREATE TABLE canonical_attributes(write_cursor INTEGER NOT NULL, attr_name TEXT NOT NULL, attr_value TEXT);
SELECT this_is_not_valid_sql_and_must_abort_the_step();",
}];
migrate_with_steps(&conn, &poisoned).expect_err("poisoned step must fail");
assert_eq!(user_version(&conn), 23, "a failed step must leave user_version at 23");
assert!(
!object_exists(&conn, "canonical_attributes"),
"a failed step must roll back its CREATE TABLE — otherwise the retry hits \
'table already exists' and the DB is wedged"
);
migrate_with_steps(&conn, &steps_through(24)).expect("retry must succeed");
assert_eq!(user_version(&conn), 24);
assert!(object_exists(&conn, "canonical_attributes"));
}
#[test]
fn s24_carries_the_accretion_exemption_marker() {
let step = MIGRATIONS.iter().find(|m| m.step_id == 24).expect("step-24 must exist");
check_migration_accretion("step-24", step.sql)
.expect("step-24 must satisfy the accretion guard");
assert!(
step.sql.contains("-- MIGRATION-ACCRETION-EXEMPTION: "),
"an additive CREATE TABLE step must carry the exemption marker"
);
}
#[test]
fn s24_precedes_the_nested_source_head_migration() {
register_sqlite_vec_once();
let conn = Connection::open_in_memory().unwrap();
set_user_version(&conn, 1);
migrate_with_steps(&conn, MIGRATIONS).expect("migration must succeed");
assert_eq!(user_version(&conn), SCHEMA_VERSION);
assert_eq!(SCHEMA_VERSION, 26, "SCHEMA_VERSION must be 26 at the Slice-19 head");
assert_eq!(
MIGRATIONS.last().expect("at least one migration").step_id,
26,
"step-26 (canonical FTS-hydration join indexes) must be the last (head) migration"
);
}