use fathomdb_schema::{check_migration_accretion, migrate_with_steps, MIGRATIONS, SCHEMA_VERSION};
use rusqlite::Connection;
use std::sync::Once;
const LEGACY_SOURCE_ID: &str = "_legacy:pre-0.8.20";
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 source_id_of(conn: &Connection, table: &str, cursor: i64) -> Option<String> {
conn.query_row(
&format!("SELECT source_id FROM {table} WHERE write_cursor = ?1"),
[cursor],
|row| row.get::<_, Option<String>>(0),
)
.unwrap_or_else(|e| panic!("{table} row {cursor} must survive the migration: {e}"))
}
fn logical_id_of(conn: &Connection, table: &str, cursor: i64) -> Option<String> {
conn.query_row(
&format!("SELECT logical_id FROM {table} WHERE write_cursor = ?1"),
[cursor],
|row| row.get::<_, Option<String>>(0),
)
.unwrap_or_else(|e| panic!("{table} row {cursor} must survive the migration: {e}"))
}
fn seed_v20_matrix() -> Connection {
register_sqlite_vec_once();
let conn = Connection::open_in_memory().unwrap();
set_user_version(&conn, 1);
let steps_to_20: Vec<_> = MIGRATIONS.iter().filter(|m| m.step_id <= 20).cloned().collect();
migrate_with_steps(&conn, &steps_to_20).expect("migrate to v20");
assert_eq!(user_version(&conn), 20, "precondition: DB at the pre-slice head 20");
conn.execute_batch(
"INSERT INTO canonical_nodes(write_cursor, kind, body, source_id, logical_id)
VALUES(1, 'doc', 'ungoverned null-provenance', NULL, NULL);
INSERT INTO canonical_nodes(write_cursor, kind, body, source_id, logical_id)
VALUES(2, 'doc', 'governed null-provenance', NULL, 'lid-node-governed');
INSERT INTO canonical_nodes(write_cursor, kind, body, source_id, logical_id)
VALUES(3, 'doc', 'ungoverned provenanced', 'doc-caller-1', NULL);
INSERT INTO canonical_nodes(write_cursor, kind, body, source_id, logical_id)
VALUES(4, 'doc', 'governed provenanced', 'doc-caller-2', 'lid-node-both');
INSERT INTO canonical_edges(write_cursor, kind, from_id, to_id, source_id, logical_id)
VALUES(5, 'rel', 'a', 'b', NULL, NULL);
INSERT INTO canonical_edges(write_cursor, kind, from_id, to_id, source_id, logical_id)
VALUES(6, 'rel', 'c', 'd', NULL, 'lid-edge-governed');
INSERT INTO canonical_edges(write_cursor, kind, from_id, to_id, source_id, logical_id)
VALUES(7, 'rel', 'e', 'f', 'doc-caller-3', NULL);",
)
.expect("seed the v20 provenance matrix");
conn
}
#[test]
fn legacy_backfill_spares_governed_rows() {
let conn = seed_v20_matrix();
let step21_only: Vec<_> = MIGRATIONS.iter().filter(|m| m.step_id == 21).cloned().collect();
assert_eq!(step21_only.len(), 1, "step-21 (legacy provenance backfill) must exist");
let report = migrate_with_steps(&conn, &step21_only).expect("forward migrate to v21");
assert_eq!(report.schema_version_before, 20);
assert_eq!(report.schema_version_after, 21);
assert_eq!(
source_id_of(&conn, "canonical_nodes", 1).as_deref(),
Some(LEGACY_SOURCE_ID),
"an ungoverned NULL-provenance node must be back-filled (it is otherwise un-erasable)"
);
assert_eq!(
source_id_of(&conn, "canonical_edges", 5).as_deref(),
Some(LEGACY_SOURCE_ID),
"an ungoverned NULL-provenance edge must be back-filled"
);
assert_eq!(
source_id_of(&conn, "canonical_nodes", 2),
None,
"TC-11 pin: a GOVERNED row keeps NULL source_id — the backfill gate is \
`WHERE logical_id IS NULL` ONLY"
);
assert_eq!(
source_id_of(&conn, "canonical_edges", 6).as_deref(),
Some(LEGACY_SOURCE_ID),
"an edge `logical_id` is not purge-addressable, so a governed edge must \
still be back-filled or it is erasable by no verb at all"
);
assert_eq!(
source_id_of(&conn, "canonical_nodes", 3).as_deref(),
Some("doc-caller-1"),
"the backfill must not overwrite caller-supplied provenance"
);
assert_eq!(
source_id_of(&conn, "canonical_nodes", 4).as_deref(),
Some("doc-caller-2"),
"the backfill must not overwrite caller-supplied provenance on a governed row"
);
assert_eq!(
source_id_of(&conn, "canonical_edges", 7).as_deref(),
Some("doc-caller-3"),
"the backfill must not overwrite caller-supplied edge provenance"
);
}
#[test]
fn legacy_backfill_covers_governed_edges() {
let conn = seed_v20_matrix();
assert_eq!(source_id_of(&conn, "canonical_edges", 6), None);
assert_eq!(
logical_id_of(&conn, "canonical_edges", 6).as_deref(),
Some("lid-edge-governed"),
"seed: the edge under test must carry a `logical_id`"
);
let step21_only: Vec<_> = MIGRATIONS.iter().filter(|m| m.step_id == 21).cloned().collect();
assert_eq!(step21_only.len(), 1, "step-21 must exist for this guard to mean anything");
migrate_with_steps(&conn, &step21_only).expect("forward migrate to v21");
assert_eq!(
source_id_of(&conn, "canonical_edges", 6).as_deref(),
Some(LEGACY_SOURCE_ID),
"a legacy edge with NULL provenance and a non-NULL `logical_id` must be \
back-filled: edge `logical_id` is NOT purge-addressable, so without this \
the row is erasable by no verb at all (R-20-E8)"
);
assert_eq!(
source_id_of(&conn, "canonical_nodes", 2),
None,
"the node gate must remain `source_id IS NULL AND logical_id IS NULL`"
);
assert_eq!(
source_id_of(&conn, "canonical_edges", 7).as_deref(),
Some("doc-caller-3"),
"caller-supplied edge provenance must never be overwritten"
);
}
#[test]
fn s21_backfill_populates_no_logical_id() {
let conn = seed_v20_matrix();
let before: Vec<(i64, Option<String>)> = conn
.prepare("SELECT write_cursor, logical_id FROM canonical_nodes ORDER BY write_cursor")
.unwrap()
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.unwrap()
.filter_map(Result::ok)
.collect();
let step21_only: Vec<_> = MIGRATIONS.iter().filter(|m| m.step_id == 21).cloned().collect();
assert_eq!(step21_only.len(), 1, "step-21 must exist for this guard to mean anything");
migrate_with_steps(&conn, &step21_only).expect("forward migrate to v21");
let after: Vec<(i64, Option<String>)> = conn
.prepare("SELECT write_cursor, logical_id FROM canonical_nodes ORDER BY write_cursor")
.unwrap()
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(
before, after,
"TC-11 pin: no migration may populate or re-derive `logical_id` on an existing \
canonical row"
);
let transitioned =
before.iter().zip(&after).filter(|((_, b), (_, a))| b.is_none() && a.is_some()).count();
assert_eq!(transitioned, 0, "rows transitioning logical_id NULL → NOT NULL must be 0");
assert_eq!(logical_id_of(&conn, "canonical_nodes", 1), None);
assert_eq!(logical_id_of(&conn, "canonical_edges", 5), None);
}
#[test]
fn s21_is_in_registry_and_schema_version_is_head() {
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 (step-26 canonical FTS-hydration join indexes, Slice 19)"
);
assert!(
MIGRATIONS.iter().any(|m| m.step_id == 21),
"step-21 (legacy provenance backfill) must remain in the registry"
);
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"
);
}
#[test]
fn s21_passes_accretion_guard_without_marker() {
let step21 = MIGRATIONS
.iter()
.find(|m| m.step_id == 21)
.expect("step 21 (legacy provenance backfill) must exist");
assert!(
!step21.sql.contains("MIGRATION-ACCRETION-EXEMPTION"),
"step-21 is a pure data backfill and needs no accretion exemption"
);
check_migration_accretion("021_legacy_provenance_backfill.sql", step21.sql)
.expect("a pure UPDATE migration must pass the accretion guard unmarked");
}