use std::collections::HashMap;
use crate::mvcc::visibility::VersionedRecord;
use crate::types::{
ETypeId, EdgeVersionData, NodeDelta, NodeId, NodeVersionData, PropKeyId, PropValue, Timestamp,
TxId,
};
const NULL_IDX: u32 = u32::MAX;
#[derive(Debug)]
pub struct SoaPropertyVersions<T> {
data: Vec<T>,
txids: Vec<TxId>,
commit_ts: Vec<Timestamp>,
prev_idx: Vec<u32>,
deleted: Vec<bool>,
heads: HashMap<u64, u32>,
free_list: Vec<u32>,
}
impl<T: Clone> SoaPropertyVersions<T> {
pub fn new() -> Self {
Self {
data: Vec::new(),
txids: Vec::new(),
commit_ts: Vec::new(),
prev_idx: Vec::new(),
deleted: Vec::new(),
heads: HashMap::new(),
free_list: Vec::new(),
}
}
pub fn append(&mut self, key: u64, value: T, txid: TxId, commit_ts: Timestamp) {
let prev = self.heads.get(&key).copied().unwrap_or(NULL_IDX);
let idx = if let Some(free_idx) = self.free_list.pop() {
self.data[free_idx as usize] = value;
self.txids[free_idx as usize] = txid;
self.commit_ts[free_idx as usize] = commit_ts;
self.prev_idx[free_idx as usize] = prev;
self.deleted[free_idx as usize] = false;
free_idx
} else {
let idx = self.data.len() as u32;
self.data.push(value);
self.txids.push(txid);
self.commit_ts.push(commit_ts);
self.prev_idx.push(prev);
self.deleted.push(false);
idx
};
self.heads.insert(key, idx);
}
pub fn get_head(&self, key: u64) -> Option<PooledVersion<&T>> {
let idx = *self.heads.get(&key)?;
self.get_at(idx)
}
pub fn get_at(&self, idx: u32) -> Option<PooledVersion<&T>> {
if idx == NULL_IDX || idx as usize >= self.data.len() {
return None;
}
let i = idx as usize;
Some(PooledVersion {
data: &self.data[i],
txid: self.txids[i],
commit_ts: self.commit_ts[i],
prev_idx: self.prev_idx[i],
deleted: self.deleted[i],
})
}
pub fn prune_old_versions(&mut self, horizon_ts: Timestamp) -> usize {
let mut pruned = 0;
let mut keys_to_remove = Vec::new();
for (&key, &head_idx) in &self.heads {
let mut current_idx = head_idx;
let mut keep_idx = NULL_IDX;
let mut prev_keep_idx = NULL_IDX;
while current_idx != NULL_IDX {
let i = current_idx as usize;
let ts = self.commit_ts[i];
if ts < horizon_ts {
if keep_idx == NULL_IDX {
keep_idx = current_idx;
} else {
self.free_list.push(current_idx);
pruned += 1;
}
} else {
prev_keep_idx = current_idx;
}
current_idx = self.prev_idx[i];
}
if head_idx != NULL_IDX && self.commit_ts[head_idx as usize] < horizon_ts {
keys_to_remove.push(key);
self.free_list.push(head_idx);
pruned += 1;
} else if keep_idx != NULL_IDX && prev_keep_idx != NULL_IDX {
self.prev_idx[keep_idx as usize] = NULL_IDX;
}
}
for key in keys_to_remove {
self.heads.remove(&key);
}
pruned
}
pub fn truncate_deep_chains(
&mut self,
max_depth: usize,
min_active_ts: Option<Timestamp>,
) -> usize {
let mut truncated = 0;
for &head_idx in self.heads.values() {
let mut depth = 0;
let mut current_idx = head_idx;
let mut truncate_at = NULL_IDX;
while current_idx != NULL_IDX && depth < max_depth {
let i = current_idx as usize;
if let Some(min_ts) = min_active_ts {
if self.commit_ts[i] >= min_ts {
truncate_at = current_idx;
}
} else {
truncate_at = current_idx;
}
depth += 1;
current_idx = self.prev_idx[i];
}
if current_idx != NULL_IDX && truncate_at != NULL_IDX {
let mut to_free = self.prev_idx[truncate_at as usize];
self.prev_idx[truncate_at as usize] = NULL_IDX;
while to_free != NULL_IDX {
let next = self.prev_idx[to_free as usize];
self.free_list.push(to_free);
to_free = next;
}
truncated += 1;
}
}
truncated
}
pub fn clear(&mut self) {
self.data.clear();
self.txids.clear();
self.commit_ts.clear();
self.prev_idx.clear();
self.deleted.clear();
self.heads.clear();
self.free_list.clear();
}
pub fn get_memory_usage(&self) -> usize {
let data_size = std::mem::size_of::<T>() * self.data.capacity();
let meta_size = (std::mem::size_of::<TxId>()
+ std::mem::size_of::<Timestamp>()
+ std::mem::size_of::<u32>()
+ std::mem::size_of::<bool>())
* self.data.capacity();
let heads_size = std::mem::size_of::<(u64, u32)>() * self.heads.capacity();
let free_list_size = std::mem::size_of::<u32>() * self.free_list.capacity();
data_size + meta_size + heads_size + free_list_size
}
pub fn len(&self) -> usize {
self.heads.len()
}
pub fn is_empty(&self) -> bool {
self.heads.is_empty()
}
}
impl<T: Clone> Default for SoaPropertyVersions<T> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct PooledVersion<T> {
pub data: T,
pub txid: TxId,
pub commit_ts: Timestamp,
pub prev_idx: u32,
pub deleted: bool,
}
#[derive(Debug)]
pub struct VersionChainManager {
node_versions: HashMap<NodeId, Box<VersionedRecord<NodeVersionData>>>,
edge_versions: HashMap<u64, Box<VersionedRecord<EdgeVersionData>>>,
soa_node_props: SoaPropertyVersions<Option<PropValue>>,
soa_edge_props: SoaPropertyVersions<Option<PropValue>>,
use_soa: bool,
legacy_node_props: HashMap<u64, Box<VersionedRecord<Option<PropValue>>>>,
legacy_edge_props: HashMap<u64, Box<VersionedRecord<Option<PropValue>>>>,
}
impl VersionChainManager {
pub fn new() -> Self {
Self::with_soa(true)
}
pub fn with_soa(use_soa: bool) -> Self {
Self {
node_versions: HashMap::new(),
edge_versions: HashMap::new(),
soa_node_props: SoaPropertyVersions::new(),
soa_edge_props: SoaPropertyVersions::new(),
use_soa,
legacy_node_props: HashMap::new(),
legacy_edge_props: HashMap::new(),
}
}
#[inline]
fn edge_key(src: NodeId, etype: ETypeId, dst: NodeId) -> u64 {
((src & 0xFFFFF) << 40) | ((etype as u64 & 0xFFFFF) << 20) | (dst & 0xFFFFF)
}
#[inline]
pub fn node_prop_key(node_id: NodeId, prop_key_id: PropKeyId) -> u64 {
(node_id << 24) | (prop_key_id as u64)
}
#[inline]
pub fn edge_prop_key(src: NodeId, etype: ETypeId, dst: NodeId, prop_key_id: PropKeyId) -> u64 {
((src & 0xFFFFF) << 44)
| ((etype as u64 & 0xFFF) << 32)
| ((dst & 0xFFFFF) << 12)
| (prop_key_id as u64 & 0xFFF)
}
pub fn append_node_version(
&mut self,
node_id: NodeId,
data: NodeVersionData,
txid: TxId,
commit_ts: Timestamp,
) {
let existing = self.node_versions.remove(&node_id);
let new_version = Box::new(VersionedRecord {
data,
txid,
commit_ts,
prev: existing,
deleted: false,
});
self.node_versions.insert(node_id, new_version);
}
pub fn delete_node_version(&mut self, node_id: NodeId, txid: TxId, commit_ts: Timestamp) {
let existing = self.node_versions.remove(&node_id);
let deleted_version = Box::new(VersionedRecord {
data: NodeVersionData {
node_id,
delta: NodeDelta::default(),
},
txid,
commit_ts,
prev: existing,
deleted: true,
});
self.node_versions.insert(node_id, deleted_version);
}
pub fn get_node_version(&self, node_id: NodeId) -> Option<&VersionedRecord<NodeVersionData>> {
self.node_versions.get(&node_id).map(|b| b.as_ref())
}
pub fn append_edge_version(
&mut self,
src: NodeId,
etype: ETypeId,
dst: NodeId,
added: bool,
txid: TxId,
commit_ts: Timestamp,
) {
let key = Self::edge_key(src, etype, dst);
let existing = self.edge_versions.remove(&key);
let new_version = Box::new(VersionedRecord {
data: EdgeVersionData {
src,
etype,
dst,
added,
},
txid,
commit_ts,
prev: existing,
deleted: false,
});
self.edge_versions.insert(key, new_version);
}
pub fn get_edge_version(
&self,
src: NodeId,
etype: ETypeId,
dst: NodeId,
) -> Option<&VersionedRecord<EdgeVersionData>> {
let key = Self::edge_key(src, etype, dst);
self.edge_versions.get(&key).map(|b| b.as_ref())
}
pub fn append_node_prop_version(
&mut self,
node_id: NodeId,
prop_key_id: PropKeyId,
value: Option<PropValue>,
txid: TxId,
commit_ts: Timestamp,
) {
let key = Self::node_prop_key(node_id, prop_key_id);
if self.use_soa {
self.soa_node_props.append(key, value, txid, commit_ts);
} else {
let existing = self.legacy_node_props.remove(&key);
let new_version = Box::new(VersionedRecord {
data: value,
txid,
commit_ts,
prev: existing,
deleted: false,
});
self.legacy_node_props.insert(key, new_version);
}
}
pub fn get_node_prop_version(
&self,
node_id: NodeId,
prop_key_id: PropKeyId,
) -> Option<VersionedRecord<Option<PropValue>>> {
let key = Self::node_prop_key(node_id, prop_key_id);
if self.use_soa {
self
.soa_node_props
.get_head(key)
.map(|pooled| Self::pooled_to_versioned(&self.soa_node_props, pooled))
} else {
self.legacy_node_props.get(&key).map(|b| {
Self::clone_versioned_record(b.as_ref())
})
}
}
#[allow(clippy::too_many_arguments)]
pub fn append_edge_prop_version(
&mut self,
src: NodeId,
etype: ETypeId,
dst: NodeId,
prop_key_id: PropKeyId,
value: Option<PropValue>,
txid: TxId,
commit_ts: Timestamp,
) {
let key = Self::edge_prop_key(src, etype, dst, prop_key_id);
if self.use_soa {
self.soa_edge_props.append(key, value, txid, commit_ts);
} else {
let existing = self.legacy_edge_props.remove(&key);
let new_version = Box::new(VersionedRecord {
data: value,
txid,
commit_ts,
prev: existing,
deleted: false,
});
self.legacy_edge_props.insert(key, new_version);
}
}
pub fn get_edge_prop_version(
&self,
src: NodeId,
etype: ETypeId,
dst: NodeId,
prop_key_id: PropKeyId,
) -> Option<VersionedRecord<Option<PropValue>>> {
let key = Self::edge_prop_key(src, etype, dst, prop_key_id);
if self.use_soa {
self
.soa_edge_props
.get_head(key)
.map(|pooled| Self::pooled_to_versioned(&self.soa_edge_props, pooled))
} else {
self
.legacy_edge_props
.get(&key)
.map(|b| Self::clone_versioned_record(b.as_ref()))
}
}
fn pooled_to_versioned(
store: &SoaPropertyVersions<Option<PropValue>>,
pooled: PooledVersion<&Option<PropValue>>,
) -> VersionedRecord<Option<PropValue>> {
let prev = if pooled.prev_idx != NULL_IDX {
store
.get_at(pooled.prev_idx)
.map(|prev_pooled| Box::new(Self::pooled_to_versioned(store, prev_pooled)))
} else {
None
};
VersionedRecord {
data: pooled.data.clone(),
txid: pooled.txid,
commit_ts: pooled.commit_ts,
prev,
deleted: pooled.deleted,
}
}
fn clone_versioned_record(
record: &VersionedRecord<Option<PropValue>>,
) -> VersionedRecord<Option<PropValue>> {
VersionedRecord {
data: record.data.clone(),
txid: record.txid,
commit_ts: record.commit_ts,
prev: record
.prev
.as_ref()
.map(|p| Box::new(Self::clone_versioned_record(p))),
deleted: record.deleted,
}
}
pub fn prune_old_versions(&mut self, horizon_ts: Timestamp) -> usize {
let mut pruned = 0;
let node_ids: Vec<_> = self.node_versions.keys().copied().collect();
for node_id in node_ids {
if let Some(version) = self.node_versions.get_mut(&node_id) {
let result = Self::prune_chain(version, horizon_ts);
if result == -1 {
self.node_versions.remove(&node_id);
pruned += 1;
} else {
pruned += result as usize;
}
}
}
let edge_keys: Vec<_> = self.edge_versions.keys().copied().collect();
for key in edge_keys {
if let Some(version) = self.edge_versions.get_mut(&key) {
let result = Self::prune_chain(version, horizon_ts);
if result == -1 {
self.edge_versions.remove(&key);
pruned += 1;
} else {
pruned += result as usize;
}
}
}
if self.use_soa {
pruned += self.soa_node_props.prune_old_versions(horizon_ts);
pruned += self.soa_edge_props.prune_old_versions(horizon_ts);
} else {
let node_prop_keys: Vec<_> = self.legacy_node_props.keys().copied().collect();
for key in node_prop_keys {
if let Some(version) = self.legacy_node_props.get_mut(&key) {
let result = Self::prune_chain(version, horizon_ts);
if result == -1 {
self.legacy_node_props.remove(&key);
pruned += 1;
} else {
pruned += result as usize;
}
}
}
let edge_prop_keys: Vec<_> = self.legacy_edge_props.keys().copied().collect();
for key in edge_prop_keys {
if let Some(version) = self.legacy_edge_props.get_mut(&key) {
let result = Self::prune_chain(version, horizon_ts);
if result == -1 {
self.legacy_edge_props.remove(&key);
pruned += 1;
} else {
pruned += result as usize;
}
}
}
}
pruned
}
fn prune_chain<T>(version: &mut Box<VersionedRecord<T>>, horizon_ts: Timestamp) -> i32 {
let mut pruned_count = 0;
let mut keep_found = false;
let mut current = version.prev.as_ref();
while let Some(v) = current {
if v.commit_ts < horizon_ts {
if !keep_found {
keep_found = true;
} else {
pruned_count += 1;
}
}
current = v.prev.as_ref();
}
if version.commit_ts < horizon_ts {
return -1;
}
if keep_found {
let mut prev_ref = &mut version.prev;
while let Some(ref mut v) = prev_ref {
if v.commit_ts < horizon_ts {
v.prev = None;
break;
}
prev_ref = &mut v.prev;
}
}
pruned_count
}
pub fn truncate_deep_chains(
&mut self,
max_depth: usize,
min_active_ts: Option<Timestamp>,
) -> usize {
let mut truncated = 0;
for version in self.node_versions.values_mut() {
if Self::truncate_chain_at_depth(version, max_depth, min_active_ts) {
truncated += 1;
}
}
for version in self.edge_versions.values_mut() {
if Self::truncate_chain_at_depth(version, max_depth, min_active_ts) {
truncated += 1;
}
}
if self.use_soa {
truncated += self
.soa_node_props
.truncate_deep_chains(max_depth, min_active_ts);
truncated += self
.soa_edge_props
.truncate_deep_chains(max_depth, min_active_ts);
} else {
for version in self.legacy_node_props.values_mut() {
if Self::truncate_chain_at_depth(version, max_depth, min_active_ts) {
truncated += 1;
}
}
for version in self.legacy_edge_props.values_mut() {
if Self::truncate_chain_at_depth(version, max_depth, min_active_ts) {
truncated += 1;
}
}
}
truncated
}
fn truncate_chain_at_depth<T>(
head: &mut Box<VersionedRecord<T>>,
max_depth: usize,
min_active_ts: Option<Timestamp>,
) -> bool {
let mut total_depth = 1;
let mut current = head.prev.as_ref();
while let Some(v) = current {
total_depth += 1;
current = v.prev.as_ref();
}
if total_depth <= max_depth {
return false;
}
let mut current_depth = 1;
let mut node: &mut Box<VersionedRecord<T>> = head;
while current_depth < max_depth {
if node.prev.is_none() {
return false;
}
current_depth += 1;
node = node.prev.as_mut().unwrap();
}
if let Some(min_ts) = min_active_ts {
let mut check = node.prev.as_ref();
while let Some(c) = check {
if c.commit_ts < min_ts {
return false;
}
check = c.prev.as_ref();
}
}
node.prev = None;
true
}
pub fn has_any_edge_versions(&self) -> bool {
!self.edge_versions.is_empty()
}
pub fn is_soa_enabled(&self) -> bool {
self.use_soa
}
pub fn get_soa_memory_usage(&self) -> (usize, usize) {
(
self.soa_node_props.get_memory_usage(),
self.soa_edge_props.get_memory_usage(),
)
}
pub fn clear(&mut self) {
self.node_versions.clear();
self.edge_versions.clear();
self.soa_node_props.clear();
self.soa_edge_props.clear();
self.legacy_node_props.clear();
self.legacy_edge_props.clear();
}
pub fn get_counts(&self) -> VersionChainCounts {
VersionChainCounts {
node_versions: self.node_versions.len(),
edge_versions: self.edge_versions.len(),
node_prop_versions: if self.use_soa {
self.soa_node_props.len()
} else {
self.legacy_node_props.len()
},
edge_prop_versions: if self.use_soa {
self.soa_edge_props.len()
} else {
self.legacy_edge_props.len()
},
}
}
}
impl Default for VersionChainManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct VersionChainCounts {
pub node_versions: usize,
pub edge_versions: usize,
pub node_prop_versions: usize,
pub edge_prop_versions: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_soa_property_versions_new() {
let store: SoaPropertyVersions<i32> = SoaPropertyVersions::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
}
#[test]
fn test_soa_append_and_get() {
let mut store: SoaPropertyVersions<i32> = SoaPropertyVersions::new();
store.append(1, 42, 1, 10);
let head = store.get_head(1);
assert!(head.is_some());
let v = head.unwrap();
assert_eq!(*v.data, 42);
assert_eq!(v.txid, 1);
assert_eq!(v.commit_ts, 10);
}
#[test]
fn test_soa_version_chain() {
let mut store: SoaPropertyVersions<i32> = SoaPropertyVersions::new();
store.append(1, 10, 1, 10);
store.append(1, 20, 2, 20);
store.append(1, 30, 3, 30);
let head = store.get_head(1).unwrap();
assert_eq!(*head.data, 30);
assert_eq!(head.commit_ts, 30);
assert_ne!(head.prev_idx, NULL_IDX);
let prev = store.get_at(head.prev_idx).unwrap();
assert_eq!(*prev.data, 20);
}
#[test]
fn test_version_chain_manager_new() {
let mgr = VersionChainManager::new();
assert!(mgr.is_soa_enabled());
let counts = mgr.get_counts();
assert_eq!(counts.node_versions, 0);
assert_eq!(counts.edge_versions, 0);
}
#[test]
fn test_node_version_append_and_get() {
let mut mgr = VersionChainManager::new();
let data = NodeVersionData {
node_id: 1,
delta: NodeDelta::default(),
};
mgr.append_node_version(1, data, 1, 10);
let version = mgr.get_node_version(1);
assert!(version.is_some());
assert_eq!(version.unwrap().data.node_id, 1);
}
#[test]
fn test_node_version_chain() {
let mut mgr = VersionChainManager::new();
for i in 1..=3 {
let data = NodeVersionData {
node_id: 1,
delta: NodeDelta::default(),
};
mgr.append_node_version(1, data, i, i * 10);
}
let version = mgr.get_node_version(1).unwrap();
assert_eq!(version.commit_ts, 30);
assert!(version.prev.is_some());
assert_eq!(version.prev.as_ref().unwrap().commit_ts, 20);
}
#[test]
fn test_delete_node_version() {
let mut mgr = VersionChainManager::new();
let data = NodeVersionData {
node_id: 1,
delta: NodeDelta::default(),
};
mgr.append_node_version(1, data, 1, 10);
mgr.delete_node_version(1, 2, 20);
let version = mgr.get_node_version(1).unwrap();
assert!(version.deleted);
assert_eq!(version.commit_ts, 20);
}
#[test]
fn test_edge_version_append_and_get() {
let mut mgr = VersionChainManager::new();
mgr.append_edge_version(1, 1, 2, true, 1, 10);
let version = mgr.get_edge_version(1, 1, 2);
assert!(version.is_some());
let v = version.unwrap();
assert_eq!(v.data.src, 1);
assert_eq!(v.data.etype, 1);
assert_eq!(v.data.dst, 2);
assert!(v.data.added);
}
#[test]
fn test_edge_version_delete() {
let mut mgr = VersionChainManager::new();
mgr.append_edge_version(1, 1, 2, true, 1, 10);
mgr.append_edge_version(1, 1, 2, false, 2, 20);
let version = mgr.get_edge_version(1, 1, 2).unwrap();
assert!(!version.data.added);
}
#[test]
fn test_node_prop_version_soa() {
let mut mgr = VersionChainManager::new();
assert!(mgr.is_soa_enabled());
mgr.append_node_prop_version(1, 1, Some(PropValue::I64(42)), 1, 10);
let version = mgr.get_node_prop_version(1, 1);
assert!(version.is_some());
assert_eq!(version.unwrap().data, Some(PropValue::I64(42)));
}
#[test]
fn test_node_prop_version_legacy() {
let mut mgr = VersionChainManager::with_soa(false);
assert!(!mgr.is_soa_enabled());
mgr.append_node_prop_version(1, 1, Some(PropValue::I64(42)), 1, 10);
let version = mgr.get_node_prop_version(1, 1);
assert!(version.is_some());
assert_eq!(version.unwrap().data, Some(PropValue::I64(42)));
}
#[test]
fn test_edge_prop_version() {
let mut mgr = VersionChainManager::new();
mgr.append_edge_prop_version(1, 1, 2, 1, Some(PropValue::F64(3.14)), 1, 10);
let version = mgr.get_edge_prop_version(1, 1, 2, 1);
assert!(version.is_some());
assert_eq!(version.unwrap().data, Some(PropValue::F64(3.14)));
}
#[test]
fn test_has_any_edge_versions() {
let mut mgr = VersionChainManager::new();
assert!(!mgr.has_any_edge_versions());
mgr.append_edge_version(1, 1, 2, true, 1, 10);
assert!(mgr.has_any_edge_versions());
}
#[test]
fn test_clear() {
let mut mgr = VersionChainManager::new();
mgr.append_node_version(
1,
NodeVersionData {
node_id: 1,
delta: NodeDelta::default(),
},
1,
10,
);
mgr.append_edge_version(1, 1, 2, true, 1, 10);
mgr.append_node_prop_version(1, 1, Some(PropValue::I64(42)), 1, 10);
mgr.clear();
let counts = mgr.get_counts();
assert_eq!(counts.node_versions, 0);
assert_eq!(counts.edge_versions, 0);
assert_eq!(counts.node_prop_versions, 0);
}
#[test]
fn test_edge_key_packing() {
let key1 = VersionChainManager::edge_key(1, 2, 3);
let key2 = VersionChainManager::edge_key(1, 2, 4);
let key3 = VersionChainManager::edge_key(2, 2, 3);
assert_ne!(key1, key2);
assert_ne!(key1, key3);
assert_ne!(key2, key3);
}
#[test]
fn test_node_prop_key_packing() {
let key1 = VersionChainManager::node_prop_key(1, 1);
let key2 = VersionChainManager::node_prop_key(1, 2);
let key3 = VersionChainManager::node_prop_key(2, 1);
assert_ne!(key1, key2);
assert_ne!(key1, key3);
}
#[test]
fn test_edge_prop_key_packing() {
let key1 = VersionChainManager::edge_prop_key(1, 1, 2, 1);
let key2 = VersionChainManager::edge_prop_key(1, 1, 2, 2);
let key3 = VersionChainManager::edge_prop_key(1, 1, 3, 1);
assert_ne!(key1, key2);
assert_ne!(key1, key3);
}
#[test]
fn test_soa_memory_usage() {
let mut mgr = VersionChainManager::new();
for i in 0..100 {
mgr.append_node_prop_version(i, 1, Some(PropValue::I64(i as i64)), 1, 10);
}
let (node_mem, _edge_mem) = mgr.get_soa_memory_usage();
assert!(node_mem > 0);
}
#[test]
fn test_version_chain_depth() {
let mut mgr = VersionChainManager::new();
for i in 1..=20 {
let data = NodeVersionData {
node_id: 1,
delta: NodeDelta::default(),
};
mgr.append_node_version(1, data, i, i * 10);
}
let truncated = mgr.truncate_deep_chains(5, None);
assert!(truncated > 0);
let mut depth = 0;
let mut current = mgr.get_node_version(1);
while let Some(v) = current {
depth += 1;
current = v.prev.as_deref();
}
assert!(depth <= 5);
}
#[test]
fn test_prune_old_versions() {
let mut mgr = VersionChainManager::new();
for i in 1..=5 {
let data = NodeVersionData {
node_id: 1,
delta: NodeDelta::default(),
};
mgr.append_node_version(1, data, i, i * 10);
}
let pruned = mgr.prune_old_versions(35);
assert!(pruned > 0);
}
}