Skip to main content

linkrs_query/optimizer/heuristic/
pattern.rs

1//! Pattern matching definition
2//!
3//! Provide a pattern matching function for the planning nodes, which is used to rewrite rules in order to identify specific planning structures.
4//! This is a simplified version that has been separated from the optimizer layer, focusing on the requirements for heuristic rewriting rules.
5
6use crate::planning::plan::PlanNodeEnum;
7
8/// Macro for generating node matching methods
9///
10/// Generate a convenient node matching constructor for the Pattern structure.
11macro_rules! define_matcher {
12    ($($method:ident => $name:expr),+ $(,)?) => {
13        $(
14            /// Create a pattern for matching nodes.
15            pub fn $method() -> Self {
16                Self::new_with_name($name)
17            }
18        )+
19    };
20}
21
22/// Pattern Structure
23///
24/// Used to match the specific structure of the planning tree.
25/// The matching criteria for the current node, as well as the patterns for the child nodes.
26#[derive(Debug, Clone, Default)]
27pub struct Pattern {
28    /// The matching criteria for the current node
29    pub node: Option<MatchNode>,
30    /// List of child node patterns
31    pub dependencies: Vec<Pattern>,
32}
33
34impl Pattern {
35    /// Create an empty pattern (that matches any node).
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Create a pattern using the specified nodes.
41    pub fn with_node(node: MatchNode) -> Self {
42        Self {
43            node: Some(node),
44            dependencies: Vec::new(),
45        }
46    }
47
48    /// Create a pattern using the node names.
49    pub fn new_with_name(name: &'static str) -> Self {
50        Self::with_node(MatchNode::Single(name))
51    }
52
53    /// Create a pattern using multiple possible node names.
54    pub fn multi(node_names: Vec<&'static str>) -> Self {
55        Self::with_node(MatchNode::Multi(node_names))
56    }
57
58    /// Add child node mode
59    pub fn with_dependency(mut self, dependency: Pattern) -> Self {
60        self.dependencies.push(dependency);
61        self
62    }
63
64    /// Use the node name to add a child node pattern.
65    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    /// Add dependency pattern (variable reference version)
71    pub fn add_dependency(&mut self, dependency: Pattern) {
72        self.dependencies.push(dependency);
73    }
74
75    /// Check whether the mode matches the given scheduled node.
76    pub fn matches(&self, plan_node: &PlanNodeEnum) -> bool {
77        // Check the current node.
78        if let Some(ref node) = self.node {
79            if !node.matches(plan_node.name()) {
80                return false;
81            }
82        }
83
84        // If there is no dependency pattern, a direct match will be successful.
85        if self.dependencies.is_empty() {
86            return true;
87        }
88
89        // Obtain the names of all dependent nodes.
90        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        // Check whether each dependency pattern matches.
102        // Use dependencies_ref() to avoid cloning nodes
103        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    // ==================== Convenient Construction Methods ====================
116
117    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    /// Create a pattern that matches the Join nodes (matching any type of connection).
131    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/// Node matching enumeration
144///
145/// Define how to match a single plan node.
146#[derive(Debug, Clone)]
147pub enum MatchNode {
148    /// Match a node with a specific single name.
149    Single(&'static str),
150    /// Match any one of the multiple possible names.
151    Multi(Vec<&'static str>),
152    /// Match any node
153    Any,
154}
155
156impl MatchNode {
157    /// Check whether the node names match.
158    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    /// Retrieve a single name (in the case of the Single variant).
167    pub fn as_single(&self) -> Option<&'static str> {
168        match self {
169            MatchNode::Single(name) => Some(name),
170            _ => None,
171        }
172    }
173
174    /// Obtain multiple names (in the case of a Multi variant).
175    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/// Plan Node Matcher (Complex Conditions)
184#[derive(Debug, Clone)]
185pub enum PlanNodeMatcher {
186    /// Match a specific name
187    MatchNode(&'static str),
188    /// Does not match.
189    Not(Box<PlanNodeMatcher>),
190    /// All conditions are met.
191    And(Vec<PlanNodeMatcher>),
192    /// Any condition matching
193    Or(Vec<PlanNodeMatcher>),
194}
195
196impl PlanNodeMatcher {
197    /// Check whether it matches the planned nodes.
198    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    /// Combine with another matcher using the AND operator
208    pub fn and(self, other: PlanNodeMatcher) -> Self {
209        PlanNodeMatcher::And(vec![self, other])
210    }
211
212    /// Combine with another matcher using the OR operator.
213    pub fn or(self, other: PlanNodeMatcher) -> Self {
214        PlanNodeMatcher::Or(vec![self, other])
215    }
216}
217
218/// Pattern Builder trait
219///
220/// Allowing the customization of type implementation patterns for construction
221pub trait PatternBuilder {
222    /// Constructing patterns
223    fn build(&self) -> Pattern;
224}
225
226/// `NodeVisitor` trait
227///
228/// Used for traversing the planning tree
229pub trait NodeVisitor {
230    /// Access node
231    /// Return `true` to continue the iteration; return `false` to stop the iteration.
232    fn visit(&mut self, node: &PlanNodeEnum) -> bool;
233}
234
235/// The node records the visitors.
236///
237/// Record all visited nodes.
238#[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/// Node that searches for visitors
261///
262/// Find a node with a specific name.
263#[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; // Stop the iteration once the item is found.
283        }
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        // “The ‘Filter’ -> ‘Scan’ functions should not match each other.”
380        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}