Skip to main content

bitemporal_runtime/
lib.rs

1//! # bitemporal-runtime
2//!
3//! Bitemporal truth primitives for append-supersede temporal data.
4//!
5//! ## Core concepts
6//!
7//! - **valid_time**: When something is true in the domain (business time)
8//! - **recorded_time**: When the system learned about it (system time)
9//! - **append-supersede**: Updates never mutate; they append a new row and mark prior rows superseded
10//!
11//! ## Key types
12//!
13//! - [`BitemporalRecord<T>`]: A temporal record with valid_time, recorded_time, and value
14//! - [`SupersessionReceipt`]: Cryptographic receipt for every supersession event
15//!
16//! ## Functions
17//!
18//! - [`append_supersede`]: Insert a new record, mark prior records superseded
19//! - [`as_of_query`]: Query records valid at a specific valid_time as of a specific recorded_time
20//! - [`temporal_snapshot`]: Retrieve full state as of a specific recorded_time
21
22mod error;
23mod queries;
24#[cfg(feature = "schema")]
25pub mod schema;
26#[cfg(feature = "sqlite")]
27mod sqlite;
28mod types;
29
30pub use error::BitemporalError;
31pub use queries::{append_supersede, as_of_query, temporal_snapshot};
32#[cfg(feature = "sqlite")]
33pub use sqlite::SqliteDb;
34pub use types::{BitemporalRecord, RecordId, SupersessionReceipt, SupersessionTarget};
35
36use chrono::{DateTime, Utc};
37use std::collections::BTreeMap;
38
39/// In-memory bitemporal store for testing and development.
40#[derive(Debug, Clone)]
41pub struct InMemoryDb {
42    /// Map from record_id -> Vec of BitemporalRecord (one per version)
43    records: BTreeMap<String, Vec<types::BitemporalRecord>>,
44}
45
46impl InMemoryDb {
47    /// Create a new empty in-memory bitemporal store.
48    pub fn new() -> Self {
49        Self {
50            records: BTreeMap::new(),
51        }
52    }
53
54    /// Insert a bitemporal record into this store.
55    pub fn insert(&mut self, record: types::BitemporalRecord) {
56        let id = record.id.clone();
57        self.records.entry(id).or_default().push(record);
58    }
59
60    /// Get all versions of a record by ID.
61    pub fn get_versions(&self, id: &str) -> Option<&Vec<types::BitemporalRecord>> {
62        self.records.get(id)
63    }
64
65    /// Get all records as a snapshot at a specific recorded_time.
66    pub fn snapshot_at(&self, recorded_time: DateTime<Utc>) -> Vec<types::BitemporalRecord> {
67        let mut result = Vec::new();
68        for versions in self.records.values() {
69            let mut best: Option<&types::BitemporalRecord> = None;
70            for v in versions {
71                if v.recorded_time <= recorded_time
72                    && best
73                        .map(|b| v.recorded_time > b.recorded_time)
74                        .unwrap_or(true)
75                {
76                    best = Some(v);
77                }
78            }
79            if let Some(r) = best {
80                result.push(r.clone());
81            }
82        }
83        result
84    }
85
86    /// Get all records valid at a specific valid_time as of a specific recorded_time.
87    pub fn as_of(
88        &self,
89        valid_time: DateTime<Utc>,
90        recorded_time: DateTime<Utc>,
91    ) -> Vec<types::BitemporalRecord> {
92        let mut result = Vec::new();
93        for versions in self.records.values() {
94            let mut best: Option<&types::BitemporalRecord> = None;
95            for v in versions {
96                if v.recorded_time <= recorded_time
97                    && v.valid_time <= valid_time
98                    && best
99                        .map(|b| v.recorded_time > b.recorded_time)
100                        .unwrap_or(true)
101                {
102                    best = Some(v);
103                }
104            }
105            if let Some(r) = best {
106                result.push(r.clone());
107            }
108        }
109        result
110    }
111
112    /// Returns the count of unique record IDs in this store.
113    pub fn len(&self) -> usize {
114        self.records.len()
115    }
116
117    /// Returns true if this store has no records.
118    pub fn is_empty(&self) -> bool {
119        self.records.is_empty()
120    }
121}
122
123impl Default for InMemoryDb {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_in_memory_basic_insert() {
135        let mut db = InMemoryDb::new();
136        let record = types::BitemporalRecord {
137            id: "ep1".to_string(),
138            valid_time: Utc::now(),
139            recorded_time: Utc::now(),
140            value: (),
141        };
142        db.insert(record);
143        let versions = db.get_versions("ep1").unwrap();
144        assert_eq!(versions.len(), 1);
145    }
146}