Skip to main content

oxirs_arq/
cost_model.rs

1//! Cost Model Module
2//!
3//! Provides comprehensive cost modeling for query optimization including
4//! I/O cost modeling, CPU cost estimation, and memory usage prediction.
5
6use crate::algebra::{Algebra, Expression, TriplePattern};
7use crate::statistics_collector::StatisticsCollector;
8use anyhow::Result;
9use std::collections::HashMap;
10
11/// Cost model configuration
12#[derive(Debug, Clone)]
13pub struct CostModelConfig {
14    /// CPU cost per operation (relative units)
15    pub cpu_cost_per_op: f64,
16    /// I/O cost per page read
17    pub io_cost_per_page: f64,
18    /// Memory cost per byte allocated
19    pub memory_cost_per_byte: f64,
20    /// Network cost per byte transferred
21    pub network_cost_per_byte: f64,
22    /// Page size in bytes
23    pub page_size: usize,
24    /// Available memory in bytes
25    pub available_memory: usize,
26    /// Cost model calibration factors
27    pub calibration: CostCalibration,
28}
29
30impl Default for CostModelConfig {
31    fn default() -> Self {
32        Self {
33            cpu_cost_per_op: 1.0,
34            io_cost_per_page: 10.0,
35            memory_cost_per_byte: 0.001,
36            network_cost_per_byte: 0.1,
37            page_size: 4096,
38            available_memory: 1024 * 1024 * 1024, // 1GB
39            calibration: CostCalibration::default(),
40        }
41    }
42}
43
44/// Cost calibration factors based on system characteristics
45#[derive(Debug, Clone)]
46pub struct CostCalibration {
47    /// CPU scaling factor
48    pub cpu_scale: f64,
49    /// I/O scaling factor
50    pub io_scale: f64,
51    /// Memory scaling factor
52    pub memory_scale: f64,
53    /// Network scaling factor
54    pub network_scale: f64,
55    /// Join algorithm cost factors
56    pub join_factors: JoinCostFactors,
57}
58
59impl Default for CostCalibration {
60    fn default() -> Self {
61        Self {
62            cpu_scale: 1.0,
63            io_scale: 1.0,
64            memory_scale: 1.0,
65            network_scale: 1.0,
66            join_factors: JoinCostFactors::default(),
67        }
68    }
69}
70
71/// Cost factors for different join algorithms
72#[derive(Debug, Clone)]
73pub struct JoinCostFactors {
74    /// Hash join cost factor
75    pub hash_join_factor: f64,
76    /// Sort-merge join cost factor
77    pub sort_merge_join_factor: f64,
78    /// Nested loop join cost factor
79    pub nested_loop_join_factor: f64,
80    /// Index nested loop join cost factor
81    pub index_join_factor: f64,
82}
83
84impl Default for JoinCostFactors {
85    fn default() -> Self {
86        Self {
87            hash_join_factor: 1.0,
88            sort_merge_join_factor: 1.2,
89            nested_loop_join_factor: 2.0,
90            index_join_factor: 0.8,
91        }
92    }
93}
94
95/// Comprehensive cost estimate
96#[derive(Debug, Clone)]
97pub struct CostEstimate {
98    /// CPU cost in relative units
99    pub cpu_cost: f64,
100    /// I/O cost in relative units
101    pub io_cost: f64,
102    /// Memory cost in relative units
103    pub memory_cost: f64,
104    /// Network cost in relative units
105    pub network_cost: f64,
106    /// Total cost (weighted combination)
107    pub total_cost: f64,
108    /// Estimated cardinality
109    pub cardinality: usize,
110    /// Estimated selectivity
111    pub selectivity: f64,
112    /// Cost breakdown by operation
113    pub operation_costs: HashMap<String, f64>,
114}
115
116impl CostEstimate {
117    pub fn new(cpu: f64, io: f64, memory: f64, network: f64, cardinality: usize) -> Self {
118        let total = cpu + io + memory + network;
119        Self {
120            cpu_cost: cpu,
121            io_cost: io,
122            memory_cost: memory,
123            network_cost: network,
124            total_cost: total,
125            cardinality,
126            selectivity: 1.0,
127            operation_costs: HashMap::new(),
128        }
129    }
130
131    pub fn with_selectivity(mut self, selectivity: f64) -> Self {
132        self.selectivity = selectivity;
133        self
134    }
135
136    pub fn add_operation_cost(&mut self, operation: &str, cost: f64) {
137        self.operation_costs.insert(operation.to_string(), cost);
138        self.total_cost += cost;
139    }
140
141    pub fn zero() -> Self {
142        Self::new(0.0, 0.0, 0.0, 0.0, 0)
143    }
144
145    pub fn infinite() -> Self {
146        Self::new(
147            f64::INFINITY,
148            f64::INFINITY,
149            f64::INFINITY,
150            f64::INFINITY,
151            usize::MAX,
152        )
153    }
154}
155
156/// I/O access pattern for cost estimation
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum IOPattern {
159    /// Sequential read pattern
160    Sequential,
161    /// Random read pattern
162    Random,
163    /// Index scan pattern
164    IndexScan,
165    /// Full table scan pattern
166    FullScan,
167}
168
169/// Memory usage pattern
170#[derive(Debug, Clone)]
171pub struct MemoryUsage {
172    /// Peak memory usage in bytes
173    pub peak_usage: usize,
174    /// Average memory usage in bytes
175    pub average_usage: usize,
176    /// Memory access pattern
177    pub access_pattern: MemoryAccessPattern,
178    /// Duration of memory usage
179    pub duration_estimate: f64,
180}
181
182/// Memory access patterns
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum MemoryAccessPattern {
185    /// Sequential access
186    Sequential,
187    /// Random access
188    Random,
189    /// Locality of reference
190    Locality,
191    /// Cache-friendly access
192    CacheFriendly,
193}
194
195/// Cost model for query optimization
196#[derive(Debug, Clone)]
197pub struct CostModel {
198    config: CostModelConfig,
199    #[allow(dead_code)]
200    statistics: Option<StatisticsCollector>,
201    cached_estimates: HashMap<String, CostEstimate>,
202}
203
204impl CostModel {
205    /// Create a new cost model
206    pub fn new(config: CostModelConfig) -> Self {
207        Self {
208            config,
209            statistics: None,
210            cached_estimates: HashMap::new(),
211        }
212    }
213
214    /// Create cost model with statistics
215    pub fn with_statistics(config: CostModelConfig, statistics: StatisticsCollector) -> Self {
216        Self {
217            config,
218            statistics: Some(statistics),
219            cached_estimates: HashMap::new(),
220        }
221    }
222
223    /// Estimate cost of an algebra expression
224    pub fn estimate_cost(&mut self, algebra: &Algebra) -> Result<CostEstimate> {
225        // Check cache first
226        let algebra_key = self.algebra_to_key(algebra);
227        if let Some(cached) = self.cached_estimates.get(&algebra_key) {
228            return Ok(cached.clone());
229        }
230
231        let estimate = self.estimate_cost_recursive(algebra)?;
232
233        // Cache the result
234        self.cached_estimates.insert(algebra_key, estimate.clone());
235
236        Ok(estimate)
237    }
238
239    fn estimate_cost_recursive(&self, algebra: &Algebra) -> Result<CostEstimate> {
240        match algebra {
241            Algebra::Bgp(patterns) if patterns.len() == 1 => {
242                self.estimate_triple_pattern_cost(&patterns[0])
243            }
244            Algebra::Bgp(patterns) => self.estimate_bgp_cost(patterns),
245            Algebra::Join { left, right } => self.estimate_join_cost(left, right),
246            Algebra::LeftJoin { left, right, .. } => self.estimate_left_join_cost(left, right),
247            Algebra::Union { left, right } => self.estimate_union_cost(left, right),
248            Algebra::Filter { condition, pattern } => self.estimate_filter_cost(condition, pattern),
249            Algebra::Project { pattern, .. } => self.estimate_project_cost(pattern),
250            Algebra::Extend { expr, pattern, .. } => self.estimate_extend_cost(expr, pattern),
251            Algebra::Distinct { pattern } => self.estimate_distinct_cost(pattern),
252            Algebra::Reduced { pattern } => self.estimate_reduced_cost(pattern),
253            Algebra::OrderBy { pattern, .. } => self.estimate_order_by_cost(pattern),
254            Algebra::Slice {
255                pattern,
256                offset,
257                limit,
258            } => self.estimate_slice_cost(pattern, *offset, *limit),
259            Algebra::Group { pattern, .. } => self.estimate_group_cost(pattern),
260            Algebra::PropertyPath { .. } => {
261                // Property path has higher cost due to path traversal
262                Ok(CostEstimate::new(5000.0, 1000.0, 500.0, 0.0, 2000))
263            }
264            Algebra::Minus { left, right } => {
265                let left_cost = self.estimate_cost_recursive(left)?;
266                let right_cost = self.estimate_cost_recursive(right)?;
267                Ok(CostEstimate::new(
268                    left_cost.total_cost + right_cost.total_cost * 2.0,
269                    0.0,
270                    0.0,
271                    0.0,
272                    left_cost.cardinality,
273                ))
274            }
275            Algebra::Service { pattern, .. } => {
276                // Service calls have high latency
277                let pattern_cost = self.estimate_cost_recursive(pattern)?;
278                Ok(CostEstimate::new(
279                    pattern_cost.total_cost,
280                    0.0,
281                    0.0,
282                    pattern_cost.total_cost * 10.0 + 1000.0, // Network overhead
283                    pattern_cost.cardinality,
284                ))
285            }
286            Algebra::Graph { pattern, .. } => {
287                // Graph patterns similar to regular patterns
288                self.estimate_cost_recursive(pattern)
289            }
290            Algebra::Having { pattern, condition } => {
291                let pattern_cost = self.estimate_cost_recursive(pattern)?;
292                let filter_selectivity = self.estimate_expression_selectivity(condition);
293                Ok(CostEstimate::new(
294                    pattern_cost.total_cost * 1.1, // Small overhead for having
295                    0.0,
296                    0.0,
297                    0.0,
298                    (pattern_cost.cardinality as f64 * filter_selectivity) as usize,
299                ))
300            }
301            Algebra::Values { bindings, .. } => {
302                // Values clause cost depends on number of bindings
303                Ok(CostEstimate::new(
304                    bindings.len() as f64 * 0.1,
305                    0.0,
306                    0.0,
307                    0.0,
308                    bindings.len(),
309                ))
310            }
311            Algebra::Table => {
312                // Empty table - minimal cost
313                Ok(CostEstimate::new(0.1, 0.0, 0.0, 0.0, 1))
314            }
315            Algebra::Zero => {
316                // Zero results - minimal cost
317                Ok(CostEstimate::new(0.1, 0.0, 0.0, 0.0, 0))
318            }
319            Algebra::Empty => {
320                // Empty result set - minimal cost
321                Ok(CostEstimate::new(0.1, 0.0, 0.0, 0.0, 0))
322            }
323        }
324    }
325
326    /// Estimate cost of a triple pattern
327    fn estimate_triple_pattern_cost(&self, pattern: &TriplePattern) -> Result<CostEstimate> {
328        // Calculate selectivity based on pattern specificity
329        let selectivity = self.estimate_pattern_selectivity(pattern);
330
331        // Estimate cardinality (would use actual statistics in production)
332        let base_cardinality = 100000; // Assume 100k triples
333        let cardinality = (base_cardinality as f64 * selectivity) as usize;
334
335        // I/O cost depends on access pattern
336        let io_pattern = self.determine_io_pattern(pattern);
337        let pages_accessed = self.estimate_pages_accessed(cardinality, io_pattern);
338        let io_cost =
339            pages_accessed as f64 * self.config.io_cost_per_page * self.config.calibration.io_scale;
340
341        // CPU cost for pattern matching
342        let cpu_cost =
343            cardinality as f64 * self.config.cpu_cost_per_op * self.config.calibration.cpu_scale;
344
345        // Memory cost for buffering results
346        let memory_usage = cardinality * 100; // Assume 100 bytes per triple
347        let memory_cost = memory_usage as f64
348            * self.config.memory_cost_per_byte
349            * self.config.calibration.memory_scale;
350
351        let mut estimate = CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, cardinality)
352            .with_selectivity(selectivity);
353
354        estimate.add_operation_cost("pattern_scan", cpu_cost + io_cost);
355
356        Ok(estimate)
357    }
358
359    /// Estimate cost of a basic graph pattern
360    fn estimate_bgp_cost(&self, patterns: &[TriplePattern]) -> Result<CostEstimate> {
361        if patterns.is_empty() {
362            return Ok(CostEstimate::zero());
363        }
364
365        if patterns.len() == 1 {
366            return self.estimate_triple_pattern_cost(&patterns[0]);
367        }
368
369        // For multiple patterns, estimate as a series of joins
370        let mut total_cost = CostEstimate::zero();
371        let mut current_cardinality = 1;
372
373        for pattern in patterns {
374            let pattern_cost = self.estimate_triple_pattern_cost(pattern)?;
375
376            // Join cost with previous results
377            let join_cost = self.estimate_join_cost_detailed(
378                current_cardinality,
379                pattern_cost.cardinality,
380                0.1, // Assume 10% selectivity
381                JoinAlgorithm::HashJoin,
382            );
383
384            total_cost.cpu_cost += pattern_cost.cpu_cost + join_cost.cpu_cost;
385            total_cost.io_cost += pattern_cost.io_cost + join_cost.io_cost;
386            total_cost.memory_cost += pattern_cost.memory_cost + join_cost.memory_cost;
387
388            current_cardinality = join_cost.cardinality;
389        }
390
391        total_cost.cardinality = current_cardinality;
392        total_cost.total_cost = total_cost.cpu_cost + total_cost.io_cost + total_cost.memory_cost;
393
394        Ok(total_cost)
395    }
396
397    /// Estimate cost of a join operation
398    fn estimate_join_cost(&self, left: &Algebra, right: &Algebra) -> Result<CostEstimate> {
399        let left_cost = self.estimate_cost_recursive(left)?;
400        let right_cost = self.estimate_cost_recursive(right)?;
401
402        // Choose join algorithm based on sizes
403        let algorithm = self.choose_join_algorithm(left_cost.cardinality, right_cost.cardinality);
404
405        // Estimate join selectivity (would be based on actual statistics)
406        let join_selectivity = 0.1; // Assume 10% selectivity
407
408        let join_cost = self.estimate_join_cost_detailed(
409            left_cost.cardinality,
410            right_cost.cardinality,
411            join_selectivity,
412            algorithm,
413        );
414
415        let total_cpu = left_cost.cpu_cost + right_cost.cpu_cost + join_cost.cpu_cost;
416        let total_io = left_cost.io_cost + right_cost.io_cost + join_cost.io_cost;
417        let total_memory =
418            left_cost.memory_cost.max(right_cost.memory_cost) + join_cost.memory_cost;
419
420        let mut estimate = CostEstimate::new(
421            total_cpu,
422            total_io,
423            total_memory,
424            0.0,
425            join_cost.cardinality,
426        );
427        estimate.add_operation_cost("join", join_cost.total_cost);
428
429        Ok(estimate)
430    }
431
432    /// Estimate cost of a left join (optional) operation
433    fn estimate_left_join_cost(&self, left: &Algebra, right: &Algebra) -> Result<CostEstimate> {
434        let left_cost = self.estimate_cost_recursive(left)?;
435        let right_cost = self.estimate_cost_recursive(right)?;
436
437        // Left join preserves all left-side tuples
438        let result_cardinality =
439            left_cost.cardinality + (right_cost.cardinality as f64 * 0.5) as usize;
440
441        // Cost similar to inner join but with additional overhead
442        let base_join_cost = self.estimate_join_cost_detailed(
443            left_cost.cardinality,
444            right_cost.cardinality,
445            0.5, // Higher selectivity for left join
446            self.choose_join_algorithm(left_cost.cardinality, right_cost.cardinality),
447        );
448
449        let total_cpu = left_cost.cpu_cost + right_cost.cpu_cost + base_join_cost.cpu_cost * 1.2;
450        let total_io = left_cost.io_cost + right_cost.io_cost + base_join_cost.io_cost;
451        let total_memory =
452            left_cost.memory_cost.max(right_cost.memory_cost) + base_join_cost.memory_cost;
453
454        Ok(CostEstimate::new(
455            total_cpu,
456            total_io,
457            total_memory,
458            0.0,
459            result_cardinality,
460        ))
461    }
462
463    /// Estimate cost of a union operation
464    fn estimate_union_cost(&self, left: &Algebra, right: &Algebra) -> Result<CostEstimate> {
465        let left_cost = self.estimate_cost_recursive(left)?;
466        let right_cost = self.estimate_cost_recursive(right)?;
467
468        // Union combines both sides
469        let result_cardinality = left_cost.cardinality + right_cost.cardinality;
470
471        // Cost is sum of both sides plus union overhead
472        let union_overhead = (result_cardinality as f64 * 0.1) * self.config.cpu_cost_per_op;
473
474        let total_cpu = left_cost.cpu_cost + right_cost.cpu_cost + union_overhead;
475        let total_io = left_cost.io_cost + right_cost.io_cost;
476        let total_memory = left_cost.memory_cost + right_cost.memory_cost;
477
478        Ok(CostEstimate::new(
479            total_cpu,
480            total_io,
481            total_memory,
482            0.0,
483            result_cardinality,
484        ))
485    }
486
487    /// Estimate cost of a filter operation
488    fn estimate_filter_cost(
489        &self,
490        expression: &Expression,
491        input: &Algebra,
492    ) -> Result<CostEstimate> {
493        let input_cost = self.estimate_cost_recursive(input)?;
494
495        // Estimate filter selectivity
496        let filter_selectivity = self.estimate_expression_selectivity(expression);
497        let result_cardinality = (input_cost.cardinality as f64 * filter_selectivity) as usize;
498
499        // CPU cost for evaluating filter on each tuple
500        let filter_cpu_cost = input_cost.cardinality as f64
501            * self.config.cpu_cost_per_op
502            * self.estimate_expression_complexity(expression);
503
504        let total_cpu = input_cost.cpu_cost + filter_cpu_cost;
505        let total_io = input_cost.io_cost;
506        let total_memory = input_cost.memory_cost;
507
508        Ok(
509            CostEstimate::new(total_cpu, total_io, total_memory, 0.0, result_cardinality)
510                .with_selectivity(filter_selectivity),
511        )
512    }
513
514    /// Estimate cost of a projection operation
515    fn estimate_project_cost(&self, input: &Algebra) -> Result<CostEstimate> {
516        let input_cost = self.estimate_cost_recursive(input)?;
517
518        // Projection doesn't change cardinality but may reduce memory usage
519        let memory_reduction = 0.8; // Assume 20% memory reduction
520        let projection_cpu = input_cost.cardinality as f64 * self.config.cpu_cost_per_op * 0.1;
521
522        let total_cpu = input_cost.cpu_cost + projection_cpu;
523        let total_memory = input_cost.memory_cost * memory_reduction;
524
525        Ok(CostEstimate::new(
526            total_cpu,
527            input_cost.io_cost,
528            total_memory,
529            0.0,
530            input_cost.cardinality,
531        ))
532    }
533
534    /// Estimate cost of an extend (BIND) operation
535    fn estimate_extend_cost(
536        &self,
537        expression: &Expression,
538        input: &Algebra,
539    ) -> Result<CostEstimate> {
540        let input_cost = self.estimate_cost_recursive(input)?;
541
542        // Cost of evaluating expression for each tuple
543        let expression_cost = input_cost.cardinality as f64
544            * self.config.cpu_cost_per_op
545            * self.estimate_expression_complexity(expression);
546
547        let total_cpu = input_cost.cpu_cost + expression_cost;
548
549        Ok(CostEstimate::new(
550            total_cpu,
551            input_cost.io_cost,
552            input_cost.memory_cost,
553            0.0,
554            input_cost.cardinality,
555        ))
556    }
557
558    /// Estimate cost of a distinct operation
559    fn estimate_distinct_cost(&self, input: &Algebra) -> Result<CostEstimate> {
560        let input_cost = self.estimate_cost_recursive(input)?;
561
562        // Distinct requires sorting or hashing
563        let distinct_cardinality = (input_cost.cardinality as f64 * 0.8) as usize; // Assume 20% duplicates
564        let sort_cost = input_cost.cardinality as f64
565            * (input_cost.cardinality as f64).log2()
566            * self.config.cpu_cost_per_op;
567
568        let total_cpu = input_cost.cpu_cost + sort_cost;
569        let total_memory = input_cost.memory_cost * 1.5; // Additional memory for sorting
570
571        Ok(CostEstimate::new(
572            total_cpu,
573            input_cost.io_cost,
574            total_memory,
575            0.0,
576            distinct_cardinality,
577        ))
578    }
579
580    /// Estimate cost of a reduced operation
581    fn estimate_reduced_cost(&self, input: &Algebra) -> Result<CostEstimate> {
582        // Similar to distinct but with less strict requirements
583        let input_cost = self.estimate_cost_recursive(input)?;
584        let reduced_cpu = input_cost.cardinality as f64 * self.config.cpu_cost_per_op * 0.1;
585
586        let total_cpu = input_cost.cpu_cost + reduced_cpu;
587
588        Ok(CostEstimate::new(
589            total_cpu,
590            input_cost.io_cost,
591            input_cost.memory_cost,
592            0.0,
593            input_cost.cardinality,
594        ))
595    }
596
597    /// Estimate cost of an order by operation
598    fn estimate_order_by_cost(&self, input: &Algebra) -> Result<CostEstimate> {
599        let input_cost = self.estimate_cost_recursive(input)?;
600
601        // Sorting cost
602        let sort_cost = input_cost.cardinality as f64
603            * (input_cost.cardinality as f64).log2()
604            * self.config.cpu_cost_per_op;
605
606        let total_cpu = input_cost.cpu_cost + sort_cost;
607        let total_memory = input_cost.memory_cost * 2.0; // Additional memory for sorting
608
609        Ok(CostEstimate::new(
610            total_cpu,
611            input_cost.io_cost,
612            total_memory,
613            0.0,
614            input_cost.cardinality,
615        ))
616    }
617
618    /// Estimate cost of a slice (LIMIT/OFFSET) operation
619    fn estimate_slice_cost(
620        &self,
621        input: &Algebra,
622        offset: Option<usize>,
623        limit: Option<usize>,
624    ) -> Result<CostEstimate> {
625        let input_cost = self.estimate_cost_recursive(input)?;
626
627        let offset_val = offset.unwrap_or(0);
628        let limit_val = limit.unwrap_or(input_cost.cardinality);
629        let result_cardinality = limit_val.min(input_cost.cardinality.saturating_sub(offset_val));
630
631        // Slice doesn't add significant CPU cost
632        let slice_cpu = result_cardinality as f64 * self.config.cpu_cost_per_op * 0.01;
633
634        Ok(CostEstimate::new(
635            input_cost.cpu_cost + slice_cpu,
636            input_cost.io_cost,
637            input_cost.memory_cost,
638            0.0,
639            result_cardinality,
640        ))
641    }
642
643    /// Estimate cost of a group by operation
644    fn estimate_group_cost(&self, input: &Algebra) -> Result<CostEstimate> {
645        let input_cost = self.estimate_cost_recursive(input)?;
646
647        // Grouping requires sorting or hashing
648        let group_cardinality = (input_cost.cardinality as f64 * 0.1) as usize; // Assume 10 groups per 100 tuples
649        let group_cost = input_cost.cardinality as f64
650            * (input_cost.cardinality as f64).log2()
651            * self.config.cpu_cost_per_op;
652
653        let total_cpu = input_cost.cpu_cost + group_cost;
654        let total_memory = input_cost.memory_cost * 1.5;
655
656        Ok(CostEstimate::new(
657            total_cpu,
658            input_cost.io_cost,
659            total_memory,
660            0.0,
661            group_cardinality,
662        ))
663    }
664
665    /// Detailed join cost estimation with algorithm selection
666    fn estimate_join_cost_detailed(
667        &self,
668        left_cardinality: usize,
669        right_cardinality: usize,
670        selectivity: f64,
671        algorithm: JoinAlgorithm,
672    ) -> CostEstimate {
673        let result_cardinality =
674            (left_cardinality as f64 * right_cardinality as f64 * selectivity) as usize;
675
676        match algorithm {
677            JoinAlgorithm::HashJoin => {
678                let build_cost =
679                    left_cardinality.min(right_cardinality) as f64 * self.config.cpu_cost_per_op;
680                let probe_cost =
681                    left_cardinality.max(right_cardinality) as f64 * self.config.cpu_cost_per_op;
682                let cpu_cost = (build_cost + probe_cost)
683                    * self.config.calibration.join_factors.hash_join_factor;
684
685                let memory_cost = (left_cardinality.min(right_cardinality) * 50) as f64
686                    * self.config.memory_cost_per_byte;
687                let io_cost = 0.0; // Hash join is memory-based
688
689                CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, result_cardinality)
690            }
691            JoinAlgorithm::SortMergeJoin => {
692                let sort_cost_left = left_cardinality as f64
693                    * (left_cardinality as f64).log2()
694                    * self.config.cpu_cost_per_op;
695                let sort_cost_right = right_cardinality as f64
696                    * (right_cardinality as f64).log2()
697                    * self.config.cpu_cost_per_op;
698                let merge_cost =
699                    (left_cardinality + right_cardinality) as f64 * self.config.cpu_cost_per_op;
700                let cpu_cost = (sort_cost_left + sort_cost_right + merge_cost)
701                    * self.config.calibration.join_factors.sort_merge_join_factor;
702
703                let memory_cost = (left_cardinality + right_cardinality) as f64
704                    * 50.0
705                    * self.config.memory_cost_per_byte;
706                let io_cost = 0.0;
707
708                CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, result_cardinality)
709            }
710            JoinAlgorithm::NestedLoopJoin => {
711                let cpu_cost = (left_cardinality as f64 * right_cardinality as f64)
712                    * self.config.cpu_cost_per_op
713                    * self.config.calibration.join_factors.nested_loop_join_factor;
714                let memory_cost = 1000.0 * self.config.memory_cost_per_byte; // Minimal memory usage
715                let io_cost = 0.0;
716
717                CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, result_cardinality)
718            }
719            JoinAlgorithm::IndexJoin => {
720                let cpu_cost = (left_cardinality as f64 * (right_cardinality as f64).log2())
721                    * self.config.cpu_cost_per_op
722                    * self.config.calibration.join_factors.index_join_factor;
723                let io_cost = left_cardinality as f64 * self.config.io_cost_per_page * 0.1; // Index lookups
724                let memory_cost = 5000.0 * self.config.memory_cost_per_byte;
725
726                CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, result_cardinality)
727            }
728        }
729    }
730
731    /// Choose optimal join algorithm based on input sizes
732    fn choose_join_algorithm(&self, left_size: usize, right_size: usize) -> JoinAlgorithm {
733        let smaller = left_size.min(right_size);
734        let larger = left_size.max(right_size);
735
736        if smaller < 1000 {
737            JoinAlgorithm::NestedLoopJoin
738        } else if smaller < 10000 && larger > smaller * 10 {
739            JoinAlgorithm::IndexJoin
740        } else if smaller * 8 < self.config.available_memory / 100 {
741            JoinAlgorithm::HashJoin
742        } else {
743            JoinAlgorithm::SortMergeJoin
744        }
745    }
746
747    /// Estimate pattern selectivity based on specificity
748    fn estimate_pattern_selectivity(&self, pattern: &TriplePattern) -> f64 {
749        let mut specificity = 0;
750
751        if !matches!(pattern.subject, crate::algebra::Term::Variable(_)) {
752            specificity += 1;
753        }
754        if !matches!(pattern.predicate, crate::algebra::Term::Variable(_)) {
755            specificity += 1;
756        }
757        if !matches!(pattern.object, crate::algebra::Term::Variable(_)) {
758            specificity += 1;
759        }
760
761        match specificity {
762            0 => 1.0,   // All variables
763            1 => 0.1,   // One constant
764            2 => 0.01,  // Two constants
765            3 => 0.001, // All constants
766            _ => 0.0001,
767        }
768    }
769
770    /// Determine I/O access pattern for a triple pattern
771    fn determine_io_pattern(&self, pattern: &TriplePattern) -> IOPattern {
772        // Simple heuristic based on pattern structure
773        if matches!(pattern.subject, crate::algebra::Term::Variable(_))
774            && matches!(pattern.predicate, crate::algebra::Term::Variable(_))
775            && matches!(pattern.object, crate::algebra::Term::Variable(_))
776        {
777            IOPattern::FullScan
778        } else if !matches!(pattern.predicate, crate::algebra::Term::Variable(_)) {
779            IOPattern::IndexScan
780        } else {
781            IOPattern::Random
782        }
783    }
784
785    /// Estimate number of pages accessed
786    fn estimate_pages_accessed(&self, cardinality: usize, pattern: IOPattern) -> usize {
787        let bytes_per_triple = 100; // Estimated average
788        let total_bytes = cardinality * bytes_per_triple;
789        let pages = (total_bytes + self.config.page_size - 1) / self.config.page_size;
790
791        match pattern {
792            IOPattern::Sequential => pages,
793            IOPattern::Random => pages * 2, // Random access penalty
794            IOPattern::IndexScan => pages / 2, // Index efficiency
795            IOPattern::FullScan => pages,
796        }
797    }
798
799    /// Estimate expression selectivity
800    fn estimate_expression_selectivity(&self, expression: &Expression) -> f64 {
801        match expression {
802            Expression::Binary { op: operator, .. } => match operator {
803                crate::algebra::BinaryOperator::Equal => 0.1,
804                crate::algebra::BinaryOperator::NotEqual => 0.9,
805                crate::algebra::BinaryOperator::Less => 0.3,
806                crate::algebra::BinaryOperator::LessEqual => 0.4,
807                crate::algebra::BinaryOperator::Greater => 0.3,
808                crate::algebra::BinaryOperator::GreaterEqual => 0.4,
809                _ => 0.5,
810            },
811            Expression::Function { name, .. } => match name.as_str() {
812                "contains" => 0.2,
813                "startsWith" => 0.1,
814                "endsWith" => 0.1,
815                "regex" => 0.05,
816                _ => 0.5,
817            },
818            _ => 0.5, // Default selectivity
819        }
820    }
821
822    /// Estimate expression complexity for CPU cost
823    fn estimate_expression_complexity(&self, expression: &Expression) -> f64 {
824        match expression {
825            Expression::Variable(_) | Expression::Literal(_) => 1.0,
826            Expression::Binary { .. } => 2.0,
827            Expression::Unary { .. } => 1.5,
828            Expression::Function { name, args } => {
829                let base_cost = match name.as_str() {
830                    "regex" => 10.0,
831                    "contains" => 3.0,
832                    "startsWith" | "endsWith" => 2.0,
833                    _ => 5.0,
834                };
835                base_cost + args.len() as f64
836            }
837            Expression::Conditional { .. } => 3.0,
838            _ => 2.0,
839        }
840    }
841
842    /// Generate a cache key for an algebra expression
843    fn algebra_to_key(&self, algebra: &Algebra) -> String {
844        // Simple implementation - in production would use better hashing
845        format!("{algebra:?}")
846    }
847
848    /// Clear the cost estimation cache
849    pub fn clear_cache(&mut self) {
850        self.cached_estimates.clear();
851    }
852
853    /// Update cost model with execution feedback
854    pub fn update_with_feedback(
855        &mut self,
856        algebra: &Algebra,
857        actual_cost: f64,
858        actual_cardinality: usize,
859    ) {
860        // Update calibration factors based on actual vs predicted costs
861        let predicted = self.estimate_cost(algebra).unwrap_or(CostEstimate::zero());
862
863        if predicted.total_cost > 0.0 {
864            let cost_ratio = actual_cost / predicted.total_cost;
865            let _cardinality_ratio = actual_cardinality as f64 / predicted.cardinality as f64;
866
867            // Adjust calibration factors (simple approach)
868            self.config.calibration.cpu_scale =
869                (self.config.calibration.cpu_scale + cost_ratio) / 2.0;
870
871            // Clear cache to force recalculation with new factors
872            self.clear_cache();
873        }
874    }
875}
876
877/// Available join algorithms
878#[derive(Debug, Clone, PartialEq, Eq)]
879pub enum JoinAlgorithm {
880    HashJoin,
881    SortMergeJoin,
882    NestedLoopJoin,
883    IndexJoin,
884}
885
886#[cfg(test)]
887mod tests {
888    use super::*;
889    use crate::algebra::{Term, Variable};
890    use oxirs_core::model::NamedNode;
891
892    #[test]
893    fn test_triple_pattern_cost_estimation() {
894        let cost_model = CostModel::new(CostModelConfig::default());
895
896        let pattern = TriplePattern {
897            subject: Term::Variable(Variable::new("s").unwrap()),
898            predicate: Term::Iri(NamedNode::new_unchecked("http://example.org/predicate")),
899            object: Term::Variable(Variable::new("o").unwrap()),
900        };
901
902        let cost = cost_model.estimate_triple_pattern_cost(&pattern).unwrap();
903
904        assert!(cost.total_cost > 0.0);
905        assert!(cost.cardinality > 0);
906        assert!(cost.selectivity > 0.0 && cost.selectivity <= 1.0);
907    }
908
909    #[test]
910    fn test_join_algorithm_selection() {
911        let cost_model = CostModel::new(CostModelConfig::default());
912
913        assert_eq!(
914            cost_model.choose_join_algorithm(100, 1000),
915            JoinAlgorithm::NestedLoopJoin
916        );
917        assert_eq!(
918            cost_model.choose_join_algorithm(10000, 100000),
919            JoinAlgorithm::HashJoin
920        );
921    }
922
923    #[test]
924    fn test_cost_estimate_operations() {
925        let mut estimate = CostEstimate::new(10.0, 5.0, 2.0, 1.0, 1000);
926        estimate.add_operation_cost("test_op", 3.0);
927
928        assert_eq!(estimate.total_cost, 21.0); // 10+5+2+1+3
929        assert!(estimate.operation_costs.contains_key("test_op"));
930    }
931
932    #[test]
933    fn test_pattern_selectivity_estimation() {
934        let cost_model = CostModel::new(CostModelConfig::default());
935
936        // All variables
937        let pattern1 = TriplePattern {
938            subject: Term::Variable(Variable::new("s").unwrap()),
939            predicate: Term::Variable(Variable::new("p").unwrap()),
940            object: Term::Variable(Variable::new("o").unwrap()),
941        };
942
943        let selectivity1 = cost_model.estimate_pattern_selectivity(&pattern1);
944        assert_eq!(selectivity1, 1.0);
945
946        // One constant
947        let pattern2 = TriplePattern {
948            subject: Term::Variable(Variable::new("s").unwrap()),
949            predicate: Term::Iri(NamedNode::new_unchecked("http://example.org/predicate")),
950            object: Term::Variable(Variable::new("o").unwrap()),
951        };
952
953        let selectivity2 = cost_model.estimate_pattern_selectivity(&pattern2);
954        assert_eq!(selectivity2, 0.1);
955    }
956}