use std::fs;
use std::path::Path;
use nedb_engine::Db;
use serde_json::json;
fn put_one(db: &Db, coll: &str, id: &str, v: &str) {
db.put_batch(vec![(
coll.to_string(),
id.to_string(),
json!({ "v": v }),
Vec::new(),
None,
None,
)])
.expect("put_batch");
db.flush_all();
}
fn value(db: &Db, coll: &str, id: &str) -> Option<String> {
db.get(coll, id)
.and_then(|n| n.data.get("v").and_then(|x| x.as_str().map(|s| s.to_string())))
}
#[test]
fn compaction_prunes_history_and_degrades_gracefully() {
std::env::set_var("NEDB_DAG_V3", "1");
let tmp = std::env::temp_dir().join(format!("nedb_compact_hist_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
let db = Db::open(&tmp, None).unwrap();
put_one(&db, "o", "a", "first");
put_one(&db, "o", "live", "stays");
let v1_seq = db.get("o", "a").expect("just written").seq;
assert!(db.delete("o", "a").unwrap());
assert_eq!(
db.get_as_of("o", "a", v1_seq).map(|n| n.data["v"].clone()),
Some(json!("first")),
"a deleted document's history must be readable before compaction",
);
db.compact().expect("compact");
assert!(db.get_as_of("o", "a", v1_seq).is_none(),
"a pruned version must read as absent, never as a stale value");
assert!(db.get("o", "a").is_none(), "and it is still deleted");
assert_eq!(value(&db, "o", "live").as_deref(), Some("stays"),
"the living are untouched by compaction");
let (_ok, bad) = db.verify();
assert!(bad.is_empty(), "verify stays clean after compaction: {:?}", bad);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn v3_segment_substrate_end_to_end() {
let tmp = std::env::temp_dir().join(format!("nedb_v3_it_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
std::env::set_var("NEDB_DAG_V3", "1");
let d1 = tmp.join("v3");
{
let db = Db::open(&d1, None).unwrap();
for i in 0..200 {
put_one(&db, "coins", &format!("u-{:04}", i), &format!("val-{}", i));
}
assert_eq!(value(&db, "coins", "u-0000").as_deref(), Some("val-0"));
assert_eq!(value(&db, "coins", "u-0199").as_deref(), Some("val-199"));
let (ok, bad) = db.verify();
assert!(bad.is_empty(), "v3 verify must be clean: {:?}", bad);
assert!(ok >= 200, "v3 verify should cover all objects");
}
assert!(d1.join("objects").join("segments").is_dir(), "v3 must create objects/segments/");
{
let db = Db::open(&d1, None).unwrap();
assert_eq!(value(&db, "coins", "u-0123").as_deref(), Some("val-123"));
put_one(&db, "coins", "u-0001", "val-1-NEW"); let stats = db.compact().expect("compact");
assert!(stats.dropped_objects >= 1, "compaction should drop the dead old version");
assert_eq!(value(&db, "coins", "u-0001").as_deref(), Some("val-1-NEW"), "current version survives compaction");
assert_eq!(value(&db, "coins", "u-0150").as_deref(), Some("val-150"), "untouched current version survives");
let (_ok, bad) = db.verify();
assert!(bad.is_empty(), "verify clean after compaction: {:?}", bad);
}
{
let db = Db::open(&d1, None).unwrap();
assert_eq!(value(&db, "coins", "u-0001").as_deref(), Some("val-1-NEW"));
assert_eq!(value(&db, "coins", "u-0150").as_deref(), Some("val-150"));
}
let d2 = tmp.join("migrate");
std::env::remove_var("NEDB_DAG_V3"); {
let db = Db::open(&d2, None).unwrap();
put_one(&db, "coins", "old-1", "loose-1");
put_one(&db, "coins", "old-2", "loose-2");
}
assert!(!d2.join("objects").join("segments").is_dir(), "v2 mode must NOT create segments/");
std::env::set_var("NEDB_DAG_V3", "1"); {
let db = Db::open(&d2, None).unwrap();
assert_eq!(value(&db, "coins", "old-1").as_deref(), Some("loose-1"));
assert_eq!(value(&db, "coins", "old-2").as_deref(), Some("loose-2"));
put_one(&db, "coins", "new-1", "seg-1");
assert_eq!(value(&db, "coins", "new-1").as_deref(), Some("seg-1"));
assert!(d2.join("objects").join("segments").is_dir(), "new writes create segments/");
}
{
let db = Db::open(&d2, None).unwrap();
assert_eq!(value(&db, "coins", "old-1").as_deref(), Some("loose-1"));
assert_eq!(value(&db, "coins", "new-1").as_deref(), Some("seg-1"));
}
std::env::remove_var("NEDB_DAG_V3");
let d3 = tmp.join("default");
{
let db = Db::open(&d3, None).unwrap();
put_one(&db, "coins", "x", "y");
assert_eq!(value(&db, "coins", "x").as_deref(), Some("y"));
}
assert!(!d3.join("objects").join("segments").is_dir(), "default (no flag) must stay v2 loose");
let _ = fs::remove_dir_all(&tmp);
}
#[allow(dead_code)]
fn _unused(_p: &Path) {}
#[test]
fn compaction_prunes_history_and_verification_says_so_plainly() {
use nedb_engine::db::Db;
use nedb_engine::root::{RecordStatus, Recomputation, UnavailableReason};
std::env::set_var("NEDB_DAG_V3", "1");
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path(), None).unwrap();
db.put("orders", "1", serde_json::json!({"v": 1}), vec![], None, None).unwrap();
let rec = db.create_root().unwrap();
db.put("orders", "1", serde_json::json!({"v": 2}), vec![], None, None).unwrap();
db.flush_all();
assert!(db.verify_root(rec.at_seq).is_verified(), "verifiable before the prune");
assert_eq!(db.history_floor(), 0);
let stats = db.compact().expect("compact");
assert!(stats.dropped_objects > 0, "v3 compaction must actually drop the superseded version");
assert!(db.history_floor() > 0, "and must record where history now begins");
let v = db.verify_root(rec.at_seq);
assert_eq!(v.record, RecordStatus::Valid, "the record itself is still fine");
assert_eq!(
v.recomputation,
Recomputation::Unavailable(UnavailableReason::HistoryPruned),
"and the engine says plainly that it could not check it"
);
assert!(!v.is_verified(), "unavailable is not verified");
assert!(!v.is_mismatch(), "and it is not a mismatch either");
assert_eq!(v.exit_code(), 3, "its own exit code, distinct from pass and fail");
}