1use crate::algebra::{Solution, Variable};
14use crate::cost_model::CostModel;
15use crate::executor::parallel_optimized::{CacheFriendlyHashJoin, SortMergeJoin};
16use anyhow::Result;
17use std::collections::HashSet;
18
19pub 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 pub fn new(cost_model: CostModel, memory_threshold: usize) -> Self {
31 Self {
32 cost_model,
33 hash_join: CacheFriendlyHashJoin::new(16), sort_merge_join: SortMergeJoin::new(memory_threshold),
35 memory_threshold,
36 }
37 }
38
39 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 let join_info =
50 self.analyze_join_characteristics(&left_solutions, &right_solutions, join_variables);
51
52 let selected_algorithm = self.select_join_algorithm(&join_info)?;
54
55 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 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 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 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 };
108
109 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 let memory_requirement = (left_cardinality + right_cardinality) * 100; 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 fn select_join_algorithm(
131 &mut self,
132 join_info: &JoinCharacteristics,
133 ) -> Result<OptimalJoinAlgorithm> {
134 let candidate_algorithm =
136 if join_info.left_cardinality < 1000 || join_info.right_cardinality < 1000 {
137 OptimalJoinAlgorithm::NestedLoopJoin
139 } else if join_info.left_sorted && join_info.right_sorted {
140 OptimalJoinAlgorithm::SortMergeJoin
142 } else if join_info.memory_requirement > self.memory_threshold {
143 OptimalJoinAlgorithm::SortMergeJoin
145 } else if join_info.estimated_selectivity < 0.01 {
146 OptimalJoinAlgorithm::HashJoin
148 } else {
149 OptimalJoinAlgorithm::HashJoin
151 };
152
153 let final_algorithm = self.validate_with_cost_model(candidate_algorithm, join_info)?;
155
156 Ok(final_algorithm)
157 }
158
159 fn validate_with_cost_model(
164 &self,
165 candidate: OptimalJoinAlgorithm,
166 join_info: &JoinCharacteristics,
167 ) -> Result<OptimalJoinAlgorithm> {
168 let mut algorithm_costs = Vec::new();
170
171 let hash_join_cost = self.estimate_hash_join_cost(join_info);
173 algorithm_costs.push((OptimalJoinAlgorithm::HashJoin, hash_join_cost));
174
175 let sort_merge_cost = self.estimate_sort_merge_cost(join_info);
177 algorithm_costs.push((OptimalJoinAlgorithm::SortMergeJoin, sort_merge_cost));
178
179 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 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 let candidate_cost = algorithm_costs
198 .iter()
199 .find(|(algo, _)| *algo == candidate)
200 .map(|(_, cost)| *cost)
201 .unwrap_or(optimal_cost);
202
203 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 Ok(candidate)
218 }
219 }
220
221 fn estimate_hash_join_cost(&self, join_info: &JoinCharacteristics) -> f64 {
223 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 let lookup_cost = probe_cost;
231
232 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 fn estimate_sort_merge_cost(&self, join_info: &JoinCharacteristics) -> f64 {
243 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 let merge_cost = (join_info.left_cardinality + join_info.right_cardinality) as f64;
258
259 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 fn estimate_nested_loop_cost(&self, join_info: &JoinCharacteristics) -> f64 {
270 let scan_cost = join_info.left_cardinality as f64 * join_info.right_cardinality as f64;
272
273 let output_cost = scan_cost * join_info.estimated_selectivity;
275
276 scan_cost + output_cost
277 }
278
279 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 fn is_sorted_by_join_keys(&self, solutions: &[Solution], join_variables: &[Variable]) -> bool {
302 if solutions.len() <= 1 {
303 return true;
304 }
305
306 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 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 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 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 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 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 let mut merged_binding = left_binding.clone();
418 for (var, term) in right_binding {
419 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 fn estimate_memory_usage(&self, solutions: &[Solution]) -> usize {
438 solutions.len() * 1024 }
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum OptimalJoinAlgorithm {
445 HashJoin,
446 SortMergeJoin,
447 NestedLoopJoin,
448 IndexJoin,
449}
450
451#[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#[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 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#[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 pub fn new() -> Self {
509 Self
510 }
511
512 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 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 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 const CHUNK_SIZE: usize = 64;
545
546 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 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 Ok(matches.into_iter().flatten().collect())
575 }
576
577 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 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 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 let right_partitions = Arc::new(right_partitions);
606
607 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 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 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); 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 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 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); let var_x = Variable::new("x").unwrap();
817
818 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 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 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 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 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 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 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 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}