use nodedb_types::Namespace;
use crate::error::LiteError;
use crate::storage::engine::{StorageEngine, WriteOp};
use super::engine::StrictEngine;
const HISTORY_TRAILER_LEN: usize = 8;
impl<S: StorageEngine> StrictEngine<S> {
pub(super) async fn record_history_supersession(
&self,
collection: &str,
pk_bytes: &[u8],
old_tuple: &[u8],
system_to_ms: i64,
) -> Result<(), LiteError> {
let system_from_ms = extract_system_from_ms(old_tuple);
let hist_key = history_key(collection, system_from_ms, pk_bytes);
let hist_value = history_value(old_tuple, system_to_ms);
self.storage
.put(Namespace::StrictHistory, &hist_key, &hist_value)
.await
}
pub async fn purge_history_before(
&self,
collection: &str,
cutoff_ms: i64,
) -> Result<u64, LiteError> {
let state = self.get_state(collection)?;
if !state.schema.bitemporal {
return Ok(0);
}
let prefix = history_prefix(collection);
let entries = self
.storage
.scan_prefix(Namespace::StrictHistory, &prefix)
.await?;
let mut to_delete: Vec<Vec<u8>> = Vec::new();
for (key, value) in &entries {
if let Some(system_to_ms) = extract_system_to_from_value(value) {
if system_to_ms < u64::MAX && (system_to_ms as i64) < cutoff_ms {
to_delete.push(key.clone());
}
}
}
let count = to_delete.len() as u64;
let ops: Vec<WriteOp> = to_delete
.into_iter()
.map(|key| WriteOp::Delete {
ns: Namespace::StrictHistory,
key,
})
.collect();
if !ops.is_empty() {
self.storage.batch_write(&ops).await?;
}
Ok(count)
}
}
pub(super) fn history_key(collection: &str, system_from_ms: i64, pk_bytes: &[u8]) -> Vec<u8> {
let mut key = collection.as_bytes().to_vec();
key.push(b':');
key.extend_from_slice(&(system_from_ms as u64).to_be_bytes());
key.push(b':');
key.extend_from_slice(pk_bytes);
key
}
fn history_prefix(collection: &str) -> Vec<u8> {
let mut prefix = collection.as_bytes().to_vec();
prefix.push(b':');
prefix
}
pub(super) fn history_value(tuple: &[u8], system_to_ms: i64) -> Vec<u8> {
let mut v = tuple.to_vec();
v.extend_from_slice(&(system_to_ms as u64).to_be_bytes());
v
}
fn extract_system_to_from_value(value: &[u8]) -> Option<u64> {
if value.len() < HISTORY_TRAILER_LEN {
return None;
}
let trailer_start = value.len() - HISTORY_TRAILER_LEN;
let bytes: [u8; 8] = value[trailer_start..].try_into().ok()?;
Some(u64::from_be_bytes(bytes))
}
fn extract_system_from_ms(tuple: &[u8]) -> i64 {
let _ = tuple;
0i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn history_key_roundtrip() {
let key = history_key("orders", 1_700_000_000_000, b"pk42");
assert!(key.starts_with(b"orders:"));
let from_bytes: [u8; 8] = key[7..15].try_into().unwrap();
assert_eq!(u64::from_be_bytes(from_bytes), 1_700_000_000_000u64);
}
#[test]
fn history_value_system_to_extraction() {
let tuple = vec![1u8, 2, 3, 4, 5];
let system_to: i64 = 9_999_999;
let val = history_value(&tuple, system_to);
let extracted = extract_system_to_from_value(&val).unwrap();
assert_eq!(extracted, system_to as u64);
}
#[test]
fn history_value_too_short() {
assert!(extract_system_to_from_value(&[1, 2, 3]).is_none());
}
}