Skip to main content

kyu_delta/
delta.rs

1use hashbrown::HashMap;
2use smol_str::SmolStr;
3
4use crate::node_key::NodeKey;
5use crate::value::DeltaValue;
6
7/// A single graph mutation in the delta fast path.
8///
9/// Each variant is idempotent and conflict-free. "Last write wins"
10/// semantics: if two deltas target the same entity, the one with the
11/// higher batch timestamp takes precedence.
12#[derive(Clone, Debug, PartialEq)]
13pub enum GraphDelta {
14    /// Insert or update a node. Properties are merged (new overwrite existing;
15    /// unmentioned properties are kept).
16    UpsertNode {
17        key: NodeKey,
18        labels: Vec<SmolStr>,
19        props: HashMap<SmolStr, DeltaValue>,
20    },
21
22    /// Insert or update an edge. Properties are merged.
23    UpsertEdge {
24        src: NodeKey,
25        rel_type: SmolStr,
26        dst: NodeKey,
27        props: HashMap<SmolStr, DeltaValue>,
28    },
29
30    /// Delete a node and all its incident edges.
31    DeleteNode { key: NodeKey },
32
33    /// Delete a specific edge.
34    DeleteEdge {
35        src: NodeKey,
36        rel_type: SmolStr,
37        dst: NodeKey,
38    },
39}
40
41impl GraphDelta {
42    pub fn is_node_op(&self) -> bool {
43        matches!(self, Self::UpsertNode { .. } | Self::DeleteNode { .. })
44    }
45
46    pub fn is_edge_op(&self) -> bool {
47        matches!(self, Self::UpsertEdge { .. } | Self::DeleteEdge { .. })
48    }
49
50    pub fn is_delete(&self) -> bool {
51        matches!(self, Self::DeleteNode { .. } | Self::DeleteEdge { .. })
52    }
53
54    pub fn is_upsert(&self) -> bool {
55        matches!(self, Self::UpsertNode { .. } | Self::UpsertEdge { .. })
56    }
57
58    /// Returns the primary `NodeKey` involved in this delta.
59    /// For edges, returns the source node key.
60    pub fn primary_key(&self) -> &NodeKey {
61        match self {
62            Self::UpsertNode { key, .. } | Self::DeleteNode { key } => key,
63            Self::UpsertEdge { src, .. } | Self::DeleteEdge { src, .. } => src,
64        }
65    }
66
67    /// Returns all `NodeKey`s referenced by this delta.
68    pub fn referenced_keys(&self) -> Vec<&NodeKey> {
69        match self {
70            Self::UpsertNode { key, .. } | Self::DeleteNode { key } => vec![key],
71            Self::UpsertEdge { src, dst, .. } | Self::DeleteEdge { src, dst, .. } => {
72                vec![src, dst]
73            }
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    fn sample_upsert_node() -> GraphDelta {
83        GraphDelta::UpsertNode {
84            key: NodeKey::new("Function", "main"),
85            labels: vec![],
86            props: HashMap::new(),
87        }
88    }
89
90    fn sample_upsert_edge() -> GraphDelta {
91        GraphDelta::UpsertEdge {
92            src: NodeKey::new("Function", "main"),
93            rel_type: SmolStr::new("calls"),
94            dst: NodeKey::new("Function", "helper"),
95            props: HashMap::new(),
96        }
97    }
98
99    fn sample_delete_node() -> GraphDelta {
100        GraphDelta::DeleteNode {
101            key: NodeKey::new("Function", "old"),
102        }
103    }
104
105    fn sample_delete_edge() -> GraphDelta {
106        GraphDelta::DeleteEdge {
107            src: NodeKey::new("Function", "main"),
108            rel_type: SmolStr::new("calls"),
109            dst: NodeKey::new("Function", "removed"),
110        }
111    }
112
113    #[test]
114    fn upsert_node_is_node_op() {
115        assert!(sample_upsert_node().is_node_op());
116        assert!(!sample_upsert_node().is_edge_op());
117    }
118
119    #[test]
120    fn upsert_edge_is_edge_op() {
121        assert!(sample_upsert_edge().is_edge_op());
122        assert!(!sample_upsert_edge().is_node_op());
123    }
124
125    #[test]
126    fn delete_node_is_delete() {
127        assert!(sample_delete_node().is_delete());
128        assert!(!sample_delete_node().is_upsert());
129    }
130
131    #[test]
132    fn delete_edge_is_delete() {
133        assert!(sample_delete_edge().is_delete());
134        assert!(!sample_delete_edge().is_upsert());
135    }
136
137    #[test]
138    fn upsert_is_not_delete() {
139        assert!(sample_upsert_node().is_upsert());
140        assert!(!sample_upsert_node().is_delete());
141    }
142
143    #[test]
144    fn primary_key_for_node() {
145        let d = sample_upsert_node();
146        assert_eq!(d.primary_key().label, "Function");
147        assert_eq!(d.primary_key().primary_key, "main");
148    }
149
150    #[test]
151    fn primary_key_for_edge_is_src() {
152        let d = sample_upsert_edge();
153        assert_eq!(d.primary_key().label, "Function");
154        assert_eq!(d.primary_key().primary_key, "main");
155    }
156
157    #[test]
158    fn referenced_keys_node() {
159        let d = sample_upsert_node();
160        assert_eq!(d.referenced_keys().len(), 1);
161    }
162
163    #[test]
164    fn referenced_keys_edge() {
165        let d = sample_upsert_edge();
166        let keys = d.referenced_keys();
167        assert_eq!(keys.len(), 2);
168        assert_eq!(keys[0].primary_key, "main");
169        assert_eq!(keys[1].primary_key, "helper");
170    }
171
172    #[test]
173    fn clone_and_eq() {
174        let a = sample_upsert_node();
175        let b = a.clone();
176        assert_eq!(a, b);
177    }
178}