use chrono::{DateTime, Utc};
use crate::error::BitemporalError;
use crate::types::{BitemporalRecord, SupersessionReceipt};
pub fn append_supersede<T>(
records: &mut Vec<BitemporalRecord<T>>,
new_record: BitemporalRecord<T>,
) -> Result<Vec<SupersessionReceipt>, BitemporalError>
where
T: Clone,
{
let prior_versions: Vec<_> = records
.iter()
.filter(|r| r.id == new_record.id)
.cloned()
.collect();
let mut receipts = Vec::new();
for prior in prior_versions {
let receipt = SupersessionReceipt::new(
BitemporalRecord {
id: prior.id.clone(),
valid_time: prior.valid_time,
recorded_time: prior.recorded_time,
value: (),
},
BitemporalRecord {
id: new_record.id.clone(),
valid_time: new_record.valid_time,
recorded_time: new_record.recorded_time,
value: (),
},
);
receipts.push(receipt);
}
records.push(new_record);
Ok(receipts)
}
pub fn as_of_query<T>(
records: &[BitemporalRecord<T>],
valid_time: DateTime<Utc>,
recorded_time: DateTime<Utc>,
) -> Vec<BitemporalRecord<T>>
where
T: Clone,
{
use std::collections::HashMap;
let mut latest_by_id: HashMap<String, BitemporalRecord<T>> = HashMap::new();
for record in records {
if record.recorded_time <= recorded_time && record.valid_time <= valid_time {
let existing = latest_by_id.get(&record.id);
if existing
.map(|e| record.recorded_time > e.recorded_time)
.unwrap_or(true)
{
latest_by_id.insert(record.id.clone(), record.clone());
}
}
}
latest_by_id.into_values().collect()
}
pub fn temporal_snapshot<T>(
records: &[BitemporalRecord<T>],
as_of_time: DateTime<Utc>,
) -> Vec<BitemporalRecord<T>>
where
T: Clone,
{
as_of_query(records, as_of_time, as_of_time)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_append_supersede_creates_receipt() {
let mut records: Vec<BitemporalRecord<&str>> = Vec::new();
let t0 = Utc.timestamp_opt(1000, 0).unwrap();
let v1 = BitemporalRecord {
id: "ep1".to_string(),
valid_time: t0,
recorded_time: t0,
value: "version1",
};
let receipts = append_supersede(&mut records, v1).unwrap();
assert!(receipts.is_empty());
assert_eq!(records.len(), 1);
let t1 = Utc.timestamp_opt(2000, 0).unwrap();
let v2 = BitemporalRecord {
id: "ep1".to_string(),
valid_time: t0,
recorded_time: t1,
value: "version2",
};
let receipts = append_supersede(&mut records, v2).unwrap();
assert_eq!(receipts.len(), 1);
assert_eq!(receipts[0].superseded.superseded_id, "ep1");
assert_eq!(receipts[0].superseding_id, "ep1");
assert_eq!(records.len(), 2);
}
#[test]
fn test_as_of_query_returns_correct_version() {
let t0 = Utc.timestamp_opt(1000, 0).unwrap();
let t1 = Utc.timestamp_opt(2000, 0).unwrap();
let t2 = Utc.timestamp_opt(3000, 0).unwrap();
let records: Vec<BitemporalRecord<&str>> = vec![
BitemporalRecord {
id: "ep1".to_string(),
valid_time: t0,
recorded_time: t0,
value: "v1",
},
BitemporalRecord {
id: "ep1".to_string(),
valid_time: t0,
recorded_time: t1,
value: "v2",
},
BitemporalRecord {
id: "ep1".to_string(),
valid_time: t1,
recorded_time: t2,
value: "v3",
},
];
let result = as_of_query(&records, t0, t0);
assert_eq!(result.len(), 1);
assert_eq!(result[0].value, "v1");
let result = as_of_query(&records, t0, t1);
assert_eq!(result.len(), 1);
assert_eq!(result[0].value, "v2");
let result = as_of_query(&records, t1, t2);
assert_eq!(result.len(), 1);
assert_eq!(result[0].value, "v3");
}
#[test]
fn test_temporal_snapshot() {
let t0 = Utc.timestamp_opt(1000, 0).unwrap();
let t1 = Utc.timestamp_opt(2000, 0).unwrap();
let records: Vec<BitemporalRecord<&str>> = vec![
BitemporalRecord {
id: "ep1".to_string(),
valid_time: t0,
recorded_time: t0,
value: "v1",
},
BitemporalRecord {
id: "ep1".to_string(),
valid_time: t0,
recorded_time: t1,
value: "v2",
},
BitemporalRecord {
id: "ep2".to_string(),
valid_time: t0,
recorded_time: t0,
value: "ep2_v1",
},
];
let snap = temporal_snapshot(&records, t0);
assert_eq!(snap.len(), 2);
let ep1 = snap.iter().find(|r| r.id == "ep1").unwrap();
assert_eq!(ep1.value, "v1");
let snap = temporal_snapshot(&records, t1);
assert_eq!(snap.len(), 2);
let ep1 = snap.iter().find(|r| r.id == "ep1").unwrap();
assert_eq!(ep1.value, "v2");
}
}