Skip to main content

linkrs_query/optimizer/cost_based/
expression_precomputation.rs

1//! Expression Precomputation Optimizer Module
2//!
3//! Determines which expressions should be precomputed based on their cost
4//! and reference count to avoid redundant computations.
5//!
6//! ## Usage Examples
7//!
8//! ```rust
9//! use linkrs_query::optimizer::cost::CostCalculator;
10//! use linkrs_query::optimizer::cost_based::ExpressionPrecomputationOptimizer;
11//! use linkrs_query::optimizer::stats::StatisticsManager;
12//! use std::sync::Arc;
13//!
14//! let stats_manager = Arc::new(StatisticsManager::new());
15//! let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
16//! let optimizer = ExpressionPrecomputationOptimizer::new(cost_calculator);
17//!
18//! // let decision = optimizer.should_precompute(&expression, reference_count);
19//! ```
20
21use std::collections::HashMap;
22use std::sync::Arc;
23
24use crate::optimizer::cost::CostCalculator;
25use linkrs_core::types::expr::Expression;
26use linkrs_core::types::ContextualExpression;
27
28/// Precomputation decision
29#[derive(Debug, Clone, PartialEq)]
30pub enum PrecomputationDecision {
31    /// Should precompute the expression
32    Precompute {
33        /// Reason for precomputation
34        reason: PrecomputeReason,
35        /// Estimated benefit (cost savings)
36        benefit: f64,
37        /// Estimated cost of precomputation
38        cost: f64,
39    },
40    /// Should not precompute
41    DoNotPrecompute {
42        /// Reason for not precomputing
43        reason: NoPrecomputeReason,
44    },
45}
46
47/// Reasons for precomputation
48#[derive(Debug, Clone, PartialEq)]
49pub enum PrecomputeReason {
50    /// Expression is complex and referenced multiple times
51    ComplexAndFrequentlyUsed,
52    /// Expression contains expensive function calls
53    ExpensiveFunctionCalls,
54    /// Expression is deterministic and reused
55    DeterministicAndReused,
56    /// Cost-benefit analysis favors precomputation
57    CostBenefit,
58}
59
60/// Reasons for not precomputing
61#[derive(Debug, Clone, PartialEq)]
62pub enum NoPrecomputeReason {
63    /// Expression is too simple
64    TooSimple,
65    /// Expression is referenced only once
66    SingleUse,
67    /// Expression is non-deterministic
68    NonDeterministic,
69    /// Precomputation cost exceeds benefit
70    NotCostEffective,
71    /// Expression contains volatile functions
72    ContainsVolatileFunctions,
73}
74
75/// Expression precomputation candidate
76#[derive(Debug, Clone)]
77pub struct PrecomputationCandidate {
78    /// The expression to potentially precompute
79    pub expression: ContextualExpression,
80    /// Number of times the expression is referenced
81    pub reference_count: usize,
82    /// Estimated cost of the expression
83    pub estimated_cost: f64,
84    /// Whether the expression is deterministic
85    pub is_deterministic: bool,
86}
87
88/// Expression Precomputation Optimizer
89///
90/// Determines which expressions should be precomputed to avoid redundant computations.
91#[derive(Debug, Clone)]
92pub struct ExpressionPrecomputationOptimizer {
93    cost_calculator: Arc<CostCalculator>,
94    /// Threshold for precomputation (benefit/cost ratio)
95    precompute_threshold: f64,
96    /// Minimum expression cost to consider precomputation
97    min_expression_cost: f64,
98    /// Overhead for precomputation (storage, lookup, etc.)
99    precompute_overhead: f64,
100}
101
102impl ExpressionPrecomputationOptimizer {
103    /// Create a new expression precomputation optimizer
104    pub fn new(cost_calculator: Arc<CostCalculator>) -> Self {
105        Self {
106            cost_calculator,
107            precompute_threshold: 2.0, // Benefit must be 2x the cost
108            min_expression_cost: 0.01,
109            precompute_overhead: 0.005,
110        }
111    }
112
113    /// Set the precomputation threshold
114    pub fn with_threshold(mut self, threshold: f64) -> Self {
115        self.precompute_threshold = threshold.max(1.0);
116        self
117    }
118
119    /// Set minimum expression cost
120    pub fn with_min_cost(mut self, min_cost: f64) -> Self {
121        self.min_expression_cost = min_cost.max(0.0);
122        self
123    }
124
125    /// Set precomputation overhead
126    pub fn with_overhead(mut self, overhead: f64) -> Self {
127        self.precompute_overhead = overhead.max(0.0);
128        self
129    }
130
131    /// Determine if an expression should be precomputed
132    ///
133    /// # Parameters
134    /// - `expression`: The expression to evaluate
135    /// - `reference_count`: Number of times the expression is referenced
136    ///
137    /// # Returns
138    /// Precomputation decision
139    pub fn should_precompute(
140        &self,
141        expression: &ContextualExpression,
142        reference_count: usize,
143    ) -> PrecomputationDecision {
144        // Check if expression is referenced multiple times
145        if reference_count <= 1 {
146            return PrecomputationDecision::DoNotPrecompute {
147                reason: NoPrecomputeReason::SingleUse,
148            };
149        }
150
151        // Get the expression
152        let expr = match expression.get_expression() {
153            Some(e) => e,
154            None => {
155                return PrecomputationDecision::DoNotPrecompute {
156                    reason: NoPrecomputeReason::TooSimple,
157                };
158            }
159        };
160
161        // Calculate expression cost
162        let expression_cost = self.cost_calculator.calculate_expression_cost(&expr);
163
164        // Check if expression is complex enough
165        if expression_cost < self.min_expression_cost {
166            return PrecomputationDecision::DoNotPrecompute {
167                reason: NoPrecomputeReason::TooSimple,
168            };
169        }
170
171        // Check if expression is deterministic
172        let is_deterministic = self.check_expression_deterministic(&expr);
173        if !is_deterministic {
174            return PrecomputationDecision::DoNotPrecompute {
175                reason: NoPrecomputeReason::NonDeterministic,
176            };
177        }
178
179        // Calculate benefit and cost
180        let precompute_benefit = expression_cost * reference_count as f64;
181        let precompute_cost = expression_cost + self.precompute_overhead;
182
183        // Check cost-benefit ratio
184        let ratio = precompute_benefit / precompute_cost;
185
186        if ratio >= self.precompute_threshold {
187            let reason =
188                if expression_cost > self.cost_calculator.config().function_call_base_cost * 2.0 {
189                    PrecomputeReason::ExpensiveFunctionCalls
190                } else if reference_count > 3 {
191                    PrecomputeReason::ComplexAndFrequentlyUsed
192                } else {
193                    PrecomputeReason::CostBenefit
194                };
195
196            PrecomputationDecision::Precompute {
197                reason,
198                benefit: precompute_benefit,
199                cost: precompute_cost,
200            }
201        } else {
202            PrecomputationDecision::DoNotPrecompute {
203                reason: NoPrecomputeReason::NotCostEffective,
204            }
205        }
206    }
207
208    /// Analyze multiple expressions and return precomputation candidates
209    ///
210    /// # Parameters
211    /// - `expressions`: Map of expression to reference count
212    ///
213    /// # Returns
214    /// List of expressions that should be precomputed
215    #[allow(clippy::mutable_key_type)]
216    pub fn analyze_expressions(
217        &self,
218        expressions: &HashMap<ContextualExpression, usize>,
219    ) -> Vec<(ContextualExpression, PrecomputationDecision)> {
220        let mut results = Vec::new();
221
222        for (expr, ref_count) in expressions {
223            let decision = self.should_precompute(expr, *ref_count);
224            if matches!(decision, PrecomputationDecision::Precompute { .. }) {
225                results.push((expr.clone(), decision));
226            }
227        }
228
229        // Sort by benefit (descending)
230        results.sort_by(|a, b| {
231            let benefit_a = match &a.1 {
232                PrecomputationDecision::Precompute { benefit, .. } => *benefit,
233                _ => 0.0,
234            };
235            let benefit_b = match &b.1 {
236                PrecomputationDecision::Precompute { benefit, .. } => *benefit,
237                _ => 0.0,
238            };
239            benefit_b
240                .partial_cmp(&benefit_a)
241                .unwrap_or(std::cmp::Ordering::Equal)
242        });
243
244        results
245    }
246
247    /// Recursively check if expression is deterministic
248    fn check_expression_deterministic(&self, expr: &Expression) -> bool {
249        match expr {
250            Expression::Literal(_) => true,
251            Expression::Variable(_) => true,
252            Expression::Parameter(_) => true, // Parameters are deterministic within a query
253            Expression::SessionVariable(_) => true, // Session variables are snapshot-fixed per statement
254            Expression::Label(_) => true,
255            Expression::Vector(_) => true, // Vector literals are deterministic
256            Expression::Exists { .. } => false, // Subqueries are not deterministic
257            Expression::In { .. } => false, // Subqueries are not deterministic
258            Expression::ScalarSubquery { .. } => false, // Subqueries are not deterministic
259            Expression::Unary { operand, .. } => self.check_expression_deterministic(operand),
260            Expression::Binary { left, right, .. } => {
261                self.check_expression_deterministic(left)
262                    && self.check_expression_deterministic(right)
263            }
264            Expression::Function { name, args, .. } => {
265                // Check if function is deterministic
266                let is_deterministic_func = !self.is_volatile_function(name);
267                is_deterministic_func
268                    && args
269                        .iter()
270                        .all(|arg| self.check_expression_deterministic(arg.as_expr()))
271            }
272            Expression::Case {
273                conditions,
274                default,
275                ..
276            } => {
277                let conditions_deterministic = conditions.iter().all(|(cond, result)| {
278                    self.check_expression_deterministic(cond)
279                        && self.check_expression_deterministic(result)
280                });
281                let default_deterministic = default
282                    .as_ref()
283                    .map(|e| self.check_expression_deterministic(e))
284                    .unwrap_or(true);
285                conditions_deterministic && default_deterministic
286            }
287            Expression::List(elements) => elements
288                .iter()
289                .all(|e| self.check_expression_deterministic(e)),
290            Expression::Map(entries) => entries
291                .iter()
292                .all(|(_, v)| self.check_expression_deterministic(v)),
293            // Property access is deterministic if the object is deterministic
294            Expression::Property { object, .. } => self.check_expression_deterministic(object),
295            // Struct field access is deterministic if the base is deterministic
296            Expression::StructField { base, .. } => self.check_expression_deterministic(base),
297            Expression::TagProperty { .. } => true,
298            Expression::EdgeProperty { .. } => true,
299            Expression::LabelTagProperty { tag, .. } => self.check_expression_deterministic(tag),
300            // Type cast is deterministic if the expression is deterministic
301            Expression::TypeCast { expression, .. } => {
302                self.check_expression_deterministic(expression)
303            }
304            // Subscript access is deterministic if both collection and index are deterministic
305            Expression::Subscript { collection, index } => {
306                self.check_expression_deterministic(collection)
307                    && self.check_expression_deterministic(index)
308            }
309            // Range access is deterministic if all parts are deterministic
310            Expression::Range {
311                collection,
312                start,
313                end,
314            } => {
315                self.check_expression_deterministic(collection)
316                    && start
317                        .as_ref()
318                        .is_none_or(|e| self.check_expression_deterministic(e))
319                    && end
320                        .as_ref()
321                        .is_none_or(|e| self.check_expression_deterministic(e))
322            }
323            // Path is deterministic if all elements are deterministic
324            Expression::Path(exprs) => exprs.iter().all(|e| self.check_expression_deterministic(e)),
325            // Path build is deterministic if all elements are deterministic
326            Expression::PathBuild(exprs) => {
327                exprs.iter().all(|e| self.check_expression_deterministic(e))
328            }
329            // List comprehension is deterministic if all parts are deterministic
330            Expression::ListComprehension {
331                source,
332                filter,
333                map,
334                ..
335            } => {
336                self.check_expression_deterministic(source)
337                    && filter
338                        .as_ref()
339                        .is_none_or(|e| self.check_expression_deterministic(e))
340                    && map
341                        .as_ref()
342                        .is_none_or(|e| self.check_expression_deterministic(e))
343            }
344            // Reduce expression is deterministic if all parts are deterministic
345            Expression::Reduce {
346                initial,
347                source,
348                mapping,
349                ..
350            } => {
351                self.check_expression_deterministic(initial)
352                    && self.check_expression_deterministic(source)
353                    && self.check_expression_deterministic(mapping)
354            }
355            // Aggregate expressions are deterministic if the argument is deterministic
356            Expression::Aggregate { args, .. } => args
357                .iter()
358                .all(|arg| self.check_expression_deterministic(arg)),
359            // Predicate expressions are deterministic if all arguments are deterministic
360            Expression::Predicate { args, .. } => args
361                .iter()
362                .all(|arg| self.check_expression_deterministic(arg)), // Pattern is now exhaustive - all Expression variants are handled above
363            // If new variants are added in the future, they should be handled here
364            // For now, this branch is unreachable but kept for future-proofing
365            Expression::WindowFunction {
366                args,
367                over_partition_by,
368                over_order_by,
369                ..
370            } => {
371                args.iter()
372                    .all(|arg| self.check_expression_deterministic(arg))
373                    && over_partition_by
374                        .iter()
375                        .all(|e| self.check_expression_deterministic(e))
376                    && over_order_by
377                        .iter()
378                        .all(|e| self.check_expression_deterministic(e))
379            }
380            Expression::CountSubquery { .. } => false,
381            Expression::Lambda { body, .. } => self.check_expression_deterministic(body),
382        }
383    }
384
385    /// Check if a function is volatile (non-deterministic)
386    fn is_volatile_function(&self, name: &str) -> bool {
387        let volatile_functions: &[&str] = &[
388            "rand",
389            "random",
390            "now",
391            "current_time",
392            "current_timestamp",
393            "uuid",
394            "row_number",
395            // Add more as needed
396        ];
397        volatile_functions.contains(&name.to_lowercase().as_str())
398    }
399
400    /// Get total estimated benefit from precomputing a list of expressions
401    pub fn total_precomputation_benefit(
402        &self,
403        decisions: &[(ContextualExpression, PrecomputationDecision)],
404    ) -> f64 {
405        decisions
406            .iter()
407            .filter_map(|(_, decision)| match decision {
408                PrecomputationDecision::Precompute { benefit, cost, .. } => Some(benefit - cost),
409                _ => None,
410            })
411            .sum()
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::optimizer::stats::StatisticsManager;
419    use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
420    use linkrs_core::types::expr::Expression;
421    use linkrs_core::value::Value;
422
423    fn create_test_optimizer() -> ExpressionPrecomputationOptimizer {
424        let stats_manager = Arc::new(StatisticsManager::new());
425        let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
426        ExpressionPrecomputationOptimizer::new(cost_calculator)
427    }
428
429    fn create_simple_expression() -> ContextualExpression {
430        let ctx = ExpressionAnalysisContext::new();
431        let expr = Expression::Literal(Value::Int(42));
432        let id = ctx.register_expression(linkrs_core::types::expr::ExpressionMeta::new(expr));
433        ContextualExpression::new(id, std::sync::Arc::new(ctx))
434    }
435
436    #[test]
437    fn test_optimizer_creation() {
438        let optimizer = create_test_optimizer();
439        assert_eq!(optimizer.precompute_threshold, 2.0);
440        assert_eq!(optimizer.min_expression_cost, 0.01);
441    }
442
443    #[test]
444    fn test_single_use_not_precomputed() {
445        let optimizer = create_test_optimizer();
446        let expr = create_simple_expression();
447
448        let decision = optimizer.should_precompute(&expr, 1);
449        assert_eq!(
450            decision,
451            PrecomputationDecision::DoNotPrecompute {
452                reason: NoPrecomputeReason::SingleUse,
453            }
454        );
455    }
456
457    #[test]
458    fn test_multiple_use_considered() {
459        let optimizer = create_test_optimizer();
460        let expr = create_simple_expression();
461
462        // Even with multiple uses, simple expressions may not be precomputed
463        let decision = optimizer.should_precompute(&expr, 5);
464        // Result depends on cost calculation
465        match decision {
466            PrecomputationDecision::DoNotPrecompute { .. } => {
467                // Expected for simple expressions
468            }
469            PrecomputationDecision::Precompute { .. } => {
470                // Also acceptable if cost-benefit analysis favors it
471            }
472        }
473    }
474
475    #[test]
476    fn test_volatile_function_detection() {
477        let optimizer = create_test_optimizer();
478
479        assert!(optimizer.is_volatile_function("rand"));
480        assert!(optimizer.is_volatile_function("RAND"));
481        assert!(optimizer.is_volatile_function("now"));
482        assert!(optimizer.is_volatile_function("NOW"));
483        assert!(!optimizer.is_volatile_function("abs"));
484        assert!(!optimizer.is_volatile_function("upper"));
485    }
486
487    #[test]
488    fn test_deterministic_check() {
489        let optimizer = create_test_optimizer();
490
491        // Literal is deterministic
492        let literal_expr = Expression::Literal(Value::Int(42));
493        assert!(optimizer.check_expression_deterministic(&literal_expr));
494
495        // Variable is deterministic
496        let var_expr = Expression::Variable("x".to_string());
497        assert!(optimizer.check_expression_deterministic(&var_expr));
498    }
499
500    #[test]
501    fn test_with_threshold() {
502        let stats_manager = Arc::new(StatisticsManager::new());
503        let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
504        let optimizer = ExpressionPrecomputationOptimizer::new(cost_calculator).with_threshold(3.0);
505
506        assert_eq!(optimizer.precompute_threshold, 3.0);
507    }
508
509    #[test]
510    #[allow(clippy::mutable_key_type)]
511    fn test_analyze_expressions() {
512        let optimizer = create_test_optimizer();
513        let mut expressions = HashMap::new();
514
515        let expr1 = create_simple_expression();
516        expressions.insert(expr1.clone(), 5);
517
518        let results = optimizer.analyze_expressions(&expressions);
519        // Results depend on cost calculations
520        assert!(results.is_empty() || results.len() == 1);
521    }
522
523    #[test]
524    fn test_total_benefit_calculation() {
525        let optimizer = create_test_optimizer();
526        let expr = create_simple_expression();
527
528        let decisions = vec![(
529            expr.clone(),
530            PrecomputationDecision::Precompute {
531                reason: PrecomputeReason::CostBenefit,
532                benefit: 10.0,
533                cost: 3.0,
534            },
535        )];
536
537        let total_benefit = optimizer.total_precomputation_benefit(&decisions);
538        assert_eq!(total_benefit, 7.0);
539    }
540}