Skip to main content

linkrs_query/optimizer/analysis/
batch.rs

1//! Batch Plan Analyzer Module
2//!
3//! Performs all plan analysis in a single traversal, collecting:
4//! - Reference counts (for materialization decisions)
5//! - Structural fingerprints (for subplan identification)
6//! - Expression summaries (aggregated from all nodes)
7//!
8//! This avoids multiple traversals of the same plan tree.
9
10use std::collections::HashMap;
11
12use crate::optimizer::analysis::{
13    ExpressionAnalysis, ExpressionAnalyzer, FingerprintCalculator, PlanFingerprint,
14    ReferenceCountAnalysis, SubplanId, SubplanReferenceInfo,
15};
16use crate::planning::plan::core::nodes::PlanNodeEnum;
17use linkrs_core::types::ContextualExpression;
18
19/// Combined plan analysis result from a single traversal
20#[derive(Debug, Clone)]
21pub struct BatchPlanAnalysis {
22    /// Reference count analysis (for detecting repeated subplans)
23    pub reference_count: ReferenceCountAnalysis,
24    /// Aggregated expression analysis from all nodes
25    pub expression_summary: AggregatedExpressionAnalysis,
26    /// Node fingerprint mapping
27    pub fingerprints: HashMap<i64, PlanFingerprint>,
28}
29
30/// Aggregated expression analysis across all nodes in the plan
31#[derive(Debug, Clone, Default)]
32pub struct AggregatedExpressionAnalysis {
33    /// Whether all expressions in the plan are deterministic
34    pub is_fully_deterministic: bool,
35    /// Total complexity score across all expressions
36    pub total_complexity: u32,
37    /// All referenced properties (deduplicated)
38    pub all_referenced_properties: Vec<String>,
39    /// All referenced variables (deduplicated)
40    pub all_referenced_variables: Vec<String>,
41    /// All called functions (deduplicated)
42    pub all_called_functions: Vec<String>,
43    /// Whether any expression contains aggregate functions
44    pub contains_aggregates: bool,
45    /// Whether any expression contains subqueries
46    pub contains_subqueries: bool,
47    /// Total expression node count
48    pub total_expression_nodes: u32,
49}
50
51/// Context shared during the single traversal
52struct AnalysisContext {
53    /// Fingerprint calculator
54    fingerprint_calculator: FingerprintCalculator,
55    /// Expression analyzer
56    expression_analyzer: ExpressionAnalyzer,
57    /// Reference count tracking
58    fingerprint_map: HashMap<u64, SubplanReferenceInfo>,
59    node_fingerprint_map: HashMap<i64, u64>,
60    node_count_map: HashMap<i64, usize>,
61    /// Collected expression analyses
62    expression_analyses: Vec<ExpressionAnalysis>,
63    /// Node ID to fingerprint mapping
64    fingerprints: HashMap<i64, PlanFingerprint>,
65}
66
67impl AnalysisContext {
68    fn new() -> Self {
69        Self {
70            fingerprint_calculator: FingerprintCalculator::new(),
71            expression_analyzer: ExpressionAnalyzer::new(),
72            fingerprint_map: HashMap::new(),
73            node_fingerprint_map: HashMap::new(),
74            node_count_map: HashMap::new(),
75            expression_analyses: Vec::new(),
76            fingerprints: HashMap::new(),
77        }
78    }
79
80    /// Record a subplan reference
81    fn record_reference(&mut self, fingerprint: u64, node_id: i64, parent_id: Option<i64>) {
82        self.node_fingerprint_map.insert(node_id, fingerprint);
83
84        let info = self
85            .fingerprint_map
86            .entry(fingerprint)
87            .or_insert_with(|| SubplanReferenceInfo::new(SubplanId::new(fingerprint), node_id));
88
89        if let Some(parent) = parent_id {
90            info.add_reference(parent);
91        } else {
92            info.reference_count = info.reference_count.max(1);
93        }
94    }
95
96    /// Record node count for a subplan
97    fn record_node_count(&mut self, node_id: i64, count: usize) {
98        self.node_count_map.insert(node_id, count);
99    }
100
101    /// Record fingerprint for a node
102    fn record_fingerprint(&mut self, node_id: i64, fingerprint: PlanFingerprint) {
103        self.fingerprints.insert(node_id, fingerprint);
104    }
105
106    /// Analyze an expression and collect results
107    fn analyze_expression(&mut self, ctx_expr: &ContextualExpression) {
108        let analysis = self.expression_analyzer.analyze(ctx_expr);
109        self.expression_analyses.push(analysis);
110    }
111
112    /// Build reference count analysis result
113    fn build_reference_analysis(&self) -> ReferenceCountAnalysis {
114        let mut repeated_subplans = Vec::new();
115        let mut node_reference_map = HashMap::new();
116
117        for (fingerprint, info) in &self.fingerprint_map {
118            if info.reference_count >= 2 {
119                let mut info_clone = info.clone();
120                if let Some(&count) = self.node_count_map.get(&info.root_node_id) {
121                    info_clone.node_count = count;
122                }
123
124                for (node_id, fp) in &self.node_fingerprint_map {
125                    if *fp == *fingerprint {
126                        node_reference_map.insert(*node_id, info_clone.clone());
127                    }
128                }
129
130                repeated_subplans.push(info_clone);
131            }
132        }
133
134        ReferenceCountAnalysis {
135            repeated_subplans,
136            node_reference_map,
137        }
138    }
139
140    /// Build aggregated expression analysis
141    fn build_expression_summary(&self) -> AggregatedExpressionAnalysis {
142        let mut summary = AggregatedExpressionAnalysis {
143            is_fully_deterministic: true,
144            ..Default::default()
145        };
146
147        let mut properties_set = std::collections::HashSet::new();
148        let mut variables_set = std::collections::HashSet::new();
149        let mut functions_set = std::collections::HashSet::new();
150
151        for analysis in &self.expression_analyses {
152            if !analysis.is_deterministic {
153                summary.is_fully_deterministic = false;
154            }
155            summary.total_complexity += analysis.complexity_score;
156            summary.total_expression_nodes += analysis.node_count;
157            summary.contains_aggregates |= analysis.contains_aggregate;
158            summary.contains_subqueries |= analysis.contains_subquery;
159
160            for prop in &analysis.referenced_properties {
161                properties_set.insert(prop.clone());
162            }
163            for var in &analysis.referenced_variables {
164                variables_set.insert(var.clone());
165            }
166            for func in &analysis.called_functions {
167                functions_set.insert(func.clone());
168            }
169        }
170
171        summary.all_referenced_properties = properties_set.into_iter().collect();
172        summary.all_referenced_variables = variables_set.into_iter().collect();
173        summary.all_called_functions = functions_set.into_iter().collect();
174
175        summary
176    }
177}
178
179/// Batch plan analyzer - performs all analysis in a single traversal
180#[derive(Debug, Clone, Default)]
181pub struct BatchPlanAnalyzer;
182
183impl BatchPlanAnalyzer {
184    /// Create a new batch analyzer
185    pub fn new() -> Self {
186        Self
187    }
188
189    /// Analyze the entire plan in a single traversal
190    ///
191    /// This method performs:
192    /// 1. Reference counting (for materialization decisions)
193    /// 2. Fingerprint calculation (for subplan identification)
194    /// 3. Expression analysis aggregation (from all nodes)
195    ///
196    /// All operations happen in one post-order traversal of the plan tree.
197    pub fn analyze(&self, root: &PlanNodeEnum) -> BatchPlanAnalysis {
198        let mut context = AnalysisContext::new();
199        self.analyze_recursive(root, &mut context, None);
200
201        BatchPlanAnalysis {
202            reference_count: context.build_reference_analysis(),
203            expression_summary: context.build_expression_summary(),
204            fingerprints: context.fingerprints,
205        }
206    }
207
208    /// Recursive single-pass analysis
209    ///
210    /// Returns the number of nodes in the subtree
211    fn analyze_recursive(
212        &self,
213        node: &PlanNodeEnum,
214        context: &mut AnalysisContext,
215        parent_id: Option<i64>,
216    ) -> usize {
217        // 1. Calculate fingerprint (shared computation)
218        let fingerprint = context.fingerprint_calculator.calculate_fingerprint(node);
219        let node_id = node.id();
220
221        // Record fingerprint
222        context.record_fingerprint(node_id, fingerprint);
223
224        // 2. Record reference for reference counting
225        context.record_reference(fingerprint.value(), node_id, parent_id);
226
227        // 3. Analyze expressions in the current node
228        self.analyze_node_expressions(node, context);
229
230        // 4. Recursively analyze children
231        let child_count = self.analyze_children(node, context, node_id);
232        let total_count = child_count + 1;
233
234        // 5. Record node count for this subtree
235        context.record_node_count(node_id, total_count);
236
237        total_count
238    }
239
240    /// Analyze expressions contained in a specific node
241    fn analyze_node_expressions(&self, node: &PlanNodeEnum, context: &mut AnalysisContext) {
242        use crate::planning::plan::core::nodes::*;
243
244        match node {
245            PlanNodeEnum::Filter(n) => {
246                context.analyze_expression(n.condition());
247            }
248            PlanNodeEnum::Project(n) => {
249                for col in n.columns() {
250                    context.analyze_expression(&col.expression);
251                }
252            }
253            PlanNodeEnum::Aggregate(n) => {
254                // Aggregate node itself doesn't have expressions directly,
255                // but we could analyze group keys if needed
256                for key in n.group_keys() {
257                    // Group keys are strings, not expressions
258                    // Skip for now as they don't need expression analysis
259                    let _ = key;
260                }
261            }
262            PlanNodeEnum::Sort(n) => {
263                // Sort items contain column names (strings), not expressions
264                // Skip expression analysis for sort items
265                for _item in n.sort_items() {
266                    // Sort items are SortItem with column name (String) and direction
267                    // No ContextualExpression to analyze here
268                }
269            }
270            PlanNodeEnum::InnerJoin(n) => {
271                for key in n.hash_keys() {
272                    context.analyze_expression(key);
273                }
274                for key in n.probe_keys() {
275                    context.analyze_expression(key);
276                }
277            }
278            PlanNodeEnum::LeftJoin(n) => {
279                for key in n.hash_keys() {
280                    context.analyze_expression(key);
281                }
282                for key in n.probe_keys() {
283                    context.analyze_expression(key);
284                }
285            }
286            PlanNodeEnum::Loop(n) => {
287                context.analyze_expression(n.condition());
288            }
289            PlanNodeEnum::Select(n) => {
290                context.analyze_expression(n.condition());
291            }
292            _ => {}
293        }
294    }
295
296    /// Analyze child nodes and return total count
297    fn analyze_children(
298        &self,
299        node: &PlanNodeEnum,
300        context: &mut AnalysisContext,
301        node_id: i64,
302    ) -> usize {
303        use crate::planning::plan::core::nodes::*;
304
305        match node {
306            // Zero-input nodes
307            PlanNodeEnum::Start(_)
308            | PlanNodeEnum::GetVertices(_)
309            | PlanNodeEnum::GetEdges(_)
310            | PlanNodeEnum::GetNeighbors(_)
311            | PlanNodeEnum::ScanVertices(_)
312            | PlanNodeEnum::ScanEdges(_)
313            | PlanNodeEnum::IndexScan(_)
314            | PlanNodeEnum::Argument(_) => 0,
315
316            // Single-input nodes
317            PlanNodeEnum::Filter(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
318            PlanNodeEnum::Project(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
319            PlanNodeEnum::Sort(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
320            PlanNodeEnum::Limit(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
321            PlanNodeEnum::TopN(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
322            PlanNodeEnum::Sample(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
323            PlanNodeEnum::Aggregate(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
324            PlanNodeEnum::Dedup(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
325            PlanNodeEnum::PassThrough(_) => {
326                // PassThroughNode is a ZeroInputNode, no children to analyze
327                0
328            }
329            PlanNodeEnum::DataCollect(n) => {
330                self.analyze_recursive(n.input(), context, Some(node_id))
331            }
332            PlanNodeEnum::Unwind(n) => self.analyze_recursive(n.input(), context, Some(node_id)),
333            PlanNodeEnum::Materialize(n) => {
334                self.analyze_recursive(n.input(), context, Some(node_id))
335            }
336
337            // Two-input nodes (joins)
338            PlanNodeEnum::InnerJoin(n) => {
339                let left_count = self.analyze_recursive(n.left_input(), context, Some(node_id));
340                let right_count = self.analyze_recursive(n.right_input(), context, Some(node_id));
341                left_count + right_count
342            }
343            PlanNodeEnum::LeftJoin(n) => {
344                let left_count = self.analyze_recursive(n.left_input(), context, Some(node_id));
345                let right_count = self.analyze_recursive(n.right_input(), context, Some(node_id));
346                left_count + right_count
347            }
348            PlanNodeEnum::CrossJoin(n) => {
349                let left_count = self.analyze_recursive(n.left_input(), context, Some(node_id));
350                let right_count = self.analyze_recursive(n.right_input(), context, Some(node_id));
351                left_count + right_count
352            }
353            PlanNodeEnum::FullOuterJoin(n) => {
354                let left_count = self.analyze_recursive(n.left_input(), context, Some(node_id));
355                let right_count = self.analyze_recursive(n.right_input(), context, Some(node_id));
356                left_count + right_count
357            }
358
359            // Multi-input nodes using dependencies()
360            PlanNodeEnum::Union(n) => n
361                .dependencies()
362                .iter()
363                .map(|dep| self.analyze_recursive(dep, context, Some(node_id)))
364                .sum(),
365            PlanNodeEnum::Minus(n) => {
366                let main_count = self.analyze_recursive(n.input(), context, Some(node_id));
367                let minus_count = self.analyze_recursive(n.minus_input(), context, Some(node_id));
368                main_count + minus_count
369            }
370            PlanNodeEnum::Intersect(n) => {
371                let main_count = self.analyze_recursive(n.input(), context, Some(node_id));
372                let intersect_count =
373                    self.analyze_recursive(n.intersect_input(), context, Some(node_id));
374                main_count + intersect_count
375            }
376
377            // Traversal nodes
378            PlanNodeEnum::Expand(n) => n
379                .inputs()
380                .iter()
381                .map(|dep| self.analyze_recursive(dep, context, Some(node_id)))
382                .sum(),
383            PlanNodeEnum::ExpandAll(n) => n
384                .inputs()
385                .iter()
386                .map(|dep| self.analyze_recursive(dep, context, Some(node_id)))
387                .sum(),
388            PlanNodeEnum::Traverse(n) => {
389                // TraverseNode uses SingleInputNode trait, input() method panics when input is None
390                // Here, we directly access the deps field to traverse the child nodes.
391                n.dependencies()
392                    .iter()
393                    .map(|dep| self.analyze_recursive(dep, context, Some(node_id)))
394                    .sum()
395            }
396            PlanNodeEnum::AppendVertices(n) => n
397                .inputs()
398                .iter()
399                .map(|dep| self.analyze_recursive(dep, context, Some(node_id)))
400                .sum(),
401
402            // Control flow nodes
403            PlanNodeEnum::Loop(n) => {
404                let body_count = n
405                    .body()
406                    .as_ref()
407                    .map(|body| self.analyze_recursive(body.as_ref(), context, Some(node_id)))
408                    .unwrap_or(0);
409                body_count
410            }
411            PlanNodeEnum::RecursiveCte(n) => {
412                let anchor_count = self.analyze_recursive(n.anchor(), context, Some(node_id));
413                let step_count = n
414                    .step()
415                    .map(|step| self.analyze_recursive(step, context, Some(node_id)))
416                    .unwrap_or(0);
417                anchor_count + step_count
418            }
419            PlanNodeEnum::Select(n) => {
420                let if_count = n
421                    .if_branch()
422                    .as_ref()
423                    .map(|branch| self.analyze_recursive(branch.as_ref(), context, Some(node_id)))
424                    .unwrap_or(0);
425                let else_count = n
426                    .else_branch()
427                    .as_ref()
428                    .map(|branch| self.analyze_recursive(branch.as_ref(), context, Some(node_id)))
429                    .unwrap_or(0);
430                if_count + else_count
431            }
432
433            // Pattern apply nodes
434            PlanNodeEnum::PatternApply(n) => {
435                let left_count = self.analyze_recursive(n.left_input(), context, Some(node_id));
436                let right_count = self.analyze_recursive(n.right_input(), context, Some(node_id));
437                left_count + right_count
438            }
439            PlanNodeEnum::CorrelatedApply(n) => {
440                let left_count = self.analyze_recursive(n.left_input(), context, Some(node_id));
441                let right_count = self.analyze_recursive(n.right_input(), context, Some(node_id));
442                left_count + right_count
443            }
444            PlanNodeEnum::RollUpApply(n) => {
445                let left_count = self.analyze_recursive(n.left_input(), context, Some(node_id));
446                let right_count = self.analyze_recursive(n.right_input(), context, Some(node_id));
447                left_count + right_count
448            }
449
450            // Algorithm nodes
451            PlanNodeEnum::ShortestPath(n) => {
452                self.analyze_recursive(n.left_input(), context, Some(node_id))
453                    + self.analyze_recursive(n.right_input(), context, Some(node_id))
454            }
455            PlanNodeEnum::MultiShortestPath(n) => {
456                self.analyze_recursive(n.left_input(), context, Some(node_id))
457                    + self.analyze_recursive(n.right_input(), context, Some(node_id))
458            }
459            PlanNodeEnum::BFSShortest(n) => {
460                self.analyze_recursive(n.left_input(), context, Some(node_id))
461                    + self.analyze_recursive(n.right_input(), context, Some(node_id))
462            }
463            PlanNodeEnum::AllPaths(n) => {
464                self.analyze_recursive(n.left_input(), context, Some(node_id))
465                    + self.analyze_recursive(n.right_input(), context, Some(node_id))
466            }
467
468            // Data processing nodes
469            PlanNodeEnum::Assign(n) => n
470                .dependencies()
471                .iter()
472                .map(|dep| self.analyze_recursive(dep, context, Some(node_id)))
473                .sum(),
474
475            // Management nodes (typically leaf nodes)
476            _ => 0,
477        }
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use crate::planning::plan::core::nodes::StartNode;
485
486    #[test]
487    fn test_batch_analyzer_creation() {
488        let _analyzer = BatchPlanAnalyzer::new();
489        let _default_analyzer = BatchPlanAnalyzer;
490    }
491
492    #[test]
493    fn test_analyze_simple_plan() {
494        let analyzer = BatchPlanAnalyzer::new();
495        let root = PlanNodeEnum::Start(StartNode::new());
496
497        let result = analyzer.analyze(&root);
498
499        assert_eq!(result.reference_count.repeated_count(), 0);
500        assert!(result.expression_summary.is_fully_deterministic);
501        assert_eq!(result.expression_summary.total_expression_nodes, 0);
502    }
503}