use fathomdb_schema::{check_migration_accretion, migrate_with_steps, 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 table_exists(conn: &Connection, name: &str) -> bool {
conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
[name],
|row| row.get::<_, i64>(0),
)
.unwrap()
> 0
}
#[test]
fn s17_search_index_v2_present_and_schema_version_is_17() {
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!(table_exists(&conn, "search_index_v2"), "search_index_v2 must exist after step-17");
assert!(table_exists(&conn, "search_index"), "single-column search_index must be RETAINED");
assert_eq!(user_version(&conn), SCHEMA_VERSION);
assert_eq!(
SCHEMA_VERSION, 26,
"SCHEMA_VERSION must be 26 (step-26 canonical FTS-hydration join indexes, Slice 19)"
);
assert_eq!(
MIGRATIONS.last().expect("at least one migration").step_id,
26,
"step-26 (canonical FTS-hydration join indexes, Slice 19) must be the last (head) migration"
);
let cols: Vec<String> = conn
.prepare("PRAGMA table_info(search_index_v2)")
.unwrap()
.query_map([], |row| row.get::<_, String>(1))
.unwrap()
.filter_map(Result::ok)
.collect();
for field in ["kind", "body", "status", "write_cursor"] {
assert!(
cols.contains(&field.to_string()),
"search_index_v2 must have {field}, got {cols:?}"
);
}
}
#[test]
fn s17_forward_only_from_v16_reindexes_existing_rows() {
register_sqlite_vec_once();
let conn = Connection::open_in_memory().unwrap();
set_user_version(&conn, 1);
let steps_to_16: Vec<_> = MIGRATIONS.iter().filter(|m| m.step_id <= 16).cloned().collect();
migrate_with_steps(&conn, &steps_to_16).expect("migrate to v16");
assert_eq!(user_version(&conn), 16, "precondition: DB is at the old schema version 16");
conn.execute(
"INSERT INTO canonical_nodes(write_cursor, kind, body, row_kind)
VALUES(1, 'note', 'plain body about widgets', 'leaf')",
[],
)
.expect("plain node insert");
conn.execute(
"INSERT INTO canonical_nodes(write_cursor, kind, body, row_kind)
VALUES(2, 'todo', '{\"status\":\"open\",\"text\":\"buy milk\"}', 'leaf')",
[],
)
.expect("json node insert");
let step17_only: Vec<_> = MIGRATIONS.iter().filter(|m| m.step_id == 17).cloned().collect();
let report = migrate_with_steps(&conn, &step17_only).expect("forward migrate to v17");
assert_eq!(report.schema_version_before, 16);
assert_eq!(report.schema_version_after, 17);
let ran: Vec<u32> = report.migration_steps.iter().map(|s| s.step_id).collect();
assert_eq!(ran, vec![17], "only step-17 must run when starting from v16");
assert_eq!(user_version(&conn), 17);
let v2_count: i64 =
conn.query_row("SELECT COUNT(*) FROM search_index_v2", [], |r| r.get(0)).expect("v2 count");
assert_eq!(v2_count, 2, "search_index_v2 must be re-indexed from the 2 existing rows");
let status_plain: String = conn
.query_row("SELECT status FROM search_index_v2 WHERE write_cursor = 1", [], |r| r.get(0))
.expect("plain status");
let status_json: String = conn
.query_row("SELECT status FROM search_index_v2 WHERE write_cursor = 2", [], |r| r.get(0))
.expect("json status");
assert_eq!(status_plain, "", "non-JSON body must index an empty status (json_valid guard)");
assert_eq!(status_json, "open", "JSON body must index its $.status field");
let hit_wc: i64 = conn
.query_row(
"SELECT write_cursor FROM search_index_v2 WHERE search_index_v2 MATCH 'kind:todo'",
[],
|r| r.get(0),
)
.expect("kind-column match must resolve");
assert_eq!(hit_wc, 2, "kind-column BM25F match must find the 'todo' row");
}
#[test]
fn s17_passes_accretion_guard_with_marker() {
let step17 = MIGRATIONS
.iter()
.find(|m| m.step_id == 17)
.expect("step 17 (F5 search_index_v2) must exist");
assert!(
step17.sql.contains("-- MIGRATION-ACCRETION-EXEMPTION: "),
"step 17 must carry the exemption marker documenting the additive re-index"
);
check_migration_accretion("017_f5_search_index_v2.sql", step17.sql)
.expect("step 17 must pass the accretion guard");
}