Skip to main content

linkrs_query/optimizer/analysis/required_properties/
analyzer.rs

1//! Typed required-property demand analyzer.
2
3use crate::planning::plan::core::nodes::base::plan_node_traits::{
4    MultipleInputNode, SingleInputNode,
5};
6use crate::planning::plan::PlanNodeEnum;
7use linkrs_core::types::ContextualExpression;
8use linkrs_core::Expression;
9
10use super::map::RequiredPropertiesMap;
11use super::ref_collector::CollectedRefs;
12use super::requirement::PropertyRequirement;
13
14/// Typed required-property demand analyzer.
15#[derive(Debug, Clone, Default)]
16pub struct RequiredPropertyAnalyzer;
17
18impl RequiredPropertyAnalyzer {
19    pub fn new() -> Self {
20        Self
21    }
22
23    /// Analyze a plan tree and return the per-node requirement map.
24    ///
25    /// The traversal is top-down: requirements accumulated from consumers
26    /// are merged with each node's own expression references and passed to
27    /// the node's children. Graph sources record the requirement for their
28    /// binding variable, tagged with the resolved tag / edge type when the
29    /// binding is single-typed.
30    pub fn analyze(&self, root: &PlanNodeEnum) -> RequiredPropertiesMap {
31        let mut map = RequiredPropertiesMap::default();
32        self.propagate(root, &[], &mut map);
33        map
34    }
35
36    /// Propagate requirements down a plan subtree.
37    fn propagate(
38        &self,
39        node: &PlanNodeEnum,
40        incoming: &[PropertyRequirement],
41        map: &mut RequiredPropertiesMap,
42    ) {
43        use PlanNodeEnum::*;
44
45        // Leaf graph sources: resolve the binding variable, tag the
46        // requirements that match it, and stop.
47        match node {
48            GetVertices(n) => {
49                self.record_leaf(
50                    map,
51                    node.id(),
52                    incoming,
53                    binding_var(n.output_var(), n.src_vids()),
54                    single_tag(n.tag_props()),
55                );
56                return;
57            }
58            GetNeighbors(n) => {
59                self.record_leaf(
60                    map,
61                    node.id(),
62                    incoming,
63                    binding_var(n.output_var(), n.src_vids()),
64                    single_tag(n.tag_props()),
65                );
66                return;
67            }
68            ScanVertices(n) => {
69                self.record_leaf(
70                    map,
71                    node.id(),
72                    incoming,
73                    n.col_names().first().map(String::as_str),
74                    n.tag().map(|t| t.to_string()),
75                );
76                return;
77            }
78            ScanEdges(n) => {
79                self.record_leaf(
80                    map,
81                    node.id(),
82                    incoming,
83                    n.col_names().first().map(String::as_str),
84                    n.edge_type().map(|e| e.to_string()),
85                );
86                return;
87            }
88            GetEdges(n) => {
89                self.record_leaf(
90                    map,
91                    node.id(),
92                    incoming,
93                    Some(n.src()),
94                    Some(n.edge_type().to_string()),
95                );
96                return;
97            }
98            AppendVertices(n) => {
99                self.record_leaf(
100                    map,
101                    node.id(),
102                    incoming,
103                    n.input_var(),
104                    Some(n.vertex_tag().to_string()),
105                );
106                return;
107            }
108            IndexScan(_) | Start(_) | Argument(_) => {
109                map.record(node.id(), incoming);
110                return;
111            }
112            _ => {}
113        }
114
115        // Intermediate nodes: merge the node's own expression references
116        // into the requirements passed down. The node's incoming
117        // requirements are recorded as-is (what its consumers demand).
118        map.record(node.id(), incoming);
119        let mut passed: Vec<PropertyRequirement> = Vec::new();
120        for req in incoming {
121            passed.push(req.clone());
122        }
123
124        match node {
125            Project(n) => {
126                for col in n.columns() {
127                    if let Some(meta) = col.expression.expression() {
128                        merge_collected(meta.inner(), &mut passed);
129                    }
130                }
131                // A bare-variable column passes the incoming requirements
132                // for that variable through to the input unchanged.
133                for col in n.columns() {
134                    if let Some(meta) = col.expression.expression() {
135                        if let Expression::Variable(var) = meta.inner() {
136                            if let Some(req) = incoming.iter().find(|r| r.alias == *var) {
137                                merge_requirement(&mut passed, req);
138                            }
139                        }
140                    }
141                }
142                self.propagate(n.input(), &passed, map);
143            }
144            Filter(n) => {
145                merge_expr(n.condition(), &mut passed);
146                self.propagate(n.input(), &passed, map);
147            }
148            Sort(n) => self.propagate_single(n.input(), &passed, map),
149            Limit(n) => self.propagate_single(n.input(), &passed, map),
150            TopN(n) => self.propagate_single(n.input(), &passed, map),
151            Sample(n) => self.propagate_single(n.input(), &passed, map),
152            Dedup(n) => self.propagate_single(n.input(), &passed, map),
153            Aggregate(n) => self.propagate_single(n.input(), &passed, map),
154            Window(n) => self.propagate_single(n.input(), &passed, map),
155            Unwind(n) => self.propagate_single(n.input(), &passed, map),
156            Materialize(n) => self.propagate_single(n.input(), &passed, map),
157            DataCollect(n) => self.propagate_single(n.input(), &passed, map),
158
159            // Binary join nodes: the join key expressions may reference
160            // properties; both inputs receive the full requirement set and
161            // each leaf keeps only the entries matching its binding.
162            InnerJoin(n) => {
163                Self::merge_join_keys(n.hash_keys(), n.probe_keys(), &mut passed);
164                self.propagate(n.left_input(), &passed, map);
165                self.propagate(n.right_input(), &passed, map);
166            }
167            LeftJoin(n) => {
168                Self::merge_join_keys(n.hash_keys(), n.probe_keys(), &mut passed);
169                self.propagate(n.left_input(), &passed, map);
170                self.propagate(n.right_input(), &passed, map);
171            }
172            FullOuterJoin(n) => {
173                Self::merge_join_keys(n.hash_keys(), n.probe_keys(), &mut passed);
174                self.propagate(n.left_input(), &passed, map);
175                self.propagate(n.right_input(), &passed, map);
176            }
177            RightJoin(n) => {
178                Self::merge_join_keys(n.hash_keys(), n.probe_keys(), &mut passed);
179                self.propagate(n.left_input(), &passed, map);
180                self.propagate(n.right_input(), &passed, map);
181            }
182            SemiJoin(n) => {
183                Self::merge_join_keys(n.hash_keys(), n.probe_keys(), &mut passed);
184                self.propagate(n.left_input(), &passed, map);
185                self.propagate(n.right_input(), &passed, map);
186            }
187            CrossJoin(n) => {
188                self.propagate(n.left_input(), &passed, map);
189                self.propagate(n.right_input(), &passed, map);
190            }
191
192            // Set operations: every input provides the same columns.
193            Union(n) => {
194                for dep in n.dependencies() {
195                    self.propagate(dep, &passed, map);
196                }
197            }
198            Minus(n) => {
199                self.propagate(n.input(), &passed, map);
200                self.propagate(n.minus_input(), &passed, map);
201            }
202            Intersect(n) => {
203                self.propagate(n.input(), &passed, map);
204                self.propagate(n.intersect_input(), &passed, map);
205            }
206
207            // Traversal nodes pass requirements through to their inputs.
208            Expand(n) => {
209                for dep in n.inputs() {
210                    self.propagate(dep, &passed, map);
211                }
212            }
213            ExpandAll(n) => {
214                for dep in n.inputs() {
215                    self.propagate(dep, &passed, map);
216                }
217            }
218            Traverse(n) => {
219                for dep in n.dependencies() {
220                    self.propagate(dep, &passed, map);
221                }
222            }
223            BiExpand(n) => {
224                self.propagate(n.left_input(), &passed, map);
225                self.propagate(n.right_input(), &passed, map);
226            }
227            BiTraverse(n) => {
228                self.propagate(n.left_input(), &passed, map);
229                self.propagate(n.right_input(), &passed, map);
230            }
231
232            // Pattern apply: the left input is the main pipeline; the right
233            // input is the subquery pattern (correlated columns are part of
234            // the subquery's own expressions).
235            PatternApply(n) => {
236                self.propagate(n.left_input(), &passed, map);
237                self.propagate(n.right_input(), &[], map);
238            }
239            CorrelatedApply(n) => {
240                self.propagate(n.left_input(), &passed, map);
241                self.propagate(n.right_input(), &[], map);
242            }
243            RollUpApply(n) => {
244                self.propagate(n.left_input(), &passed, map);
245                self.propagate(n.right_input(), &[], map);
246            }
247
248            // Control flow nodes: propagate into every branch.
249            Loop(n) => {
250                if let Some(body) = n.body() {
251                    self.propagate(body, &passed, map);
252                }
253            }
254            Select(n) => {
255                if let Some(branch) = n.if_branch() {
256                    self.propagate(branch, &passed, map);
257                }
258                if let Some(branch) = n.else_branch() {
259                    self.propagate(branch, &passed, map);
260                }
261            }
262
263            // Algorithm nodes: pass requirements into their inputs.
264            ShortestPath(n) => {
265                self.propagate(n.left_input(), &passed, map);
266                self.propagate(n.right_input(), &passed, map);
267            }
268            MultiShortestPath(n) => {
269                self.propagate(n.left_input(), &passed, map);
270                self.propagate(n.right_input(), &passed, map);
271            }
272            BFSShortest(n) => {
273                self.propagate(n.left_input(), &passed, map);
274                self.propagate(n.right_input(), &passed, map);
275            }
276            AllPaths(n) => {
277                self.propagate(n.left_input(), &passed, map);
278                self.propagate(n.right_input(), &passed, map);
279            }
280
281            // Data processing nodes.
282            Assign(n) => {
283                for dep in n.dependencies() {
284                    self.propagate(dep, &passed, map);
285                }
286            }
287
288            // Remaining nodes have no property-consuming expressions and no
289            // graph source children; requirements stop here.
290            _ => {}
291        }
292    }
293
294    fn propagate_single(
295        &self,
296        input: &PlanNodeEnum,
297        passed: &[PropertyRequirement],
298        map: &mut RequiredPropertiesMap,
299    ) {
300        self.propagate(input, passed, map);
301    }
302
303    /// Merge the property references of join hash/probe keys into the
304    /// requirement list so keyed columns are never pruned away.
305    fn merge_join_keys(
306        hash_keys: &[ContextualExpression],
307        probe_keys: &[ContextualExpression],
308        passed: &mut Vec<PropertyRequirement>,
309    ) {
310        for key in hash_keys.iter().chain(probe_keys) {
311            merge_expr(key, passed);
312        }
313    }
314
315    /// Record the requirements demanded of a leaf graph source, tagged with
316    /// the binding's resolved tag / edge type.
317    fn record_leaf(
318        &self,
319        map: &mut RequiredPropertiesMap,
320        node_id: i64,
321        incoming: &[PropertyRequirement],
322        binding_var: Option<&str>,
323        tag_name: Option<String>,
324    ) {
325        let Some(var) = binding_var else {
326            return;
327        };
328        let mut resolved: Vec<PropertyRequirement> = Vec::new();
329        for req in incoming {
330            if req.alias == var {
331                let mut tagged = req.clone();
332                tagged.tag_name = tag_name.clone();
333                resolved.push(tagged);
334            } else if let Some(tag) = tag_name.as_deref() {
335                // `TagProperty{tag, prop}` / `EdgeProperty{edge, prop}` references
336                // are collected under the tag/edge name; remap them onto the
337                // binding variable when the names match so scans can narrow.
338                // `full_value` stays sticky: an opaque use still blocks pruning.
339                if req.alias == tag {
340                    let mut remapped = req.clone();
341                    remapped.alias = var.to_string();
342                    remapped.tag_name = tag_name.clone();
343                    resolved.push(remapped);
344                }
345            }
346        }
347        if !resolved.is_empty() {
348            map.record(node_id, &resolved);
349        }
350    }
351}
352
353/// The binding variable of a graph source, preferring the planner-assigned
354/// output variable and falling back to a single non-constant `src_vids`.
355///
356/// Shared with the projection pushdown rules that narrow graph operator
357/// `projected_properties`.
358pub fn binding_var<'a>(output_var: Option<&'a str>, src_vids: &'a str) -> Option<&'a str> {
359    if let Some(var) = output_var {
360        if !var.is_empty() {
361            return Some(var);
362        }
363    }
364    let trimmed = src_vids.trim();
365    if trimmed.is_empty() || trimmed.split(',').count() != 1 {
366        return None;
367    }
368    // Constant vertex ids are numeric literals, not binding variables.
369    if trimmed.parse::<i64>().is_ok() {
370        return None;
371    }
372    Some(trimmed)
373}
374
375/// The single tag of a `TagProp` list, or `None` for multi-tag bindings.
376fn single_tag(tag_props: &[crate::planning::plan::core::common::TagProp]) -> Option<String> {
377    let mut tags = tag_props
378        .iter()
379        .map(|tp| tp.tag.clone())
380        .collect::<Vec<_>>();
381    tags.sort();
382    tags.dedup();
383    if tags.len() == 1 {
384        tags.pop()
385    } else {
386        None
387    }
388}
389
390/// Merge the collected references of `expr` into the requirement list,
391/// tracking which variables need their full value.
392fn merge_collected(expr: &Expression, passed: &mut Vec<PropertyRequirement>) {
393    let collected = CollectedRefs::visit(expr);
394    for (var, prop) in collected.props {
395        merge_property(passed, &var, &prop);
396    }
397    for var in collected.full_vars {
398        // A full-value variable suppresses pruning: the requirement flag is
399        // sticky, so later property references cannot re-open the binding.
400        merge_property(passed, &var, "");
401    }
402}
403
404/// Merge the property references of a context expression into the
405/// requirement list.
406fn merge_expr(expr: &ContextualExpression, passed: &mut Vec<PropertyRequirement>) {
407    let Some(meta) = expr.expression() else {
408        return;
409    };
410    merge_collected(meta.inner(), passed);
411}
412
413/// Add (or merge into) the requirement for `var` and property `prop`.
414///
415/// An empty `prop` marks the variable as full-value (non-narrowable).
416fn merge_property(passed: &mut Vec<PropertyRequirement>, var: &str, prop: &str) {
417    if let Some(req) = passed.iter_mut().find(|r| r.alias == var) {
418        if prop.is_empty() {
419            req.full_value = true;
420        } else {
421            req.prop_names.insert(prop.to_string());
422        }
423    } else {
424        let mut req = PropertyRequirement::new(var);
425        if prop.is_empty() {
426            req.full_value = true;
427        } else {
428            req.prop_names.insert(prop.to_string());
429        }
430        passed.push(req);
431    }
432}
433
434/// Merge a whole requirement into the list.
435fn merge_requirement(passed: &mut Vec<PropertyRequirement>, req: &PropertyRequirement) {
436    match passed.iter_mut().find(|r| r.alias == req.alias) {
437        Some(existing) => existing.merge(req),
438        None => passed.push(req.clone()),
439    }
440}