Skip to main content

mati_core/store/db/
history.rs

1//! Version history queries (M-14).
2
3use super::*;
4
5/// A single versioned entry from the SurrealKV history iterator.
6///
7/// Timestamps come from SurrealKV's internal clock (nanoseconds since epoch).
8/// Both seconds and nanoseconds are exposed for callers that need either
9/// precision level.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct HistoryEntry {
12    /// Timestamp in whole seconds (nanosecond timestamp / 1_000_000_000).
13    pub timestamp_secs: u64,
14    /// Raw nanosecond timestamp from SurrealKV.
15    pub timestamp_ns: u64,
16    /// Deserialized record, `None` for tombstones or corrupt values.
17    pub record: Option<Record>,
18    /// `true` when this version represents a deletion.
19    pub is_tombstone: bool,
20}
21
22/// Shared synchronous implementation for key history queries.
23///
24/// Iterates all versions of `key` using `history_with_options` with the tight
25/// upper bound `key + \0` (not `prefix_end`) to guarantee no adjacent key
26/// spills. Returns entries sorted newest first.
27fn history_impl(txn: &Transaction, key: &str, opts: &HistoryOptions) -> Result<Vec<HistoryEntry>> {
28    // Upper bound: key + NUL byte — tighter than prefix_end which increments
29    // the last byte. This ensures only exact-key versions are returned.
30    let mut upper = key.as_bytes().to_vec();
31    upper.push(0x00);
32
33    let mut cursor = txn.history_with_options(key.as_bytes(), upper.as_slice(), opts)?;
34
35    let mut entries = Vec::new();
36    while cursor.next()? {
37        let key_ref = cursor.key();
38
39        // Guard: only process entries whose user_key matches exactly
40        if key_ref.user_key() != key.as_bytes() {
41            continue;
42        }
43
44        let is_tombstone = key_ref.is_tombstone();
45        let ts_ns = key_ref.timestamp();
46        let ts_secs = ts_ns / 1_000_000_000;
47
48        let record = if is_tombstone {
49            None
50        } else {
51            match cursor.value() {
52                Ok(bytes) => rmps::from_slice::<Record>(&bytes).ok(),
53                Err(_) => None,
54            }
55        };
56
57        entries.push(HistoryEntry {
58            timestamp_secs: ts_secs,
59            timestamp_ns: ts_ns,
60            record,
61            is_tombstone,
62        });
63    }
64
65    // Newest first — SurrealKV history iterator order is not guaranteed to be
66    // reverse-chronological, so sort explicitly.
67    entries.sort_by_key(|e| std::cmp::Reverse(e.timestamp_ns));
68    Ok(entries)
69}
70
71impl Store {
72    /// Return version history for a single key, newest first.
73    ///
74    /// Includes tombstones (deletions). Uses the tight upper bound `key + \0`
75    /// so adjacent keys never spill into the result set.
76    ///
77    /// `limit` caps the number of entries returned; `0` means unlimited.
78    pub fn history(&self, key: &str, limit: usize) -> Result<Vec<HistoryEntry>> {
79        anyhow::ensure!(!key.is_empty(), "history key must not be empty");
80        let tree = self.tree_for(key);
81        let txn = tree.begin_with_mode(Mode::ReadOnly)?;
82
83        let mut opts = HistoryOptions::new().with_tombstones(true);
84        if limit > 0 {
85            opts = opts.with_limit(limit);
86        }
87
88        history_impl(&txn, key, &opts)
89    }
90
91    /// Return version history for a single key since `since_ts` (seconds),
92    /// newest first.
93    ///
94    /// Timestamps are converted to nanoseconds for the SurrealKV range filter.
95    pub fn history_since(
96        &self,
97        key: &str,
98        since_ts: u64,
99        limit: usize,
100    ) -> Result<Vec<HistoryEntry>> {
101        anyhow::ensure!(!key.is_empty(), "history key must not be empty");
102        let tree = self.tree_for(key);
103        let txn = tree.begin_with_mode(Mode::ReadOnly)?;
104
105        let since_ns = since_ts.saturating_mul(1_000_000_000);
106        let mut opts = HistoryOptions::new()
107            .with_tombstones(true)
108            .with_ts_range(since_ns, u64::MAX);
109        if limit > 0 {
110            opts = opts.with_limit(limit);
111        }
112
113        history_impl(&txn, key, &opts)
114    }
115
116    /// Return all records updated since `since_ts` (seconds), newest first.
117    ///
118    /// Scans every knowledge namespace (including `dep:`) and returns records
119    /// whose `updated_at >= since_ts`. Results are sorted by `updated_at`
120    /// descending with secondary sort by key for deterministic ordering.
121    pub async fn records_since(&self, since_ts: u64, limit: usize) -> Result<Vec<Record>> {
122        let mut results = Vec::new();
123        for ns in KNOWLEDGE_NAMESPACES {
124            let records = self.scan_prefix(ns).await?;
125            for r in records {
126                if r.updated_at >= since_ts {
127                    results.push(r);
128                }
129            }
130        }
131        // Newest first, secondary sort by key for determinism
132        results.sort_by(|a, b| {
133            b.updated_at
134                .cmp(&a.updated_at)
135                .then_with(|| a.key.cmp(&b.key))
136        });
137        if limit > 0 && results.len() > limit {
138            results.truncate(limit);
139        }
140        Ok(results)
141    }
142}