Skip to main content

linkrs_query/planning/statements/seeks/
edge_seek.rs

1//! Edge search strategy
2//!
3//! A search strategy that starts from the edge mode, used for MATCH operations where the search begins from an edge.
4//!
5//! Applicable scenarios:
6//! - MATCH ()-[e:KNOWS]->() WHERE e.since > 2020
7//! - MATCH (a)-[e]->(b) WHERE e.weight > 5
8//! - Start the search from the edge index.
9
10use super::seek_strategy::SeekStrategy;
11use super::seek_strategy_base::{SeekResult, SeekStrategyContext, SeekStrategyType};
12use crate::storage::StorageReader;
13use linkrs_core::{StorageError, Value};
14use std::sync::Arc;
15
16/// Edge pattern information
17#[derive(Debug, Clone, PartialEq)]
18pub struct EdgePattern {
19    pub edge_types: Vec<String>,
20    pub direction: EdgeDirection,
21    pub src_vid: Option<Value>,
22    pub dst_vid: Option<Value>,
23    pub properties: Vec<(Arc<str>, Value)>,
24}
25
26/// edgewise
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum EdgeDirection {
29    Outgoing, // ->
30    Incoming, // <-
31    Both,     // -
32}
33
34impl EdgeDirection {
35    pub fn as_str(&self) -> &'static str {
36        match self {
37            EdgeDirection::Outgoing => "OUT",
38            EdgeDirection::Incoming => "IN",
39            EdgeDirection::Both => "BOTH",
40        }
41    }
42}
43
44impl std::str::FromStr for EdgeDirection {
45    type Err = String;
46
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        match s.to_uppercase().as_str() {
49            "OUT" | "OUTGOING" | "->" => Ok(EdgeDirection::Outgoing),
50            "IN" | "INCOMING" | "<-" => Ok(EdgeDirection::Incoming),
51            "BOTH" | "-" => Ok(EdgeDirection::Both),
52            _ => Err(format!("Invalid edge direction: {}", s)),
53        }
54    }
55}
56
57/// Edge search strategy
58#[derive(Debug, Clone)]
59pub struct EdgeSeek {
60    pub edge_pattern: EdgePattern,
61}
62
63impl EdgeSeek {
64    pub fn new(edge_pattern: EdgePattern) -> Self {
65        Self { edge_pattern }
66    }
67
68    /// Evaluating whether a border matches a pattern
69    fn edge_matches_pattern(&self, edge: &linkrs_core::Edge) -> bool {
70        // Check edge type
71        if !self.edge_pattern.edge_types.is_empty()
72            && !self.edge_pattern.edge_types.contains(&edge.edge_type)
73        {
74            return false;
75        }
76
77        if let Some(ref src_vid) = self.edge_pattern.src_vid {
78            if Value::from(edge.src) != *src_vid {
79                return false;
80            }
81        }
82
83        if let Some(ref dst_vid) = self.edge_pattern.dst_vid {
84            if Value::from(edge.dst) != *dst_vid {
85                return false;
86            }
87        }
88
89        // Check properties
90        for (prop_name, prop_value) in &self.edge_pattern.properties {
91            let found = edge
92                .get_property(prop_name)
93                .map(|v| v == prop_value)
94                .unwrap_or(false);
95            if !found {
96                return false;
97            }
98        }
99
100        true
101    }
102}
103
104impl SeekStrategy for EdgeSeek {
105    fn execute<S: StorageReader>(
106        &self,
107        storage: &S,
108        _context: &SeekStrategyContext,
109    ) -> Result<SeekResult, StorageError> {
110        let mut edge_ids = Vec::new();
111        let mut rows_scanned = 0;
112
113        let space_name = "default";
114
115        // Strategy 1: Use edge type scan if edge types are specified
116        if !self.edge_pattern.edge_types.is_empty() {
117            for edge_type in &self.edge_pattern.edge_types {
118                let edges = storage.scan_edges_by_type(space_name, edge_type)?;
119                rows_scanned += edges.len();
120
121                for edge in edges {
122                    let edge_id = format!("{}->{}@{}", edge.src, edge.dst, edge.edge_type);
123                    edge_ids.push(Value::string(edge_id));
124                }
125            }
126        } else {
127            // Strategy 2: Scan all edges in the table
128            let edges = storage.scan_all_edges(space_name)?;
129            rows_scanned = edges.len();
130
131            for edge in edges {
132                if self.edge_matches_pattern(&edge) {
133                    let edge_id = format!("{}->{}@{}", edge.src, edge.dst, edge.edge_type);
134                    edge_ids.push(Value::string(edge_id));
135                }
136            }
137        }
138
139        // Remove duplicates
140        edge_ids.sort();
141        edge_ids.dedup();
142
143        Ok(SeekResult {
144            vertex_ids: edge_ids,
145            strategy_used: SeekStrategyType::EdgeSeek,
146            rows_scanned,
147        })
148    }
149
150    fn supports(&self, _context: &SeekStrategyContext) -> bool {
151        // Supported as long as there is a side mode
152        true
153    }
154}
155
156/// Expand the search results as needed.
157#[derive(Debug)]
158pub struct EdgeSeekResult {
159    pub base: SeekResult,
160    pub start_vids: Vec<Value>,
161    pub end_vids: Vec<Value>,
162}
163
164impl EdgeSeekResult {
165    pub fn from_seek_result(
166        result: SeekResult,
167        start_vids: Vec<Value>,
168        end_vids: Vec<Value>,
169    ) -> Self {
170        Self {
171            base: result,
172            start_vids,
173            end_vids,
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use linkrs_core::types::VertexId;
182
183    #[test]
184    fn test_edge_direction_from_str() {
185        assert_eq!(
186            "OUT".parse::<EdgeDirection>().ok(),
187            Some(EdgeDirection::Outgoing)
188        );
189        assert_eq!(
190            "->".parse::<EdgeDirection>().ok(),
191            Some(EdgeDirection::Outgoing)
192        );
193        assert_eq!(
194            "IN".parse::<EdgeDirection>().ok(),
195            Some(EdgeDirection::Incoming)
196        );
197        assert_eq!(
198            "<-".parse::<EdgeDirection>().ok(),
199            Some(EdgeDirection::Incoming)
200        );
201        assert_eq!(
202            "BOTH".parse::<EdgeDirection>().ok(),
203            Some(EdgeDirection::Both)
204        );
205        assert_eq!("-".parse::<EdgeDirection>().ok(), Some(EdgeDirection::Both));
206        assert!("unknown".parse::<EdgeDirection>().is_err());
207    }
208
209    #[test]
210    fn test_edge_pattern_matching() {
211        let seek = EdgeSeek::new(EdgePattern {
212            edge_types: vec!["KNOWS".to_string()],
213            direction: EdgeDirection::Outgoing,
214            src_vid: None,
215            dst_vid: None,
216            properties: vec![],
217        });
218
219        // Test edge type matching
220        let edge = linkrs_core::Edge::new_empty(
221            VertexId::try_from_int64(1).expect("valid vertex id"),
222            VertexId::try_from_int64(2).expect("valid vertex id"),
223            "KNOWS".to_string(),
224            0,
225        );
226        assert!(seek.edge_matches_pattern(&edge));
227
228        // Test edge type mismatch
229        let edge2 = linkrs_core::Edge::new_empty(
230            VertexId::try_from_int64(1).expect("valid vertex id"),
231            VertexId::try_from_int64(2).expect("valid vertex id"),
232            "FOLLOWS".to_string(),
233            0,
234        );
235        assert!(!seek.edge_matches_pattern(&edge2));
236    }
237
238    #[test]
239    fn test_edge_pattern_with_src_vid() {
240        let seek = EdgeSeek::new(EdgePattern {
241            edge_types: vec![],
242            direction: EdgeDirection::Outgoing,
243            src_vid: Some(Value::Int(1)),
244            dst_vid: None,
245            properties: vec![],
246        });
247
248        let edge = linkrs_core::Edge::new_empty(
249            VertexId::try_from_int64(1).expect("valid vertex id"),
250            VertexId::try_from_int64(2).expect("valid vertex id"),
251            "KNOWS".to_string(),
252            0,
253        );
254        assert!(seek.edge_matches_pattern(&edge));
255
256        let edge2 = linkrs_core::Edge::new_empty(
257            VertexId::try_from_int64(3).expect("valid vertex id"),
258            VertexId::try_from_int64(2).expect("valid vertex id"),
259            "KNOWS".to_string(),
260            0,
261        );
262        assert!(!seek.edge_matches_pattern(&edge2));
263    }
264}