Skip to main content

icydb_core/
view.rs

1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3
4///
5/// ListPatch
6///
7/// Positional list patches applied in order.
8/// Indices refer to the list state at the time each patch executes.
9/// `Insert` clamps out-of-bounds indices to the tail; `Remove` ignores invalid indices.
10/// `Update` only applies to existing elements and never creates new entries.
11/// `Overwrite` replaces the entire list with the provided values.
12///
13
14#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
15pub enum ListPatch<U> {
16    Update { index: usize, patch: U },
17    Insert { index: usize, value: U },
18    Push { value: U },
19    Overwrite { values: Vec<U> },
20    Remove { index: usize },
21    Clear,
22}
23
24///
25/// SetPatch
26///
27/// Set operations applied in-order; `Overwrite` replaces the entire set.
28///
29
30#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
31pub enum SetPatch<U> {
32    Insert(U),
33    Remove(U),
34    Overwrite { values: Vec<U> },
35    Clear,
36}
37
38///
39/// MapPatch
40///
41/// Deterministic map mutations.
42///
43/// - Maps are unordered values; insertion order is discarded.
44/// - `Insert` is an upsert.
45/// - `Replace` requires an existing key.
46/// - `Remove` requires an existing key.
47/// - `Clear` must be the only patch in the batch.
48///
49/// Invalid patch shapes and missing-key operations are returned by
50/// `UpdateView::merge` as `ViewPatchError`.
51///
52
53#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
54pub enum MapPatch<K, V> {
55    Insert { key: K, value: V },
56    Remove { key: K },
57    Replace { key: K, value: V },
58    Clear,
59}
60
61impl<K, V> From<(K, Option<V>)> for MapPatch<K, V> {
62    fn from((key, value): (K, Option<V>)) -> Self {
63        match value {
64            Some(value) => Self::Insert { key, value },
65            None => Self::Remove { key },
66        }
67    }
68}
69
70///
71/// TESTS
72///
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::traits::{UpdateView, ViewPatchError};
78    use std::collections::{BTreeMap, HashMap, HashSet};
79
80    #[test]
81    fn vec_partial_patches() {
82        let mut values = vec![10u8, 20, 30];
83        let patches = vec![
84            ListPatch::Update {
85                index: 1,
86                patch: 99,
87            },
88            ListPatch::Insert {
89                index: 1,
90                value: 11,
91            },
92            ListPatch::Remove { index: 0 },
93        ];
94
95        values
96            .merge(patches)
97            .expect("list patch merge should succeed");
98        assert_eq!(values, vec![11, 99, 30]);
99    }
100
101    #[test]
102    fn vec_overwrite_replaces_contents() {
103        let mut values = vec![1u8, 2, 3];
104        let patches = vec![ListPatch::Overwrite {
105            values: vec![9u8, 8],
106        }];
107
108        values
109            .merge(patches)
110            .expect("list patch merge should succeed");
111        assert_eq!(values, vec![9, 8]);
112    }
113
114    #[test]
115    fn set_insert_remove_without_clear() {
116        let mut set: HashSet<u8> = [1, 2, 3].into_iter().collect();
117        let patches = vec![SetPatch::Remove(2), SetPatch::Insert(4)];
118
119        set.merge(patches).expect("set patch merge should succeed");
120        let expected: HashSet<u8> = [1, 3, 4].into_iter().collect();
121        assert_eq!(set, expected);
122    }
123
124    #[test]
125    fn set_overwrite_replaces_contents() {
126        let mut set: HashSet<u8> = [1, 2, 3].into_iter().collect();
127        let patches = vec![SetPatch::Overwrite {
128            values: vec![3u8, 4, 5],
129        }];
130
131        set.merge(patches).expect("set patch merge should succeed");
132        let expected: HashSet<u8> = [3, 4, 5].into_iter().collect();
133        assert_eq!(set, expected);
134    }
135
136    #[test]
137    fn map_insert_in_place_and_remove() {
138        let mut map: HashMap<String, u8> = [("a".into(), 1u8), ("keep".into(), 9u8)]
139            .into_iter()
140            .collect();
141
142        let patches = vec![
143            MapPatch::Insert {
144                key: "a".to_string(),
145                value: 5u8,
146            },
147            MapPatch::Remove {
148                key: "keep".to_string(),
149            },
150            MapPatch::Insert {
151                key: "insert".to_string(),
152                value: 7u8,
153            },
154        ];
155
156        map.merge(patches).expect("map patch merge should succeed");
157
158        assert_eq!(map.get("a"), Some(&5));
159        assert_eq!(map.get("insert"), Some(&7));
160        assert!(!map.contains_key("keep"));
161    }
162
163    #[test]
164    fn map_replace_updates_existing_entry() {
165        let mut map: HashMap<String, u8> = [("keep".into(), 1u8), ("replace".into(), 2u8)]
166            .into_iter()
167            .collect();
168
169        let patches = vec![MapPatch::Replace {
170            key: "replace".to_string(),
171            value: 8u8,
172        }];
173
174        map.merge(patches).expect("map patch merge should succeed");
175
176        assert_eq!(map.get("keep"), Some(&1));
177        assert_eq!(map.get("replace"), Some(&8));
178    }
179
180    #[test]
181    fn btree_map_clear_replaces_with_empty_map() {
182        let mut map: BTreeMap<String, u8> =
183            [("a".into(), 1u8), ("b".into(), 2u8)].into_iter().collect();
184
185        map.merge(vec![MapPatch::Clear])
186            .expect("map clear patch should succeed");
187
188        assert!(map.is_empty());
189    }
190
191    #[test]
192    fn map_remove_missing_key_returns_error() {
193        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
194        let err = map
195            .merge(vec![MapPatch::Remove {
196                key: "missing".to_string(),
197            }])
198            .expect_err("missing remove key should fail");
199        assert!(matches!(
200            err.leaf(),
201            ViewPatchError::MissingKey {
202                operation: "remove"
203            }
204        ));
205    }
206
207    #[test]
208    fn map_replace_missing_key_returns_error() {
209        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
210        let err = map
211            .merge(vec![MapPatch::Replace {
212                key: "missing".to_string(),
213                value: 3u8,
214            }])
215            .expect_err("missing replace key should fail");
216        assert!(matches!(
217            err.leaf(),
218            ViewPatchError::MissingKey {
219                operation: "replace"
220            }
221        ));
222    }
223
224    #[test]
225    fn map_clear_with_other_operations_returns_error() {
226        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
227        let err = map
228            .merge(vec![
229                MapPatch::Clear,
230                MapPatch::Insert {
231                    key: "b".to_string(),
232                    value: 2u8,
233                },
234            ])
235            .expect_err("clear combined with key ops should fail");
236        assert!(matches!(
237            err.leaf(),
238            ViewPatchError::CardinalityViolation {
239                expected: 1,
240                actual: 2,
241            }
242        ));
243    }
244
245    #[test]
246    fn map_duplicate_key_operations_returns_error() {
247        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
248        let err = map
249            .merge(vec![
250                MapPatch::Insert {
251                    key: "a".to_string(),
252                    value: 3u8,
253                },
254                MapPatch::Replace {
255                    key: "a".to_string(),
256                    value: 4u8,
257                },
258            ])
259            .expect_err("duplicate key operations should fail");
260        assert!(matches!(
261            err.leaf(),
262            ViewPatchError::InvalidPatchShape {
263                expected: "unique key operations per map patch batch",
264                actual: "duplicate key operation",
265            }
266        ));
267    }
268}