Skip to main content

agentic_forge_core/query/
delta.rs

1//! Delta retrieval — change-proportional queries.
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum ChangeType {
7    Created,
8    Updated,
9    Deleted,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Change<T> {
14    pub id: String,
15    pub change_type: ChangeType,
16    pub timestamp: i64,
17    pub value: Option<T>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DeltaResult<T> {
22    pub changes: Vec<Change<T>>,
23    pub from_version: u64,
24    pub to_version: u64,
25    pub has_more: bool,
26}
27
28impl<T> DeltaResult<T> {
29    pub fn empty(version: u64) -> Self {
30        Self {
31            changes: Vec::new(),
32            from_version: version,
33            to_version: version,
34            has_more: false,
35        }
36    }
37
38    pub fn is_empty(&self) -> bool {
39        self.changes.is_empty()
40    }
41
42    pub fn len(&self) -> usize {
43        self.changes.len()
44    }
45}
46
47#[derive(Debug, Clone)]
48pub struct VersionedState {
49    version: u64,
50    timestamps: Vec<(String, i64, ChangeType)>,
51}
52
53impl VersionedState {
54    pub fn new() -> Self {
55        Self {
56            version: 0,
57            timestamps: Vec::new(),
58        }
59    }
60
61    pub fn record_change(&mut self, id: impl Into<String>, change_type: ChangeType) {
62        self.version += 1;
63        let ts = chrono::Utc::now().timestamp_micros();
64        self.timestamps.push((id.into(), ts, change_type));
65    }
66
67    pub fn version(&self) -> u64 {
68        self.version
69    }
70
71    pub fn changes_since(&self, since_ts: i64) -> Vec<&(String, i64, ChangeType)> {
72        self.timestamps
73            .iter()
74            .filter(|(_, ts, _)| *ts > since_ts)
75            .collect()
76    }
77
78    pub fn changes_since_version(&self, since_version: u64) -> Vec<&(String, i64, ChangeType)> {
79        if since_version as usize >= self.timestamps.len() {
80            return Vec::new();
81        }
82        self.timestamps[since_version as usize..].iter().collect()
83    }
84
85    pub fn last_change_timestamp(&self) -> i64 {
86        self.timestamps.last().map(|(_, ts, _)| *ts).unwrap_or(0)
87    }
88
89    pub fn is_unchanged_since(&self, since_version: u64) -> bool {
90        self.version <= since_version
91    }
92}
93
94impl Default for VersionedState {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_versioned_state_new() {
106        let state = VersionedState::new();
107        assert_eq!(state.version(), 0);
108    }
109
110    #[test]
111    fn test_versioned_state_record() {
112        let mut state = VersionedState::new();
113        state.record_change("entity_1", ChangeType::Created);
114        assert_eq!(state.version(), 1);
115        state.record_change("entity_2", ChangeType::Created);
116        assert_eq!(state.version(), 2);
117    }
118
119    #[test]
120    fn test_versioned_state_changes_since_version() {
121        let mut state = VersionedState::new();
122        state.record_change("a", ChangeType::Created);
123        state.record_change("b", ChangeType::Created);
124        state.record_change("c", ChangeType::Updated);
125
126        let changes = state.changes_since_version(1);
127        assert_eq!(changes.len(), 2); // b and c
128    }
129
130    #[test]
131    fn test_versioned_state_unchanged() {
132        let mut state = VersionedState::new();
133        state.record_change("a", ChangeType::Created);
134        assert!(state.is_unchanged_since(1));
135        assert!(!state.is_unchanged_since(0));
136    }
137
138    #[test]
139    fn test_delta_result_empty() {
140        let result: DeltaResult<String> = DeltaResult::empty(5);
141        assert!(result.is_empty());
142        assert_eq!(result.from_version, 5);
143    }
144
145    #[test]
146    fn test_delta_proportional_cost() {
147        let mut state = VersionedState::new();
148        for i in 0..100 {
149            state.record_change(format!("item_{}", i), ChangeType::Created);
150        }
151        let v_before = state.version();
152
153        // Add one more
154        state.record_change("item_100", ChangeType::Created);
155
156        let all_changes = state.changes_since_version(0);
157        let delta_changes = state.changes_since_version(v_before);
158
159        assert_eq!(all_changes.len(), 101);
160        assert_eq!(delta_changes.len(), 1);
161        // Delta is 101x cheaper than full scan
162        assert!(delta_changes.len() < all_changes.len() / 50);
163    }
164
165    #[test]
166    fn test_unchanged_state_free() {
167        let mut state = VersionedState::new();
168        state.record_change("a", ChangeType::Created);
169        let v = state.version();
170        // No changes since v
171        let changes = state.changes_since_version(v);
172        assert!(changes.is_empty());
173        assert!(state.is_unchanged_since(v));
174    }
175}