use serde::{Deserialize, Serialize};
use crate::graph::unified::edge::id::EdgeId;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct EdgeProvenance {
pub first_seen_epoch: u64,
pub last_seen_epoch: u64,
}
impl EdgeProvenance {
#[must_use]
pub fn fresh(epoch: u64) -> Self {
Self {
first_seen_epoch: epoch,
last_seen_epoch: epoch,
}
}
pub fn touch(&mut self, epoch: u64) {
if epoch > self.last_seen_epoch {
self.last_seen_epoch = epoch;
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct EdgeProvenanceStore {
slots: Vec<Option<EdgeProvenance>>,
}
impl EdgeProvenanceStore {
#[must_use]
pub fn new() -> Self {
Self { slots: Vec::new() }
}
#[must_use]
pub fn slot_count(&self) -> usize {
self.slots.len()
}
#[must_use]
pub fn len(&self) -> usize {
self.slots.iter().filter(|s| s.is_some()).count()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn resize_to(&mut self, edge_slot_count: usize) {
self.slots.resize(edge_slot_count, None);
}
pub fn insert(&mut self, id: EdgeId, provenance: EdgeProvenance) {
assert!(
id.is_valid(),
"EdgeProvenanceStore::insert called with EdgeId::INVALID"
);
let index = id.as_usize();
if index >= self.slots.len() {
self.slots.resize(index + 1, None);
}
self.slots[index] = Some(provenance);
}
#[must_use]
pub fn lookup(&self, id: EdgeId) -> Option<&EdgeProvenance> {
if id.is_invalid() {
return None;
}
self.slots.get(id.as_usize())?.as_ref()
}
pub fn clear_slot(&mut self, index: u32) {
let i = index as usize;
if let Some(entry) = self.slots.get_mut(i) {
*entry = None;
}
}
}
impl crate::graph::unified::memory::GraphMemorySize for EdgeProvenanceStore {
fn heap_bytes(&self) -> usize {
self.slots.capacity() * std::mem::size_of::<Option<EdgeProvenance>>()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn prov(epoch: u64) -> EdgeProvenance {
EdgeProvenance::fresh(epoch)
}
#[test]
fn new_store_is_empty_and_has_zero_slots() {
let store = EdgeProvenanceStore::new();
assert_eq!(store.slot_count(), 0);
assert_eq!(store.len(), 0);
assert!(store.is_empty());
}
#[test]
fn resize_extends_with_none() {
let mut store = EdgeProvenanceStore::new();
store.resize_to(16);
assert_eq!(store.slot_count(), 16);
assert_eq!(store.len(), 0);
assert!(store.is_empty());
}
#[test]
fn insert_lookup_round_trip() {
let mut store = EdgeProvenanceStore::new();
let id = EdgeId::new(7);
store.insert(id, prov(100));
let got = store.lookup(id).expect("provenance present");
assert_eq!(got.first_seen_epoch, 100);
assert_eq!(got.last_seen_epoch, 100);
assert_eq!(store.len(), 1);
assert!(store.slot_count() >= 8);
}
#[test]
fn insert_auto_extends_slots() {
let mut store = EdgeProvenanceStore::new();
store.insert(EdgeId::new(0), prov(1));
store.insert(EdgeId::new(5), prov(2));
store.insert(EdgeId::new(3), prov(3));
assert_eq!(store.slot_count(), 6);
assert_eq!(store.len(), 3);
}
#[test]
fn lookup_out_of_range_returns_none() {
let store = EdgeProvenanceStore::new();
assert!(store.lookup(EdgeId::new(999)).is_none());
}
#[test]
fn lookup_vacant_slot_returns_none() {
let mut store = EdgeProvenanceStore::new();
store.resize_to(10);
assert!(store.lookup(EdgeId::new(5)).is_none());
}
#[test]
fn lookup_invalid_sentinel_returns_none() {
let mut store = EdgeProvenanceStore::new();
store.resize_to(10);
assert!(store.lookup(EdgeId::INVALID).is_none());
}
#[test]
#[should_panic(expected = "EdgeId::INVALID")]
fn insert_invalid_sentinel_panics() {
let mut store = EdgeProvenanceStore::new();
store.insert(EdgeId::INVALID, prov(1));
}
#[test]
fn clear_slot_drops_provenance() {
let mut store = EdgeProvenanceStore::new();
let id = EdgeId::new(2);
store.insert(id, prov(100));
assert!(store.lookup(id).is_some());
store.clear_slot(2);
assert!(store.lookup(id).is_none());
assert_eq!(store.len(), 0);
}
#[test]
fn clear_slot_out_of_range_is_noop() {
let mut store = EdgeProvenanceStore::new();
store.resize_to(4);
store.clear_slot(99);
assert_eq!(store.slot_count(), 4);
}
#[test]
fn provenance_touch_advances_only_forward() {
let mut p = prov(100);
p.touch(50);
assert_eq!(
p.last_seen_epoch, 100,
"stale touch must not move last_seen backwards"
);
p.touch(200);
assert_eq!(p.last_seen_epoch, 200);
assert_eq!(p.first_seen_epoch, 100, "first_seen must never move");
}
#[test]
fn postcard_round_trip_preserves_dense_layout() {
let mut store = EdgeProvenanceStore::new();
store.resize_to(4);
store.insert(EdgeId::new(0), prov(10));
store.insert(EdgeId::new(2), prov(20));
let encoded = postcard::to_allocvec(&store).expect("encode");
let decoded: EdgeProvenanceStore = postcard::from_bytes(&encoded).expect("decode");
assert_eq!(decoded.slot_count(), 4);
assert_eq!(decoded.len(), 2);
assert_eq!(decoded.lookup(EdgeId::new(0)).unwrap().first_seen_epoch, 10);
assert_eq!(decoded.lookup(EdgeId::new(2)).unwrap().last_seen_epoch, 20);
assert!(decoded.lookup(EdgeId::new(1)).is_none());
}
#[test]
fn default_is_new() {
let a = EdgeProvenanceStore::default();
let b = EdgeProvenanceStore::new();
assert_eq!(a.slot_count(), b.slot_count());
assert_eq!(a.len(), b.len());
}
#[test]
fn memory_budget_24_bytes_per_slot_serialized_at_1k_edges() {
let mut store = EdgeProvenanceStore::new();
for i in 0..1000u32 {
store.insert(EdgeId::new(i), prov(u64::from(i) + 1));
}
let encoded = postcard::to_allocvec(&store).expect("encode");
assert!(
encoded.len() <= 24 * 1000,
"dense edge store budget exceeded: {} bytes > {} bytes ceiling",
encoded.len(),
24 * 1000
);
}
}