#![allow(clippy::unwrap_used, clippy::expect_used)]
use sqlite_core::rebuild::{
build_recovered_db, build_recovered_db_with_fragments, FragmentRow, RebuildRow,
};
use sqlite_core::{Database, Value};
const LEAD_COLS: usize = 5;
const FRAG_LEAD_COLS: usize = 4;
fn frag(
page: u32,
offset: usize,
missing: usize,
confidence: f32,
surviving: Vec<(usize, Value)>,
) -> FragmentRow {
FragmentRow {
page,
offset,
missing,
confidence,
surviving,
}
}
fn read_named_table(db: &Database, name: &str) -> Vec<sqlite_core::Row> {
let schema = db.read_table(1, 5).expect("schema readable");
let root = schema
.iter()
.find_map(
|r| match (r.values.first(), r.values.get(1), r.values.get(3)) {
(Some(Value::Text(t)), Some(Value::Text(n)), Some(Value::Integer(root)))
if t == "table" && n == name =>
{
Some(*root as u32)
}
_ => None,
},
)
.unwrap_or_else(|| panic!("{name} table present in schema"));
db.read_table(root, 64).expect("table readable")
}
fn row(rowid: Option<i64>, source: &str, confidence: f32, cells: Vec<Value>) -> RebuildRow {
RebuildRow {
page: 7,
offset: 128,
rowid,
source: source.to_string(),
confidence,
cells,
}
}
fn read_back(bytes: Vec<u8>) -> Vec<sqlite_core::Row> {
let db = Database::open(bytes).expect("rebuilt db must re-open");
let schema = db.read_table(1, 5).expect("schema readable");
let root = schema
.iter()
.find_map(|r| match (r.values.first(), r.values.get(3)) {
(Some(Value::Text(t)), Some(Value::Integer(root))) if t == "table" => {
Some(*root as u32)
}
_ => None,
})
.expect("recovered_records table present in schema");
db.read_table(root, 64).expect("table readable")
}
#[test]
fn single_small_row_round_trips_through_self_reader() {
let rows = vec![row(
Some(42),
"freelist-page",
0.9,
vec![
Value::Integer(7),
Value::Text("alice".into()),
Value::Real(1.5),
Value::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]),
],
)];
let bytes = build_recovered_db(&rows);
let back = read_back(bytes);
assert_eq!(back.len(), 1, "one carved record -> one row");
let r = &back[0];
assert_eq!(r.values[0], Value::Integer(7)); assert_eq!(r.values[1], Value::Integer(128)); assert_eq!(r.values[2], Value::Integer(42)); assert_eq!(r.values[3], Value::Text("freelist-page".into())); match &r.values[4] {
Value::Real(c) => assert!((*c - 0.9).abs() < 1e-6, "confidence {c}"),
other => panic!("_confidence must be REAL, got {other:?}"),
}
assert_eq!(r.values[LEAD_COLS], Value::Integer(7));
assert_eq!(r.values[LEAD_COLS + 1], Value::Text("alice".into()));
assert_eq!(r.values[LEAD_COLS + 2], Value::Real(1.5));
assert_eq!(
r.values[LEAD_COLS + 3],
Value::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]),
"BLOB stored natively, byte-for-byte"
);
}
#[test]
fn unknown_rowid_is_null_and_missing_trailing_cells_are_null() {
let rows = vec![
row(
Some(10),
"in-page-freeblock",
0.8,
vec![
Value::Integer(1),
Value::Text("x".into()),
Value::Integer(3),
],
),
row(
None,
"freeblock-reconstructed",
0.4,
vec![Value::Text("solo".into())],
),
];
let bytes = build_recovered_db(&rows);
let back = read_back(bytes);
assert_eq!(back.len(), 2);
let a = back
.iter()
.find(|r| r.values[2] == Value::Integer(10))
.unwrap();
assert_eq!(a.values[LEAD_COLS], Value::Integer(1));
assert_eq!(a.values[LEAD_COLS + 2], Value::Integer(3));
let b = back.iter().find(|r| r.values[2] == Value::Null).unwrap();
assert_eq!(b.values[3], Value::Text("freeblock-reconstructed".into()));
assert_eq!(b.values[LEAD_COLS], Value::Text("solo".into()));
assert_eq!(b.values[LEAD_COLS + 1], Value::Null);
assert_eq!(b.values[LEAD_COLS + 2], Value::Null);
}
#[test]
fn empty_input_produces_a_valid_empty_table() {
let bytes = build_recovered_db(&[]);
let back = read_back(bytes);
assert!(back.is_empty(), "no rows -> empty (but valid) table");
}
#[test]
fn large_blob_spills_to_overflow_pages_and_round_trips() {
let big: Vec<u8> = (0..9000u32).map(|i| (i % 251) as u8).collect();
let rows = vec![row(
Some(1),
"freelist-page",
0.95,
vec![Value::Text("spilled".into()), Value::Blob(big.clone())],
)];
let bytes = build_recovered_db(&rows);
let back = read_back(bytes);
assert_eq!(back.len(), 1);
assert_eq!(back[0].values[LEAD_COLS], Value::Text("spilled".into()));
assert_eq!(
back[0].values[LEAD_COLS + 1],
Value::Blob(big),
"overflow-spilled BLOB reassembles byte-for-byte"
);
}
#[test]
fn fragments_land_at_their_native_column_index() {
let records = vec![
row(Some(1), "freelist-page", 0.9, vec![Value::Integer(10)]),
row(
Some(2),
"freelist-page",
0.8,
vec![Value::Text("r2".into())],
),
];
let fragments = vec![
frag(
2,
3965,
1,
0.2,
vec![
(0, Value::Integer(20004)),
(1, Value::Text("Anja".into())),
(2, Value::Text("Frank".into())),
],
),
frag(
5,
64,
2,
0.2,
vec![(2, Value::Blob(vec![0xCA, 0xFE, 0xBA]))],
),
];
let bytes = build_recovered_db_with_fragments(&records, Some(&fragments));
let db = Database::open(bytes).expect("rebuilt db must re-open");
let recs = read_named_table(&db, "recovered_records");
assert_eq!(recs.len(), 2, "both full records present");
let frags = read_named_table(&db, "recovered_fragments");
assert_eq!(frags.len(), 2, "both fragments present");
let a = frags
.iter()
.find(|r| r.values[FRAG_LEAD_COLS] == Value::Integer(20004))
.expect("fragment A by its surviving c0");
assert_eq!(a.values[0], Value::Integer(2)); assert_eq!(a.values[1], Value::Integer(3965)); assert_eq!(a.values[2], Value::Integer(1)); match &a.values[3] {
Value::Real(c) => assert!((*c - 0.2).abs() < 1e-6, "_confidence {c}"),
other => panic!("_confidence must be REAL, got {other:?}"),
}
assert_eq!(a.values[FRAG_LEAD_COLS + 1], Value::Text("Anja".into())); assert_eq!(a.values[FRAG_LEAD_COLS + 2], Value::Text("Frank".into()));
let b = frags
.iter()
.find(|r| r.values[FRAG_LEAD_COLS + 2] == Value::Blob(vec![0xCA, 0xFE, 0xBA]))
.expect("fragment B by its surviving c2 BLOB");
assert_eq!(
b.values[FRAG_LEAD_COLS],
Value::Null,
"c0 NULL (not surviving)"
);
assert_eq!(
b.values[FRAG_LEAD_COLS + 1],
Value::Null,
"c1 NULL (not surviving)"
);
assert_eq!(b.values[2], Value::Integer(2)); }
#[test]
fn fragments_none_is_byte_identical_to_single_table_output() {
let records = vec![
row(Some(1), "freelist-page", 0.9, vec![Value::Integer(10)]),
row(
None,
"freeblock-reconstructed",
0.4,
vec![Value::Text("x".into())],
),
];
let single = build_recovered_db(&records);
let two_table_none = build_recovered_db_with_fragments(&records, None);
assert_eq!(
single, two_table_none,
"None fragments must be byte-identical to the single-table writer"
);
}
#[test]
fn empty_fragment_set_still_creates_the_table() {
let records = vec![row(Some(1), "freelist-page", 0.9, vec![Value::Integer(10)])];
let bytes = build_recovered_db_with_fragments(&records, Some(&[]));
let db = Database::open(bytes).expect("rebuilt db must re-open");
let frags = read_named_table(&db, "recovered_fragments");
assert!(frags.is_empty(), "no fragments -> empty fragment table");
assert_eq!(read_named_table(&db, "recovered_records").len(), 1);
}