linkrs_query/optimizer/cost_based/
expression_precomputation.rs1use 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#[derive(Debug, Clone, PartialEq)]
30pub enum PrecomputationDecision {
31 Precompute {
33 reason: PrecomputeReason,
35 benefit: f64,
37 cost: f64,
39 },
40 DoNotPrecompute {
42 reason: NoPrecomputeReason,
44 },
45}
46
47#[derive(Debug, Clone, PartialEq)]
49pub enum PrecomputeReason {
50 ComplexAndFrequentlyUsed,
52 ExpensiveFunctionCalls,
54 DeterministicAndReused,
56 CostBenefit,
58}
59
60#[derive(Debug, Clone, PartialEq)]
62pub enum NoPrecomputeReason {
63 TooSimple,
65 SingleUse,
67 NonDeterministic,
69 NotCostEffective,
71 ContainsVolatileFunctions,
73}
74
75#[derive(Debug, Clone)]
77pub struct PrecomputationCandidate {
78 pub expression: ContextualExpression,
80 pub reference_count: usize,
82 pub estimated_cost: f64,
84 pub is_deterministic: bool,
86}
87
88#[derive(Debug, Clone)]
92pub struct ExpressionPrecomputationOptimizer {
93 cost_calculator: Arc<CostCalculator>,
94 precompute_threshold: f64,
96 min_expression_cost: f64,
98 precompute_overhead: f64,
100}
101
102impl ExpressionPrecomputationOptimizer {
103 pub fn new(cost_calculator: Arc<CostCalculator>) -> Self {
105 Self {
106 cost_calculator,
107 precompute_threshold: 2.0, min_expression_cost: 0.01,
109 precompute_overhead: 0.005,
110 }
111 }
112
113 pub fn with_threshold(mut self, threshold: f64) -> Self {
115 self.precompute_threshold = threshold.max(1.0);
116 self
117 }
118
119 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 pub fn with_overhead(mut self, overhead: f64) -> Self {
127 self.precompute_overhead = overhead.max(0.0);
128 self
129 }
130
131 pub fn should_precompute(
140 &self,
141 expression: &ContextualExpression,
142 reference_count: usize,
143 ) -> PrecomputationDecision {
144 if reference_count <= 1 {
146 return PrecomputationDecision::DoNotPrecompute {
147 reason: NoPrecomputeReason::SingleUse,
148 };
149 }
150
151 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 let expression_cost = self.cost_calculator.calculate_expression_cost(&expr);
163
164 if expression_cost < self.min_expression_cost {
166 return PrecomputationDecision::DoNotPrecompute {
167 reason: NoPrecomputeReason::TooSimple,
168 };
169 }
170
171 let is_deterministic = self.check_expression_deterministic(&expr);
173 if !is_deterministic {
174 return PrecomputationDecision::DoNotPrecompute {
175 reason: NoPrecomputeReason::NonDeterministic,
176 };
177 }
178
179 let precompute_benefit = expression_cost * reference_count as f64;
181 let precompute_cost = expression_cost + self.precompute_overhead;
182
183 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 #[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 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 fn check_expression_deterministic(&self, expr: &Expression) -> bool {
249 match expr {
250 Expression::Literal(_) => true,
251 Expression::Variable(_) => true,
252 Expression::Parameter(_) => true, Expression::SessionVariable(_) => true, Expression::Label(_) => true,
255 Expression::Vector(_) => true, Expression::Exists { .. } => false, Expression::In { .. } => false, Expression::ScalarSubquery { .. } => false, 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 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 Expression::Property { object, .. } => self.check_expression_deterministic(object),
295 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 Expression::TypeCast { expression, .. } => {
302 self.check_expression_deterministic(expression)
303 }
304 Expression::Subscript { collection, index } => {
306 self.check_expression_deterministic(collection)
307 && self.check_expression_deterministic(index)
308 }
309 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 Expression::Path(exprs) => exprs.iter().all(|e| self.check_expression_deterministic(e)),
325 Expression::PathBuild(exprs) => {
327 exprs.iter().all(|e| self.check_expression_deterministic(e))
328 }
329 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 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 Expression::Aggregate { args, .. } => args
357 .iter()
358 .all(|arg| self.check_expression_deterministic(arg)),
359 Expression::Predicate { args, .. } => args
361 .iter()
362 .all(|arg| self.check_expression_deterministic(arg)), 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 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 ];
397 volatile_functions.contains(&name.to_lowercase().as_str())
398 }
399
400 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 let decision = optimizer.should_precompute(&expr, 5);
464 match decision {
466 PrecomputationDecision::DoNotPrecompute { .. } => {
467 }
469 PrecomputationDecision::Precompute { .. } => {
470 }
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 let literal_expr = Expression::Literal(Value::Int(42));
493 assert!(optimizer.check_expression_deterministic(&literal_expr));
494
495 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 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}