use fathomdb_engine::Engine;
use fathomdb_schema::{migrate_with_steps, Migration, MIGRATIONS, SCHEMA_VERSION, SQLITE_SUFFIX};
use rusqlite::Connection;
use tempfile::TempDir;
const V10_MIGRATIONS: &[Migration] = {
let (head, _tail) = MIGRATIONS.split_at(10);
head
};
struct Doc {
body: &'static str,
}
const CORPUS: &[Doc] = &[
Doc { body: "migration tokenizer recall validation corpus" },
Doc { body: "structured search hits carry score and branch" },
Doc { body: "forward only schema migrations are immutable" },
Doc { body: "porter unicode diacritics tokenizer upgrade" },
Doc { body: "canonical nodes project into the fts index" },
Doc { body: "vector branch reranks with euclidean distance" },
Doc { body: "bm twentyfive scores the text retrieval branch" },
Doc { body: "deduplicate on body keep vector ordering first" },
Doc { body: "write cursor is the interim identity carrier" },
Doc { body: "recall floor must hold across the migration boundary" },
];
const QUERIES: &[(&str, &str)] = &[
("tokenizer", "migration tokenizer recall validation corpus"),
("structured", "structured search hits carry score and branch"),
("immutable", "forward only schema migrations are immutable"),
("diacritics", "porter unicode diacritics tokenizer upgrade"),
("canonical", "canonical nodes project into the fts index"),
("euclidean", "vector branch reranks with euclidean distance"),
("twentyfive", "bm twentyfive scores the text retrieval branch"),
("deduplicate", "deduplicate on body keep vector ordering first"),
("interim", "write cursor is the interim identity carrier"),
("boundary", "recall floor must hold across the migration boundary"),
];
fn seed_v10_corpus(conn: &Connection) {
for (i, doc) in CORPUS.iter().enumerate() {
let cursor = (i + 1) as i64;
conn.execute(
"INSERT INTO canonical_nodes(write_cursor, kind, body) VALUES(?1, 'doc', ?2)",
rusqlite::params![cursor, doc.body],
)
.expect("seed canonical row");
conn.execute(
"INSERT INTO search_index(body, kind, write_cursor) VALUES(?1, 'doc', ?2)",
rusqlite::params![doc.body, cursor],
)
.expect("seed unicode61 fts row");
}
}
fn measure_recall(engine: &Engine) -> f64 {
let mut hits = 0usize;
for (query, relevant) in QUERIES {
let result = engine.search(query).expect("recall search");
let found = result.results.iter().any(|h| h.body == *relevant);
if found {
hits += 1;
}
}
hits as f64 / QUERIES.len() as f64
}
const FLOOR: f64 = 0.90;
#[test]
fn ac_fts_tokenizer_floor_holds_across_migration() {
let dir = TempDir::new().unwrap();
let path = dir.path().join(format!("tok_recall{SQLITE_SUFFIX}"));
{
let raw = Connection::open(&path).expect("raw open for v10 ingest");
migrate_with_steps(&raw, V10_MIGRATIONS).expect("migrate to v10");
seed_v10_corpus(&raw);
}
let before_recall = {
let opened =
Engine::open_with_migrations_for_test(&path, V10_MIGRATIONS, |_| {}).expect("open v10");
assert_eq!(
opened.report.schema_version_after, 10,
"phase A must open at SCHEMA_VERSION 10"
);
let r = measure_recall(&opened.engine);
opened.engine.close().unwrap();
r
};
eprintln!("[pr_g1_tokenizer_recall] BEFORE (v10 unicode61) recall = {before_recall:.3}");
assert!(
before_recall >= FLOOR,
"BEFORE-migration recall {before_recall:.3} is below the {FLOOR} floor"
);
let after_recall = {
let opened =
Engine::open_with_migrations_for_test(&path, MIGRATIONS, |_| {}).expect("open head");
assert_eq!(
opened.report.schema_version_after, SCHEMA_VERSION,
"phase B must migrate to head SCHEMA_VERSION (runs the step-11 tokenizer upgrade)"
);
assert!(
opened.report.schema_version_before == 10,
"phase B must observe a 10 -> 14 migration, saw before={}",
opened.report.schema_version_before
);
let r = measure_recall(&opened.engine);
opened.engine.close().unwrap();
r
};
eprintln!(
"[pr_g1_tokenizer_recall] AFTER (v11 porter unicode61 remove_diacritics) recall = \
{after_recall:.3} (delta {:+.3})",
after_recall - before_recall
);
assert!(
after_recall >= FLOOR,
"AFTER-migration recall {after_recall:.3} is below the {FLOOR} floor \
(before={before_recall:.3}); the tokenizer drop+recreate left the FTS \
index unpopulated on the migrated DB"
);
}