use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::error::{Result, TdbError};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VersionedTriple {
pub subject: String,
pub predicate: String,
pub object: String,
pub valid_from: DateTime<Utc>,
pub valid_to: Option<DateTime<Utc>>,
pub transaction_id: u64,
}
impl VersionedTriple {
pub fn new(
subject: impl Into<String>,
predicate: impl Into<String>,
object: impl Into<String>,
valid_from: DateTime<Utc>,
transaction_id: u64,
) -> Self {
Self {
subject: subject.into(),
predicate: predicate.into(),
object: object.into(),
valid_from,
valid_to: None,
transaction_id,
}
}
pub fn is_active_at(&self, ts: DateTime<Utc>) -> bool {
self.valid_from <= ts && self.valid_to.map_or(true, |end| ts < end)
}
pub fn key(&self) -> TripleKey {
TripleKey {
subject: self.subject.clone(),
predicate: self.predicate.clone(),
object: self.object.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TripleKey {
pub subject: String,
pub predicate: String,
pub object: String,
}
impl TripleKey {
pub fn new(
subject: impl Into<String>,
predicate: impl Into<String>,
object: impl Into<String>,
) -> Self {
Self {
subject: subject.into(),
predicate: predicate.into(),
object: object.into(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TemporalDiff {
pub added: Vec<VersionedTriple>,
pub removed: Vec<VersionedTriple>,
}
impl TemporalDiff {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty()
}
}
#[derive(Debug, Default)]
pub struct TemporalVersionStore {
versions: HashMap<TripleKey, Vec<VersionedTriple>>,
next_txn_id: u64,
}
impl TemporalVersionStore {
pub fn new() -> Self {
Self {
versions: HashMap::new(),
next_txn_id: 1,
}
}
fn alloc_txn_id(&mut self) -> u64 {
let id = self.next_txn_id;
self.next_txn_id += 1;
id
}
pub fn insert(&mut self, triple: VersionedTriple) -> Result<()> {
let key = triple.key();
if let Some(versions) = self.versions.get(&key) {
for v in versions {
if v.valid_to.is_none() {
return Ok(());
}
}
}
self.versions.entry(key).or_default().push(triple);
Ok(())
}
pub fn insert_now(
&mut self,
subject: impl Into<String>,
predicate: impl Into<String>,
object: impl Into<String>,
) -> Result<u64> {
let txn_id = self.alloc_txn_id();
let triple = VersionedTriple::new(subject, predicate, object, Utc::now(), txn_id);
self.insert(triple)?;
Ok(txn_id)
}
pub fn insert_at(
&mut self,
subject: impl Into<String>,
predicate: impl Into<String>,
object: impl Into<String>,
ts: DateTime<Utc>,
) -> Result<u64> {
let txn_id = self.alloc_txn_id();
let triple = VersionedTriple::new(subject, predicate, object, ts, txn_id);
self.insert(triple)?;
Ok(txn_id)
}
pub fn delete_at(
&mut self,
subject: &str,
predicate: &str,
object: &str,
ts: DateTime<Utc>,
) -> Result<()> {
let key = TripleKey::new(subject, predicate, object);
match self.versions.get_mut(&key) {
None => Err(TdbError::InvalidInput(format!(
"Triple not found: ({subject}, {predicate}, {object})"
))),
Some(versions) => {
let live = versions.iter_mut().find(|v| v.valid_to.is_none());
match live {
None => Err(TdbError::InvalidInput(format!(
"No live version for triple: ({subject}, {predicate}, {object})"
))),
Some(v) => {
if ts < v.valid_from {
return Err(TdbError::InvalidInput(format!(
"Delete timestamp {ts} is before valid_from {}",
v.valid_from
)));
}
v.valid_to = Some(ts);
Ok(())
}
}
}
}
}
pub fn delete(&mut self, subject: &str, predicate: &str, object: &str) -> Result<()> {
self.delete_at(subject, predicate, object, Utc::now())
}
pub fn query_at(&self, timestamp: DateTime<Utc>) -> Vec<VersionedTriple> {
self.versions
.values()
.flatten()
.filter(|v| v.is_active_at(timestamp))
.cloned()
.collect()
}
pub fn history(&self, subject: &str, predicate: &str, object: &str) -> Vec<VersionedTriple> {
let key = TripleKey::new(subject, predicate, object);
self.versions.get(&key).cloned().unwrap_or_default()
}
pub fn current(&self) -> Vec<VersionedTriple> {
self.versions
.values()
.flatten()
.filter(|v| v.valid_to.is_none())
.cloned()
.collect()
}
pub fn diff(&self, from: DateTime<Utc>, to: DateTime<Utc>) -> TemporalDiff {
let mut diff = TemporalDiff::default();
for v in self.versions.values().flatten() {
if v.valid_from >= from && v.valid_from < to {
diff.added.push(v.clone());
}
if let Some(end) = v.valid_to {
if end >= from && end < to {
diff.removed.push(v.clone());
}
}
}
diff
}
pub fn version_count(&self) -> usize {
self.versions.values().map(|v| v.len()).sum()
}
pub fn triple_key_count(&self) -> usize {
self.versions.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn ts(y: i32, m: u32, d: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(y, m, d, 0, 0, 0).unwrap()
}
#[test]
fn test_insert_and_query_at() {
let mut store = TemporalVersionStore::new();
store
.insert_at("Alice", "knows", "Bob", ts(2024, 1, 1))
.unwrap();
store
.insert_at("Alice", "knows", "Carol", ts(2024, 6, 1))
.unwrap();
let snap = store.query_at(ts(2024, 3, 1));
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].object, "Bob");
}
#[test]
fn test_delete_marks_valid_to() {
let mut store = TemporalVersionStore::new();
store
.insert_at("Alice", "knows", "Bob", ts(2024, 1, 1))
.unwrap();
store
.delete_at("Alice", "knows", "Bob", ts(2024, 6, 1))
.unwrap();
let snap = store.query_at(ts(2024, 7, 1));
assert!(snap.is_empty());
let snap_before = store.query_at(ts(2024, 3, 1));
assert_eq!(snap_before.len(), 1);
}
#[test]
fn test_idempotent_insert() {
let mut store = TemporalVersionStore::new();
store.insert_at("s", "p", "o", ts(2024, 1, 1)).unwrap();
store.insert_at("s", "p", "o", ts(2024, 2, 1)).unwrap();
assert_eq!(store.version_count(), 1);
}
#[test]
fn test_delete_nonexistent_returns_error() {
let mut store = TemporalVersionStore::new();
let result = store.delete_at("s", "p", "o", ts(2024, 1, 1));
assert!(result.is_err());
}
#[test]
fn test_diff() {
let mut store = TemporalVersionStore::new();
store.insert_at("s", "p", "o1", ts(2024, 1, 1)).unwrap();
store.insert_at("s", "p", "o2", ts(2024, 4, 1)).unwrap();
store.delete_at("s", "p", "o1", ts(2024, 5, 1)).unwrap();
let diff = store.diff(ts(2024, 3, 1), ts(2024, 6, 1));
assert_eq!(diff.added.len(), 1);
assert_eq!(diff.added[0].object, "o2");
assert_eq!(diff.removed.len(), 1);
assert_eq!(diff.removed[0].object, "o1");
}
}