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 arcstr::ArcStr;
7use grafeo_common::types::{EdgeId, EpochId, NodeId, PropertyKey, PropertyMap, Value};
8use serde::{Deserialize, Serialize};
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: ArcStr,
40    /// Properties stored on this edge.
41    pub properties: PropertyMap,
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<ArcStr>) -> Self {
48        Self {
49            id,
50            src,
51            dst,
52            edge_type: edge_type.into(),
53            properties: PropertyMap::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    /// Returns the properties as a `BTreeMap` (for serialization compatibility).
74    #[must_use]
75    pub fn properties_as_btree(&self) -> std::collections::BTreeMap<PropertyKey, Value> {
76        self.properties.to_btree_map()
77    }
78
79    /// Given one endpoint, returns the other end of this edge.
80    ///
81    /// Handy in traversals when you have a node and edge but need the neighbor.
82    /// Returns `None` if `node` isn't connected to this edge.
83    #[must_use]
84    pub fn other_endpoint(&self, node: NodeId) -> Option<NodeId> {
85        if node == self.src {
86            Some(self.dst)
87        } else if node == self.dst {
88            Some(self.src)
89        } else {
90            None
91        }
92    }
93}
94
95/// The compact storage format for an edge - fits in one cache line.
96///
97/// Like [`NodeRecord`](super::NodeRecord), this is what the store keeps in memory.
98/// Properties are stored separately in columnar format.
99#[repr(C)]
100#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
101pub struct EdgeRecord {
102    /// Unique edge identifier.
103    pub id: EdgeId,
104    /// Source node ID.
105    pub src: NodeId,
106    /// Destination node ID.
107    pub dst: NodeId,
108    /// Edge type ID (index into type table).
109    pub type_id: u32,
110    /// Offset into the property arena.
111    pub props_offset: u32,
112    /// Number of properties.
113    pub props_count: u16,
114    /// Flags (deleted, has_version, etc.).
115    pub flags: EdgeFlags,
116    /// Epoch this record was created in.
117    pub epoch: EpochId,
118}
119
120impl EdgeRecord {
121    /// Flag indicating the edge is deleted.
122    pub const FLAG_DELETED: u16 = 1 << 0;
123    /// Flag indicating the edge has version history.
124    pub const FLAG_HAS_VERSION: u16 = 1 << 1;
125
126    /// Creates a new edge record.
127    #[must_use]
128    pub const fn new(id: EdgeId, src: NodeId, dst: NodeId, type_id: u32, epoch: EpochId) -> Self {
129        Self {
130            id,
131            src,
132            dst,
133            type_id,
134            props_offset: 0,
135            props_count: 0,
136            flags: EdgeFlags(0),
137            epoch,
138        }
139    }
140
141    /// Checks if this edge is deleted.
142    #[must_use]
143    pub const fn is_deleted(&self) -> bool {
144        self.flags.contains(Self::FLAG_DELETED)
145    }
146
147    /// Marks this edge as deleted.
148    pub fn set_deleted(&mut self, deleted: bool) {
149        if deleted {
150            self.flags.set(Self::FLAG_DELETED);
151        } else {
152            self.flags.clear(Self::FLAG_DELETED);
153        }
154    }
155}
156
157/// Bit flags packed into an edge record.
158#[repr(transparent)]
159#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
160pub struct EdgeFlags(pub u16);
161
162impl EdgeFlags {
163    /// Checks if a flag is set.
164    #[must_use]
165    pub const fn contains(&self, flag: u16) -> bool {
166        (self.0 & flag) != 0
167    }
168
169    /// Sets a flag.
170    pub fn set(&mut self, flag: u16) {
171        self.0 |= flag;
172    }
173
174    /// Clears a flag.
175    pub fn clear(&mut self, flag: u16) {
176        self.0 &= !flag;
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_edge_creation() {
186        let edge = Edge::new(EdgeId::new(1), NodeId::new(10), NodeId::new(20), "KNOWS");
187
188        assert_eq!(edge.id, EdgeId::new(1));
189        assert_eq!(edge.src, NodeId::new(10));
190        assert_eq!(edge.dst, NodeId::new(20));
191        assert_eq!(edge.edge_type.as_str(), "KNOWS");
192    }
193
194    #[test]
195    fn test_edge_properties() {
196        let mut edge = Edge::new(EdgeId::new(1), NodeId::new(10), NodeId::new(20), "KNOWS");
197
198        edge.set_property("since", 2020i64);
199        edge.set_property("weight", 1.5f64);
200
201        assert_eq!(
202            edge.get_property("since").and_then(|v| v.as_int64()),
203            Some(2020)
204        );
205        assert_eq!(
206            edge.get_property("weight").and_then(|v| v.as_float64()),
207            Some(1.5)
208        );
209    }
210
211    #[test]
212    fn test_edge_other_endpoint() {
213        let edge = Edge::new(EdgeId::new(1), NodeId::new(10), NodeId::new(20), "KNOWS");
214
215        assert_eq!(edge.other_endpoint(NodeId::new(10)), Some(NodeId::new(20)));
216        assert_eq!(edge.other_endpoint(NodeId::new(20)), Some(NodeId::new(10)));
217        assert_eq!(edge.other_endpoint(NodeId::new(30)), None);
218    }
219
220    #[test]
221    fn test_edge_record_flags() {
222        let mut record = EdgeRecord::new(
223            EdgeId::new(1),
224            NodeId::new(10),
225            NodeId::new(20),
226            0,
227            EpochId::INITIAL,
228        );
229
230        assert!(!record.is_deleted());
231        record.set_deleted(true);
232        assert!(record.is_deleted());
233    }
234
235    #[test]
236    fn test_edge_record_size() {
237        // EdgeRecord should be a reasonable size for cache efficiency
238        let size = std::mem::size_of::<EdgeRecord>();
239        // Should be <= 64 bytes (one cache line)
240        assert!(size <= 64, "EdgeRecord is {} bytes", size);
241    }
242}