use serde::{Deserialize, Serialize};
use crate::path::RelPath;
use crate::store::Store;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DocTombstone {
pub hash_hex: String,
pub guard_digest: String,
pub timeout_secs: u64,
}
impl Store {
pub fn upsert_doc_tombstone(&mut self, rel: impl Into<RelPath>, tombstone: DocTombstone) {
self.index.doc_tombstones.insert(rel.into(), tombstone);
}
pub fn remove_doc_tombstone(&mut self, rel: impl AsRef<[u8]>) -> bool {
self.index
.doc_tombstones
.remove(bstr::BStr::new(rel.as_ref()))
.is_some()
}
pub fn has_doc_tombstone(&self, rel: impl AsRef<[u8]>) -> bool {
self.index.doc_tombstones.contains_key(bstr::BStr::new(rel.as_ref()))
}
pub fn lookup_doc_tombstone(&self, rel: impl AsRef<[u8]>) -> Option<&DocTombstone> {
self.index.doc_tombstones.get(bstr::BStr::new(rel.as_ref()))
}
pub fn doc_tombstoned(&self, rel: impl AsRef<[u8]>, hash_hex: &str, guard_digest: &str) -> bool {
self.lookup_doc_tombstone(rel)
.is_some_and(|t| t.hash_hex == hash_hex && t.guard_digest == guard_digest)
}
pub fn retain_doc_tombstones(&mut self, mut keep: impl FnMut(&RelPath) -> bool) {
self.index.doc_tombstones.retain(|rel, _| keep(rel));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::{Index, VIEW_WORKING};
fn tombstone(hash: &str, digest: &str) -> DocTombstone {
DocTombstone {
hash_hex: hash.to_string(),
guard_digest: digest.to_string(),
timeout_secs: 120,
}
}
fn store() -> (tempfile::TempDir, Store) {
crate::store::init_isolated_cache();
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path(), VIEW_WORKING).unwrap();
(dir, store)
}
#[test]
fn a_tombstone_applies_only_to_the_same_content_and_limits() {
let (_dir, mut store) = store();
store.upsert_doc_tombstone("big.csv", tombstone("aa", "g1"));
assert!(store.doc_tombstoned("big.csv", "aa", "g1"));
assert!(!store.doc_tombstoned("big.csv", "bb", "g1"), "edited file retries");
assert!(!store.doc_tombstoned("big.csv", "aa", "g2"), "changed limits retry");
assert!(!store.doc_tombstoned("other.csv", "aa", "g1"));
}
#[test]
fn tombstones_are_removed_and_pruned() {
let (_dir, mut store) = store();
store.upsert_doc_tombstone("keep.csv", tombstone("aa", "g"));
store.upsert_doc_tombstone("gone.csv", tombstone("bb", "g"));
assert!(store.remove_doc_tombstone("keep.csv"));
assert!(!store.remove_doc_tombstone("keep.csv"));
store.upsert_doc_tombstone("keep.csv", tombstone("aa", "g"));
store.retain_doc_tombstones(|rel| rel.to_str_lossy() != "gone.csv");
assert!(store.has_doc_tombstone("keep.csv"));
assert!(!store.has_doc_tombstone("gone.csv"));
}
#[test]
fn tombstones_survive_a_flush_and_old_indexes_read_without_them() {
let (dir, mut store) = store();
store.upsert_doc_tombstone("big.csv", tombstone("aa", "g1"));
store.flush().unwrap();
drop(store);
let reopened = Store::open(dir.path(), VIEW_WORKING).unwrap();
assert!(reopened.doc_tombstoned("big.csv", "aa", "g1"));
let legacy = serde_json::json!({ "schema_ver": crate::extract::SCHEMA_VER, "files": {} });
let index: Index = serde_json::from_value(legacy).unwrap();
assert!(index.doc_tombstones.is_empty());
}
}