Skip to main content

grafeo_core/graph/lpg/
edge.rs

1//! Edge types for the LPG model.
2//!
3//! Like nodes, edges have two forms: [`Edge`] is the user-friendly version,
4//! [`EdgeRecord`] is the compact storage format.
5
6use grafeo_common::types::{EdgeId, EpochId, NodeId, PropertyKey, Value};
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10/// A relationship between two nodes, with a type and optional properties.
11///
12/// Think of edges as the "verbs" in your graph - KNOWS, WORKS_AT, PURCHASED.
13/// Each edge connects exactly one source node to one destination node.
14///
15/// # Example
16///
17/// ```
18/// use grafeo_core::graph::lpg::Edge;
19/// use grafeo_common::types::{EdgeId, NodeId};
20///
21/// let mut works_at = Edge::new(
22///     EdgeId::new(1),
23///     NodeId::new(10),  // Alice
24///     NodeId::new(20),  // Acme Corp
25///     "WORKS_AT"
26/// );
27/// works_at.set_property("since", 2020i64);
28/// works_at.set_property("role", "Engineer");
29/// ```
30#[derive(Debug, Clone)]
31pub struct Edge {
32    /// Unique identifier.
33    pub id: EdgeId,
34    /// Source node ID.
35    pub src: NodeId,
36    /// Destination node ID.
37    pub dst: NodeId,
38    /// Edge type/label.
39    pub edge_type: Arc<str>,
40    /// Properties stored on this edge.
41    pub properties: BTreeMap<PropertyKey, Value>,
42}
43
44impl Edge {
45    /// Creates a new edge.
46    #[must_use]
47    pub fn new(id: EdgeId, src: NodeId, dst: NodeId, edge_type: impl Into<Arc<str>>) -> Self {
48        Self {
49            id,
50            src,
51            dst,
52            edge_type: edge_type.into(),
53            properties: BTreeMap::new(),
54        }
55    }
56
57    /// Sets a property on this edge.
58    pub fn set_property(&mut self, key: impl Into<PropertyKey>, value: impl Into<Value>) {
59        self.properties.insert(key.into(), value.into());
60    }
61
62    /// Gets a property from this edge.
63    #[must_use]
64    pub fn get_property(&self, key: &str) -> Option<&Value> {
65        self.properties.get(&PropertyKey::new(key))
66    }
67
68    /// Removes a property from this edge.
69    pub fn remove_property(&mut self, key: &str) -> Option<Value> {
70        self.properties.remove(&PropertyKey::new(key))
71    }
72
73    /// Given one endpoint, returns the other end of this edge.
74    ///
75    /// Handy in traversals when you have a node and edge but need the neighbor.
76    /// Returns `None` if `node` isn't connected to this edge.
77    #[must_use]
78    pub fn other_endpoint(&self, node: NodeId) -> Option<NodeId> {
79        if node == self.src {
80            Some(self.dst)
81        } else if node == self.dst {
82            Some(self.src)
83        } else {
84            None
85        }
86    }
87}
88
89/// The compact storage format for an edge - fits in one cache line.
90///
91/// Like [`NodeRecord`](super::NodeRecord), this is what the store keeps in memory.
92/// Properties are stored separately in columnar format.
93#[repr(C)]
94#[derive(Debug, Clone, Copy)]
95pub struct EdgeRecord {
96    /// Unique edge identifier.
97    pub id: EdgeId,
98    /// Source node ID.
99    pub src: NodeId,
100    /// Destination node ID.
101    pub dst: NodeId,
102    /// Edge type ID (index into type table).
103    pub type_id: u32,
104    /// Offset into the property arena.
105    pub props_offset: u32,
106    /// Number of properties.
107    pub props_count: u16,
108    /// Flags (deleted, has_version, etc.).
109    pub flags: EdgeFlags,
110    /// Epoch this record was created in.
111    pub epoch: EpochId,
112}
113
114impl EdgeRecord {
115    /// Flag indicating the edge is deleted.
116    pub const FLAG_DELETED: u16 = 1 << 0;
117    /// Flag indicating the edge has version history.
118    pub const FLAG_HAS_VERSION: u16 = 1 << 1;
119
120    /// Creates a new edge record.
121    #[must_use]
122    pub const fn new(id: EdgeId, src: NodeId, dst: NodeId, type_id: u32, epoch: EpochId) -> Self {
123        Self {
124            id,
125            src,
126            dst,
127            type_id,
128            props_offset: 0,
129            props_count: 0,
130            flags: EdgeFlags(0),
131            epoch,
132        }
133    }
134
135    /// Checks if this edge is deleted.
136    #[must_use]
137    pub const fn is_deleted(&self) -> bool {
138        self.flags.contains(Self::FLAG_DELETED)
139    }
140
141    /// Marks this edge as deleted.
142    pub fn set_deleted(&mut self, deleted: bool) {
143        if deleted {
144            self.flags.set(Self::FLAG_DELETED);
145        } else {
146            self.flags.clear(Self::FLAG_DELETED);
147        }
148    }
149}
150
151/// Bit flags packed into an edge record.
152#[repr(transparent)]
153#[derive(Debug, Clone, Copy, Default)]
154pub struct EdgeFlags(pub u16);
155
156impl EdgeFlags {
157    /// Checks if a flag is set.
158    #[must_use]
159    pub const fn contains(&self, flag: u16) -> bool {
160        (self.0 & flag) != 0
161    }
162
163    /// Sets a flag.
164    pub fn set(&mut self, flag: u16) {
165        self.0 |= flag;
166    }
167
168    /// Clears a flag.
169    pub fn clear(&mut self, flag: u16) {
170        self.0 &= !flag;
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_edge_creation() {
180        let edge = Edge::new(EdgeId::new(1), NodeId::new(10), NodeId::new(20), "KNOWS");
181
182        assert_eq!(edge.id, EdgeId::new(1));
183        assert_eq!(edge.src, NodeId::new(10));
184        assert_eq!(edge.dst, NodeId::new(20));
185        assert_eq!(edge.edge_type.as_ref(), "KNOWS");
186    }
187
188    #[test]
189    fn test_edge_properties() {
190        let mut edge = Edge::new(EdgeId::new(1), NodeId::new(10), NodeId::new(20), "KNOWS");
191
192        edge.set_property("since", 2020i64);
193        edge.set_property("weight", 1.5f64);
194
195        assert_eq!(
196            edge.get_property("since").and_then(|v| v.as_int64()),
197            Some(2020)
198        );
199        assert_eq!(
200            edge.get_property("weight").and_then(|v| v.as_float64()),
201            Some(1.5)
202        );
203    }
204
205    #[test]
206    fn test_edge_other_endpoint() {
207        let edge = Edge::new(EdgeId::new(1), NodeId::new(10), NodeId::new(20), "KNOWS");
208
209        assert_eq!(edge.other_endpoint(NodeId::new(10)), Some(NodeId::new(20)));
210        assert_eq!(edge.other_endpoint(NodeId::new(20)), Some(NodeId::new(10)));
211        assert_eq!(edge.other_endpoint(NodeId::new(30)), None);
212    }
213
214    #[test]
215    fn test_edge_record_flags() {
216        let mut record = EdgeRecord::new(
217            EdgeId::new(1),
218            NodeId::new(10),
219            NodeId::new(20),
220            0,
221            EpochId::INITIAL,
222        );
223
224        assert!(!record.is_deleted());
225        record.set_deleted(true);
226        assert!(record.is_deleted());
227    }
228
229    #[test]
230    fn test_edge_record_size() {
231        // EdgeRecord should be a reasonable size for cache efficiency
232        let size = std::mem::size_of::<EdgeRecord>();
233        // Should be <= 64 bytes (one cache line)
234        assert!(size <= 64, "EdgeRecord is {} bytes", size);
235    }
236}