Skip to main content

linkrs_query/planning/statements/
pattern_planner.rs

1//! Pattern planner: turns AST node/edge/path patterns into sub-plans.
2//!
3//! ## Module Structure
4//!
5//! - `logical_mirror` - pure-logical mirror nodes attached to each sub-plan
6//! - `node` - node pattern planning
7//! - `edge` - edge pattern planning
8//! - `path` - path pattern planning (alternatives, optional, repeated)
9
10use std::sync::Arc;
11
12use crate::binder::validation::ValidationInfo;
13use crate::metadata::MetadataContext;
14use crate::parser::ast::pattern::{PathElement, Pattern, VariablePattern};
15use crate::parser::ast::stmt::{MatchDeleteClause, MatchStmt};
16use crate::planning::plan::core::next_node_id;
17use crate::planning::plan::core::nodes::base::plan_node_traits::PlanNode;
18use crate::planning::plan::core::nodes::data_modification::delete_nodes::{
19    PipeDeleteEdgesNode, PipeDeleteVerticesNode,
20};
21use crate::planning::plan::core::nodes::data_modification::info::{
22    EdgeDeleteInfo, VertexDeleteInfo,
23};
24use crate::planning::plan::core::nodes::ArgumentNode;
25use crate::planning::plan::SubPlan;
26use crate::planning::planner::PlannerError;
27use crate::QueryContext;
28use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
29use linkrs_core::types::expr::ContextualExpression;
30
31mod edge;
32mod logical_mirror;
33mod node;
34mod path;
35
36pub use edge::{plan_pattern_edge, plan_pattern_edge_with_input};
37pub use node::{plan_node_pattern, plan_pattern_node};
38pub use path::{
39    plan_alternative_patterns, plan_optional_element, plan_path_pattern, plan_repeated_element,
40};
41
42use logical_mirror::logical_argument;
43
44pub struct PlanningContext<'a> {
45    pub space_id: u64,
46    pub space_name: &'a str,
47    pub validation_info: &'a ValidationInfo,
48    pub qctx: &'a Arc<QueryContext>,
49    pub enable_index_optimization: bool,
50    pub metadata_context: &'a Option<MetadataContext>,
51    pub expr_context: &'a Option<Arc<ExpressionAnalysisContext>>,
52    pub where_expression: Option<&'a ContextualExpression>,
53    pub optional: bool,
54}
55
56pub fn plan_match_delete(
57    input_plan: SubPlan,
58    delete_clause: &MatchDeleteClause,
59    space_name: &str,
60    match_stmt: &MatchStmt,
61) -> Result<SubPlan, PlannerError> {
62    let input_node = input_plan.root().as_ref().ok_or_else(|| {
63        PlannerError::PlanGenerationFailed("The input plan has no root node".to_string())
64    })?;
65
66    let delete_node = match &delete_clause.target {
67        crate::parser::ast::stmt::MatchDeleteTarget::Vertices(vertex_exprs) => {
68            let info = VertexDeleteInfo {
69                space_name: space_name.to_string(),
70                tag: None,
71                vertex_ids: vertex_exprs.clone(),
72                with_edge: delete_clause.with_edge,
73                cascade: delete_clause.with_edge,
74            };
75            PipeDeleteVerticesNode::new(next_node_id(), info, input_node.clone()).into_enum()
76        }
77        crate::parser::ast::stmt::MatchDeleteTarget::Edges(edge_exprs) => {
78            let edges: Vec<_> = edge_exprs
79                .iter()
80                .map(|e| (e.clone(), e.clone(), None))
81                .collect();
82            let edge_type = extract_edge_type_from_patterns(&match_stmt.patterns);
83
84            let info = EdgeDeleteInfo {
85                space_name: space_name.to_string(),
86                edges,
87                edge_type,
88            };
89            PipeDeleteEdgesNode::new(next_node_id(), info, input_node.clone()).into_enum()
90        }
91        crate::parser::ast::stmt::MatchDeleteTarget::EdgeRefs(edge_refs) => {
92            let edges = edge_refs.clone();
93            let edge_type = extract_edge_type_from_patterns(&match_stmt.patterns);
94
95            let info = EdgeDeleteInfo {
96                space_name: space_name.to_string(),
97                edges,
98                edge_type,
99            };
100            PipeDeleteEdgesNode::new(next_node_id(), info, input_node.clone()).into_enum()
101        }
102    };
103
104    Ok(SubPlan::new(Some(delete_node), input_plan.tail))
105}
106
107/// Bound-statement counterpart of [`plan_match_delete`].
108///
109/// Plans the MATCH DELETE clause from the binder output instead of the AST,
110/// so the bound validation and the executed plan cannot drift apart.
111pub fn plan_match_delete_bound(
112    input_plan: SubPlan,
113    delete_clause: &crate::binder::bound::BoundMatchDeleteClause,
114    space_name: &str,
115    edge_type: Option<String>,
116) -> Result<SubPlan, PlannerError> {
117    let input_node = input_plan.root().as_ref().ok_or_else(|| {
118        PlannerError::PlanGenerationFailed("The input plan has no root node".to_string())
119    })?;
120
121    let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
122    let convert = |bound_expr: &crate::binder::bound::BoundExpression| {
123        crate::binder::expr_converter::bound_expr_to_contextual(bound_expr, &expr_ctx)
124            .map_err(PlannerError::PlanGenerationFailed)
125    };
126
127    let delete_node = match &delete_clause.target {
128        crate::binder::bound::BoundMatchDeleteTarget::Vertices(exprs) => {
129            let vertex_ids = exprs
130                .iter()
131                .map(convert)
132                .collect::<Result<Vec<ContextualExpression>, _>>()?;
133            let info = VertexDeleteInfo {
134                space_name: space_name.to_string(),
135                tag: None,
136                vertex_ids,
137                with_edge: delete_clause.with_edge,
138                cascade: delete_clause.with_edge,
139            };
140            PipeDeleteVerticesNode::new(next_node_id(), info, input_node.clone()).into_enum()
141        }
142        crate::binder::bound::BoundMatchDeleteTarget::Edges(exprs) => {
143            let edges: Vec<ContextualExpression> =
144                exprs.iter().map(convert).collect::<Result<Vec<_>, _>>()?;
145            let edges = edges.iter().map(|e| (e.clone(), e.clone(), None)).collect();
146            let info = EdgeDeleteInfo {
147                space_name: space_name.to_string(),
148                edges,
149                edge_type,
150            };
151            PipeDeleteEdgesNode::new(next_node_id(), info, input_node.clone()).into_enum()
152        }
153        crate::binder::bound::BoundMatchDeleteTarget::EdgeRefs(refs) => {
154            let mut edges = Vec::with_capacity(refs.len());
155            for (src, dst, rank) in refs {
156                edges.push((
157                    convert(src)?,
158                    convert(dst)?,
159                    rank.as_ref().map(convert).transpose()?,
160                ));
161            }
162            let info = EdgeDeleteInfo {
163                space_name: space_name.to_string(),
164                edges,
165                edge_type,
166            };
167            PipeDeleteEdgesNode::new(next_node_id(), info, input_node.clone()).into_enum()
168        }
169    };
170
171    Ok(SubPlan::new(Some(delete_node), input_plan.tail))
172}
173
174pub fn plan_pattern(pattern: &Pattern, ctx: &PlanningContext) -> Result<SubPlan, PlannerError> {
175    match pattern {
176        Pattern::Node(node) => plan_pattern_node(
177            node,
178            ctx.space_id,
179            ctx.space_name,
180            ctx.enable_index_optimization,
181            ctx.metadata_context,
182            ctx.expr_context,
183            ctx.where_expression,
184        ),
185        Pattern::Edge(edge) => plan_pattern_edge(
186            edge,
187            ctx.space_id,
188            ctx.space_name,
189            ctx.expr_context,
190            ctx.optional,
191        ),
192        Pattern::Path(_) => plan_path_pattern(pattern, ctx),
193        Pattern::Variable(var) => plan_variable_pattern(var, ctx.space_id, ctx.validation_info),
194    }
195}
196
197pub fn plan_variable_pattern(
198    var: &VariablePattern,
199    _space_id: u64,
200    validation_info: &ValidationInfo,
201) -> Result<SubPlan, PlannerError> {
202    if !validation_info.alias_map.contains_key(&var.name) {
203        return Err(PlannerError::PlanGenerationFailed(format!(
204            "Variable '{}' undefined",
205            var.name
206        )));
207    }
208
209    let argument_node = ArgumentNode::new(0, &var.name);
210    let arg_root = argument_node.into_enum();
211    let logical_root = logical_argument(&var.name);
212    let sub_plan = SubPlan {
213        root: Some(arg_root.clone()),
214        tail: Some(arg_root),
215        logical_root: Some(logical_root),
216    };
217    Ok(sub_plan)
218}
219
220pub(crate) fn extract_edge_type_from_patterns(patterns: &[Pattern]) -> Option<String> {
221    for pattern in patterns {
222        if let Pattern::Path(path_pattern) = pattern {
223            for element in &path_pattern.elements {
224                if let PathElement::Edge(edge_pattern) = element {
225                    if let Some(edge_type) = edge_pattern.edge_types.first() {
226                        return Some(edge_type.clone());
227                    }
228                }
229            }
230        }
231    }
232    None
233}
234
235#[cfg(test)]
236mod tests;