Skip to main content

icydb_core/patch/
map.rs

1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3
4///
5/// MapPatch
6///
7/// Deterministic map mutations.
8///
9/// - Maps are unordered values; insertion order is discarded.
10/// - `Insert` is an upsert.
11/// - `Replace` is a no-op when the key is missing.
12/// - `Remove` is a no-op when the key is missing.
13/// - `Clear` must be the only patch in the batch.
14///
15
16#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
17pub enum MapPatch<K, V> {
18    Insert { key: K, value: V },
19    Remove { key: K },
20    Replace { key: K, value: V },
21    Clear,
22}
23
24impl<K, V> From<(K, Option<V>)> for MapPatch<K, V> {
25    fn from((key, value): (K, Option<V>)) -> Self {
26        match value {
27            Some(value) => Self::Insert { key, value },
28            None => Self::Remove { key },
29        }
30    }
31}
32
33///
34/// TESTS
35///
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use crate::{patch::MergePatchError, traits::UpdateView};
41    use std::collections::{BTreeMap, HashMap};
42
43    #[test]
44    fn map_replace_updates_existing_entry() {
45        let mut map: HashMap<String, u8> = [("keep".into(), 1u8), ("replace".into(), 2u8)]
46            .into_iter()
47            .collect();
48
49        let patches = vec![MapPatch::Replace {
50            key: "replace".to_string(),
51            value: 8u8,
52        }];
53
54        map.merge(patches).expect("map patch merge should succeed");
55
56        assert_eq!(map.get("keep"), Some(&1));
57        assert_eq!(map.get("replace"), Some(&8));
58    }
59
60    #[test]
61    fn btree_map_clear_replaces_with_empty_map() {
62        let mut map: BTreeMap<String, u8> =
63            [("a".into(), 1u8), ("b".into(), 2u8)].into_iter().collect();
64
65        map.merge(vec![MapPatch::Clear])
66            .expect("map clear patch should succeed");
67
68        assert!(map.is_empty());
69    }
70
71    #[test]
72    fn map_remove_missing_key_is_noop() {
73        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
74        map.merge(vec![MapPatch::Remove {
75            key: "missing".to_string(),
76        }])
77        .expect("missing remove key should be ignored");
78        assert_eq!(map.get("a"), Some(&1));
79    }
80
81    #[test]
82    fn map_replace_missing_key_is_noop() {
83        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
84        map.merge(vec![MapPatch::Replace {
85            key: "missing".to_string(),
86            value: 3u8,
87        }])
88        .expect("missing replace key should be ignored");
89        assert_eq!(map.get("a"), Some(&1));
90    }
91
92    #[test]
93    fn map_clear_with_other_operations_returns_error() {
94        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
95        let err = map
96            .merge(vec![
97                MapPatch::Clear,
98                MapPatch::Insert {
99                    key: "b".to_string(),
100                    value: 2u8,
101                },
102            ])
103            .expect_err("clear combined with key ops should fail");
104        assert!(matches!(
105            err.leaf(),
106            MergePatchError::CardinalityViolation {
107                expected: 1,
108                actual: 2,
109            }
110        ));
111    }
112
113    #[test]
114    fn map_duplicate_key_operations_returns_error() {
115        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
116        let err = map
117            .merge(vec![
118                MapPatch::Insert {
119                    key: "a".to_string(),
120                    value: 3u8,
121                },
122                MapPatch::Replace {
123                    key: "a".to_string(),
124                    value: 4u8,
125                },
126            ])
127            .expect_err("duplicate key operations should fail");
128        assert!(matches!(
129            err.leaf(),
130            MergePatchError::InvalidShape {
131                expected: "unique key operations per map patch batch",
132                actual: "duplicate key operation",
133            }
134        ));
135    }
136}