mod common;
use chisel::{Chisel, ChiselError, Handle, Tag};
use common::{open_chisel, Backing};
use tempfile::NamedTempFile;
fn tag_survives_in_session_body(b: &Backing) {
let mut db = open_chisel(b);
db.begin().unwrap();
let h = db
.allocate_tagged(b"relation row", Tag::new(77).unwrap())
.unwrap();
db.commit().unwrap();
assert_eq!(db.tag(h).unwrap().unwrap(), 77);
assert_eq!(db.handles_with_tag(Tag::new(77).unwrap()).unwrap(), vec![h]);
db.close().unwrap();
}
dual_backing_test!(tag_survives_in_session, tag_survives_in_session_body);
#[test]
fn tag_and_index_survive_reopen() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_owned();
let h;
{
let mut db = Chisel::open(&path, Default::default()).unwrap();
db.begin().unwrap();
h = db
.allocate_tagged(b"survive", Tag::new(9).unwrap())
.unwrap();
db.allocate_tagged(b"survive2", Tag::new(9).unwrap())
.unwrap();
db.commit().unwrap();
db.close().unwrap();
}
{
let db = Chisel::open(&path, Default::default()).unwrap();
assert_eq!(db.tag(h).unwrap().unwrap(), 9);
assert_eq!(db.handles_with_tag(Tag::new(9).unwrap()).unwrap().len(), 2);
db.close().unwrap();
}
}
#[test]
fn delete_tagged_verifies_tag_then_self_maintains_index() {
let mut db = Chisel::open_in_memory().unwrap();
db.begin().unwrap();
let a = db.allocate_tagged(b"a", Tag::new(42).unwrap()).unwrap();
let b = db.allocate_tagged(b"b", Tag::new(42).unwrap()).unwrap();
db.commit().unwrap();
db.begin().unwrap();
let err = match db.delete_tagged(a, Tag::new(99).unwrap()) {
Ok(()) => panic!("delete_tagged with a non-matching tag must fail"),
Err(e) => e,
};
assert!(
matches!(
err,
ChiselError::TagMismatch { handle, expected, actual }
if handle == a && expected == 99 && actual == 42
),
"expected TagMismatch {{handle: a, expected: 99, actual: 42}}, got {err:?}"
);
db.commit().unwrap();
assert_eq!(
db.read(a).unwrap(),
b"a",
"chunk untouched after TagMismatch"
);
assert_eq!(
db.handles_with_tag(Tag::new(42).unwrap()).unwrap().len(),
2,
"reverse index untouched after TagMismatch"
);
db.begin().unwrap();
db.delete_tagged(a, Tag::new(42).unwrap()).unwrap();
db.commit().unwrap();
assert!(
matches!(db.read(a), Err(ChiselError::InvalidHandle(_))),
"chunk gone after delete_tagged: expected InvalidHandle"
);
assert_eq!(
db.handles_with_tag(Tag::new(42).unwrap()).unwrap(),
vec![b],
"a removed from the reverse index; b remains"
);
}
#[test]
fn dropping_a_relation_removes_all_handles_no_orphans() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_owned();
let mut db = Chisel::open(&path, Default::default()).unwrap();
db.begin().unwrap();
let mut hs = Vec::new();
for i in 0..200u64 {
hs.push(
db.allocate_tagged(format!("row {i}").as_bytes(), Tag::new(1).unwrap())
.unwrap(),
);
}
let keep = db
.allocate_tagged(b"other relation", Tag::new(2).unwrap())
.unwrap();
db.commit().unwrap();
assert_eq!(
db.handles_with_tag(Tag::new(1).unwrap()).unwrap().len(),
200
);
db.begin().unwrap();
let mut dropped = Vec::new();
loop {
let p = db.delete_with_tag(Tag::new(1).unwrap(), 64).unwrap();
dropped.extend(p.deleted);
if p.complete {
break;
}
}
db.commit().unwrap();
dropped.sort_unstable();
let mut expected = hs.clone();
expected.sort_unstable();
assert_eq!(
dropped, expected,
"every tagged handle reported dropped, once"
);
assert_eq!(
db.handles_with_tag(Tag::new(1).unwrap()).unwrap(),
Vec::<Handle>::new(),
"no orphan index entries remain for the dropped tag"
);
for h in &hs {
assert!(
matches!(db.read(*h), Err(ChiselError::InvalidHandle(_))),
"dropped chunk {h} must be InvalidHandle after delete_with_tag"
);
}
assert_eq!(db.tag(keep).unwrap().unwrap(), 2);
assert_eq!(
db.handles_with_tag(Tag::new(2).unwrap()).unwrap(),
vec![keep]
);
db.close().unwrap();
let db = Chisel::open(&path, Default::default()).unwrap();
assert_eq!(
db.handles_with_tag(Tag::new(1).unwrap()).unwrap(),
Vec::<Handle>::new(),
"dropped tag-1 relation must stay empty across reopen (no resurrection)"
);
for h in &hs {
assert!(
db.read(*h).is_err(),
"dropped chunk {h} must be unreadable after reopen"
);
}
assert_eq!(db.tag(keep).unwrap().unwrap(), 2);
assert_eq!(
db.handles_with_tag(Tag::new(2).unwrap()).unwrap(),
vec![keep]
);
assert_eq!(db.read(keep).unwrap(), b"other relation");
db.close().unwrap();
}
#[test]
fn old_database_opens_with_all_untagged() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_owned();
let h;
{
let mut db = Chisel::open(&path, Default::default()).unwrap();
db.begin().unwrap();
h = db.allocate(b"legacy").unwrap();
db.commit().unwrap();
db.close().unwrap();
}
{
let db = Chisel::open(&path, Default::default()).unwrap();
assert_eq!(db.tag(h).unwrap(), None);
assert_eq!(
db.handles_with_tag(Tag::new(1).unwrap()).unwrap(),
Vec::<Handle>::new()
);
db.close().unwrap();
}
}
#[test]
fn tag_of_untagged_handle_is_none() {
let mut db = Chisel::open_in_memory().unwrap();
db.begin().unwrap();
let h = db.allocate(b"plain").unwrap();
db.commit().unwrap();
assert_eq!(db.tag(h).unwrap(), None);
}
#[test]
fn tag_of_tagged_handle_is_some() {
let mut db = Chisel::open_in_memory().unwrap();
db.begin().unwrap();
let h = db.allocate_tagged(b"row", Tag::new(42).unwrap()).unwrap();
db.commit().unwrap();
assert_eq!(db.tag(h).unwrap(), Some(Tag::new(42).unwrap()));
assert_eq!(db.tag(h).unwrap().unwrap(), 42);
}
#[test]
fn recreating_a_dropped_tag_reuses_freed_pages() {
let mut db = Chisel::open_in_memory().unwrap();
db.begin().unwrap();
db.allocate_tagged(b"row", Tag::new(1).unwrap()).unwrap();
db.commit().unwrap();
db.begin().unwrap();
loop {
let p = db.delete_with_tag(Tag::new(1).unwrap(), 64).unwrap();
if p.complete {
break;
}
}
db.commit().unwrap();
let pages_after_drop = db.stats().unwrap().total_pages;
db.begin().unwrap();
db.allocate_tagged(b"row again", Tag::new(1).unwrap())
.unwrap();
db.commit().unwrap();
let pages_after_recreate = db.stats().unwrap().total_pages;
assert!(
pages_after_recreate <= pages_after_drop,
"re-creating a dropped tag must reuse freed pages, not extend: \
{pages_after_drop} -> {pages_after_recreate}"
);
}
#[test]
fn dropping_a_deep_tag_reopens_empty() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_owned();
let n = 1100u64; {
let mut db = Chisel::open(&path, Default::default()).unwrap();
let mut i = 0u64;
while i < n {
db.begin().unwrap();
let end = (i + 256).min(n);
while i < end {
db.allocate_tagged(format!("row {i}").as_bytes(), Tag::new(1).unwrap())
.unwrap();
i += 1;
}
db.commit().unwrap();
}
assert_eq!(
db.handles_with_tag(Tag::new(1).unwrap()).unwrap().len(),
n as usize
);
loop {
db.begin().unwrap();
let p = db.delete_with_tag(Tag::new(1).unwrap(), 256).unwrap();
db.commit().unwrap();
if p.complete {
break;
}
}
assert_eq!(
db.handles_with_tag(Tag::new(1).unwrap()).unwrap(),
Vec::<Handle>::new()
);
db.close().unwrap();
}
let db = Chisel::open(&path, Default::default()).unwrap();
assert_eq!(
db.handles_with_tag(Tag::new(1).unwrap()).unwrap(),
Vec::<Handle>::new(),
"a fully-dropped deep (multi-level) tag must stay empty across reopen"
);
db.close().unwrap();
}
#[test]
fn deleted_handle_is_invalid_across_all_entry_points() {
let mut db = Chisel::open_in_memory().unwrap();
db.begin().unwrap();
let h = db.allocate_tagged(b"row", Tag::new(7).unwrap()).unwrap();
db.delete(h).unwrap();
assert!(matches!(db.read(h), Err(ChiselError::InvalidHandle(_))));
assert!(matches!(db.tag(h), Err(ChiselError::InvalidHandle(_))));
assert!(matches!(
db.delete_tagged(h, Tag::new(7).unwrap()),
Err(ChiselError::InvalidHandle(_))
));
db.rollback().unwrap();
}