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