Skip to main content

graphmind/graph/
edge.rs

1//! # Edge Implementation -- Directed, Typed Relationships in a Multigraph
2//!
3//! An [`Edge`] represents a **directed relationship** between two nodes. In graph
4//! theory, a directed edge (also called an *arc*) `(u, v)` is distinct from `(v, u)` --
5//! the edge has a source and a target, establishing an asymmetric relationship.
6//! For example, `(Alice)-[:FOLLOWS]->(Bob)` does not imply `(Bob)-[:FOLLOWS]->(Alice)`.
7//!
8//! ## Edge types as first-class citizens
9//!
10//! Every edge carries an [`EdgeType`] (e.g., `"KNOWS"`,
11//! `"WORKS_AT"`, `"PURCHASED"`) that classifies the relationship. This is analogous
12//! to labels on nodes but for relationships. The type is used by the query engine
13//! to filter traversals: `MATCH (a)-[:KNOWS]->(b)` only follows edges of type `KNOWS`.
14//! Edge types are indexed in [`GraphStore`](super::store::GraphStore) via a secondary
15//! `HashMap<EdgeType, HashSet<EdgeId>>` for fast type-filtered scans.
16//!
17//! ## Multigraph semantics
18//!
19//! Graphmind supports **multigraphs**: multiple edges can exist between the same pair
20//! of nodes, even with the same type. This contrasts with a **simple graph**, which
21//! allows at most one edge between any pair. Multigraph support is essential for
22//! real-world modeling -- for instance, a person may have multiple financial
23//! transactions with the same counterparty, or multiple flights between the same
24//! airports. Each edge has a unique [`EdgeId`], so they remain
25//! individually addressable.
26//!
27//! ## MVCC and identity
28//!
29//! Like nodes, edges carry a `version: u64` for MVCC concurrency control, and
30//! `PartialEq`/`Hash` are defined on `id` alone -- two edges with the same ID
31//! are considered equal regardless of other fields.
32//!
33//! ## Requirements coverage
34//!
35//! - REQ-GRAPH-003: Edges with types
36//! - REQ-GRAPH-004: Properties on edges
37//! - REQ-GRAPH-007: Directed edges
38//! - REQ-GRAPH-008: Multiple edges between same nodes
39
40use super::property::{PropertyMap, PropertyValue};
41use super::types::{EdgeId, EdgeType, NodeId};
42use serde::{Deserialize, Serialize};
43
44/// A directed edge in the property graph
45///
46/// Edges have:
47/// - A unique ID
48/// - A source node (REQ-GRAPH-007: directed)
49/// - A target node
50/// - An edge type (relationship type)
51/// - Properties (key-value pairs)
52/// - Creation timestamp
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Edge {
55    /// Unique identifier for this edge
56    pub id: EdgeId,
57
58    /// Version for MVCC
59    pub version: u64,
60
61    /// Source node (edge goes FROM this node)
62    pub source: NodeId,
63
64    /// Target node (edge goes TO this node)
65    pub target: NodeId,
66
67    /// Type of relationship (e.g., "KNOWS", "WORKS_AT")
68    pub edge_type: EdgeType,
69
70    /// Properties associated with this edge
71    pub properties: PropertyMap,
72
73    /// Creation timestamp (Unix milliseconds)
74    pub created_at: i64,
75}
76
77impl Edge {
78    /// Create a new directed edge
79    pub fn new(id: EdgeId, source: NodeId, target: NodeId, edge_type: impl Into<EdgeType>) -> Self {
80        let now = Self::current_timestamp();
81
82        Edge {
83            id,
84            version: 1,
85            source,
86            target,
87            edge_type: edge_type.into(),
88            properties: PropertyMap::new(),
89            created_at: now,
90        }
91    }
92
93    /// Create a new edge with properties
94    pub fn new_with_properties(
95        id: EdgeId,
96        source: NodeId,
97        target: NodeId,
98        edge_type: impl Into<EdgeType>,
99        properties: PropertyMap,
100    ) -> Self {
101        let now = Self::current_timestamp();
102
103        Edge {
104            id,
105            version: 1,
106            source,
107            target,
108            edge_type: edge_type.into(),
109            properties,
110            created_at: now,
111        }
112    }
113
114    /// Set a property value
115    pub fn set_property(&mut self, key: impl Into<String>, value: impl Into<PropertyValue>) {
116        self.properties.insert(key.into(), value.into());
117    }
118
119    /// Get a property value
120    pub fn get_property(&self, key: &str) -> Option<&PropertyValue> {
121        self.properties.get(key)
122    }
123
124    /// Remove a property
125    pub fn remove_property(&mut self, key: &str) -> Option<PropertyValue> {
126        self.properties.remove(key)
127    }
128
129    /// Check if property exists
130    pub fn has_property(&self, key: &str) -> bool {
131        self.properties.contains_key(key)
132    }
133
134    /// Get number of properties
135    pub fn property_count(&self) -> usize {
136        self.properties.len()
137    }
138
139    /// Check if this edge connects two specific nodes (in either direction)
140    pub fn connects(&self, node1: NodeId, node2: NodeId) -> bool {
141        (self.source == node1 && self.target == node2)
142            || (self.source == node2 && self.target == node1)
143    }
144
145    /// Check if this edge goes FROM a specific node
146    pub fn starts_from(&self, node: NodeId) -> bool {
147        self.source == node
148    }
149
150    /// Check if this edge goes TO a specific node
151    pub fn ends_at(&self, node: NodeId) -> bool {
152        self.target == node
153    }
154
155    /// Get current timestamp
156    fn current_timestamp() -> i64 {
157        std::time::SystemTime::now()
158            .duration_since(std::time::UNIX_EPOCH)
159            .unwrap()
160            .as_millis() as i64
161    }
162}
163
164impl PartialEq for Edge {
165    fn eq(&self, other: &Self) -> bool {
166        self.id == other.id
167    }
168}
169
170impl Eq for Edge {}
171
172impl std::hash::Hash for Edge {
173    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
174        self.id.hash(state);
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_create_edge() {
184        // REQ-GRAPH-003: Edges with types
185        // REQ-GRAPH-007: Directed edges
186        let edge = Edge::new(EdgeId::new(1), NodeId::new(1), NodeId::new(2), "KNOWS");
187
188        assert_eq!(edge.id, EdgeId::new(1));
189        assert_eq!(edge.source, NodeId::new(1));
190        assert_eq!(edge.target, NodeId::new(2));
191        assert_eq!(edge.edge_type.as_str(), "KNOWS");
192    }
193
194    #[test]
195    fn test_edge_direction() {
196        // REQ-GRAPH-007: Directed edges
197        let edge = Edge::new(EdgeId::new(2), NodeId::new(10), NodeId::new(20), "FOLLOWS");
198
199        assert!(edge.starts_from(NodeId::new(10)));
200        assert!(edge.ends_at(NodeId::new(20)));
201        assert!(!edge.starts_from(NodeId::new(20)));
202        assert!(!edge.ends_at(NodeId::new(10)));
203    }
204
205    #[test]
206    fn test_edge_properties() {
207        // REQ-GRAPH-004: Properties on edges
208        let mut edge = Edge::new(EdgeId::new(3), NodeId::new(1), NodeId::new(2), "KNOWS");
209
210        // Set properties
211        edge.set_property("since", 2020i64);
212        edge.set_property("strength", 0.95);
213        edge.set_property("verified", true);
214
215        // Get properties
216        assert_eq!(edge.get_property("since").unwrap().as_integer(), Some(2020));
217        assert_eq!(
218            edge.get_property("strength").unwrap().as_float(),
219            Some(0.95)
220        );
221        assert_eq!(
222            edge.get_property("verified").unwrap().as_boolean(),
223            Some(true)
224        );
225        assert_eq!(edge.property_count(), 3);
226    }
227
228    #[test]
229    fn test_edge_with_properties() {
230        // REQ-GRAPH-005: Multiple data types in properties
231        let mut props = PropertyMap::new();
232        props.insert("weight".to_string(), 10i64.into());
233        props.insert("label".to_string(), "important".into());
234
235        let edge = Edge::new_with_properties(
236            EdgeId::new(4),
237            NodeId::new(5),
238            NodeId::new(6),
239            "RELATED_TO",
240            props,
241        );
242
243        assert_eq!(edge.property_count(), 2);
244        assert_eq!(edge.get_property("weight").unwrap().as_integer(), Some(10));
245        assert_eq!(
246            edge.get_property("label").unwrap().as_string(),
247            Some("important")
248        );
249    }
250
251    #[test]
252    fn test_multiple_edges_between_nodes() {
253        // REQ-GRAPH-008: Multiple edges between same pair of nodes
254        let node1 = NodeId::new(100);
255        let node2 = NodeId::new(200);
256
257        let edge1 = Edge::new(EdgeId::new(1), node1, node2, "KNOWS");
258        let edge2 = Edge::new(EdgeId::new(2), node1, node2, "WORKS_WITH");
259        let edge3 = Edge::new(EdgeId::new(3), node1, node2, "KNOWS");
260
261        // All three edges connect same nodes but are distinct
262        assert_ne!(edge1, edge2);
263        assert_ne!(edge1, edge3);
264        assert_ne!(edge2, edge3);
265
266        // All connect the same nodes
267        assert!(edge1.connects(node1, node2));
268        assert!(edge2.connects(node1, node2));
269        assert!(edge3.connects(node1, node2));
270
271        // Different types
272        assert_eq!(edge1.edge_type, EdgeType::new("KNOWS"));
273        assert_eq!(edge2.edge_type, EdgeType::new("WORKS_WITH"));
274    }
275
276    #[test]
277    fn test_edge_connects() {
278        let edge = Edge::new(EdgeId::new(5), NodeId::new(10), NodeId::new(20), "LINKS");
279
280        assert!(edge.connects(NodeId::new(10), NodeId::new(20)));
281        assert!(edge.connects(NodeId::new(20), NodeId::new(10))); // Order doesn't matter for connects()
282        assert!(!edge.connects(NodeId::new(10), NodeId::new(30)));
283    }
284
285    #[test]
286    fn test_remove_property() {
287        let mut edge = Edge::new(EdgeId::new(6), NodeId::new(1), NodeId::new(2), "TEST");
288
289        edge.set_property("temp", "value");
290        assert!(edge.has_property("temp"));
291
292        let removed = edge.remove_property("temp");
293        assert!(removed.is_some());
294        assert!(!edge.has_property("temp"));
295        assert_eq!(edge.property_count(), 0);
296    }
297}