Skip to main content

agentic_evolve_core/query/
delta.rs

1//! Delta queries — track state versions and return only changes since a given version.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Tracks versioned state for delta queries.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct VersionedState<T> {
9    /// The current data.
10    data: Vec<T>,
11    /// Monotonically increasing version counter.
12    version: u64,
13    /// Timestamp of the last modification.
14    last_modified: DateTime<Utc>,
15    /// History of changes: (version, change_type, index_or_id).
16    #[serde(skip)]
17    change_log: Vec<ChangeEntry>,
18}
19
20/// A single change entry in the version history.
21#[derive(Debug, Clone)]
22struct ChangeEntry {
23    version: u64,
24    change_type: ChangeType,
25    index: usize,
26}
27
28/// The type of change recorded.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub enum ChangeType {
31    /// A new item was added.
32    Created,
33    /// An existing item was modified.
34    Updated,
35    /// An item was removed.
36    Deleted,
37}
38
39/// The result of a delta query: either unchanged or a set of changes.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub enum DeltaResult<T> {
42    /// No changes since the requested version.
43    Unchanged {
44        /// The current version.
45        version: u64,
46    },
47    /// Changes that occurred since the requested version.
48    Changed {
49        /// Items that were created or updated.
50        items: Vec<T>,
51        /// Number of deletions.
52        deletions: usize,
53        /// The old version the client had.
54        from_version: u64,
55        /// The current version after changes.
56        to_version: u64,
57    },
58}
59
60impl<T: Clone> VersionedState<T> {
61    /// Create a new versioned state with no data.
62    pub fn new() -> Self {
63        Self {
64            data: Vec::new(),
65            version: 0,
66            last_modified: Utc::now(),
67            change_log: Vec::new(),
68        }
69    }
70
71    /// Create from initial data at version 1.
72    pub fn from_data(data: Vec<T>) -> Self {
73        let len = data.len();
74        let mut state = Self {
75            data,
76            version: 1,
77            last_modified: Utc::now(),
78            change_log: Vec::new(),
79        };
80        for i in 0..len {
81            state.change_log.push(ChangeEntry {
82                version: 1,
83                change_type: ChangeType::Created,
84                index: i,
85            });
86        }
87        state
88    }
89
90    /// Current version number.
91    pub fn version(&self) -> u64 {
92        self.version
93    }
94
95    /// Timestamp of last modification.
96    pub fn last_modified(&self) -> DateTime<Utc> {
97        self.last_modified
98    }
99
100    /// Read the full data slice.
101    pub fn data(&self) -> &[T] {
102        &self.data
103    }
104
105    /// Add a new item, incrementing the version.
106    pub fn add(&mut self, item: T) {
107        self.version += 1;
108        self.last_modified = Utc::now();
109        self.data.push(item);
110        self.change_log.push(ChangeEntry {
111            version: self.version,
112            change_type: ChangeType::Created,
113            index: self.data.len() - 1,
114        });
115    }
116
117    /// Update an item at the given index.
118    pub fn update(&mut self, index: usize, item: T) {
119        if index < self.data.len() {
120            self.version += 1;
121            self.last_modified = Utc::now();
122            self.data[index] = item;
123            self.change_log.push(ChangeEntry {
124                version: self.version,
125                change_type: ChangeType::Updated,
126                index,
127            });
128        }
129    }
130
131    /// Mark an item as deleted (removes from data vec).
132    pub fn delete(&mut self, index: usize) {
133        if index < self.data.len() {
134            self.version += 1;
135            self.last_modified = Utc::now();
136            self.data.remove(index);
137            self.change_log.push(ChangeEntry {
138                version: self.version,
139                change_type: ChangeType::Deleted,
140                index,
141            });
142        }
143    }
144
145    /// Query changes since a given version.
146    ///
147    /// Returns `DeltaResult::Unchanged` if there have been no changes, or
148    /// `DeltaResult::Changed` with the affected items.
149    pub fn changes_since_version(&self, since_version: u64) -> DeltaResult<T> {
150        if since_version >= self.version {
151            return DeltaResult::Unchanged {
152                version: self.version,
153            };
154        }
155
156        let relevant: Vec<&ChangeEntry> = self
157            .change_log
158            .iter()
159            .filter(|e| e.version > since_version)
160            .collect();
161
162        if relevant.is_empty() {
163            return DeltaResult::Unchanged {
164                version: self.version,
165            };
166        }
167
168        let mut items = Vec::new();
169        let mut deletions = 0usize;
170
171        for entry in &relevant {
172            match entry.change_type {
173                ChangeType::Created | ChangeType::Updated => {
174                    if entry.index < self.data.len() {
175                        items.push(self.data[entry.index].clone());
176                    }
177                }
178                ChangeType::Deleted => {
179                    deletions += 1;
180                }
181            }
182        }
183
184        DeltaResult::Changed {
185            items,
186            deletions,
187            from_version: since_version,
188            to_version: self.version,
189        }
190    }
191
192    /// Check whether the state is unchanged since a given version.
193    pub fn is_unchanged_since(&self, since_version: u64) -> bool {
194        since_version >= self.version
195    }
196}
197
198impl<T: Clone> Default for VersionedState<T> {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn new_state_version_zero() {
210        let state = VersionedState::<String>::new();
211        assert_eq!(state.version(), 0);
212        assert!(state.data().is_empty());
213    }
214
215    #[test]
216    fn from_data_version_one() {
217        let state = VersionedState::from_data(vec![1, 2, 3]);
218        assert_eq!(state.version(), 1);
219        assert_eq!(state.data().len(), 3);
220    }
221
222    #[test]
223    fn add_increments_version() {
224        let mut state = VersionedState::new();
225        state.add("hello".to_string());
226        assert_eq!(state.version(), 1);
227        state.add("world".to_string());
228        assert_eq!(state.version(), 2);
229    }
230
231    #[test]
232    fn changes_since_returns_unchanged_when_current() {
233        let state = VersionedState::from_data(vec![1, 2, 3]);
234        let result = state.changes_since_version(1);
235        assert!(matches!(result, DeltaResult::Unchanged { .. }));
236    }
237
238    #[test]
239    fn changes_since_returns_changed_items() {
240        let mut state = VersionedState::from_data(vec![1, 2, 3]);
241        state.add(4);
242        let result = state.changes_since_version(1);
243        match result {
244            DeltaResult::Changed {
245                items, to_version, ..
246            } => {
247                assert_eq!(items.len(), 1);
248                assert_eq!(items[0], 4);
249                assert_eq!(to_version, 2);
250            }
251            _ => panic!("Expected Changed"),
252        }
253    }
254
255    #[test]
256    fn is_unchanged_since_current_version() {
257        let state = VersionedState::from_data(vec![1]);
258        assert!(state.is_unchanged_since(1));
259        assert!(state.is_unchanged_since(99));
260        assert!(!state.is_unchanged_since(0));
261    }
262
263    #[test]
264    fn delete_increments_version() {
265        let mut state = VersionedState::from_data(vec![1, 2, 3]);
266        state.delete(0);
267        assert_eq!(state.version(), 2);
268        assert_eq!(state.data().len(), 2);
269    }
270
271    #[test]
272    fn update_tracks_change() {
273        let mut state = VersionedState::from_data(vec!["a".to_string()]);
274        state.update(0, "b".to_string());
275        assert_eq!(state.version(), 2);
276        assert_eq!(state.data()[0], "b");
277    }
278
279    #[test]
280    fn delta_proportional_to_changes() {
281        let mut state = VersionedState::from_data(vec![1, 2, 3, 4, 5]);
282        let baseline = state.version();
283        state.add(6);
284        state.add(7);
285        let result = state.changes_since_version(baseline);
286        match result {
287            DeltaResult::Changed { items, .. } => {
288                // Only the 2 new items, not the full 7.
289                assert_eq!(items.len(), 2);
290            }
291            _ => panic!("Expected Changed"),
292        }
293    }
294}