#![allow(clippy::unwrap_used, clippy::expect_used)]
use sqlite_core::Database;
use sqlite_forensic::{
attribute_records, carve_all_deleted_records, table_instance_risks, TableInstanceRisk,
};
const B_AUTOINC: &[u8] = include_bytes!("../../tests/data/drop_recreate/b_autoinc.db");
const B_PLAINPK: &[u8] = include_bytes!("../../tests/data/drop_recreate/b_plainpk.db");
const UPD_AUTOINC: &[u8] = include_bytes!("../../tests/data/drop_recreate/upd_autoinc.db");
fn fired(recs: &[sqlite_forensic::CarvedRecord], risks: &[TableInstanceRisk]) -> Vec<(i64, i64)> {
let mut out: Vec<(i64, i64)> = recs
.iter()
.zip(risks)
.filter_map(|(_rec, risk)| match risk {
TableInstanceRisk::RowidExceedsAutoincHighwater { rowid, seq, .. } => {
Some((*rowid, *seq))
}
_ => None,
})
.collect();
out.sort_unstable();
out
}
fn risks_for(bytes: &[u8]) -> (Vec<sqlite_forensic::CarvedRecord>, Vec<TableInstanceRisk>) {
let db = Database::open(bytes.to_vec()).expect("open fixture");
let recs = carve_all_deleted_records(&db);
let attrs = attribute_records(&db, &recs);
let risks = table_instance_risks(&db, &recs, &attrs);
assert_eq!(risks.len(), recs.len(), "one risk per record");
(recs, risks)
}
#[test]
fn b_autoinc_flags_residue_above_highwater() {
let (recs, risks) = risks_for(B_AUTOINC);
let hits = fired(&recs, &risks);
let rowids: Vec<i64> = hits.iter().map(|(r, _)| *r).collect();
assert_eq!(
rowids,
vec![6, 7, 8, 9, 10],
"flag fires on residue rowids 6..=10 only"
);
for (_, seq) in &hits {
assert_eq!(*seq, 5, "evidence seq is the high-water mark 5");
}
}
#[test]
fn b_plainpk_never_flags() {
let (recs, risks) = risks_for(B_PLAINPK);
assert!(
!recs.is_empty(),
"plain-PK residue is still recovered (only the flag is withheld)"
);
assert!(
fired(&recs, &risks).is_empty(),
"no sqlite_sequence ⟹ the flag must NEVER fire"
);
}
#[test]
fn upd_autoinc_flags_a_current_instance_row_proving_hint_not_assertion() {
let (recs, risks) = risks_for(UPD_AUTOINC);
let hits = fired(&recs, &risks);
assert_eq!(
hits,
vec![(1000, 5)],
"flag fires on the moved-then-deleted current row (rowid 1000 > seq 5)"
);
}
#[test]
fn nemetz_databases_never_spuriously_flag() {
for rel in ["nemetz/0C/0C-01.db", "nemetz/0D/0D-01.db"] {
let path = format!("{}/../tests/data/{rel}", env!("CARGO_MANIFEST_DIR"));
let Ok(bytes) = std::fs::read(&path) else {
continue; };
let db = Database::open(bytes).expect("open nemetz db");
let recs = carve_all_deleted_records(&db);
let attrs = attribute_records(&db, &recs);
let risks = table_instance_risks(&db, &recs, &attrs);
assert!(
risks.iter().all(|r| *r == TableInstanceRisk::None),
"{rel}: ordinary Nemetz deleted rows must NEVER trip the flag"
);
}
}
#[test]
fn note_is_evidence_bearing_and_non_overclaiming() {
let risk = TableInstanceRisk::RowidExceedsAutoincHighwater {
table: "students".to_string(),
rowid: 10,
seq: 5,
};
let note = risk.note();
assert!(note.contains("rowid 10"), "note shows the rowid");
assert!(note.contains("students"), "note shows the table");
assert!(
note.contains("sqlite_sequence=5"),
"note shows the seq evidence"
);
assert!(
note.contains("consistent with"),
"note uses consistent-with framing"
);
assert!(
!note.to_lowercase().contains("predecessor"),
"note must never assert 'predecessor'"
);
assert!(TableInstanceRisk::None.note().is_empty());
}