#![allow(clippy::unwrap_used, clippy::expect_used)]
use sqlite_core::{Database, Value};
use sqlite_forensic::{carve_all_deleted_records, carve_with_fragments, RecoverySource};
mod nemetz_support;
const NEMETZ_0E_01: &[u8] = include_bytes!("../../tests/data/nemetz/0E/0E-01.db");
fn expected_code(nid: &str, id_col0: &str) -> String {
for el in nemetz_support::manifest().db(nid).elements() {
for row in el.deleted() {
let cells = row.cells();
if cells.first().map(String::as_str) == Some(id_col0) {
return cells[2].clone();
}
}
}
panic!("ground-truth manifest missing deleted row {id_col0} in {nid}");
}
#[test]
fn recovers_surviving_overflow_chain_as_full_row() {
let db = Database::open(NEMETZ_0E_01.to_vec()).unwrap();
let carved = carve_all_deleted_records(&db);
let ella = carved
.iter()
.find(|r| {
matches!(r.values.first(), Some(Value::Integer(20012)))
&& matches!(r.values.get(1), Some(Value::Text(t)) if t == "Ella")
})
.expect("Ella (rowid 20012) must be recovered as a full Tier-1 row");
assert_eq!(ella.rowid, 12);
match ella.values.get(2) {
Some(Value::Text(code)) => {
assert_eq!(
*code,
expected_code("0E-01", "20012"),
"reassembled code must byte-equal the ground-truth answer key"
);
}
other => panic!("code column should be Text, got {other:?}"),
}
assert!(matches!(ella.values.get(3), Some(Value::Integer(38533))));
let prov = ella
.overflow
.as_ref()
.expect("a chain-reassembled row carries OverflowProvenance");
assert_eq!(prov.first_page, 13);
assert_eq!(prov.chain, vec![13]);
}
#[test]
fn rejects_trunk_clobbered_chain_from_tier1() {
let db = Database::open(NEMETZ_0E_01.to_vec()).unwrap();
let carved = carve_all_deleted_records(&db);
let matteo_full = carved.iter().any(|r| {
matches!(r.values.first(), Some(Value::Integer(20003)))
&& matches!(r.values.get(1), Some(Value::Text(t)) if t == "Matteo")
&& matches!(r.values.get(2), Some(Value::Text(c)) if c.len() == 4091)
});
assert!(
!matteo_full,
"trunk-clobbered Matteo chain must NOT be a Tier-1 full row (false positive)"
);
}
#[test]
fn overflow_tier1_confidence_is_graded_below_in_page() {
let db = Database::open(NEMETZ_0E_01.to_vec()).unwrap();
let carved = carve_all_deleted_records(&db);
let ella = carved
.iter()
.find(|r| matches!(r.values.first(), Some(Value::Integer(20012))))
.expect("Ella recovered");
assert!(
ella.confidence < 0.72,
"overflow Tier-1 confidence {} must be graded below the in-page full-row tier",
ella.confidence
);
assert!(matches!(
ella.source,
RecoverySource::InPageFreeBlock | RecoverySource::FreelistPage
));
assert!(!ella.allocated);
}
#[test]
fn carve_with_fragments_full_tier_includes_surviving_chain() {
let db = Database::open(NEMETZ_0E_01.to_vec()).unwrap();
let tiers = carve_with_fragments(&db);
let full = carve_all_deleted_records(&db);
assert_eq!(tiers.full, full);
assert!(tiers
.full
.iter()
.any(|r| matches!(r.values.first(), Some(Value::Integer(20012)))));
}
#[test]
fn matteo_broken_chain_surfaces_as_fragment() {
let db = Database::open(NEMETZ_0E_01.to_vec()).unwrap();
let tiers = carve_with_fragments(&db);
let matteo = tiers
.fragments
.iter()
.find(|f| {
f.surviving
.iter()
.any(|(_, v)| matches!(v, Value::Text(t) if t == "Matteo"))
})
.expect("Matteo's broken chain must surface as a Tier-2 fragment");
assert!(matteo
.surviving
.iter()
.any(|(i, v)| *i == 0 && matches!(v, Value::Integer(20003))));
assert!(matteo
.surviving
.iter()
.any(|(i, v)| *i == 1 && matches!(v, Value::Text(t) if t == "Matteo")));
assert!(!matteo
.surviving
.iter()
.any(|(_, v)| matches!(v, Value::Text(t) if t.len() == 4091)));
assert!(!tiers.full.iter().any(|r| {
matches!(r.values.first(), Some(Value::Integer(20003)))
&& matches!(r.values.get(2), Some(Value::Text(c)) if c.len() == 4091)
}));
}