Skip to main content

oxirs_arq/
join_algorithms.rs

1//! Advanced Join Algorithm Selection and Execution
2//!
3//! This module provides intelligent join algorithm selection based on cost estimates
4//! and adaptive execution strategies for optimal performance.
5//!
6//! # Advanced SciRS2 Integration
7//!
8//! - **SIMD Hash Computation**: Vectorized hash calculations for join keys
9//! - **Parallel Processing**: Multi-threaded join execution with work stealing
10//! - **Cache-Friendly Algorithms**: Optimized memory access patterns
11//! - **Adaptive Selection**: Runtime algorithm selection based on data characteristics
12
13use crate::algebra::{Solution, Variable};
14use crate::cost_model::CostModel;
15use crate::executor::parallel_optimized::{CacheFriendlyHashJoin, SortMergeJoin};
16use anyhow::Result;
17use std::collections::HashSet;
18
19/// Intelligent join algorithm selector
20pub struct JoinAlgorithmSelector {
21    #[allow(dead_code)]
22    cost_model: CostModel,
23    hash_join: CacheFriendlyHashJoin,
24    sort_merge_join: SortMergeJoin,
25    memory_threshold: usize,
26}
27
28impl JoinAlgorithmSelector {
29    /// Create a new join algorithm selector
30    pub fn new(cost_model: CostModel, memory_threshold: usize) -> Self {
31        Self {
32            cost_model,
33            hash_join: CacheFriendlyHashJoin::new(16), // 16 partitions
34            sort_merge_join: SortMergeJoin::new(memory_threshold),
35            memory_threshold,
36        }
37    }
38
39    /// Select and execute optimal join algorithm
40    pub fn execute_optimal_join(
41        &mut self,
42        left_solutions: Vec<Solution>,
43        right_solutions: Vec<Solution>,
44        join_variables: &[Variable],
45    ) -> Result<(Vec<Solution>, JoinExecutionStats)> {
46        let start_time = std::time::Instant::now();
47
48        // Analyze join characteristics
49        let join_info =
50            self.analyze_join_characteristics(&left_solutions, &right_solutions, join_variables);
51
52        // Select optimal algorithm
53        let selected_algorithm = self.select_join_algorithm(&join_info)?;
54
55        // Execute selected algorithm
56        let result = match selected_algorithm {
57            OptimalJoinAlgorithm::HashJoin => {
58                self.hash_join
59                    .join_parallel(left_solutions, right_solutions, join_variables)?
60            }
61            OptimalJoinAlgorithm::SortMergeJoin => {
62                self.sort_merge_join
63                    .join(left_solutions, right_solutions, join_variables)?
64            }
65            OptimalJoinAlgorithm::NestedLoopJoin => {
66                self.execute_nested_loop_join(left_solutions, right_solutions, join_variables)?
67            }
68            OptimalJoinAlgorithm::IndexJoin => {
69                // Fall back to hash join for now
70                self.hash_join
71                    .join_parallel(left_solutions, right_solutions, join_variables)?
72            }
73        };
74
75        let execution_time = start_time.elapsed();
76        let stats = JoinExecutionStats {
77            algorithm_used: selected_algorithm,
78            execution_time,
79            input_cardinalities: (join_info.left_cardinality, join_info.right_cardinality),
80            output_cardinality: result.len(),
81            memory_used: self.estimate_memory_usage(&result),
82            join_selectivity: result.len() as f64
83                / (join_info.left_cardinality as f64 * join_info.right_cardinality as f64).max(1.0),
84        };
85
86        Ok((result, stats))
87    }
88
89    /// Analyze characteristics of the join operation
90    fn analyze_join_characteristics(
91        &self,
92        left_solutions: &[Solution],
93        right_solutions: &[Solution],
94        join_variables: &[Variable],
95    ) -> JoinCharacteristics {
96        let left_cardinality = left_solutions.len();
97        let right_cardinality = right_solutions.len();
98
99        // Estimate selectivity based on distinct values in join columns
100        let left_distinct = self.estimate_distinct_values(left_solutions, join_variables);
101        let right_distinct = self.estimate_distinct_values(right_solutions, join_variables);
102
103        let estimated_selectivity = if left_distinct > 0 && right_distinct > 0 {
104            1.0 / (left_distinct.max(right_distinct) as f64)
105        } else {
106            0.1 // Default selectivity
107        };
108
109        // Check if data is pre-sorted
110        let left_sorted = self.is_sorted_by_join_keys(left_solutions, join_variables);
111        let right_sorted = self.is_sorted_by_join_keys(right_solutions, join_variables);
112
113        // Estimate memory requirements
114        let memory_requirement = (left_cardinality + right_cardinality) * 100; // Rough estimate
115
116        JoinCharacteristics {
117            left_cardinality,
118            right_cardinality,
119            left_distinct_values: left_distinct,
120            right_distinct_values: right_distinct,
121            estimated_selectivity,
122            left_sorted,
123            right_sorted,
124            memory_requirement,
125            join_variable_count: join_variables.len(),
126        }
127    }
128
129    /// Select optimal join algorithm based on characteristics
130    fn select_join_algorithm(
131        &mut self,
132        join_info: &JoinCharacteristics,
133    ) -> Result<OptimalJoinAlgorithm> {
134        // Rule-based selection with cost model validation
135        let candidate_algorithm =
136            if join_info.left_cardinality < 1000 || join_info.right_cardinality < 1000 {
137                // Small inputs: nested loop is often fastest
138                OptimalJoinAlgorithm::NestedLoopJoin
139            } else if join_info.left_sorted && join_info.right_sorted {
140                // Both sides sorted: sort-merge is optimal
141                OptimalJoinAlgorithm::SortMergeJoin
142            } else if join_info.memory_requirement > self.memory_threshold {
143                // Large memory requirement: sort-merge with external sorting
144                OptimalJoinAlgorithm::SortMergeJoin
145            } else if join_info.estimated_selectivity < 0.01 {
146                // Very selective join: hash join is good
147                OptimalJoinAlgorithm::HashJoin
148            } else {
149                // Default: hash join
150                OptimalJoinAlgorithm::HashJoin
151            };
152
153        // Validate with cost model and potentially override if significant improvement
154        let final_algorithm = self.validate_with_cost_model(candidate_algorithm, join_info)?;
155
156        Ok(final_algorithm)
157    }
158
159    /// Validate candidate algorithm with cost model
160    ///
161    /// Calculates costs for all applicable algorithms and overrides the candidate
162    /// if the cost model suggests a significantly better alternative (>20% improvement).
163    fn validate_with_cost_model(
164        &self,
165        candidate: OptimalJoinAlgorithm,
166        join_info: &JoinCharacteristics,
167    ) -> Result<OptimalJoinAlgorithm> {
168        // Calculate cost for all applicable algorithms
169        let mut algorithm_costs = Vec::new();
170
171        // Hash Join cost
172        let hash_join_cost = self.estimate_hash_join_cost(join_info);
173        algorithm_costs.push((OptimalJoinAlgorithm::HashJoin, hash_join_cost));
174
175        // Sort-Merge Join cost
176        let sort_merge_cost = self.estimate_sort_merge_cost(join_info);
177        algorithm_costs.push((OptimalJoinAlgorithm::SortMergeJoin, sort_merge_cost));
178
179        // Nested Loop Join cost (only for small inputs)
180        if join_info.left_cardinality < 10000 && join_info.right_cardinality < 10000 {
181            let nested_loop_cost = self.estimate_nested_loop_cost(join_info);
182            algorithm_costs.push((OptimalJoinAlgorithm::NestedLoopJoin, nested_loop_cost));
183        }
184
185        // Find minimum cost algorithm
186        let (optimal_algorithm, optimal_cost) = algorithm_costs
187            .iter()
188            .min_by(|(_, cost1), (_, cost2)| {
189                cost1
190                    .partial_cmp(cost2)
191                    .unwrap_or(std::cmp::Ordering::Equal)
192            })
193            .copied()
194            .unwrap_or((candidate, f64::MAX));
195
196        // Get candidate cost
197        let candidate_cost = algorithm_costs
198            .iter()
199            .find(|(algo, _)| *algo == candidate)
200            .map(|(_, cost)| *cost)
201            .unwrap_or(optimal_cost);
202
203        // Override if cost model suggests >20% improvement
204        let improvement_threshold = 0.20;
205        if optimal_cost < candidate_cost * (1.0 - improvement_threshold) {
206            tracing::debug!(
207                "Cost model override: {:?} -> {:?} (cost: {:.2} -> {:.2}, {:.1}% improvement)",
208                candidate,
209                optimal_algorithm,
210                candidate_cost,
211                optimal_cost,
212                (candidate_cost - optimal_cost) / candidate_cost * 100.0
213            );
214            Ok(optimal_algorithm)
215        } else {
216            // Stick with rule-based selection
217            Ok(candidate)
218        }
219    }
220
221    /// Estimate cost of hash join
222    fn estimate_hash_join_cost(&self, join_info: &JoinCharacteristics) -> f64 {
223        // Cost model: build phase + probe phase
224        // Build: scan smaller relation and build hash table
225        // Probe: scan larger relation and probe hash table
226        let build_cost = join_info.left_cardinality.min(join_info.right_cardinality) as f64;
227        let probe_cost = join_info.left_cardinality.max(join_info.right_cardinality) as f64;
228
229        // Hash table lookup cost (assume O(1) average case)
230        let lookup_cost = probe_cost;
231
232        // Output cost
233        let output_cost = (join_info.left_cardinality as f64
234            * join_info.right_cardinality as f64
235            * join_info.estimated_selectivity)
236            .max(1.0);
237
238        build_cost + lookup_cost + output_cost
239    }
240
241    /// Estimate cost of sort-merge join
242    fn estimate_sort_merge_cost(&self, join_info: &JoinCharacteristics) -> f64 {
243        // Cost model: sort both relations + merge
244        let left_sort_cost = if join_info.left_sorted {
245            0.0
246        } else {
247            join_info.left_cardinality as f64 * (join_info.left_cardinality as f64).log2()
248        };
249
250        let right_sort_cost = if join_info.right_sorted {
251            0.0
252        } else {
253            join_info.right_cardinality as f64 * (join_info.right_cardinality as f64).log2()
254        };
255
256        // Merge cost: linear scan of both sorted relations
257        let merge_cost = (join_info.left_cardinality + join_info.right_cardinality) as f64;
258
259        // Output cost
260        let output_cost = (join_info.left_cardinality as f64
261            * join_info.right_cardinality as f64
262            * join_info.estimated_selectivity)
263            .max(1.0);
264
265        left_sort_cost + right_sort_cost + merge_cost + output_cost
266    }
267
268    /// Estimate cost of nested loop join
269    fn estimate_nested_loop_cost(&self, join_info: &JoinCharacteristics) -> f64 {
270        // Cost model: for each tuple in left, scan all tuples in right
271        let scan_cost = join_info.left_cardinality as f64 * join_info.right_cardinality as f64;
272
273        // Output cost
274        let output_cost = scan_cost * join_info.estimated_selectivity;
275
276        scan_cost + output_cost
277    }
278
279    /// Estimate number of distinct values in join columns
280    fn estimate_distinct_values(
281        &self,
282        solutions: &[Solution],
283        join_variables: &[Variable],
284    ) -> usize {
285        let mut distinct_values = HashSet::new();
286
287        for solution in solutions {
288            for binding in solution {
289                for var in join_variables {
290                    if let Some(term) = binding.get(var) {
291                        distinct_values.insert(term.clone());
292                    }
293                }
294            }
295        }
296
297        distinct_values.len()
298    }
299
300    /// Check if solutions are sorted by join key variables
301    fn is_sorted_by_join_keys(&self, solutions: &[Solution], join_variables: &[Variable]) -> bool {
302        if solutions.len() <= 1 {
303            return true;
304        }
305
306        // Check a sample of the data for sortedness
307        let sample_size = (solutions.len() / 10).clamp(10, 100);
308        let step = solutions.len() / sample_size;
309
310        for i in 1..sample_size {
311            let idx = i * step;
312            if idx >= solutions.len() {
313                break;
314            }
315
316            if self.compare_solutions_by_join_key(
317                &solutions[idx - step],
318                &solutions[idx],
319                join_variables,
320            ) == std::cmp::Ordering::Greater
321            {
322                return false;
323            }
324        }
325
326        true
327    }
328
329    /// Compare solutions by join key (simplified version)
330    fn compare_solutions_by_join_key(
331        &self,
332        left: &Solution,
333        right: &Solution,
334        join_variables: &[Variable],
335    ) -> std::cmp::Ordering {
336        use std::cmp::Ordering;
337
338        let left_binding = left.first();
339        let right_binding = right.first();
340
341        match (left_binding, right_binding) {
342            (Some(l_binding), Some(r_binding)) => {
343                for var in join_variables {
344                    let left_term = l_binding.get(var);
345                    let right_term = r_binding.get(var);
346
347                    let cmp = match (left_term, right_term) {
348                        (Some(l), Some(r)) => {
349                            // Simple string comparison for now
350                            format!("{l}").cmp(&format!("{r}"))
351                        }
352                        (Some(_), None) => Ordering::Greater,
353                        (None, Some(_)) => Ordering::Less,
354                        (None, None) => Ordering::Equal,
355                    };
356
357                    if cmp != Ordering::Equal {
358                        return cmp;
359                    }
360                }
361                Ordering::Equal
362            }
363            (Some(_), None) => Ordering::Greater,
364            (None, Some(_)) => Ordering::Less,
365            (None, None) => Ordering::Equal,
366        }
367    }
368
369    /// Execute nested loop join
370    fn execute_nested_loop_join(
371        &self,
372        left_solutions: Vec<Solution>,
373        right_solutions: Vec<Solution>,
374        join_variables: &[Variable],
375    ) -> Result<Vec<Solution>> {
376        let mut result = Vec::new();
377
378        for left_solution in &left_solutions {
379            for right_solution in &right_solutions {
380                if let Some(merged) =
381                    self.try_merge_solutions(left_solution, right_solution, join_variables)?
382                {
383                    result.push(merged);
384                }
385            }
386        }
387
388        Ok(result)
389    }
390
391    /// Try to merge two solutions if they are compatible on join variables
392    fn try_merge_solutions(
393        &self,
394        left: &Solution,
395        right: &Solution,
396        join_variables: &[Variable],
397    ) -> Result<Option<Solution>> {
398        let mut result = Vec::new();
399
400        for left_binding in left {
401            for right_binding in right {
402                // Check compatibility on join variables
403                let mut compatible = true;
404                for var in join_variables {
405                    if let (Some(left_term), Some(right_term)) =
406                        (left_binding.get(var), right_binding.get(var))
407                    {
408                        if left_term != right_term {
409                            compatible = false;
410                            break;
411                        }
412                    }
413                }
414
415                if compatible {
416                    // Merge bindings
417                    let mut merged_binding = left_binding.clone();
418                    for (var, term) in right_binding {
419                        // Only add if not already present (join variables will be the same)
420                        if !merged_binding.contains_key(var) {
421                            merged_binding.insert(var.clone(), term.clone());
422                        }
423                    }
424                    result.push(merged_binding);
425                }
426            }
427        }
428
429        if result.is_empty() {
430            Ok(None)
431        } else {
432            Ok(Some(result))
433        }
434    }
435
436    /// Estimate memory usage of a solution set
437    fn estimate_memory_usage(&self, solutions: &[Solution]) -> usize {
438        solutions.len() * 1024 // Rough estimate: 1KB per solution
439    }
440}
441
442/// Join algorithm options
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum OptimalJoinAlgorithm {
445    HashJoin,
446    SortMergeJoin,
447    NestedLoopJoin,
448    IndexJoin,
449}
450
451/// Join characteristics for algorithm selection
452#[derive(Debug, Clone)]
453pub struct JoinCharacteristics {
454    pub left_cardinality: usize,
455    pub right_cardinality: usize,
456    pub left_distinct_values: usize,
457    pub right_distinct_values: usize,
458    pub estimated_selectivity: f64,
459    pub left_sorted: bool,
460    pub right_sorted: bool,
461    pub memory_requirement: usize,
462    pub join_variable_count: usize,
463}
464
465/// Join execution statistics
466#[derive(Debug, Clone)]
467pub struct JoinExecutionStats {
468    pub algorithm_used: OptimalJoinAlgorithm,
469    pub execution_time: std::time::Duration,
470    pub input_cardinalities: (usize, usize),
471    pub output_cardinality: usize,
472    pub memory_used: usize,
473    pub join_selectivity: f64,
474}
475
476impl JoinExecutionStats {
477    /// Get performance metrics as a human-readable string
478    pub fn performance_summary(&self) -> String {
479        format!(
480            "Algorithm: {:?}, Time: {:?}, Input: ({}, {}), Output: {}, Selectivity: {:.4}, Memory: {} bytes",
481            self.algorithm_used,
482            self.execution_time,
483            self.input_cardinalities.0,
484            self.input_cardinalities.1,
485            self.output_cardinality,
486            self.join_selectivity,
487            self.memory_used
488        )
489    }
490}
491
492/// Parallel Hash Join Accelerator
493///
494/// Provides parallelized hash calculation and join operations for high performance.
495#[cfg(feature = "parallel")]
496pub struct ParallelHashJoinAccelerator;
497
498#[cfg(feature = "parallel")]
499impl Default for ParallelHashJoinAccelerator {
500    fn default() -> Self {
501        Self::new()
502    }
503}
504
505#[cfg(feature = "parallel")]
506impl ParallelHashJoinAccelerator {
507    /// Create new parallel hash join accelerator
508    pub fn new() -> Self {
509        Self
510    }
511
512    /// Compute hash values for multiple join keys in parallel
513    ///
514    /// Returns hash codes computed in parallel for efficient hash table lookup.
515    pub fn compute_hashes_parallel(&self, keys: &[String]) -> Result<Vec<u64>> {
516        use rayon::prelude::*;
517        use std::collections::hash_map::DefaultHasher;
518        use std::hash::{Hash, Hasher};
519
520        // Parallel hash computation using rayon
521        let hashes: Vec<u64> = keys
522            .par_iter()
523            .map(|k| {
524                let mut hasher = DefaultHasher::new();
525                k.hash(&mut hasher);
526                hasher.finish()
527            })
528            .collect();
529
530        Ok(hashes)
531    }
532
533    /// Perform parallel equi-join on sequences
534    ///
535    /// Uses parallel processing for faster matching with cache-friendly chunking.
536    pub fn parallel_equi_join(
537        &self,
538        left_keys: &[u64],
539        right_keys: &[u64],
540    ) -> Result<Vec<(usize, usize)>> {
541        use rayon::prelude::*;
542
543        // Process in cache-friendly chunks
544        const CHUNK_SIZE: usize = 64;
545
546        // Build hash table from right side (smaller if possible)
547        let right_map: std::collections::HashMap<u64, Vec<usize>> = {
548            let mut map = std::collections::HashMap::new();
549            for (idx, &key) in right_keys.iter().enumerate() {
550                map.entry(key).or_insert_with(Vec::new).push(idx);
551            }
552            map
553        };
554
555        // Parallel probe from left side
556        let matches: Vec<Vec<(usize, usize)>> = left_keys
557            .par_chunks(CHUNK_SIZE)
558            .enumerate()
559            .map(|(chunk_idx, chunk)| {
560                let mut chunk_matches = Vec::new();
561                for (offset, &key) in chunk.iter().enumerate() {
562                    if let Some(right_indices) = right_map.get(&key) {
563                        let left_idx = chunk_idx * CHUNK_SIZE + offset;
564                        for &right_idx in right_indices {
565                            chunk_matches.push((left_idx, right_idx));
566                        }
567                    }
568                }
569                chunk_matches
570            })
571            .collect();
572
573        // Flatten results
574        Ok(matches.into_iter().flatten().collect())
575    }
576
577    /// Parallel partition-based join for large datasets
578    ///
579    /// Splits data into partitions and processes them in parallel for optimal cache usage.
580    pub fn parallel_partition_join(
581        &self,
582        left: Vec<(u64, usize)>,
583        right: Vec<(u64, usize)>,
584        num_partitions: usize,
585    ) -> Result<Vec<(usize, usize)>> {
586        use rayon::prelude::*;
587        use std::sync::Arc;
588
589        // Partition data for parallel processing
590        let partition_mask = num_partitions - 1;
591        let mut left_partitions: Vec<Vec<(u64, usize)>> = vec![Vec::new(); num_partitions];
592        let mut right_partitions: Vec<Vec<(u64, usize)>> = vec![Vec::new(); num_partitions];
593
594        // Distribute to partitions
595        for (key, idx) in left {
596            let partition = (key as usize) & partition_mask;
597            left_partitions[partition].push((key, idx));
598        }
599        for (key, idx) in right {
600            let partition = (key as usize) & partition_mask;
601            right_partitions[partition].push((key, idx));
602        }
603
604        // Move to Arc for safe sharing
605        let right_partitions = Arc::new(right_partitions);
606
607        // Process partitions in parallel
608        let matches: Vec<Vec<(usize, usize)>> = left_partitions
609            .into_par_iter()
610            .enumerate()
611            .map(|(p, left_partition)| {
612                let mut partition_matches = Vec::new();
613                let left_keys: Vec<u64> = left_partition.iter().map(|(k, _)| *k).collect();
614                let right_keys: Vec<u64> = right_partitions[p].iter().map(|(k, _)| *k).collect();
615
616                // Use parallel join for partition
617                if let Ok(local_matches) = self.parallel_equi_join(&left_keys, &right_keys) {
618                    for (l, r) in local_matches {
619                        if l < left_partition.len() && r < right_partitions[p].len() {
620                            partition_matches.push((left_partition[l].1, right_partitions[p][r].1));
621                        }
622                    }
623                }
624                partition_matches
625            })
626            .collect();
627
628        // Flatten results
629        Ok(matches.into_iter().flatten().collect())
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636    use crate::algebra::{Binding, Term, Variable};
637    use crate::cost_model::{CostModel, CostModelConfig};
638    use oxirs_core::model::NamedNode;
639
640    #[test]
641    fn test_join_algorithm_selection() {
642        let cost_model = CostModel::new(CostModelConfig::default());
643        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024); // 1MB threshold
644
645        // Create test data
646        let var_x = Variable::new("x").unwrap();
647        let _var_y = Variable::new("y").unwrap();
648
649        let left_solutions = vec![create_test_solution(&var_x, "value1")];
650        let right_solutions = vec![create_test_solution(&var_x, "value1")];
651        let join_variables = vec![var_x];
652
653        let result =
654            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
655        assert!(result.is_ok());
656
657        let (solutions, stats) = result.unwrap();
658        assert!(!solutions.is_empty());
659        println!("Join stats: {}", stats.performance_summary());
660    }
661
662    fn create_test_solution(variable: &Variable, value: &str) -> Solution {
663        let mut binding = Binding::new();
664        binding.insert(
665            variable.clone(),
666            Term::Iri(NamedNode::new_unchecked(format!(
667                "http://example.org/{value}"
668            ))),
669        );
670        vec![binding]
671    }
672
673    fn create_multi_var_solution(
674        var_x: &Variable,
675        val_x: &str,
676        var_y: &Variable,
677        val_y: &str,
678    ) -> Solution {
679        let mut binding = Binding::new();
680        binding.insert(
681            var_x.clone(),
682            Term::Iri(NamedNode::new_unchecked(format!(
683                "http://example.org/x/{val_x}"
684            ))),
685        );
686        binding.insert(
687            var_y.clone(),
688            Term::Iri(NamedNode::new_unchecked(format!(
689                "http://example.org/y/{val_y}"
690            ))),
691        );
692        vec![binding]
693    }
694
695    #[test]
696    fn test_empty_left_input() {
697        let cost_model = CostModel::new(CostModelConfig::default());
698        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024);
699
700        let var_x = Variable::new("x").unwrap();
701        let left_solutions = vec![];
702        let right_solutions = vec![create_test_solution(&var_x, "value1")];
703        let join_variables = vec![var_x];
704
705        let result =
706            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
707        assert!(result.is_ok());
708
709        let (solutions, stats) = result.unwrap();
710        assert!(solutions.is_empty());
711        assert_eq!(stats.output_cardinality, 0);
712    }
713
714    #[test]
715    fn test_empty_right_input() {
716        let cost_model = CostModel::new(CostModelConfig::default());
717        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024);
718
719        let var_x = Variable::new("x").unwrap();
720        let left_solutions = vec![create_test_solution(&var_x, "value1")];
721        let right_solutions = vec![];
722        let join_variables = vec![var_x];
723
724        let result =
725            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
726        assert!(result.is_ok());
727
728        let (solutions, stats) = result.unwrap();
729        assert!(solutions.is_empty());
730        assert_eq!(stats.output_cardinality, 0);
731    }
732
733    #[test]
734    fn test_no_matching_values() {
735        let cost_model = CostModel::new(CostModelConfig::default());
736        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024);
737
738        let var_x = Variable::new("x").unwrap();
739        let left_solutions = vec![create_test_solution(&var_x, "value1")];
740        let right_solutions = vec![create_test_solution(&var_x, "value2")];
741        let join_variables = vec![var_x];
742
743        let result =
744            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
745        assert!(result.is_ok());
746
747        let (solutions, _stats) = result.unwrap();
748        assert!(
749            solutions.is_empty(),
750            "No matches should result in empty output"
751        );
752    }
753
754    #[test]
755    fn test_multiple_join_variables() {
756        let cost_model = CostModel::new(CostModelConfig::default());
757        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024);
758
759        let var_x = Variable::new("x").unwrap();
760        let var_y = Variable::new("y").unwrap();
761
762        let left_solutions = vec![
763            create_multi_var_solution(&var_x, "a", &var_y, "1"),
764            create_multi_var_solution(&var_x, "b", &var_y, "2"),
765        ];
766        let right_solutions = vec![
767            create_multi_var_solution(&var_x, "a", &var_y, "1"),
768            create_multi_var_solution(&var_x, "c", &var_y, "3"),
769        ];
770        let join_variables = vec![var_x, var_y];
771
772        let result =
773            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
774        assert!(result.is_ok());
775
776        let (solutions, stats) = result.unwrap();
777        assert_eq!(solutions.len(), 1, "Should find one matching solution");
778        assert!(stats.join_selectivity > 0.0);
779    }
780
781    #[test]
782    fn test_large_dataset_join() {
783        let cost_model = CostModel::new(CostModelConfig::default());
784        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024);
785
786        let var_x = Variable::new("x").unwrap();
787
788        // Create 100 solutions on each side
789        let left_solutions: Vec<_> = (0..100)
790            .map(|i| create_test_solution(&var_x, &format!("value{}", i)))
791            .collect();
792        let right_solutions: Vec<_> = (50..150)
793            .map(|i| create_test_solution(&var_x, &format!("value{}", i)))
794            .collect();
795        let join_variables = vec![var_x];
796
797        let result =
798            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
799        assert!(result.is_ok());
800
801        let (solutions, stats) = result.unwrap();
802        // Overlap is from 50 to 99, so 50 matches
803        assert_eq!(solutions.len(), 50);
804        assert!(
805            !stats.execution_time.is_zero(),
806            "Execution time should be measured"
807        );
808        println!("Large join stats: {}", stats.performance_summary());
809    }
810
811    #[test]
812    fn test_hash_join_preferred_for_large_inputs() {
813        let cost_model = CostModel::new(CostModelConfig::default());
814        let mut selector = JoinAlgorithmSelector::new(cost_model, 10 * 1024 * 1024); // 10MB threshold
815
816        let var_x = Variable::new("x").unwrap();
817
818        // Create large enough datasets to prefer hash join
819        let left_solutions: Vec<_> = (0..1000)
820            .map(|i| create_test_solution(&var_x, &format!("value{}", i)))
821            .collect();
822        let right_solutions: Vec<_> = (0..1000)
823            .map(|i| create_test_solution(&var_x, &format!("value{}", i)))
824            .collect();
825        let join_variables = vec![var_x];
826
827        let result =
828            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
829        assert!(result.is_ok());
830
831        let (_solutions, stats) = result.unwrap();
832        // Hash join or sort-merge should be selected for large inputs
833        assert!(
834            matches!(
835                stats.algorithm_used,
836                OptimalJoinAlgorithm::HashJoin | OptimalJoinAlgorithm::SortMergeJoin
837            ),
838            "Large inputs should use hash join or sort-merge join"
839        );
840    }
841
842    #[test]
843    fn test_selectivity_calculation() {
844        let cost_model = CostModel::new(CostModelConfig::default());
845        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024);
846
847        let var_x = Variable::new("x").unwrap();
848
849        // 10 left solutions, 10 right solutions, 5 matches
850        let left_solutions: Vec<_> = (0..10)
851            .map(|i| create_test_solution(&var_x, &format!("value{}", i)))
852            .collect();
853        let right_solutions: Vec<_> = (5..15)
854            .map(|i| create_test_solution(&var_x, &format!("value{}", i)))
855            .collect();
856        let join_variables = vec![var_x];
857
858        let result =
859            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
860        assert!(result.is_ok());
861
862        let (solutions, stats) = result.unwrap();
863        assert_eq!(solutions.len(), 5);
864        // Selectivity should be 5 / (10 * 10) = 0.05
865        assert!(
866            (stats.join_selectivity - 0.05).abs() < 0.01,
867            "Selectivity calculation incorrect"
868        );
869    }
870
871    #[test]
872    fn test_join_stats_reporting() {
873        let cost_model = CostModel::new(CostModelConfig::default());
874        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024);
875
876        let var_x = Variable::new("x").unwrap();
877        let left_solutions = vec![create_test_solution(&var_x, "value1")];
878        let right_solutions = vec![create_test_solution(&var_x, "value1")];
879        let join_variables = vec![var_x];
880
881        let result =
882            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
883        assert!(result.is_ok());
884
885        let (_solutions, stats) = result.unwrap();
886
887        // Verify stats are populated correctly
888        assert_eq!(stats.input_cardinalities.0, 1);
889        assert_eq!(stats.input_cardinalities.1, 1);
890        assert_eq!(stats.output_cardinality, 1);
891        assert!(stats.memory_used > 0);
892        assert!(stats.join_selectivity > 0.0);
893
894        let summary = stats.performance_summary();
895        assert!(
896            summary.contains("Algorithm"),
897            "Summary should contain algorithm info: {}",
898            summary
899        );
900    }
901
902    #[test]
903    fn test_cartesian_product() {
904        let cost_model = CostModel::new(CostModelConfig::default());
905        let mut selector = JoinAlgorithmSelector::new(cost_model, 1024 * 1024);
906
907        let var_x = Variable::new("x").unwrap();
908        let var_y = Variable::new("y").unwrap();
909
910        // Join on different variables - should produce cartesian product
911        let left_solutions = vec![
912            create_test_solution(&var_x, "a"),
913            create_test_solution(&var_x, "b"),
914        ];
915        let right_solutions = vec![
916            create_test_solution(&var_y, "1"),
917            create_test_solution(&var_y, "2"),
918        ];
919
920        // Empty join variables means cartesian product
921        let join_variables = vec![];
922
923        let result =
924            selector.execute_optimal_join(left_solutions, right_solutions, &join_variables);
925        assert!(result.is_ok());
926
927        let (solutions, stats) = result.unwrap();
928        // Cartesian product: 2 * 2 = 4
929        assert_eq!(solutions.len(), 4);
930        assert!(
931            (stats.join_selectivity - 1.0).abs() < 0.01,
932            "Cartesian product selectivity should be ~1.0"
933        );
934    }
935}