use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::{Result, TdbError};
use super::snapshot::TemporalSnapshot;
use super::version_store::{TemporalVersionStore, VersionedTriple};
pub type PatternComponent = Option<String>;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TriplePattern {
pub subject: PatternComponent,
pub predicate: PatternComponent,
pub object: PatternComponent,
}
impl TriplePattern {
pub fn wildcard() -> Self {
Self::default()
}
pub fn with_subject(subject: impl Into<String>) -> Self {
Self {
subject: Some(subject.into()),
..Default::default()
}
}
pub fn with_predicate(predicate: impl Into<String>) -> Self {
Self {
predicate: Some(predicate.into()),
..Default::default()
}
}
pub fn with_object(object: impl Into<String>) -> Self {
Self {
object: Some(object.into()),
..Default::default()
}
}
pub fn exact(
subject: impl Into<String>,
predicate: impl Into<String>,
object: impl Into<String>,
) -> Self {
Self {
subject: Some(subject.into()),
predicate: Some(predicate.into()),
object: Some(object.into()),
}
}
pub fn matches(&self, triple: &VersionedTriple) -> bool {
self.subject
.as_deref()
.map_or(true, |s| s == triple.subject)
&& self
.predicate
.as_deref()
.map_or(true, |p| p == triple.predicate)
&& self.object.as_deref().map_or(true, |o| o == triple.object)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeTravelQuery {
pub as_of: DateTime<Utc>,
pub pattern: TriplePattern,
}
impl TimeTravelQuery {
pub fn new(as_of: DateTime<Utc>, pattern: TriplePattern) -> Self {
Self { as_of, pattern }
}
pub fn at(as_of: DateTime<Utc>) -> Self {
Self {
as_of,
pattern: TriplePattern::wildcard(),
}
}
pub fn execute(&self, store: &TemporalVersionStore) -> Result<TemporalSnapshot> {
if self.as_of > Utc::now() + chrono::Duration::days(365 * 10) {
return Err(TdbError::InvalidInput(
"as_of timestamp is implausibly far in the future".into(),
));
}
let matching: Vec<VersionedTriple> = store
.query_at(self.as_of)
.into_iter()
.filter(|t| self.pattern.matches(t))
.collect();
Ok(TemporalSnapshot::new(self.as_of, matching))
}
}
#[derive(Debug, Default)]
pub struct TimeTravelQueryBuilder {
as_of: Option<DateTime<Utc>>,
pattern: TriplePattern,
}
impl TimeTravelQueryBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn as_of(mut self, ts: DateTime<Utc>) -> Self {
self.as_of = Some(ts);
self
}
pub fn pattern(mut self, pattern: TriplePattern) -> Self {
self.pattern = pattern;
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.pattern.subject = Some(subject.into());
self
}
pub fn predicate(mut self, predicate: impl Into<String>) -> Self {
self.pattern.predicate = Some(predicate.into());
self
}
pub fn object(mut self, object: impl Into<String>) -> Self {
self.pattern.object = Some(object.into());
self
}
pub fn build(self) -> Result<TimeTravelQuery> {
let as_of = self.as_of.ok_or_else(|| {
TdbError::InvalidInput("TimeTravelQueryBuilder: as_of timestamp is required".into())
})?;
Ok(TimeTravelQuery {
as_of,
pattern: self.pattern,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::temporal::version_store::TemporalVersionStore;
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()
}
fn populated_store() -> TemporalVersionStore {
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();
store
.insert_at("Bob", "likes", "Rust", ts(2024, 3, 1))
.unwrap();
store
.delete_at("Alice", "knows", "Bob", ts(2024, 9, 1))
.unwrap();
store
}
#[test]
fn test_time_travel_wildcard() {
let store = populated_store();
let q = TimeTravelQuery::at(ts(2024, 2, 1));
let snap = q.execute(&store).unwrap();
assert_eq!(snap.len(), 1);
assert_eq!(snap.triples()[0].object, "Bob");
}
#[test]
fn test_time_travel_with_pattern() {
let store = populated_store();
let pattern = TriplePattern::with_subject("Alice");
let q = TimeTravelQuery::new(ts(2024, 7, 1), pattern);
let snap = q.execute(&store).unwrap();
assert_eq!(snap.len(), 2);
}
#[test]
fn test_time_travel_after_delete() {
let store = populated_store();
let pattern = TriplePattern::exact("Alice", "knows", "Bob");
let q = TimeTravelQuery::new(ts(2024, 10, 1), pattern);
let snap = q.execute(&store).unwrap();
assert!(snap.is_empty(), "Bob should be deleted after Sept 2024");
}
#[test]
fn test_builder() {
let store = populated_store();
let q = TimeTravelQueryBuilder::new()
.as_of(ts(2024, 4, 1))
.subject("Bob")
.build()
.unwrap();
let snap = q.execute(&store).unwrap();
assert_eq!(snap.len(), 1);
assert_eq!(snap.triples()[0].predicate, "likes");
}
#[test]
fn test_builder_missing_as_of() {
let result = TimeTravelQueryBuilder::new().build();
assert!(result.is_err());
}
#[test]
fn test_pattern_matches() {
let t = VersionedTriple::new("s", "p", "o", Utc::now(), 1);
assert!(TriplePattern::wildcard().matches(&t));
assert!(TriplePattern::with_subject("s").matches(&t));
assert!(!TriplePattern::with_subject("x").matches(&t));
assert!(TriplePattern::exact("s", "p", "o").matches(&t));
assert!(!TriplePattern::exact("s", "p", "z").matches(&t));
}
}