linkrs_query/optimizer/heuristic/
pattern.rs1use crate::planning::plan::PlanNodeEnum;
7
8macro_rules! define_matcher {
12 ($($method:ident => $name:expr),+ $(,)?) => {
13 $(
14 pub fn $method() -> Self {
16 Self::new_with_name($name)
17 }
18 )+
19 };
20}
21
22#[derive(Debug, Clone, Default)]
27pub struct Pattern {
28 pub node: Option<MatchNode>,
30 pub dependencies: Vec<Pattern>,
32}
33
34impl Pattern {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn with_node(node: MatchNode) -> Self {
42 Self {
43 node: Some(node),
44 dependencies: Vec::new(),
45 }
46 }
47
48 pub fn new_with_name(name: &'static str) -> Self {
50 Self::with_node(MatchNode::Single(name))
51 }
52
53 pub fn multi(node_names: Vec<&'static str>) -> Self {
55 Self::with_node(MatchNode::Multi(node_names))
56 }
57
58 pub fn with_dependency(mut self, dependency: Pattern) -> Self {
60 self.dependencies.push(dependency);
61 self
62 }
63
64 pub fn with_dependency_name(mut self, name: &'static str) -> Self {
66 self.dependencies.push(Self::new_with_name(name));
67 self
68 }
69
70 pub fn add_dependency(&mut self, dependency: Pattern) {
72 self.dependencies.push(dependency);
73 }
74
75 pub fn matches(&self, plan_node: &PlanNodeEnum) -> bool {
77 if let Some(ref node) = self.node {
79 if !node.matches(plan_node.name()) {
80 return false;
81 }
82 }
83
84 if self.dependencies.is_empty() {
86 return true;
87 }
88
89 let dep_names: Vec<&str> = self
91 .dependencies
92 .iter()
93 .filter_map(|d| d.node.as_ref())
94 .filter_map(|n| n.as_single())
95 .collect();
96
97 if dep_names.is_empty() {
98 return true;
99 }
100
101 let deps = plan_node.dependencies_ref();
104 for dep_pattern in &self.dependencies {
105 let dep_matches = deps.iter().any(|input| dep_pattern.matches(input));
106
107 if !dep_matches {
108 return false;
109 }
110 }
111
112 true
113 }
114
115 define_matcher! {
118 with_project_matcher => "Project",
119 with_filter_matcher => "Filter",
120 with_scan_vertices_matcher => "ScanVertices",
121 with_get_vertices_matcher => "GetVertices",
122 with_limit_matcher => "Limit",
123 with_sort_matcher => "Sort",
124 with_aggregate_matcher => "Aggregate",
125 with_dedup_matcher => "Dedup",
126 with_get_neighbors_matcher => "GetNeighbors",
127 with_traverse_matcher => "Traverse",
128 }
129
130 pub fn with_join_matcher() -> Self {
132 Self::multi(vec![
133 "InnerJoin",
134 "LeftJoin",
135 "InnerJoin",
136 "LeftJoin",
137 "CrossJoin",
138 "FullOuterJoin",
139 ])
140 }
141}
142
143#[derive(Debug, Clone)]
147pub enum MatchNode {
148 Single(&'static str),
150 Multi(Vec<&'static str>),
152 Any,
154}
155
156impl MatchNode {
157 pub fn matches(&self, node_name: &str) -> bool {
159 match self {
160 MatchNode::Single(name) => *name == node_name,
161 MatchNode::Multi(names) => names.contains(&node_name),
162 MatchNode::Any => true,
163 }
164 }
165
166 pub fn as_single(&self) -> Option<&'static str> {
168 match self {
169 MatchNode::Single(name) => Some(name),
170 _ => None,
171 }
172 }
173
174 pub fn as_multi(&self) -> Option<&Vec<&'static str>> {
176 match self {
177 MatchNode::Multi(names) => Some(names),
178 _ => None,
179 }
180 }
181}
182
183#[derive(Debug, Clone)]
185pub enum PlanNodeMatcher {
186 MatchNode(&'static str),
188 Not(Box<PlanNodeMatcher>),
190 And(Vec<PlanNodeMatcher>),
192 Or(Vec<PlanNodeMatcher>),
194}
195
196impl PlanNodeMatcher {
197 pub fn matches(&self, plan_node: &PlanNodeEnum) -> bool {
199 match self {
200 PlanNodeMatcher::MatchNode(name) => plan_node.name() == *name,
201 PlanNodeMatcher::Not(matcher) => !matcher.matches(plan_node),
202 PlanNodeMatcher::And(matchers) => matchers.iter().all(|m| m.matches(plan_node)),
203 PlanNodeMatcher::Or(matchers) => matchers.iter().any(|m| m.matches(plan_node)),
204 }
205 }
206
207 pub fn and(self, other: PlanNodeMatcher) -> Self {
209 PlanNodeMatcher::And(vec![self, other])
210 }
211
212 pub fn or(self, other: PlanNodeMatcher) -> Self {
214 PlanNodeMatcher::Or(vec![self, other])
215 }
216}
217
218pub trait PatternBuilder {
222 fn build(&self) -> Pattern;
224}
225
226pub trait NodeVisitor {
230 fn visit(&mut self, node: &PlanNodeEnum) -> bool;
233}
234
235#[derive(Debug, Default)]
239pub struct NodeVisitorRecorder {
240 pub nodes: Vec<PlanNodeEnum>,
241}
242
243impl NodeVisitorRecorder {
244 pub fn new() -> Self {
245 Self::default()
246 }
247
248 pub fn record(&mut self, node: &PlanNodeEnum) {
249 self.nodes.push(node.clone());
250 }
251}
252
253impl NodeVisitor for NodeVisitorRecorder {
254 fn visit(&mut self, node: &PlanNodeEnum) -> bool {
255 self.record(node);
256 true
257 }
258}
259
260#[derive(Debug)]
264pub struct NodeVisitorFinder {
265 pub target_name: String,
266 pub found_node: Option<PlanNodeEnum>,
267}
268
269impl NodeVisitorFinder {
270 pub fn new(target_name: &str) -> Self {
271 Self {
272 target_name: target_name.to_string(),
273 found_node: None,
274 }
275 }
276}
277
278impl NodeVisitor for NodeVisitorFinder {
279 fn visit(&mut self, node: &PlanNodeEnum) -> bool {
280 if node.name() == self.target_name {
281 self.found_node = Some(node.clone());
282 return false; }
284 true
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::planning::plan::core::nodes::access::graph_scan_node::ScanVerticesNode;
292 use crate::planning::plan::core::nodes::operation::filter_node::FilterNode;
293 use crate::planning::plan::core::nodes::operation::project_node::ProjectNode;
294 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
295 use linkrs_core::types::ContextualExpression;
296 use linkrs_core::Expression;
297 use linkrs_core::Value;
298 use std::sync::Arc;
299
300 #[test]
301 fn test_pattern_matches() {
302 let pattern = Pattern::new_with_name("Project");
303 let input_node = PlanNodeEnum::ScanVertices(ScanVerticesNode::new(1, "default"));
304 let ctx = Arc::new(ExpressionAnalysisContext::new());
305 let proj_meta =
306 linkrs_core::types::expr::ExpressionMeta::new(Expression::Variable("a".to_string()));
307 let proj_id = ctx.register_expression(proj_meta);
308 let proj_expr = ContextualExpression::new(proj_id, ctx.clone());
309 let project_node = PlanNodeEnum::Project(
310 ProjectNode::new(
311 input_node.clone(),
312 vec![linkrs_core::YieldColumn::new(proj_expr, "a".to_string())],
313 )
314 .expect("Creating the ProjectNode should succeed"),
315 );
316 let expr_meta =
317 linkrs_core::types::expr::ExpressionMeta::new(Expression::Literal(Value::Bool(true)));
318 let id = ctx.register_expression(expr_meta);
319 let ctx_expr = ContextualExpression::new(id, ctx);
320 let filter_node = PlanNodeEnum::Filter(
321 FilterNode::new(input_node, ctx_expr).expect("Creating the FilterNode should succeed"),
322 );
323
324 assert!(pattern.matches(&project_node));
325 assert!(!pattern.matches(&filter_node));
326 }
327
328 #[test]
329 fn test_match_node_single() {
330 let matcher = MatchNode::Single("Project");
331 assert!(matcher.matches("Project"));
332 assert!(!matcher.matches("Filter"));
333 }
334
335 #[test]
336 fn test_match_node_multi() {
337 let matcher = MatchNode::Multi(vec!["Project", "Filter"]);
338 assert!(matcher.matches("Project"));
339 assert!(matcher.matches("Filter"));
340 assert!(!matcher.matches("ScanVertices"));
341 }
342
343 #[test]
344 fn test_match_node_any() {
345 let matcher = MatchNode::Any;
346 assert!(matcher.matches("Project"));
347 assert!(matcher.matches("Filter"));
348 assert!(matcher.matches("ScanVertices"));
349 }
350
351 #[test]
352 fn test_pattern_with_dependency() {
353 let pattern = Pattern::new_with_name("Filter").with_dependency_name("Project");
354
355 let scan = PlanNodeEnum::ScanVertices(ScanVerticesNode::new(1, "default"));
356 let ctx = Arc::new(ExpressionAnalysisContext::new());
357 let proj_meta =
358 linkrs_core::types::expr::ExpressionMeta::new(Expression::Variable("a".to_string()));
359 let proj_id = ctx.register_expression(proj_meta);
360 let proj_expr = ContextualExpression::new(proj_id, ctx.clone());
361 let project = PlanNodeEnum::Project(
362 ProjectNode::new(
363 scan.clone(),
364 vec![linkrs_core::YieldColumn::new(proj_expr, "a".to_string())],
365 )
366 .expect("Creating the ProjectNode should succeed"),
367 );
368 let expr_meta =
369 linkrs_core::types::expr::ExpressionMeta::new(Expression::Literal(Value::Bool(true)));
370 let id = ctx.register_expression(expr_meta);
371 let ctx_expr = ContextualExpression::new(id, ctx.clone());
372 let filter = PlanNodeEnum::Filter(
373 FilterNode::new(project.clone(), ctx_expr)
374 .expect("Creating the FilterNode should succeed"),
375 );
376
377 assert!(pattern.matches(&filter));
378
379 let expr_meta2 =
381 linkrs_core::types::expr::ExpressionMeta::new(Expression::Literal(Value::Bool(true)));
382 let id2 = ctx.register_expression(expr_meta2);
383 let ctx_expr2 = ContextualExpression::new(id2, ctx);
384 let filter2 = PlanNodeEnum::Filter(
385 FilterNode::new(scan, ctx_expr2).expect("Creating the FilterNode should succeed"),
386 );
387 assert!(!pattern.matches(&filter2));
388 }
389}