1use std::collections::HashMap;
33use std::sync::Arc;
34use xlog_core::{RelId, Schema};
35use xlog_ir::{CompareOp, Expr, JoinType, RirNode};
36use xlog_stats::StatsManager;
37
38#[derive(Debug, Clone)]
43#[non_exhaustive]
44pub struct OptimizerConfig {
45 pub dp_threshold: usize,
51
52 pub index_heat_threshold: f32,
57
58 pub enable_pushdown: bool,
63
64 pub default_filter_selectivity: f64,
69
70 pub transfer_cost_multiplier: f64,
75
76 pub default_bytes_per_row: u64,
80}
81
82impl Default for OptimizerConfig {
83 fn default() -> Self {
84 Self {
85 dp_threshold: 10,
86 index_heat_threshold: 0.7,
87 enable_pushdown: true,
88 default_filter_selectivity: 0.1,
89 transfer_cost_multiplier: 100.0,
90 default_bytes_per_row: 32,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Default, PartialEq)]
100pub struct PlanCost {
101 pub rows: u64,
103
104 pub cpu_cost: f64,
109
110 pub gpu_mem: u64,
115
116 pub transfers: u32,
121}
122
123impl PlanCost {
124 pub fn with_rows(rows: u64) -> Self {
126 Self {
127 rows,
128 ..Default::default()
129 }
130 }
131
132 pub fn total_cost(&self, transfer_weight: f64) -> f64 {
143 self.cpu_cost + (self.gpu_mem as f64 * 0.001) + (self.transfers as f64 * transfer_weight)
144 }
145
146 pub fn then(self, other: PlanCost) -> PlanCost {
150 PlanCost {
151 rows: other.rows,
152 cpu_cost: self.cpu_cost + other.cpu_cost,
153 gpu_mem: self.gpu_mem.max(other.gpu_mem), transfers: self.transfers + other.transfers,
155 }
156 }
157}
158
159pub struct Optimizer {
165 stats: Arc<StatsManager>,
166 config: OptimizerConfig,
167 schemas: HashMap<RelId, Schema>,
169}
170
171impl Optimizer {
172 pub fn new(stats: Arc<StatsManager>) -> Self {
178 Self {
179 stats,
180 config: OptimizerConfig::default(),
181 schemas: HashMap::new(),
182 }
183 }
184
185 pub fn with_config(stats: Arc<StatsManager>, config: OptimizerConfig) -> Self {
192 Self {
193 stats,
194 config,
195 schemas: HashMap::new(),
196 }
197 }
198
199 pub fn set_schemas(&mut self, schemas: HashMap<RelId, Schema>) {
204 self.schemas = schemas;
205 }
206
207 pub fn config(&self) -> &OptimizerConfig {
209 &self.config
210 }
211
212 pub fn stats(&self) -> &Arc<StatsManager> {
214 &self.stats
215 }
216
217 pub fn optimize(&self, node: RirNode) -> RirNode {
235 if self.config.enable_pushdown {
236 self.predicate_pushdown(node)
237 } else {
238 node
239 }
240 }
241
242 fn predicate_pushdown(&self, node: RirNode) -> RirNode {
260 match node {
261 RirNode::Unit => RirNode::Unit,
263 RirNode::Scan { rel } => RirNode::Scan { rel },
264
265 RirNode::Filter { input, predicate } => {
267 let optimized_input = self.predicate_pushdown(*input);
269
270 match optimized_input {
271 RirNode::Filter {
273 input: inner_input,
274 predicate: inner_pred,
275 } => {
276 let merged = Expr::And(vec![inner_pred, predicate]);
277 RirNode::Filter {
278 input: inner_input,
279 predicate: merged,
280 }
281 }
282
283 RirNode::Project {
285 input: proj_input,
286 columns,
287 } => {
288 if let Some(remapped) =
290 self.remap_predicate_through_project(&predicate, &columns)
291 {
292 RirNode::Project {
294 input: Box::new(RirNode::Filter {
295 input: proj_input,
296 predicate: remapped,
297 }),
298 columns,
299 }
300 } else {
301 RirNode::Filter {
303 input: Box::new(RirNode::Project {
304 input: proj_input,
305 columns,
306 }),
307 predicate,
308 }
309 }
310 }
311
312 RirNode::Join {
314 left,
315 right,
316 left_keys,
317 right_keys,
318 join_type,
319 } => {
320 let left_width = self.estimate_width(&left);
321 let (left_preds, right_preds, remaining) =
322 self.split_predicate_for_join(&predicate, left_width);
323
324 let new_left = if !left_preds.is_empty() {
326 Box::new(RirNode::Filter {
327 input: left,
328 predicate: Self::conjoin(left_preds),
329 })
330 } else {
331 left
332 };
333
334 let new_right = if !right_preds.is_empty() {
335 Box::new(RirNode::Filter {
336 input: right,
337 predicate: Self::conjoin(right_preds),
338 })
339 } else {
340 right
341 };
342
343 let join_node = RirNode::Join {
344 left: new_left,
345 right: new_right,
346 left_keys,
347 right_keys,
348 join_type,
349 };
350
351 if !remaining.is_empty() {
353 RirNode::Filter {
354 input: Box::new(join_node),
355 predicate: Self::conjoin(remaining),
356 }
357 } else {
358 join_node
359 }
360 }
361
362 other => RirNode::Filter {
364 input: Box::new(other),
365 predicate,
366 },
367 }
368 }
369
370 RirNode::Project { input, columns } => {
375 let mut projections = vec![columns];
376 let mut base = *input;
377 while let RirNode::Project { input, columns } = base {
378 projections.push(columns);
379 base = *input;
380 }
381
382 let mut optimized = self.predicate_pushdown(base);
383 for columns in projections.into_iter().rev() {
384 optimized = RirNode::Project {
385 input: Box::new(optimized),
386 columns,
387 };
388 }
389 optimized
390 }
391
392 RirNode::Join {
394 left,
395 right,
396 left_keys,
397 right_keys,
398 join_type,
399 } => RirNode::Join {
400 left: Box::new(self.predicate_pushdown(*left)),
401 right: Box::new(self.predicate_pushdown(*right)),
402 left_keys,
403 right_keys,
404 join_type,
405 },
406
407 RirNode::GroupBy {
409 input,
410 key_cols,
411 aggs,
412 } => RirNode::GroupBy {
413 input: Box::new(self.predicate_pushdown(*input)),
414 key_cols,
415 aggs,
416 },
417
418 RirNode::Union { inputs } => RirNode::Union {
420 inputs: inputs
421 .into_iter()
422 .map(|i| self.predicate_pushdown(i))
423 .collect(),
424 },
425
426 RirNode::Distinct { input, key_cols } => RirNode::Distinct {
428 input: Box::new(self.predicate_pushdown(*input)),
429 key_cols,
430 },
431
432 RirNode::Diff { left, right } => RirNode::Diff {
434 left: Box::new(self.predicate_pushdown(*left)),
435 right: Box::new(self.predicate_pushdown(*right)),
436 },
437
438 RirNode::Fixpoint {
440 scc_id,
441 base,
442 recursive,
443 delta_rel,
444 full_rel,
445 } => RirNode::Fixpoint {
446 scc_id,
447 base: Box::new(self.predicate_pushdown(*base)),
448 recursive: Box::new(self.predicate_pushdown(*recursive)),
449 delta_rel,
450 full_rel,
451 },
452
453 RirNode::TensorMaskedJoin { .. } => node, RirNode::MultiWayJoin { .. } | RirNode::ChainJoin { .. } => node,
459 }
460 }
461
462 fn remap_predicate_through_project(
468 &self,
469 predicate: &Expr,
470 columns: &[xlog_ir::ProjectExpr],
471 ) -> Option<Expr> {
472 let mut output_to_input: std::collections::HashMap<usize, usize> =
475 std::collections::HashMap::new();
476
477 for (out_idx, proj_expr) in columns.iter().enumerate() {
478 if let xlog_ir::ProjectExpr::Column(in_idx) = proj_expr {
479 output_to_input.insert(out_idx, *in_idx);
480 }
481 }
482
483 self.remap_expr(predicate, &output_to_input)
484 }
485
486 fn remap_expr(
488 &self,
489 expr: &Expr,
490 mapping: &std::collections::HashMap<usize, usize>,
491 ) -> Option<Expr> {
492 match expr {
493 Expr::Column(idx) => mapping.get(idx).map(|&new_idx| Expr::Column(new_idx)),
494
495 Expr::Const(val) => Some(Expr::Const(val.clone())),
496
497 Expr::Compare { left, op, right } => {
498 let new_left = self.remap_expr(left, mapping)?;
499 let new_right = self.remap_expr(right, mapping)?;
500 Some(Expr::Compare {
501 left: Box::new(new_left),
502 op: *op,
503 right: Box::new(new_right),
504 })
505 }
506
507 Expr::And(exprs) => {
508 let remapped: Option<Vec<_>> =
509 exprs.iter().map(|e| self.remap_expr(e, mapping)).collect();
510 remapped.map(Expr::And)
511 }
512
513 Expr::Or(exprs) => {
514 let remapped: Option<Vec<_>> =
515 exprs.iter().map(|e| self.remap_expr(e, mapping)).collect();
516 remapped.map(Expr::Or)
517 }
518
519 Expr::Not(inner) => {
520 let remapped = self.remap_expr(inner, mapping)?;
521 Some(Expr::Not(Box::new(remapped)))
522 }
523
524 Expr::Add(l, r) => {
526 let new_l = self.remap_expr(l, mapping)?;
527 let new_r = self.remap_expr(r, mapping)?;
528 Some(Expr::Add(Box::new(new_l), Box::new(new_r)))
529 }
530 Expr::Sub(l, r) => {
531 let new_l = self.remap_expr(l, mapping)?;
532 let new_r = self.remap_expr(r, mapping)?;
533 Some(Expr::Sub(Box::new(new_l), Box::new(new_r)))
534 }
535 Expr::Mul(l, r) => {
536 let new_l = self.remap_expr(l, mapping)?;
537 let new_r = self.remap_expr(r, mapping)?;
538 Some(Expr::Mul(Box::new(new_l), Box::new(new_r)))
539 }
540 Expr::Div(l, r) => {
541 let new_l = self.remap_expr(l, mapping)?;
542 let new_r = self.remap_expr(r, mapping)?;
543 Some(Expr::Div(Box::new(new_l), Box::new(new_r)))
544 }
545 Expr::Mod(l, r) => {
546 let new_l = self.remap_expr(l, mapping)?;
547 let new_r = self.remap_expr(r, mapping)?;
548 Some(Expr::Mod(Box::new(new_l), Box::new(new_r)))
549 }
550
551 Expr::Abs(inner) => {
553 let remapped = self.remap_expr(inner, mapping)?;
554 Some(Expr::Abs(Box::new(remapped)))
555 }
556 Expr::Min(l, r) => {
557 let new_l = self.remap_expr(l, mapping)?;
558 let new_r = self.remap_expr(r, mapping)?;
559 Some(Expr::Min(Box::new(new_l), Box::new(new_r)))
560 }
561 Expr::Max(l, r) => {
562 let new_l = self.remap_expr(l, mapping)?;
563 let new_r = self.remap_expr(r, mapping)?;
564 Some(Expr::Max(Box::new(new_l), Box::new(new_r)))
565 }
566 Expr::Pow(l, r) => {
567 let new_l = self.remap_expr(l, mapping)?;
568 let new_r = self.remap_expr(r, mapping)?;
569 Some(Expr::Pow(Box::new(new_l), Box::new(new_r)))
570 }
571 Expr::Cast(inner, scalar_type) => {
572 let remapped = self.remap_expr(inner, mapping)?;
573 Some(Expr::Cast(Box::new(remapped), *scalar_type))
574 }
575 Expr::Conditional {
576 condition,
577 then_expr,
578 else_expr,
579 } => {
580 let new_condition = self.remap_expr(condition, mapping)?;
581 let new_then = self.remap_expr(then_expr, mapping)?;
582 let new_else = self.remap_expr(else_expr, mapping)?;
583 Some(Expr::Conditional {
584 condition: Box::new(new_condition),
585 then_expr: Box::new(new_then),
586 else_expr: Box::new(new_else),
587 })
588 }
589 }
590 }
591
592 fn estimate_width(&self, node: &RirNode) -> usize {
594 match node {
595 RirNode::Unit => 0,
596 RirNode::Scan { rel } => {
597 if let Some(schema) = self.schemas.get(rel) {
599 schema.arity()
600 } else if let Some(stats) = self.stats.get_relation_stats(*rel) {
601 stats.column_stats.len().max(1)
602 } else {
603 4 }
605 }
606 RirNode::Filter { input, .. } => self.estimate_width(input),
607 RirNode::Project { columns, .. } => columns.len(),
608 RirNode::Join { left, right, .. } => {
609 self.estimate_width(left) + self.estimate_width(right)
610 }
611 RirNode::ChainJoin { output_columns, .. } => output_columns.len(),
612 RirNode::GroupBy { key_cols, aggs, .. } => key_cols.len() + aggs.len(),
613 RirNode::Union { inputs } => {
614 inputs.first().map(|i| self.estimate_width(i)).unwrap_or(0)
615 }
616 RirNode::Distinct { input, .. } => self.estimate_width(input),
617 RirNode::Diff { left, .. } => self.estimate_width(left),
618 RirNode::Fixpoint { base, .. } => self.estimate_width(base),
619 RirNode::TensorMaskedJoin { head_rel_id, .. } => self
622 .schemas
623 .get(head_rel_id)
624 .map(|s| s.arity())
625 .unwrap_or(2),
626 RirNode::MultiWayJoin { output_columns, .. } => output_columns.len(),
629 }
630 }
631
632 fn split_predicate_for_join(
636 &self,
637 predicate: &Expr,
638 left_width: usize,
639 ) -> (Vec<Expr>, Vec<Expr>, Vec<Expr>) {
640 let mut left_preds = Vec::new();
641 let mut right_preds = Vec::new();
642 let mut remaining = Vec::new();
643
644 let conjuncts = Self::flatten_and(predicate);
646
647 for conj in conjuncts {
648 let cols = Self::collect_columns(&conj);
649 let max_col = cols.iter().copied().max().unwrap_or(0);
650 let min_col = cols.iter().copied().min().unwrap_or(0);
651
652 if cols.is_empty() {
653 left_preds.push(conj);
655 } else if max_col < left_width {
656 left_preds.push(conj);
658 } else if min_col >= left_width {
659 let remapped = Self::remap_columns(&conj, |c| c - left_width);
661 right_preds.push(remapped);
662 } else {
663 remaining.push(conj);
665 }
666 }
667
668 (left_preds, right_preds, remaining)
669 }
670
671 fn flatten_and(expr: &Expr) -> Vec<Expr> {
673 match expr {
674 Expr::And(exprs) => exprs.iter().flat_map(Self::flatten_and).collect(),
675 other => vec![other.clone()],
676 }
677 }
678
679 fn collect_columns(expr: &Expr) -> Vec<usize> {
681 match expr {
682 Expr::Column(idx) => vec![*idx],
683 Expr::Const(_) => vec![],
684 Expr::Compare { left, right, .. } => {
685 let mut cols = Self::collect_columns(left);
686 cols.extend(Self::collect_columns(right));
687 cols
688 }
689 Expr::And(exprs) | Expr::Or(exprs) => {
690 exprs.iter().flat_map(Self::collect_columns).collect()
691 }
692 Expr::Not(inner) | Expr::Abs(inner) | Expr::Cast(inner, _) => {
693 Self::collect_columns(inner)
694 }
695 Expr::Add(l, r)
696 | Expr::Sub(l, r)
697 | Expr::Mul(l, r)
698 | Expr::Div(l, r)
699 | Expr::Mod(l, r)
700 | Expr::Min(l, r)
701 | Expr::Max(l, r)
702 | Expr::Pow(l, r) => {
703 let mut cols = Self::collect_columns(l);
704 cols.extend(Self::collect_columns(r));
705 cols
706 }
707 Expr::Conditional {
708 condition,
709 then_expr,
710 else_expr,
711 } => {
712 let mut cols = Self::collect_columns(condition);
713 cols.extend(Self::collect_columns(then_expr));
714 cols.extend(Self::collect_columns(else_expr));
715 cols
716 }
717 }
718 }
719
720 fn remap_columns<F: Fn(usize) -> usize + Copy>(expr: &Expr, f: F) -> Expr {
722 match expr {
723 Expr::Column(idx) => Expr::Column(f(*idx)),
724 Expr::Const(v) => Expr::Const(v.clone()),
725 Expr::Compare { left, op, right } => Expr::Compare {
726 left: Box::new(Self::remap_columns(left, f)),
727 op: *op,
728 right: Box::new(Self::remap_columns(right, f)),
729 },
730 Expr::And(exprs) => {
731 Expr::And(exprs.iter().map(|e| Self::remap_columns(e, f)).collect())
732 }
733 Expr::Or(exprs) => Expr::Or(exprs.iter().map(|e| Self::remap_columns(e, f)).collect()),
734 Expr::Not(inner) => Expr::Not(Box::new(Self::remap_columns(inner, f))),
735 Expr::Add(l, r) => Expr::Add(
736 Box::new(Self::remap_columns(l, f)),
737 Box::new(Self::remap_columns(r, f)),
738 ),
739 Expr::Sub(l, r) => Expr::Sub(
740 Box::new(Self::remap_columns(l, f)),
741 Box::new(Self::remap_columns(r, f)),
742 ),
743 Expr::Mul(l, r) => Expr::Mul(
744 Box::new(Self::remap_columns(l, f)),
745 Box::new(Self::remap_columns(r, f)),
746 ),
747 Expr::Div(l, r) => Expr::Div(
748 Box::new(Self::remap_columns(l, f)),
749 Box::new(Self::remap_columns(r, f)),
750 ),
751 Expr::Mod(l, r) => Expr::Mod(
752 Box::new(Self::remap_columns(l, f)),
753 Box::new(Self::remap_columns(r, f)),
754 ),
755 Expr::Abs(inner) => Expr::Abs(Box::new(Self::remap_columns(inner, f))),
756 Expr::Min(l, r) => Expr::Min(
757 Box::new(Self::remap_columns(l, f)),
758 Box::new(Self::remap_columns(r, f)),
759 ),
760 Expr::Max(l, r) => Expr::Max(
761 Box::new(Self::remap_columns(l, f)),
762 Box::new(Self::remap_columns(r, f)),
763 ),
764 Expr::Pow(l, r) => Expr::Pow(
765 Box::new(Self::remap_columns(l, f)),
766 Box::new(Self::remap_columns(r, f)),
767 ),
768 Expr::Cast(inner, t) => Expr::Cast(Box::new(Self::remap_columns(inner, f)), *t),
769 Expr::Conditional {
770 condition,
771 then_expr,
772 else_expr,
773 } => Expr::Conditional {
774 condition: Box::new(Self::remap_columns(condition, f)),
775 then_expr: Box::new(Self::remap_columns(then_expr, f)),
776 else_expr: Box::new(Self::remap_columns(else_expr, f)),
777 },
778 }
779 }
780
781 fn conjoin(predicates: Vec<Expr>) -> Expr {
783 debug_assert!(!predicates.is_empty());
784 if predicates.len() == 1 {
785 predicates.into_iter().next().unwrap()
786 } else {
787 Expr::And(predicates)
788 }
789 }
790
791 pub fn estimate_cost(&self, node: &RirNode) -> PlanCost {
804 match node {
805 RirNode::Unit => PlanCost {
806 rows: 1,
807 cpu_cost: 0.0,
808 gpu_mem: 0,
809 transfers: 0,
810 },
811 RirNode::Scan { rel } => self.estimate_scan_cost(*rel),
812
813 RirNode::Filter { input, predicate } => {
814 let input_cost = self.estimate_cost(input);
815 self.estimate_filter_cost(input_cost, predicate, input)
816 }
817
818 RirNode::Project { input, columns } => {
819 let input_cost = self.estimate_cost(input);
820 self.estimate_project_cost(input_cost, columns)
821 }
822
823 RirNode::Join {
824 left,
825 right,
826 left_keys,
827 right_keys,
828 join_type,
829 } => {
830 let left_cost = self.estimate_cost(left);
831 let right_cost = self.estimate_cost(right);
832 self.estimate_join_cost(
833 left_cost, right_cost, left, right, left_keys, right_keys, *join_type,
834 )
835 }
836
837 RirNode::ChainJoin {
838 left,
839 right,
840 left_key,
841 right_key,
842 output_columns,
843 ..
844 } => {
845 let left_cost = self.estimate_cost(left);
846 let right_cost = self.estimate_cost(right);
847 let join_cost = self.estimate_join_cost(
848 left_cost,
849 right_cost,
850 left,
851 right,
852 &[*left_key],
853 &[*right_key],
854 JoinType::Inner,
855 );
856 self.estimate_project_cost(join_cost, output_columns)
857 }
858
859 RirNode::GroupBy {
860 input,
861 key_cols,
862 aggs,
863 } => {
864 let input_cost = self.estimate_cost(input);
865 self.estimate_groupby_cost(input_cost, key_cols, aggs)
866 }
867
868 RirNode::Union { inputs } => {
869 let costs: Vec<_> = inputs.iter().map(|i| self.estimate_cost(i)).collect();
870 self.estimate_union_cost(costs)
871 }
872
873 RirNode::Distinct { input, key_cols } => {
874 let input_cost = self.estimate_cost(input);
875 self.estimate_distinct_cost(input_cost, key_cols)
876 }
877
878 RirNode::Diff { left, right } => {
879 let left_cost = self.estimate_cost(left);
880 let right_cost = self.estimate_cost(right);
881 self.estimate_diff_cost(left_cost, right_cost)
882 }
883
884 RirNode::Fixpoint {
885 base, recursive, ..
886 } => {
887 let base_cost = self.estimate_cost(base);
888 let recursive_cost = self.estimate_cost(recursive);
889 self.estimate_fixpoint_cost(base_cost, recursive_cost)
890 }
891
892 RirNode::TensorMaskedJoin {
893 max_active_rules, ..
894 } => PlanCost {
895 rows: *max_active_rules as u64,
896 cpu_cost: *max_active_rules as f64 * 100.0,
897 gpu_mem: *max_active_rules as u64 * 1024,
898 transfers: 1,
899 },
900 RirNode::MultiWayJoin { inputs, .. } => {
905 let mut total = PlanCost::default();
906 for inp in inputs {
907 let c = self.estimate_cost(inp);
908 total.rows = total.rows.saturating_add(c.rows);
909 total.cpu_cost += c.cpu_cost;
910 total.gpu_mem = total.gpu_mem.saturating_add(c.gpu_mem);
911 total.transfers = total.transfers.saturating_add(c.transfers);
912 }
913 total
914 }
915 }
916 }
917
918 fn estimate_scan_cost(&self, rel: RelId) -> PlanCost {
920 if let Some(stats) = self.stats.get_relation_stats(rel) {
921 PlanCost {
922 rows: stats.cardinality,
923 cpu_cost: stats.cardinality as f64 * 0.01, gpu_mem: stats
925 .byte_size
926 .max(stats.cardinality * self.config.default_bytes_per_row),
927 transfers: 0, }
929 } else {
930 let default_rows = 1000;
932 PlanCost {
933 rows: default_rows,
934 cpu_cost: default_rows as f64 * 0.01,
935 gpu_mem: default_rows * self.config.default_bytes_per_row,
936 transfers: 0,
937 }
938 }
939 }
940
941 fn estimate_filter_cost(
943 &self,
944 input_cost: PlanCost,
945 predicate: &Expr,
946 input: &RirNode,
947 ) -> PlanCost {
948 let selectivity = self.estimate_predicate_selectivity(predicate, input);
949 let output_rows = ((input_cost.rows as f64 * selectivity) as u64).max(1);
950
951 PlanCost {
952 rows: output_rows,
953 cpu_cost: input_cost.cpu_cost + input_cost.rows as f64 * 0.02, gpu_mem: input_cost.gpu_mem, transfers: input_cost.transfers,
956 }
957 }
958
959 fn estimate_project_cost(
961 &self,
962 input_cost: PlanCost,
963 columns: &[xlog_ir::ProjectExpr],
964 ) -> PlanCost {
965 let computed_count = columns
967 .iter()
968 .filter(|c| matches!(c, xlog_ir::ProjectExpr::Computed(_, _)))
969 .count();
970
971 let compute_cost = computed_count as f64 * input_cost.rows as f64 * 0.05;
973
974 let output_width_ratio = columns.len() as f64 / (columns.len() + 2) as f64; PlanCost {
978 rows: input_cost.rows,
979 cpu_cost: input_cost.cpu_cost + compute_cost,
980 gpu_mem: (input_cost.gpu_mem as f64 * output_width_ratio) as u64,
981 transfers: input_cost.transfers,
982 }
983 }
984
985 #[expect(
987 clippy::too_many_arguments,
988 reason = "join cost estimation consumes both child costs, both plans, both key sets, and the join semantics as distinct inputs"
989 )]
990 fn estimate_join_cost(
991 &self,
992 left_cost: PlanCost,
993 right_cost: PlanCost,
994 left: &RirNode,
995 right: &RirNode,
996 left_keys: &[usize],
997 right_keys: &[usize],
998 join_type: JoinType,
999 ) -> PlanCost {
1000 let output_rows = match join_type {
1003 JoinType::Semi => {
1004 ((left_cost.rows as f64 * 0.5) as u64).max(1)
1006 }
1007 JoinType::Anti => {
1008 ((left_cost.rows as f64 * 0.5) as u64).max(1)
1010 }
1011 JoinType::Inner | JoinType::LeftOuter => {
1012 let left_rels = left.referenced_relations();
1014 let right_rels = right.referenced_relations();
1015
1016 if left_rels.len() == 1 && right_rels.len() == 1 {
1017 let estimated = self.stats.estimate_join_cardinality(
1019 left_rels[0],
1020 right_rels[0],
1021 left_keys,
1022 right_keys,
1023 );
1024
1025 match join_type {
1026 JoinType::LeftOuter => estimated.max(left_cost.rows),
1027 _ => estimated,
1028 }
1029 } else {
1030 match join_type {
1032 JoinType::Inner => {
1033 ((left_cost.rows as f64 * right_cost.rows as f64 * 0.1) as u64).max(1)
1035 }
1036 JoinType::LeftOuter => {
1037 left_cost.rows.max(
1039 ((left_cost.rows as f64 * right_cost.rows as f64 * 0.1) as u64)
1040 .max(1),
1041 )
1042 }
1043 _ => unreachable!(),
1044 }
1045 }
1046 }
1047 };
1048
1049 let build_cost = right_cost.rows as f64 * 1.0; let probe_cost = left_cost.rows as f64 * 0.5; let cpu_cost = left_cost.cpu_cost + right_cost.cpu_cost + build_cost + probe_cost;
1053
1054 let hash_table_overhead = right_cost.gpu_mem / 2; let gpu_mem = left_cost.gpu_mem + right_cost.gpu_mem + hash_table_overhead;
1057
1058 PlanCost {
1059 rows: output_rows,
1060 cpu_cost,
1061 gpu_mem,
1062 transfers: left_cost.transfers + right_cost.transfers,
1063 }
1064 }
1065
1066 fn estimate_groupby_cost(
1068 &self,
1069 input_cost: PlanCost,
1070 key_cols: &[usize],
1071 _aggs: &[(usize, xlog_core::AggOp)],
1072 ) -> PlanCost {
1073 let estimated_groups = if key_cols.is_empty() {
1076 1 } else {
1078 ((input_cost.rows as f64).sqrt() as u64).max(1)
1080 };
1081
1082 PlanCost {
1083 rows: estimated_groups,
1084 cpu_cost: input_cost.cpu_cost + input_cost.rows as f64 * 0.5, gpu_mem: input_cost.gpu_mem + estimated_groups * self.config.default_bytes_per_row,
1086 transfers: input_cost.transfers,
1087 }
1088 }
1089
1090 fn estimate_union_cost(&self, input_costs: Vec<PlanCost>) -> PlanCost {
1092 let total_rows: u64 = input_costs.iter().map(|c| c.rows).sum();
1093 let total_cpu: f64 = input_costs.iter().map(|c| c.cpu_cost).sum();
1094 let max_gpu: u64 = input_costs.iter().map(|c| c.gpu_mem).max().unwrap_or(0);
1095 let total_transfers: u32 = input_costs.iter().map(|c| c.transfers).sum();
1096
1097 PlanCost {
1098 rows: total_rows,
1099 cpu_cost: total_cpu + total_rows as f64 * 0.01, gpu_mem: max_gpu, transfers: total_transfers,
1102 }
1103 }
1104
1105 fn estimate_distinct_cost(&self, input_cost: PlanCost, _key_cols: &[usize]) -> PlanCost {
1107 let estimated_distinct = (input_cost.rows as f64 * 0.7) as u64;
1109
1110 PlanCost {
1111 rows: estimated_distinct.max(1),
1112 cpu_cost: input_cost.cpu_cost + input_cost.rows as f64 * 0.3, gpu_mem: input_cost.gpu_mem + input_cost.rows * 8, transfers: input_cost.transfers,
1115 }
1116 }
1117
1118 fn estimate_diff_cost(&self, left_cost: PlanCost, right_cost: PlanCost) -> PlanCost {
1120 let estimated_remaining = (left_cost.rows as f64 * 0.5) as u64;
1122
1123 PlanCost {
1124 rows: estimated_remaining.max(1),
1125 cpu_cost: left_cost.cpu_cost + right_cost.cpu_cost + right_cost.rows as f64 * 0.5,
1126 gpu_mem: left_cost.gpu_mem + right_cost.gpu_mem,
1127 transfers: left_cost.transfers + right_cost.transfers,
1128 }
1129 }
1130
1131 fn estimate_fixpoint_cost(&self, base_cost: PlanCost, recursive_cost: PlanCost) -> PlanCost {
1133 let estimated_iterations = ((base_cost.rows as f64).log2().ceil() as u64).max(1);
1136
1137 PlanCost {
1138 rows: base_cost.rows * estimated_iterations, cpu_cost: base_cost.cpu_cost + recursive_cost.cpu_cost * estimated_iterations as f64,
1140 gpu_mem: (base_cost.gpu_mem + recursive_cost.gpu_mem) * 2, transfers: base_cost.transfers + recursive_cost.transfers * estimated_iterations as u32,
1142 }
1143 }
1144
1145 fn estimate_predicate_selectivity(&self, predicate: &Expr, input: &RirNode) -> f64 {
1147 match predicate {
1148 Expr::Compare { left, op, right } => {
1149 self.estimate_compare_selectivity(left, *op, right, input)
1150 }
1151 Expr::And(exprs) => {
1152 exprs
1154 .iter()
1155 .map(|e| self.estimate_predicate_selectivity(e, input))
1156 .product()
1157 }
1158 Expr::Or(exprs) => {
1159 exprs
1162 .iter()
1163 .map(|e| self.estimate_predicate_selectivity(e, input))
1164 .fold(0.0, f64::max)
1165 }
1166 Expr::Not(inner) => 1.0 - self.estimate_predicate_selectivity(inner, input),
1167 _ => self.config.default_filter_selectivity,
1168 }
1169 }
1170
1171 fn estimate_compare_selectivity(
1173 &self,
1174 left: &Expr,
1175 op: CompareOp,
1176 right: &Expr,
1177 input: &RirNode,
1178 ) -> f64 {
1179 if let (Expr::Column(col_idx), Expr::Const(_)) | (Expr::Const(_), Expr::Column(col_idx)) =
1181 (left, right)
1182 {
1183 if let Some(rel_id) = self.find_column_relation(input, *col_idx) {
1185 if let Some(stats) = self.stats.get_relation_stats(rel_id) {
1186 if let Some(col_stats) = stats.get_column(*col_idx) {
1187 return match op {
1188 CompareOp::Eq => col_stats.equality_selectivity(stats.cardinality),
1189 CompareOp::Ne => {
1190 1.0 - col_stats.equality_selectivity(stats.cardinality)
1191 }
1192 CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => {
1193 0.33
1195 }
1196 };
1197 }
1198 }
1199 }
1200 }
1201
1202 match op {
1204 CompareOp::Eq => 0.1, CompareOp::Ne => 0.9, CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => 0.33, }
1208 }
1209
1210 fn find_column_relation(&self, node: &RirNode, col_idx: usize) -> Option<RelId> {
1212 match node {
1213 RirNode::Scan { rel } => Some(*rel),
1214 RirNode::Filter { input, .. } => self.find_column_relation(input, col_idx),
1215 RirNode::Project { input, columns } => {
1216 if col_idx < columns.len() {
1218 if let xlog_ir::ProjectExpr::Column(src_idx) = &columns[col_idx] {
1219 return self.find_column_relation(input, *src_idx);
1220 }
1221 }
1222 None
1223 }
1224 RirNode::Join { left, right, .. } => {
1225 let left_width = self.estimate_width(left);
1226 if col_idx < left_width {
1227 self.find_column_relation(left, col_idx)
1228 } else {
1229 self.find_column_relation(right, col_idx - left_width)
1230 }
1231 }
1232 RirNode::MultiWayJoin { .. } => None,
1238 _ => None, }
1240 }
1241
1242 pub fn recommend_indexes(&self) -> Vec<RelId> {
1247 self.stats.hot_relations(self.config.index_heat_threshold)
1248 }
1249
1250 pub fn should_use_greedy(&self, node: &RirNode) -> bool {
1254 let rels = node.referenced_relations();
1255 let unique_rels: std::collections::HashSet<_> = rels.iter().collect();
1256 unique_rels.len() > self.config.dp_threshold
1257 }
1258}
1259
1260pub mod selectivity_pass {
1273 use std::collections::HashMap;
1319 use xlog_core::RelId;
1320 use xlog_ir::ExecutionPlan;
1321 use xlog_stats::StatsManager;
1322
1323 pub fn run(plan: &mut ExecutionPlan, stats: &StatsManager, rel_ids: &HashMap<String, RelId>) {
1333 let _ = rel_ids;
1339 for rules in plan.rules_by_scc.iter_mut() {
1340 for rule in rules.iter_mut() {
1341 if let Some(rewritten) = super::reorder::try_reorder_triangle(&rule.body, stats) {
1342 rule.body = rewritten;
1343 continue;
1344 }
1345 if let Some(rewritten) = super::reorder::try_reorder_4cycle(&rule.body, stats) {
1346 rule.body = rewritten;
1347 }
1348 }
1349 }
1350 }
1351}
1352
1353pub mod helper_split_pass {
1355 use std::collections::{HashMap, HashSet};
1356
1357 use xlog_core::{RelId, ScalarType, Schema};
1358 use xlog_ir::rir::{HelperSplitSpec, KCliqueVariableOrder};
1359 use xlog_ir::{CompiledRule, ExecutionPlan, JoinType, ProjectExpr, RirMeta, RirNode, Scc};
1360 use xlog_stats::StatsManager;
1361
1362 const HEAVY_SKEW_RATIO: f64 = 10.0;
1363
1364 #[derive(Debug, Clone, PartialEq, Eq)]
1366 pub struct HelperRelationSpec {
1367 pub name: String,
1369 pub rel_id: RelId,
1371 pub schema: Schema,
1373 pub source_rels: [RelId; 2],
1375 }
1376
1377 struct JoinStep {
1378 left_keys: Vec<usize>,
1379 right_keys: Vec<usize>,
1380 }
1381
1382 struct LinearBody {
1383 leaves: Vec<RelId>,
1384 leaf_classes: Vec<Vec<u32>>,
1385 joins: Vec<JoinStep>,
1386 project: Vec<ProjectExpr>,
1387 final_classes: Vec<u32>,
1388 }
1389
1390 struct FlatJoin {
1391 leaves: Vec<RelId>,
1392 output_cols: Vec<usize>,
1393 equalities: Vec<(usize, usize)>,
1394 }
1395
1396 struct Candidate {
1397 pair_start: usize,
1398 helper_schema: Schema,
1399 helper_project: Vec<ProjectExpr>,
1400 helper_join_left_keys: Vec<usize>,
1401 helper_join_right_keys: Vec<usize>,
1402 exposed_classes: Vec<u32>,
1403 }
1404
1405 struct Rewrite {
1406 helper_body: RirNode,
1407 outer_body: RirNode,
1408 spec: HelperRelationSpec,
1409 }
1410
1411 #[derive(Clone, Copy)]
1412 struct KCliqueHelperEdge {
1413 slot: usize,
1414 rel: RelId,
1415 left: usize,
1416 right: usize,
1417 }
1418
1419 pub fn run<F>(
1421 plan: &mut ExecutionPlan,
1422 schemas: &HashMap<RelId, Schema>,
1423 stats: &StatsManager,
1424 mut allocate: F,
1425 ) -> Vec<HelperRelationSpec>
1426 where
1427 F: FnMut(Schema) -> (String, RelId),
1428 {
1429 let mut specs = Vec::new();
1430 for scc_idx in 0..plan.rules_by_scc.len() {
1431 let mut rule_idx = 0;
1432 while rule_idx < plan.rules_by_scc[scc_idx].len() {
1433 let rewrite = {
1434 let rule = &plan.rules_by_scc[scc_idx][rule_idx];
1435 try_rewrite_rule(rule, schemas, stats, &mut allocate)
1436 };
1437 if let Some(rewrite) = rewrite {
1438 let helper_rule = CompiledRule {
1439 head: rewrite.spec.name.clone(),
1440 body: rewrite.helper_body,
1441 meta: RirMeta::with_schema(rewrite.spec.schema.clone()),
1442 };
1443 plan.rules_by_scc[scc_idx].insert(rule_idx, helper_rule);
1444 rule_idx += 1;
1445 plan.rules_by_scc[scc_idx][rule_idx].body = rewrite.outer_body;
1446 add_helper_to_scc(&mut plan.sccs, scc_idx, &rewrite.spec.name);
1447 specs.push(rewrite.spec);
1448 }
1449 rule_idx += 1;
1450 }
1451 }
1452 specs
1453 }
1454
1455 pub fn run_kclique_specs<F>(
1461 plan: &mut ExecutionPlan,
1462 schemas: &HashMap<RelId, Schema>,
1463 mut allocate: F,
1464 ) -> Vec<HelperRelationSpec>
1465 where
1466 F: FnMut(Schema) -> (String, RelId),
1467 {
1468 let mut specs = Vec::new();
1469 for scc_idx in 0..plan.rules_by_scc.len() {
1470 let mut rule_idx = 0;
1471 while rule_idx < plan.rules_by_scc[scc_idx].len() {
1472 let rewrite = {
1473 let rule = &plan.rules_by_scc[scc_idx][rule_idx];
1474 try_rewrite_kclique_rule(rule, schemas, &mut allocate)
1475 };
1476 if let Some(rewrite) = rewrite {
1477 let helper_rule = CompiledRule {
1478 head: rewrite.spec.name.clone(),
1479 body: rewrite.helper_body,
1480 meta: RirMeta::with_schema(rewrite.spec.schema.clone()),
1481 };
1482 plan.rules_by_scc[scc_idx].insert(rule_idx, helper_rule);
1483 rule_idx += 1;
1484 plan.rules_by_scc[scc_idx][rule_idx].body = rewrite.outer_body;
1485 add_helper_to_scc(&mut plan.sccs, scc_idx, &rewrite.spec.name);
1486 specs.push(rewrite.spec);
1487 }
1488 rule_idx += 1;
1489 }
1490 }
1491 specs
1492 }
1493
1494 fn add_helper_to_scc(sccs: &mut [Scc], scc_idx: usize, helper: &str) {
1495 if let Some(scc) = sccs.get_mut(scc_idx) {
1496 if !scc.predicates.iter().any(|p| p == helper) {
1497 scc.predicates.push(helper.to_string());
1498 }
1499 }
1500 }
1501
1502 fn try_rewrite_rule<F>(
1503 rule: &CompiledRule,
1504 schemas: &HashMap<RelId, Schema>,
1505 stats: &StatsManager,
1506 allocate: &mut F,
1507 ) -> Option<Rewrite>
1508 where
1509 F: FnMut(Schema) -> (String, RelId),
1510 {
1511 let linear = linearize_project_body(&rule.body, schemas)?;
1512 let candidate = choose_candidate(&linear, schemas, stats)?;
1513 let (helper_name, helper_rel) = allocate(candidate.helper_schema.clone());
1514 let helper_body = build_helper_body(&linear, &candidate);
1515 let outer_body = build_outer_body(&linear, &candidate, helper_rel)?;
1516 Some(Rewrite {
1517 helper_body,
1518 outer_body,
1519 spec: HelperRelationSpec {
1520 name: helper_name,
1521 rel_id: helper_rel,
1522 schema: candidate.helper_schema,
1523 source_rels: [
1524 linear.leaves[candidate.pair_start],
1525 linear.leaves[candidate.pair_start + 1],
1526 ],
1527 },
1528 })
1529 }
1530
1531 fn try_rewrite_kclique_rule<F>(
1532 rule: &CompiledRule,
1533 schemas: &HashMap<RelId, Schema>,
1534 allocate: &mut F,
1535 ) -> Option<Rewrite>
1536 where
1537 F: FnMut(Schema) -> (String, RelId),
1538 {
1539 if !matches!(&rule.body, RirNode::MultiWayJoin { .. }) {
1540 return None;
1541 }
1542 let mut outer_body = rule.body.clone();
1543 let RirNode::MultiWayJoin {
1544 inputs, var_order, ..
1545 } = &mut outer_body
1546 else {
1547 return None;
1548 };
1549 let kclique = var_order.as_ref()?.kclique.as_ref()?;
1550 let spec = kclique.helper_split_specs.first()?;
1551 let (hot_left, hot_right, target) = kclique_helper_edges(inputs, kclique, spec)?;
1552 let helper_schema = schemas.get(&target.rel)?.clone();
1553 let (helper_name, helper_rel) = allocate(helper_schema.clone());
1554 let helper_body = build_kclique_helper_body(spec, hot_left, hot_right, target)?;
1555 *inputs.get_mut(target.slot)? = RirNode::Scan { rel: helper_rel };
1556 Some(Rewrite {
1557 helper_body,
1558 outer_body,
1559 spec: HelperRelationSpec {
1560 name: helper_name,
1561 rel_id: helper_rel,
1562 schema: helper_schema,
1563 source_rels: [hot_left.rel, hot_right.rel],
1564 },
1565 })
1566 }
1567
1568 fn kclique_helper_edges(
1569 inputs: &[RirNode],
1570 kclique: &KCliqueVariableOrder,
1571 spec: &HelperSplitSpec,
1572 ) -> Option<(KCliqueHelperEdge, KCliqueHelperEdge, KCliqueHelperEdge)> {
1573 let k = usize::from(kclique.k);
1574 let hot = usize::from(spec.variable);
1575 let mut hot_edges = Vec::new();
1576 let mut target = None;
1577 for &slot in &spec.edge_slots {
1578 let slot = usize::from(slot);
1579 let (left, right) = kclique_edge_pair(slot, k)?;
1580 let RirNode::Scan { rel } = inputs.get(slot)? else {
1581 return None;
1582 };
1583 let edge = KCliqueHelperEdge {
1584 slot,
1585 rel: *rel,
1586 left,
1587 right,
1588 };
1589 if left == hot || right == hot {
1590 hot_edges.push(edge);
1591 } else {
1592 target = Some(edge);
1593 }
1594 }
1595 if hot_edges.len() != 2 {
1596 return None;
1597 }
1598 Some((hot_edges[0], hot_edges[1], target?))
1599 }
1600
1601 fn build_kclique_helper_body(
1602 spec: &HelperSplitSpec,
1603 hot_left: KCliqueHelperEdge,
1604 hot_right: KCliqueHelperEdge,
1605 target: KCliqueHelperEdge,
1606 ) -> Option<RirNode> {
1607 let hot = usize::from(spec.variable);
1608 let target_left = target.left;
1609 let target_right = target.right;
1610 let first_other = kclique_other_endpoint(hot_left, hot)?;
1611 let second_other = kclique_other_endpoint(hot_right, hot)?;
1612 if ![first_other, second_other].contains(&target_left)
1613 || ![first_other, second_other].contains(&target_right)
1614 {
1615 return None;
1616 }
1617
1618 let first_scan = RirNode::Scan { rel: hot_left.rel };
1619 let second_scan = RirNode::Scan { rel: hot_right.rel };
1620 let target_scan = RirNode::Scan { rel: target.rel };
1621 let first_hot_col = kclique_endpoint_col(hot_left, hot)?;
1622 let second_hot_col = kclique_endpoint_col(hot_right, hot)?;
1623 let first_other_col = kclique_endpoint_col(hot_left, first_other)?;
1624 let second_other_col = 2 + kclique_endpoint_col(hot_right, second_other)?;
1625
1626 let target_left_in_join = if first_other == target_left {
1627 first_other_col
1628 } else {
1629 second_other_col
1630 };
1631 let target_right_in_join = if first_other == target_right {
1632 first_other_col
1633 } else {
1634 second_other_col
1635 };
1636 let target_left_col = kclique_endpoint_col(target, target_left)?;
1637 let target_right_col = kclique_endpoint_col(target, target_right)?;
1638
1639 let hot_join = RirNode::Join {
1640 left: Box::new(first_scan),
1641 right: Box::new(second_scan),
1642 left_keys: vec![first_hot_col],
1643 right_keys: vec![second_hot_col],
1644 join_type: JoinType::Inner,
1645 };
1646 let helper_join = RirNode::Join {
1647 left: Box::new(hot_join),
1648 right: Box::new(target_scan),
1649 left_keys: vec![target_left_in_join, target_right_in_join],
1650 right_keys: vec![target_left_col, target_right_col],
1651 join_type: JoinType::Inner,
1652 };
1653 Some(RirNode::Project {
1654 input: Box::new(helper_join),
1655 columns: vec![ProjectExpr::Column(4), ProjectExpr::Column(5)],
1656 })
1657 }
1658
1659 fn kclique_edge_pair(edge_idx: usize, k: usize) -> Option<(usize, usize)> {
1660 let mut idx = 0usize;
1661 for left in 0..k {
1662 for right in (left + 1)..k {
1663 if idx == edge_idx {
1664 return Some((left, right));
1665 }
1666 idx += 1;
1667 }
1668 }
1669 None
1670 }
1671
1672 fn kclique_endpoint_col(edge: KCliqueHelperEdge, variable: usize) -> Option<usize> {
1673 if edge.left == variable {
1674 Some(0)
1675 } else if edge.right == variable {
1676 Some(1)
1677 } else {
1678 None
1679 }
1680 }
1681
1682 fn kclique_other_endpoint(edge: KCliqueHelperEdge, variable: usize) -> Option<usize> {
1683 if edge.left == variable {
1684 Some(edge.right)
1685 } else if edge.right == variable {
1686 Some(edge.left)
1687 } else {
1688 None
1689 }
1690 }
1691
1692 fn linearize_project_body(
1693 body: &RirNode,
1694 schemas: &HashMap<RelId, Schema>,
1695 ) -> Option<LinearBody> {
1696 let RirNode::Project { input, columns } = body else {
1697 return None;
1698 };
1699 let flat = collect_join_graph(input, schemas)?;
1700 if flat.leaves.len() < 6 {
1701 return None;
1702 }
1703 let mut offsets = Vec::with_capacity(flat.leaves.len());
1704 let mut total_cols = 0usize;
1705 for rel in &flat.leaves {
1706 offsets.push(total_cols);
1707 total_cols += schemas.get(rel)?.arity();
1708 }
1709 let mut uf = UnionFind::new(total_cols);
1710 for (left, right) in flat.equalities {
1711 if left >= total_cols || right >= total_cols {
1712 return None;
1713 }
1714 uf.union(left, right);
1715 }
1716 let mut leaf_classes: Vec<Vec<u32>> = Vec::with_capacity(flat.leaves.len());
1717 for (leaf_idx, rel) in flat.leaves.iter().enumerate() {
1718 let arity = schemas.get(rel)?.arity();
1719 let offset = offsets[leaf_idx];
1720 leaf_classes.push((0..arity).map(|col| uf.find(offset + col) as u32).collect());
1721 }
1722 let final_classes = flat
1723 .output_cols
1724 .iter()
1725 .map(|col| uf.find(*col) as u32)
1726 .collect();
1727 let joins = derive_left_deep_steps(&leaf_classes)?;
1728 Some(LinearBody {
1729 leaves: flat.leaves,
1730 leaf_classes,
1731 joins,
1732 project: columns.clone(),
1733 final_classes,
1734 })
1735 }
1736
1737 fn collect_join_graph(node: &RirNode, schemas: &HashMap<RelId, Schema>) -> Option<FlatJoin> {
1738 match node {
1739 RirNode::Scan { rel } => Some(FlatJoin {
1740 leaves: vec![*rel],
1741 output_cols: (0..schemas.get(rel)?.arity()).collect(),
1742 equalities: Vec::new(),
1743 }),
1744 RirNode::Join {
1745 left,
1746 right,
1747 left_keys,
1748 right_keys,
1749 join_type,
1750 } if *join_type == JoinType::Inner => {
1751 let left_flat = collect_join_graph(left, schemas)?;
1752 let right_flat = collect_join_graph(right, schemas)?;
1753 if left_keys.len() != right_keys.len() {
1754 return None;
1755 }
1756 let right_shift = total_width(&left_flat.leaves, schemas)?;
1757 let mut leaves = left_flat.leaves;
1758 leaves.extend(right_flat.leaves);
1759 let right_output_cols: Vec<usize> = right_flat
1760 .output_cols
1761 .iter()
1762 .map(|col| col + right_shift)
1763 .collect();
1764 let mut equalities = left_flat.equalities;
1765 equalities.extend(
1766 right_flat
1767 .equalities
1768 .iter()
1769 .map(|(left, right)| (left + right_shift, right + right_shift)),
1770 );
1771 for (&left_key, &right_key) in left_keys.iter().zip(right_keys.iter()) {
1772 equalities.push((
1773 *left_flat.output_cols.get(left_key)?,
1774 *right_output_cols.get(right_key)?,
1775 ));
1776 }
1777 let mut output_cols = left_flat.output_cols;
1778 output_cols.extend(right_output_cols);
1779 Some(FlatJoin {
1780 leaves,
1781 output_cols,
1782 equalities,
1783 })
1784 }
1785 _ => None,
1786 }
1787 }
1788
1789 fn total_width(leaves: &[RelId], schemas: &HashMap<RelId, Schema>) -> Option<usize> {
1790 leaves
1791 .iter()
1792 .map(|rel| schemas.get(rel).map(Schema::arity))
1793 .try_fold(0usize, |acc, width| width.map(|width| acc + width))
1794 }
1795
1796 fn derive_left_deep_steps(leaf_classes: &[Vec<u32>]) -> Option<Vec<JoinStep>> {
1797 let mut joins = Vec::with_capacity(leaf_classes.len().saturating_sub(1));
1798 let mut current = leaf_classes.first()?.clone();
1799 for classes in leaf_classes.iter().skip(1) {
1800 let mut left_keys = Vec::new();
1801 let mut right_keys = Vec::new();
1802 for (right_col, class) in classes.iter().enumerate() {
1803 if let Some(left_col) = current
1804 .iter()
1805 .position(|current_class| current_class == class)
1806 {
1807 left_keys.push(left_col);
1808 right_keys.push(right_col);
1809 }
1810 }
1811 if left_keys.is_empty() {
1812 return None;
1813 }
1814 joins.push(JoinStep {
1815 left_keys,
1816 right_keys,
1817 });
1818 current.extend(classes.iter().copied());
1819 }
1820 Some(joins)
1821 }
1822
1823 fn choose_candidate(
1824 linear: &LinearBody,
1825 schemas: &HashMap<RelId, Schema>,
1826 stats: &StatsManager,
1827 ) -> Option<Candidate> {
1828 for pair_start in 3..linear.leaves.len().saturating_sub(1) {
1829 let candidate = build_candidate(linear, schemas, pair_start)?;
1830 if skew_ratio_for_candidate(linear, stats, &candidate) >= HEAVY_SKEW_RATIO {
1831 return Some(candidate);
1832 }
1833 }
1834 None
1835 }
1836
1837 fn build_candidate(
1838 linear: &LinearBody,
1839 schemas: &HashMap<RelId, Schema>,
1840 pair_start: usize,
1841 ) -> Option<Candidate> {
1842 let left_rel = linear.leaves[pair_start];
1843 let right_rel = linear.leaves[pair_start + 1];
1844 let left_schema = schemas.get(&left_rel)?;
1845 let right_schema = schemas.get(&right_rel)?;
1846 let internal_step = linear.joins.get(pair_start)?;
1847 let mut helper_left_keys = Vec::new();
1848 let mut helper_right_keys = Vec::new();
1849 for (&left_key, &right_key) in internal_step
1850 .left_keys
1851 .iter()
1852 .zip(internal_step.right_keys.iter())
1853 {
1854 let class = class_at_state(linear, pair_start + 1, left_key)?;
1855 let left_col = linear.leaf_classes[pair_start]
1856 .iter()
1857 .position(|c| *c == class)?;
1858 helper_left_keys.push(left_col);
1859 helper_right_keys.push(right_key);
1860 }
1861 let internal: HashSet<u32> = helper_left_keys
1862 .iter()
1863 .map(|col| linear.leaf_classes[pair_start][*col])
1864 .collect();
1865 let outside = outside_classes(linear, pair_start);
1866 let output = projected_classes(linear)?;
1867 let mut exposed_classes = Vec::new();
1868 let mut helper_project = Vec::new();
1869 let mut helper_columns = Vec::new();
1870 for (col, class) in linear.leaf_classes[pair_start].iter().copied().enumerate() {
1871 if !internal.contains(&class)
1872 && (outside.contains(&class) || output.contains(&class))
1873 && !exposed_classes.contains(&class)
1874 {
1875 exposed_classes.push(class);
1876 helper_project.push(ProjectExpr::Column(col));
1877 let ty = left_schema.column_type(col).unwrap_or(ScalarType::U32);
1878 helper_columns.push((format!("c{}", helper_columns.len()), ty));
1879 }
1880 }
1881 let right_offset = left_schema.arity();
1882 for (col, class) in linear.leaf_classes[pair_start + 1]
1883 .iter()
1884 .copied()
1885 .enumerate()
1886 {
1887 if !internal.contains(&class)
1888 && (outside.contains(&class) || output.contains(&class))
1889 && !exposed_classes.contains(&class)
1890 {
1891 exposed_classes.push(class);
1892 helper_project.push(ProjectExpr::Column(right_offset + col));
1893 let ty = right_schema.column_type(col).unwrap_or(ScalarType::U32);
1894 helper_columns.push((format!("c{}", helper_columns.len()), ty));
1895 }
1896 }
1897 if exposed_classes.len() != 2 {
1898 return None;
1899 }
1900 Some(Candidate {
1901 pair_start,
1902 helper_schema: Schema::new(helper_columns),
1903 helper_project,
1904 helper_join_left_keys: helper_left_keys,
1905 helper_join_right_keys: helper_right_keys,
1906 exposed_classes,
1907 })
1908 }
1909
1910 fn class_at_state(linear: &LinearBody, leaf_count: usize, col: usize) -> Option<u32> {
1911 let mut idx = col;
1912 for leaf_idx in 0..leaf_count {
1913 let classes = &linear.leaf_classes[leaf_idx];
1914 if idx < classes.len() {
1915 return Some(classes[idx]);
1916 }
1917 idx -= classes.len();
1918 }
1919 None
1920 }
1921
1922 fn outside_classes(linear: &LinearBody, pair_start: usize) -> HashSet<u32> {
1923 linear
1924 .leaf_classes
1925 .iter()
1926 .enumerate()
1927 .filter(|(idx, _)| *idx != pair_start && *idx != pair_start + 1)
1928 .flat_map(|(_, classes)| classes.iter().copied())
1929 .collect()
1930 }
1931
1932 fn projected_classes(linear: &LinearBody) -> Option<HashSet<u32>> {
1933 let mut out = HashSet::new();
1934 for expr in &linear.project {
1935 let ProjectExpr::Column(col) = expr else {
1936 return None;
1937 };
1938 out.insert(*linear.final_classes.get(*col)?);
1939 }
1940 Some(out)
1941 }
1942
1943 fn skew_ratio_for_candidate(
1944 linear: &LinearBody,
1945 stats: &StatsManager,
1946 candidate: &Candidate,
1947 ) -> f64 {
1948 let rel = linear.leaves[candidate.pair_start];
1949 let Some(rel_stats) = stats.get_relation_stats(rel) else {
1950 return 0.0;
1951 };
1952 let mut ratio: f64 = 0.0;
1953 for (col, class) in linear.leaf_classes[candidate.pair_start]
1954 .iter()
1955 .copied()
1956 .enumerate()
1957 {
1958 if !candidate.exposed_classes.contains(&class) {
1959 continue;
1960 }
1961 let Some(col_stats) = rel_stats.get_column(col) else {
1962 continue;
1963 };
1964 if col_stats.distinct_estimate == 0 {
1965 continue;
1966 }
1967 ratio = ratio.max(rel_stats.cardinality as f64 / col_stats.distinct_estimate as f64);
1968 }
1969 ratio
1970 }
1971
1972 fn build_helper_body(linear: &LinearBody, candidate: &Candidate) -> RirNode {
1973 let left = RirNode::Scan {
1974 rel: linear.leaves[candidate.pair_start],
1975 };
1976 let right = RirNode::Scan {
1977 rel: linear.leaves[candidate.pair_start + 1],
1978 };
1979 RirNode::Project {
1980 input: Box::new(RirNode::Join {
1981 left: Box::new(left),
1982 right: Box::new(right),
1983 left_keys: candidate.helper_join_left_keys.clone(),
1984 right_keys: candidate.helper_join_right_keys.clone(),
1985 join_type: JoinType::Inner,
1986 }),
1987 columns: candidate.helper_project.clone(),
1988 }
1989 }
1990
1991 fn build_outer_body(
1992 linear: &LinearBody,
1993 candidate: &Candidate,
1994 helper_rel: RelId,
1995 ) -> Option<RirNode> {
1996 let mut node = RirNode::Scan {
1997 rel: linear.leaves[0],
1998 };
1999 let mut classes = linear.leaf_classes[0].clone();
2000 for leaf_idx in 1..candidate.pair_start {
2001 let step = &linear.joins[leaf_idx - 1];
2002 node = RirNode::Join {
2003 left: Box::new(node),
2004 right: Box::new(RirNode::Scan {
2005 rel: linear.leaves[leaf_idx],
2006 }),
2007 left_keys: step.left_keys.clone(),
2008 right_keys: step.right_keys.clone(),
2009 join_type: JoinType::Inner,
2010 };
2011 classes.extend(linear.leaf_classes[leaf_idx].iter().copied());
2012 }
2013 let prefix_step = &linear.joins[candidate.pair_start - 1];
2014 let mut helper_right_keys = Vec::new();
2015 for &rk in &prefix_step.right_keys {
2016 let class = linear.leaf_classes[candidate.pair_start][rk];
2017 helper_right_keys.push(candidate.exposed_classes.iter().position(|c| *c == class)?);
2018 }
2019 node = RirNode::Join {
2020 left: Box::new(node),
2021 right: Box::new(RirNode::Scan { rel: helper_rel }),
2022 left_keys: prefix_step.left_keys.clone(),
2023 right_keys: helper_right_keys,
2024 join_type: JoinType::Inner,
2025 };
2026 classes.extend(candidate.exposed_classes.iter().copied());
2027 for leaf_idx in candidate.pair_start + 2..linear.leaves.len() {
2028 let step = &linear.joins[leaf_idx - 1];
2029 let mut left_keys = Vec::new();
2030 for &lk in &step.left_keys {
2031 let class = class_at_state(linear, leaf_idx, lk)?;
2032 left_keys.push(classes.iter().position(|c| *c == class)?);
2033 }
2034 node = RirNode::Join {
2035 left: Box::new(node),
2036 right: Box::new(RirNode::Scan {
2037 rel: linear.leaves[leaf_idx],
2038 }),
2039 left_keys,
2040 right_keys: step.right_keys.clone(),
2041 join_type: JoinType::Inner,
2042 };
2043 classes.extend(linear.leaf_classes[leaf_idx].iter().copied());
2044 }
2045 let mut project = Vec::with_capacity(linear.project.len());
2046 for expr in &linear.project {
2047 let ProjectExpr::Column(col) = expr else {
2048 return None;
2049 };
2050 let class = *linear.final_classes.get(*col)?;
2051 let mapped = classes.iter().position(|c| *c == class)?;
2052 project.push(ProjectExpr::Column(mapped));
2053 }
2054 Some(RirNode::Project {
2055 input: Box::new(node),
2056 columns: project,
2057 })
2058 }
2059
2060 struct UnionFind {
2061 parent: Vec<usize>,
2062 }
2063
2064 impl UnionFind {
2065 fn new(len: usize) -> Self {
2066 Self {
2067 parent: (0..len).collect(),
2068 }
2069 }
2070
2071 fn find(&mut self, x: usize) -> usize {
2072 let p = self.parent[x];
2073 if p == x {
2074 x
2075 } else {
2076 let root = self.find(p);
2077 self.parent[x] = root;
2078 root
2079 }
2080 }
2081
2082 fn union(&mut self, a: usize, b: usize) {
2083 let ra = self.find(a);
2084 let rb = self.find(b);
2085 if ra != rb {
2086 self.parent[rb] = ra;
2087 }
2088 }
2089 }
2090}
2091
2092#[path = "optimizer/stream_schedule_pass.rs"]
2093pub mod stream_schedule_pass;
2094
2095#[cfg(test)]
2096mod helper_split_pass_tests {
2097 use std::collections::HashMap;
2098
2099 use super::helper_split_pass;
2100 use xlog_core::{RelId, ScalarType, Schema};
2101 use xlog_ir::{CompiledRule, ExecutionPlan, JoinType, ProjectExpr, RirMeta, RirNode, Scc};
2102 use xlog_stats::{ColumnStats, StatsManager};
2103
2104 fn edge_schema() -> Schema {
2105 Schema::new(vec![
2106 ("c0".to_string(), ScalarType::U32),
2107 ("c1".to_string(), ScalarType::U32),
2108 ])
2109 }
2110
2111 fn helper_schema() -> Schema {
2112 Schema::new(vec![
2113 ("c0".to_string(), ScalarType::U32),
2114 ("c1".to_string(), ScalarType::U32),
2115 ])
2116 }
2117
2118 fn schemas() -> HashMap<RelId, Schema> {
2119 (0..6)
2120 .map(|idx| (RelId(idx), edge_schema()))
2121 .collect::<HashMap<_, _>>()
2122 }
2123
2124 fn left_deep_fixture_body() -> RirNode {
2125 let ab_bc = RirNode::Join {
2126 left: Box::new(RirNode::Scan { rel: RelId(0) }),
2127 right: Box::new(RirNode::Scan { rel: RelId(1) }),
2128 left_keys: vec![1],
2129 right_keys: vec![0],
2130 join_type: JoinType::Inner,
2131 };
2132 let with_cd = RirNode::Join {
2133 left: Box::new(ab_bc),
2134 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2135 left_keys: vec![3],
2136 right_keys: vec![0],
2137 join_type: JoinType::Inner,
2138 };
2139 let with_de = RirNode::Join {
2140 left: Box::new(with_cd),
2141 right: Box::new(RirNode::Scan { rel: RelId(3) }),
2142 left_keys: vec![5],
2143 right_keys: vec![0],
2144 join_type: JoinType::Inner,
2145 };
2146 let with_ef = RirNode::Join {
2147 left: Box::new(with_de),
2148 right: Box::new(RirNode::Scan { rel: RelId(4) }),
2149 left_keys: vec![7],
2150 right_keys: vec![0],
2151 join_type: JoinType::Inner,
2152 };
2153 let with_af = RirNode::Join {
2154 left: Box::new(with_ef),
2155 right: Box::new(RirNode::Scan { rel: RelId(5) }),
2156 left_keys: vec![0, 9],
2157 right_keys: vec![0, 1],
2158 join_type: JoinType::Inner,
2159 };
2160 RirNode::Project {
2161 input: Box::new(with_af),
2162 columns: vec![
2163 ProjectExpr::Column(0),
2164 ProjectExpr::Column(1),
2165 ProjectExpr::Column(3),
2166 ProjectExpr::Column(5),
2167 ProjectExpr::Column(9),
2168 ],
2169 }
2170 }
2171
2172 fn plan() -> ExecutionPlan {
2173 ExecutionPlan {
2174 sccs: vec![Scc {
2175 id: 0,
2176 predicates: vec!["out".to_string()],
2177 is_recursive: false,
2178 }],
2179 strata: vec![],
2180 rules_by_scc: vec![vec![CompiledRule {
2181 head: "out".to_string(),
2182 body: left_deep_fixture_body(),
2183 meta: RirMeta::with_schema(Schema::new(vec![
2184 ("a".to_string(), ScalarType::U32),
2185 ("b".to_string(), ScalarType::U32),
2186 ("c".to_string(), ScalarType::U32),
2187 ("d".to_string(), ScalarType::U32),
2188 ("f".to_string(), ScalarType::U32),
2189 ])),
2190 }]],
2191 generated_query_rules: vec![],
2192 est_memory_peak: 0,
2193 rel_arities: std::collections::HashMap::new(),
2194 }
2195 }
2196
2197 fn stats_for_de(distinct_d: u64) -> StatsManager {
2198 let mut stats = StatsManager::new();
2199 for idx in 0..6 {
2200 stats.register_relation(RelId(idx));
2201 stats.update_cardinality(RelId(idx), 8192);
2202 }
2203 let mut d_col = ColumnStats::new(0, ScalarType::U32);
2204 d_col.update_distinct(distinct_d);
2205 stats.add_column_stats(RelId(3), d_col);
2206 stats
2207 }
2208
2209 fn contains_scan(node: &RirNode, rel: RelId) -> bool {
2210 match node {
2211 RirNode::Scan { rel: scan_rel } => *scan_rel == rel,
2212 RirNode::Join { left, right, .. } | RirNode::ChainJoin { left, right, .. } => {
2213 contains_scan(left, rel) || contains_scan(right, rel)
2214 }
2215 RirNode::Project { input, .. }
2216 | RirNode::Filter { input, .. }
2217 | RirNode::Distinct { input, .. }
2218 | RirNode::GroupBy { input, .. } => contains_scan(input, rel),
2219 RirNode::Union { inputs } => inputs.iter().any(|input| contains_scan(input, rel)),
2220 RirNode::Diff { left, right } => contains_scan(left, rel) || contains_scan(right, rel),
2221 RirNode::Fixpoint {
2222 base, recursive, ..
2223 } => contains_scan(base, rel) || contains_scan(recursive, rel),
2224 RirNode::MultiWayJoin { inputs, .. } => {
2225 inputs.iter().any(|input| contains_scan(input, rel))
2226 }
2227 RirNode::TensorMaskedJoin { rel_index, .. } => {
2228 rel_index.iter().any(|(input_rel, _)| *input_rel == rel)
2229 }
2230 RirNode::Unit => false,
2231 }
2232 }
2233
2234 #[test]
2235 fn helper_split_extracts_buried_pair() {
2236 let mut plan = plan();
2237 let schemas = schemas();
2238 let stats = stats_for_de(1);
2239 let specs = helper_split_pass::run(&mut plan, &schemas, &stats, |_| {
2240 ("__kclique_helper_6".to_string(), RelId(6))
2241 });
2242
2243 assert_eq!(specs.len(), 1);
2244 assert_eq!(specs[0].name, "__kclique_helper_6");
2245 assert_eq!(specs[0].rel_id, RelId(6));
2246 assert_eq!(specs[0].schema, helper_schema());
2247 assert_eq!(specs[0].source_rels, [RelId(3), RelId(4)]);
2248 assert_eq!(plan.rules_by_scc[0].len(), 2);
2249 assert_eq!(plan.rules_by_scc[0][0].head, "__kclique_helper_6");
2250 assert_eq!(plan.rules_by_scc[0][1].head, "out");
2251 assert!(contains_scan(&plan.rules_by_scc[0][1].body, RelId(6)));
2252 assert!(plan.sccs[0]
2253 .predicates
2254 .iter()
2255 .any(|predicate| predicate == "__kclique_helper_6"));
2256 }
2257
2258 #[test]
2259 fn helper_split_ignores_flat_distribution() {
2260 let mut plan = plan();
2261 let schemas = schemas();
2262 let stats = stats_for_de(8192);
2263 let specs = helper_split_pass::run(&mut plan, &schemas, &stats, |_| {
2264 ("__kclique_helper_6".to_string(), RelId(6))
2265 });
2266
2267 assert!(specs.is_empty());
2268 assert_eq!(plan.rules_by_scc[0].len(), 1);
2269 assert!(!contains_scan(&plan.rules_by_scc[0][0].body, RelId(6)));
2270 }
2271}
2272
2273mod reorder {
2276 use std::collections::HashMap;
2277 use xlog_core::RelId;
2278 use xlog_ir::rir::ProjectExpr;
2279 use xlog_ir::{JoinType, RirNode};
2280 use xlog_stats::StatsManager;
2281
2282 fn ac3(atom: u8, col: u8) -> u8 {
2283 atom * 2 + col
2284 }
2285 fn ac4(atom: u8, col: u8) -> u8 {
2286 atom * 2 + col
2287 }
2288 fn uf_find_n<const N: usize>(parent: &mut [u8; N], x: u8) -> u8 {
2289 let mut root = x;
2290 while parent[root as usize] != root {
2291 root = parent[root as usize];
2292 }
2293 let mut cur = x;
2294 while parent[cur as usize] != root {
2295 let next = parent[cur as usize];
2296 parent[cur as usize] = root;
2297 cur = next;
2298 }
2299 root
2300 }
2301 fn uf_union_n<const N: usize>(parent: &mut [u8; N], a: u8, b: u8) {
2302 let ra = uf_find_n(parent, a);
2303 let rb = uf_find_n(parent, b);
2304 if ra != rb {
2305 parent[rb as usize] = ra;
2306 }
2307 }
2308
2309 fn populated_card(stats: &StatsManager, rel: RelId) -> Option<u64> {
2310 stats
2311 .get_relation_stats(rel)
2312 .map(|s| s.cardinality)
2313 .filter(|c| *c > 0)
2314 }
2315
2316 struct TriangleSemantics {
2321 rel_xy: RelId,
2322 rel_yz: RelId,
2323 rel_xz: RelId,
2324 }
2325
2326 fn match_and_infer_triangle(body: &RirNode) -> Option<TriangleSemantics> {
2327 let RirNode::Project {
2328 input: outer_input,
2329 columns,
2330 } = body
2331 else {
2332 return None;
2333 };
2334 let RirNode::Join {
2335 left: l1,
2336 right: r1,
2337 left_keys: lk1,
2338 right_keys: rk1,
2339 join_type: jt1,
2340 } = outer_input.as_ref()
2341 else {
2342 return None;
2343 };
2344 if !matches!(jt1, JoinType::Inner) {
2345 return None;
2346 }
2347 let RirNode::Scan { rel: rel_third } = r1.as_ref() else {
2348 return None;
2349 };
2350 let RirNode::Join {
2351 left: l2,
2352 right: r2,
2353 left_keys: lk2,
2354 right_keys: rk2,
2355 join_type: jt2,
2356 } = l1.as_ref()
2357 else {
2358 return None;
2359 };
2360 if !matches!(jt2, JoinType::Inner) {
2361 return None;
2362 }
2363 let RirNode::Scan { rel: rel_inner_l } = l2.as_ref() else {
2364 return None;
2365 };
2366 let RirNode::Scan { rel: rel_inner_r } = r2.as_ref() else {
2367 return None;
2368 };
2369 if lk2.len() != 1 || rk2.len() != 1 || lk1.len() != 2 || rk1.len() != 2 {
2370 return None;
2371 }
2372 if columns.len() != 3 {
2373 return None;
2374 }
2375 if lk2[0] >= 2 || rk2[0] >= 2 {
2376 return None;
2377 }
2378 if lk1.iter().any(|k| *k >= 4) || rk1.iter().any(|k| *k >= 2) {
2379 return None;
2380 }
2381
2382 let mut parent = [0u8, 1, 2, 3, 4, 5];
2383 uf_union_n::<6>(&mut parent, ac3(0, lk2[0] as u8), ac3(1, rk2[0] as u8));
2384 for i in 0..2 {
2385 let inner_ac = match lk1[i] {
2386 0 => (0u8, 0u8),
2387 1 => (0, 1),
2388 2 => (1, 0),
2389 3 => (1, 1),
2390 _ => return None,
2391 };
2392 uf_union_n::<6>(
2393 &mut parent,
2394 ac3(inner_ac.0, inner_ac.1),
2395 ac3(2, rk1[i] as u8),
2396 );
2397 }
2398 let roots: [u8; 6] = std::array::from_fn(|i| uf_find_n::<6>(&mut parent, i as u8));
2399 let mut counts: HashMap<u8, u8> = HashMap::new();
2400 for r in &roots {
2401 *counts.entry(*r).or_insert(0) += 1;
2402 }
2403 if counts.len() != 3 || counts.values().any(|c| *c != 2) {
2404 return None;
2405 }
2406 let mut head_classes: [u8; 3] = [0; 3];
2407 for (i, pc) in columns.iter().enumerate() {
2408 let ProjectExpr::Column(k) = pc else {
2409 return None;
2410 };
2411 let outer_ac = match *k {
2412 0 => (0u8, 0u8),
2413 1 => (0, 1),
2414 2 => (1, 0),
2415 3 => (1, 1),
2416 4 => (2, 0),
2417 5 => (2, 1),
2418 _ => return None,
2419 };
2420 head_classes[i] = uf_find_n::<6>(&mut parent, ac3(outer_ac.0, outer_ac.1));
2421 }
2422 if head_classes[0] == head_classes[1]
2423 || head_classes[0] == head_classes[2]
2424 || head_classes[1] == head_classes[2]
2425 {
2426 return None;
2427 }
2428 let x_class = head_classes[0];
2429 let y_class = head_classes[1];
2430 let z_class = head_classes[2];
2431 let atom_classes = |a: u8| (roots[ac3(a, 0) as usize], roots[ac3(a, 1) as usize]);
2432 let atom_rels = [*rel_inner_l, *rel_inner_r, *rel_third];
2433 let mut rel_xy = None;
2434 let mut rel_yz = None;
2435 let mut rel_xz = None;
2436 for atom_idx in 0..3u8 {
2437 let (c0, c1) = atom_classes(atom_idx);
2438 let bx = c0 == x_class || c1 == x_class;
2439 let by = c0 == y_class || c1 == y_class;
2440 let bz = c0 == z_class || c1 == z_class;
2441 match (bx, by, bz) {
2442 (true, true, false) => rel_xy = Some(atom_rels[atom_idx as usize]),
2443 (false, true, true) => rel_yz = Some(atom_rels[atom_idx as usize]),
2444 (true, false, true) => rel_xz = Some(atom_rels[atom_idx as usize]),
2445 _ => return None,
2446 }
2447 }
2448 Some(TriangleSemantics {
2449 rel_xy: rel_xy?,
2450 rel_yz: rel_yz?,
2451 rel_xz: rel_xz?,
2452 })
2453 }
2454
2455 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2456 enum TriangleInnerPair {
2457 SharedY,
2458 SharedX,
2459 SharedZ,
2460 }
2461
2462 fn build_triangle_body(s: &TriangleSemantics, inner_pair: TriangleInnerPair) -> RirNode {
2463 let mk_scan = |r: RelId| RirNode::Scan { rel: r };
2464 match inner_pair {
2465 TriangleInnerPair::SharedY => {
2466 let inner = RirNode::Join {
2467 left: Box::new(mk_scan(s.rel_xy)),
2468 right: Box::new(mk_scan(s.rel_yz)),
2469 left_keys: vec![1],
2470 right_keys: vec![0],
2471 join_type: JoinType::Inner,
2472 };
2473 let outer = RirNode::Join {
2474 left: Box::new(inner),
2475 right: Box::new(mk_scan(s.rel_xz)),
2476 left_keys: vec![0, 3],
2477 right_keys: vec![0, 1],
2478 join_type: JoinType::Inner,
2479 };
2480 RirNode::Project {
2481 input: Box::new(outer),
2482 columns: vec![
2483 ProjectExpr::Column(0),
2484 ProjectExpr::Column(1),
2485 ProjectExpr::Column(3),
2486 ],
2487 }
2488 }
2489 TriangleInnerPair::SharedX => {
2490 let inner = RirNode::Join {
2491 left: Box::new(mk_scan(s.rel_xy)),
2492 right: Box::new(mk_scan(s.rel_xz)),
2493 left_keys: vec![0],
2494 right_keys: vec![0],
2495 join_type: JoinType::Inner,
2496 };
2497 let outer = RirNode::Join {
2498 left: Box::new(inner),
2499 right: Box::new(mk_scan(s.rel_yz)),
2500 left_keys: vec![1, 3],
2501 right_keys: vec![0, 1],
2502 join_type: JoinType::Inner,
2503 };
2504 RirNode::Project {
2505 input: Box::new(outer),
2506 columns: vec![
2507 ProjectExpr::Column(0),
2508 ProjectExpr::Column(1),
2509 ProjectExpr::Column(3),
2510 ],
2511 }
2512 }
2513 TriangleInnerPair::SharedZ => {
2514 let inner = RirNode::Join {
2515 left: Box::new(mk_scan(s.rel_xz)),
2516 right: Box::new(mk_scan(s.rel_yz)),
2517 left_keys: vec![1],
2518 right_keys: vec![1],
2519 join_type: JoinType::Inner,
2520 };
2521 let outer = RirNode::Join {
2522 left: Box::new(inner),
2523 right: Box::new(mk_scan(s.rel_xy)),
2524 left_keys: vec![0, 2],
2525 right_keys: vec![0, 1],
2526 join_type: JoinType::Inner,
2527 };
2528 RirNode::Project {
2529 input: Box::new(outer),
2530 columns: vec![
2531 ProjectExpr::Column(0),
2532 ProjectExpr::Column(2),
2533 ProjectExpr::Column(3),
2534 ],
2535 }
2536 }
2537 }
2538 }
2539
2540 pub(super) fn try_reorder_triangle(body: &RirNode, stats: &StatsManager) -> Option<RirNode> {
2541 let s = match_and_infer_triangle(body)?;
2542 let _ = (
2543 populated_card(stats, s.rel_xy)?,
2544 populated_card(stats, s.rel_yz)?,
2545 populated_card(stats, s.rel_xz)?,
2546 );
2547 let est_y = stats.estimate_join_cardinality(s.rel_xy, s.rel_yz, &[1], &[0]);
2548 let est_x = stats.estimate_join_cardinality(s.rel_xy, s.rel_xz, &[0], &[0]);
2549 let est_z = stats.estimate_join_cardinality(s.rel_yz, s.rel_xz, &[1], &[1]);
2550 let mut best = (TriangleInnerPair::SharedY, est_y);
2551 if est_x < best.1 {
2552 best = (TriangleInnerPair::SharedX, est_x);
2553 }
2554 if est_z < best.1 {
2555 best = (TriangleInnerPair::SharedZ, est_z);
2556 }
2557 let candidate = build_triangle_body(&s, best.0);
2558 if format!("{:?}", candidate) == format!("{:?}", body) {
2564 return None;
2565 }
2566 Some(candidate)
2567 }
2568
2569 struct Cycle4Semantics {
2574 rel_wx: RelId,
2575 rel_xy: RelId,
2576 rel_yz: RelId,
2577 rel_zw: RelId,
2578 }
2579
2580 fn match_and_infer_4cycle(body: &RirNode) -> Option<Cycle4Semantics> {
2581 let RirNode::Project {
2582 input: outer_input,
2583 columns,
2584 } = body
2585 else {
2586 return None;
2587 };
2588 let RirNode::Join {
2589 left: outer_l,
2590 right: outer_r,
2591 left_keys: olk,
2592 right_keys: ork,
2593 join_type: ojt,
2594 } = outer_input.as_ref()
2595 else {
2596 return None;
2597 };
2598 if !matches!(ojt, JoinType::Inner) {
2599 return None;
2600 }
2601 let RirNode::Join {
2602 left: ll,
2603 right: lr,
2604 left_keys: ilk_l,
2605 right_keys: irk_l,
2606 join_type: ijt_l,
2607 } = outer_l.as_ref()
2608 else {
2609 return None;
2610 };
2611 if !matches!(ijt_l, JoinType::Inner) {
2612 return None;
2613 }
2614 let RirNode::Scan { rel: rel_ll } = ll.as_ref() else {
2615 return None;
2616 };
2617 let RirNode::Scan { rel: rel_lr } = lr.as_ref() else {
2618 return None;
2619 };
2620 let RirNode::Join {
2621 left: rl,
2622 right: rr,
2623 left_keys: ilk_r,
2624 right_keys: irk_r,
2625 join_type: ijt_r,
2626 } = outer_r.as_ref()
2627 else {
2628 return None;
2629 };
2630 if !matches!(ijt_r, JoinType::Inner) {
2631 return None;
2632 }
2633 let RirNode::Scan { rel: rel_rl } = rl.as_ref() else {
2634 return None;
2635 };
2636 let RirNode::Scan { rel: rel_rr } = rr.as_ref() else {
2637 return None;
2638 };
2639 if ilk_l.len() != 1 || irk_l.len() != 1 || ilk_r.len() != 1 || irk_r.len() != 1 {
2640 return None;
2641 }
2642 if olk.len() != 2 || ork.len() != 2 || columns.len() != 4 {
2643 return None;
2644 }
2645 if ilk_l[0] >= 2 || irk_l[0] >= 2 || ilk_r[0] >= 2 || irk_r[0] >= 2 {
2646 return None;
2647 }
2648 if olk.iter().any(|k| *k >= 4) || ork.iter().any(|k| *k >= 4) {
2649 return None;
2650 }
2651
2652 let mut parent = [0u8, 1, 2, 3, 4, 5, 6, 7];
2653 uf_union_n::<8>(&mut parent, ac4(0, ilk_l[0] as u8), ac4(1, irk_l[0] as u8));
2654 uf_union_n::<8>(&mut parent, ac4(2, ilk_r[0] as u8), ac4(3, irk_r[0] as u8));
2655 for i in 0..2 {
2656 let l_ac = match olk[i] {
2657 0 => (0u8, 0u8),
2658 1 => (0, 1),
2659 2 => (1, 0),
2660 3 => (1, 1),
2661 _ => return None,
2662 };
2663 let r_ac = match ork[i] {
2664 0 => (2u8, 0u8),
2665 1 => (2, 1),
2666 2 => (3, 0),
2667 3 => (3, 1),
2668 _ => return None,
2669 };
2670 uf_union_n::<8>(&mut parent, ac4(l_ac.0, l_ac.1), ac4(r_ac.0, r_ac.1));
2671 }
2672 let roots: [u8; 8] = std::array::from_fn(|i| uf_find_n::<8>(&mut parent, i as u8));
2673 let mut counts: HashMap<u8, u8> = HashMap::new();
2674 for r in &roots {
2675 *counts.entry(*r).or_insert(0) += 1;
2676 }
2677 if counts.len() != 4 || counts.values().any(|c| *c != 2) {
2678 return None;
2679 }
2680
2681 let mut head_classes: [u8; 4] = [0; 4];
2682 for (i, pc) in columns.iter().enumerate() {
2683 let ProjectExpr::Column(k) = pc else {
2684 return None;
2685 };
2686 let ac = match *k {
2687 0 => (0u8, 0u8),
2688 1 => (0, 1),
2689 2 => (1, 0),
2690 3 => (1, 1),
2691 4 => (2, 0),
2692 5 => (2, 1),
2693 6 => (3, 0),
2694 7 => (3, 1),
2695 _ => return None,
2696 };
2697 head_classes[i] = uf_find_n::<8>(&mut parent, ac4(ac.0, ac.1));
2698 }
2699 for i in 0..4 {
2700 for j in (i + 1)..4 {
2701 if head_classes[i] == head_classes[j] {
2702 return None;
2703 }
2704 }
2705 }
2706 let w_class = head_classes[0];
2707 let x_class = head_classes[1];
2708 let y_class = head_classes[2];
2709 let z_class = head_classes[3];
2710 let atom_classes = |a: u8| (roots[ac4(a, 0) as usize], roots[ac4(a, 1) as usize]);
2711 let atom_rels = [*rel_ll, *rel_lr, *rel_rl, *rel_rr];
2712 let mut rel_wx = None;
2713 let mut rel_xy = None;
2714 let mut rel_yz = None;
2715 let mut rel_zw = None;
2716 for atom_idx in 0..4u8 {
2717 let (c0, c1) = atom_classes(atom_idx);
2718 let bw = c0 == w_class || c1 == w_class;
2719 let bx = c0 == x_class || c1 == x_class;
2720 let by = c0 == y_class || c1 == y_class;
2721 let bz = c0 == z_class || c1 == z_class;
2722 match (bw, bx, by, bz) {
2723 (true, true, false, false) => rel_wx = Some(atom_rels[atom_idx as usize]),
2724 (false, true, true, false) => rel_xy = Some(atom_rels[atom_idx as usize]),
2725 (false, false, true, true) => rel_yz = Some(atom_rels[atom_idx as usize]),
2726 (true, false, false, true) => rel_zw = Some(atom_rels[atom_idx as usize]),
2727 _ => return None,
2728 }
2729 }
2730 Some(Cycle4Semantics {
2731 rel_wx: rel_wx?,
2732 rel_xy: rel_xy?,
2733 rel_yz: rel_yz?,
2734 rel_zw: rel_zw?,
2735 })
2736 }
2737
2738 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2739 enum Cycle4Grouping {
2740 Default,
2741 Alt,
2742 }
2743
2744 fn build_4cycle_body(s: &Cycle4Semantics, g: Cycle4Grouping) -> RirNode {
2745 let mk_scan = |r: RelId| RirNode::Scan { rel: r };
2746 match g {
2747 Cycle4Grouping::Default => {
2748 let il = RirNode::Join {
2749 left: Box::new(mk_scan(s.rel_wx)),
2750 right: Box::new(mk_scan(s.rel_xy)),
2751 left_keys: vec![1],
2752 right_keys: vec![0],
2753 join_type: JoinType::Inner,
2754 };
2755 let ir = RirNode::Join {
2756 left: Box::new(mk_scan(s.rel_yz)),
2757 right: Box::new(mk_scan(s.rel_zw)),
2758 left_keys: vec![1],
2759 right_keys: vec![0],
2760 join_type: JoinType::Inner,
2761 };
2762 let outer = RirNode::Join {
2763 left: Box::new(il),
2764 right: Box::new(ir),
2765 left_keys: vec![0, 3],
2766 right_keys: vec![3, 0],
2767 join_type: JoinType::Inner,
2768 };
2769 RirNode::Project {
2770 input: Box::new(outer),
2771 columns: vec![
2772 ProjectExpr::Column(0),
2773 ProjectExpr::Column(1),
2774 ProjectExpr::Column(3),
2775 ProjectExpr::Column(5),
2776 ],
2777 }
2778 }
2779 Cycle4Grouping::Alt => {
2780 let il = RirNode::Join {
2781 left: Box::new(mk_scan(s.rel_xy)),
2782 right: Box::new(mk_scan(s.rel_yz)),
2783 left_keys: vec![1],
2784 right_keys: vec![0],
2785 join_type: JoinType::Inner,
2786 };
2787 let ir = RirNode::Join {
2788 left: Box::new(mk_scan(s.rel_zw)),
2789 right: Box::new(mk_scan(s.rel_wx)),
2790 left_keys: vec![1],
2791 right_keys: vec![0],
2792 join_type: JoinType::Inner,
2793 };
2794 let outer = RirNode::Join {
2795 left: Box::new(il),
2796 right: Box::new(ir),
2797 left_keys: vec![0, 3],
2798 right_keys: vec![3, 0],
2799 join_type: JoinType::Inner,
2800 };
2801 RirNode::Project {
2802 input: Box::new(outer),
2803 columns: vec![
2804 ProjectExpr::Column(5),
2805 ProjectExpr::Column(0),
2806 ProjectExpr::Column(1),
2807 ProjectExpr::Column(3),
2808 ],
2809 }
2810 }
2811 }
2812 }
2813
2814 pub(super) fn try_reorder_4cycle(body: &RirNode, stats: &StatsManager) -> Option<RirNode> {
2815 let s = match_and_infer_4cycle(body)?;
2816 let _ = (
2817 populated_card(stats, s.rel_wx)?,
2818 populated_card(stats, s.rel_xy)?,
2819 populated_card(stats, s.rel_yz)?,
2820 populated_card(stats, s.rel_zw)?,
2821 );
2822 let est_default = stats
2823 .estimate_join_cardinality(s.rel_wx, s.rel_xy, &[1], &[0])
2824 .saturating_add(stats.estimate_join_cardinality(s.rel_yz, s.rel_zw, &[1], &[0]));
2825 let est_alt = stats
2826 .estimate_join_cardinality(s.rel_xy, s.rel_yz, &[1], &[0])
2827 .saturating_add(stats.estimate_join_cardinality(s.rel_zw, s.rel_wx, &[1], &[0]));
2828 let chosen = if est_alt < est_default {
2829 Cycle4Grouping::Alt
2830 } else {
2831 Cycle4Grouping::Default
2832 };
2833 let candidate = build_4cycle_body(&s, chosen);
2834 if format!("{:?}", candidate) == format!("{:?}", body) {
2835 return None;
2836 }
2837 Some(candidate)
2838 }
2839}
2840
2841#[cfg(test)]
2842mod selectivity_pass_tests {
2843 use super::selectivity_pass;
2844 use crate::Compiler;
2845 use xlog_stats::StatsManager;
2846
2847 fn body_snapshots(plan: &xlog_ir::ExecutionPlan) -> Vec<String> {
2848 plan.rules_by_scc
2849 .iter()
2850 .flatten()
2851 .map(|r| format!("{:?}", r.body))
2852 .collect()
2853 }
2854
2855 #[test]
2856 fn selectivity_pass_is_noop_for_triangle_plan() {
2857 let mut compiler = Compiler::new();
2858 let plan = compiler
2859 .compile("tri(X, Y, Z) :- e1(X, Y), e2(Y, Z), e3(X, Z).")
2860 .expect("compile");
2861 let before = body_snapshots(&plan);
2862 let stats = StatsManager::new();
2863 let mut plan2 = plan.clone();
2864 selectivity_pass::run(&mut plan2, &stats, &std::collections::HashMap::new());
2865 let after = body_snapshots(&plan2);
2866 assert_eq!(
2867 before, after,
2868 "selectivity_pass must preserve every triangle rule body byte-for-byte"
2869 );
2870 }
2871
2872 #[test]
2873 fn selectivity_pass_is_noop_for_4cycle_plan() {
2874 let mut compiler = Compiler::new();
2875 let plan = compiler
2876 .compile("cycle4(W, X, Y, Z) :- e1(W, X), e2(X, Y), e3(Y, Z), e4(Z, W).")
2877 .expect("compile");
2878 let before = body_snapshots(&plan);
2879 let stats = StatsManager::new();
2880 let mut plan2 = plan.clone();
2881 selectivity_pass::run(&mut plan2, &stats, &std::collections::HashMap::new());
2882 let after = body_snapshots(&plan2);
2883 assert_eq!(
2884 before, after,
2885 "selectivity_pass must preserve every 4-cycle rule body byte-for-byte"
2886 );
2887 }
2888
2889 #[test]
2890 fn selectivity_pass_is_noop_for_recursive_scc() {
2891 let mut compiler = Compiler::new();
2892 let plan = compiler
2893 .compile(
2894 "edge(1, 2). edge(2, 3). \
2895 reach(X, Y) :- edge(X, Y). \
2896 reach(X, Z) :- reach(X, Y), edge(Y, Z).",
2897 )
2898 .expect("compile");
2899 let before = body_snapshots(&plan);
2900 let stats = StatsManager::new();
2901 let mut plan2 = plan.clone();
2902 selectivity_pass::run(&mut plan2, &stats, &std::collections::HashMap::new());
2903 let after = body_snapshots(&plan2);
2904 assert_eq!(
2905 before, after,
2906 "selectivity_pass must preserve recursive SCC bodies byte-for-byte"
2907 );
2908 }
2909
2910 use xlog_core::RelId;
2915 use xlog_ir::plan::{CompiledRule, PlanBuilder, Scc};
2916 use xlog_ir::rir::ProjectExpr;
2917 use xlog_ir::{ExecutionPlan, JoinType, RirNode};
2918
2919 fn synth_triangle_plan() -> ExecutionPlan {
2928 let inner = RirNode::Join {
2929 left: Box::new(RirNode::Scan { rel: RelId(1) }),
2930 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2931 left_keys: vec![1],
2932 right_keys: vec![0],
2933 join_type: JoinType::Inner,
2934 };
2935 let outer = RirNode::Join {
2936 left: Box::new(inner),
2937 right: Box::new(RirNode::Scan { rel: RelId(3) }),
2938 left_keys: vec![0, 3],
2939 right_keys: vec![0, 1],
2940 join_type: JoinType::Inner,
2941 };
2942 let body = RirNode::Project {
2943 input: Box::new(outer),
2944 columns: vec![
2945 ProjectExpr::Column(0),
2946 ProjectExpr::Column(1),
2947 ProjectExpr::Column(3),
2948 ],
2949 };
2950 let mut builder = PlanBuilder::new();
2951 builder.add_scc(Scc {
2952 id: 0,
2953 predicates: vec!["tri".to_string()],
2954 is_recursive: false,
2955 });
2956 builder.add_rule(
2957 0,
2958 CompiledRule {
2959 head: "tri".to_string(),
2960 body,
2961 meta: Default::default(),
2962 },
2963 );
2964 builder.build()
2965 }
2966
2967 fn seed_triangle_stats(c1: u64, c2: u64, c3: u64) -> StatsManager {
2971 let mut stats = StatsManager::new();
2972 for (rid, card) in [(RelId(1), c1), (RelId(2), c2), (RelId(3), c3)] {
2973 stats.register_relation(rid);
2974 stats.update_cardinality(rid, card);
2975 }
2976 stats
2977 }
2978
2979 fn inspect_triangle_inner_pair(plan: &xlog_ir::ExecutionPlan) -> Option<(RelId, RelId)> {
2990 let body = &plan.rules_by_scc.iter().flatten().next()?.body;
2991 let body = match body {
2992 xlog_ir::RirNode::MultiWayJoin { fallback, .. } => fallback.as_ref(),
2993 other => other,
2994 };
2995 let xlog_ir::RirNode::Project { input, .. } = body else {
2996 return None;
2997 };
2998 let xlog_ir::RirNode::Join { left, .. } = input.as_ref() else {
2999 return None;
3000 };
3001 let xlog_ir::RirNode::Join {
3002 left: l2,
3003 right: r2,
3004 ..
3005 } = left.as_ref()
3006 else {
3007 return None;
3008 };
3009 let xlog_ir::RirNode::Scan { rel: rel_l } = l2.as_ref() else {
3010 return None;
3011 };
3012 let xlog_ir::RirNode::Scan { rel: rel_r } = r2.as_ref() else {
3013 return None;
3014 };
3015 Some((*rel_l, *rel_r))
3016 }
3017
3018 #[test]
3025 fn selectivity_pass_picks_y_shared_inner_when_e1_e2_smallest() {
3026 let mut plan = synth_triangle_plan();
3027 let stats = seed_triangle_stats(10, 10, 100_000);
3029 selectivity_pass::run(&mut plan, &stats, &std::collections::HashMap::new());
3030 let pair = inspect_triangle_inner_pair(&plan).expect("inner pair");
3031 assert!(
3033 pair == (RelId(1), RelId(2)) || pair == (RelId(2), RelId(1)),
3034 "expected (RelId(1), RelId(2)) for Y-shared; got {:?}",
3035 pair
3036 );
3037 }
3038
3039 #[test]
3042 fn selectivity_pass_picks_x_shared_inner_when_e1_e3_smallest() {
3043 let mut plan = synth_triangle_plan();
3044 let stats = seed_triangle_stats(10, 100_000, 10);
3046 selectivity_pass::run(&mut plan, &stats, &std::collections::HashMap::new());
3047 let pair = inspect_triangle_inner_pair(&plan).expect("inner pair");
3048 assert!(
3050 pair == (RelId(1), RelId(3)) || pair == (RelId(3), RelId(1)),
3051 "expected (RelId(1), RelId(3)) for X-shared; got {:?}",
3052 pair
3053 );
3054 }
3055
3056 #[test]
3059 fn selectivity_pass_picks_z_shared_inner_when_e2_e3_smallest() {
3060 let mut plan = synth_triangle_plan();
3061 let stats = seed_triangle_stats(100_000, 10, 10);
3063 selectivity_pass::run(&mut plan, &stats, &std::collections::HashMap::new());
3064 let pair = inspect_triangle_inner_pair(&plan).expect("inner pair");
3065 assert!(
3067 pair == (RelId(2), RelId(3)) || pair == (RelId(3), RelId(2)),
3068 "expected (RelId(2), RelId(3)) for Z-shared; got {:?}",
3069 pair
3070 );
3071 }
3072
3073 #[test]
3078 fn selectivity_pass_two_snapshots_produce_different_inner_pairs() {
3079 let mut plan_a = synth_triangle_plan();
3080 let stats_a = seed_triangle_stats(10, 10, 100_000); selectivity_pass::run(&mut plan_a, &stats_a, &std::collections::HashMap::new());
3082 let pair_a = inspect_triangle_inner_pair(&plan_a).expect("snapshot A pair");
3083
3084 let mut plan_b = synth_triangle_plan();
3085 let stats_b = seed_triangle_stats(100_000, 10, 10); selectivity_pass::run(&mut plan_b, &stats_b, &std::collections::HashMap::new());
3087 let pair_b = inspect_triangle_inner_pair(&plan_b).expect("snapshot B pair");
3088
3089 let normalize = |(a, b): (RelId, RelId)| -> (RelId, RelId) {
3090 if a.0 <= b.0 {
3091 (a, b)
3092 } else {
3093 (b, a)
3094 }
3095 };
3096 assert_ne!(
3097 normalize(pair_a),
3098 normalize(pair_b),
3099 "two different stats snapshots must produce different inner pairs; \
3100 got A = {:?}, B = {:?}",
3101 pair_a,
3102 pair_b
3103 );
3104 }
3105
3106 #[test]
3114 fn selectivity_pass_with_only_relation_cards_may_pick_arbitrary_pair() {
3115 let mut plan = synth_triangle_plan();
3116 let stats = seed_triangle_stats(100, 100, 100);
3118 selectivity_pass::run(&mut plan, &stats, &std::collections::HashMap::new());
3119 let _ = inspect_triangle_inner_pair(&plan);
3122 }
3123
3124 fn synth_4cycle_plan() -> ExecutionPlan {
3135 let inner_left = RirNode::Join {
3136 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3137 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3138 left_keys: vec![1],
3139 right_keys: vec![0],
3140 join_type: JoinType::Inner,
3141 };
3142 let inner_right = RirNode::Join {
3143 left: Box::new(RirNode::Scan { rel: RelId(3) }),
3144 right: Box::new(RirNode::Scan { rel: RelId(4) }),
3145 left_keys: vec![1],
3146 right_keys: vec![0],
3147 join_type: JoinType::Inner,
3148 };
3149 let outer = RirNode::Join {
3150 left: Box::new(inner_left),
3151 right: Box::new(inner_right),
3152 left_keys: vec![0, 3],
3153 right_keys: vec![3, 0],
3154 join_type: JoinType::Inner,
3155 };
3156 let body = RirNode::Project {
3157 input: Box::new(outer),
3158 columns: vec![
3159 ProjectExpr::Column(0),
3160 ProjectExpr::Column(1),
3161 ProjectExpr::Column(3),
3162 ProjectExpr::Column(5),
3163 ],
3164 };
3165 let mut builder = PlanBuilder::new();
3166 builder.add_scc(Scc {
3167 id: 0,
3168 predicates: vec!["cyc".to_string()],
3169 is_recursive: false,
3170 });
3171 builder.add_rule(
3172 0,
3173 CompiledRule {
3174 head: "cyc".to_string(),
3175 body,
3176 meta: Default::default(),
3177 },
3178 );
3179 builder.build()
3180 }
3181
3182 fn seed_4cycle_stats(c1: u64, c2: u64, c3: u64, c4: u64) -> StatsManager {
3183 let mut stats = StatsManager::new();
3184 for (rid, card) in [
3185 (RelId(1), c1),
3186 (RelId(2), c2),
3187 (RelId(3), c3),
3188 (RelId(4), c4),
3189 ] {
3190 stats.register_relation(rid);
3191 stats.update_cardinality(rid, card);
3192 }
3193 stats
3194 }
3195
3196 fn inspect_4cycle_grouping(
3200 plan: &xlog_ir::ExecutionPlan,
3201 ) -> Option<(RelId, RelId, RelId, RelId)> {
3202 let body = &plan.rules_by_scc.iter().flatten().next()?.body;
3203 let body = match body {
3204 xlog_ir::RirNode::MultiWayJoin { fallback, .. } => fallback.as_ref(),
3205 other => other,
3206 };
3207 let xlog_ir::RirNode::Project { input, .. } = body else {
3208 return None;
3209 };
3210 let xlog_ir::RirNode::Join { left, right, .. } = input.as_ref() else {
3211 return None;
3212 };
3213 let xlog_ir::RirNode::Join {
3214 left: ll,
3215 right: lr,
3216 ..
3217 } = left.as_ref()
3218 else {
3219 return None;
3220 };
3221 let xlog_ir::RirNode::Join {
3222 left: rl,
3223 right: rr,
3224 ..
3225 } = right.as_ref()
3226 else {
3227 return None;
3228 };
3229 let xlog_ir::RirNode::Scan { rel: r_ll } = ll.as_ref() else {
3230 return None;
3231 };
3232 let xlog_ir::RirNode::Scan { rel: r_lr } = lr.as_ref() else {
3233 return None;
3234 };
3235 let xlog_ir::RirNode::Scan { rel: r_rl } = rl.as_ref() else {
3236 return None;
3237 };
3238 let xlog_ir::RirNode::Scan { rel: r_rr } = rr.as_ref() else {
3239 return None;
3240 };
3241 Some((*r_ll, *r_lr, *r_rl, *r_rr))
3242 }
3243
3244 #[test]
3266 fn selectivity_pass_4cycle_picks_default_grouping_when_corners_smallest() {
3267 let mut plan = synth_4cycle_plan();
3268 let stats = seed_4cycle_stats(10, 10_000, 10_000, 10);
3269 selectivity_pass::run(&mut plan, &stats, &std::collections::HashMap::new());
3270 let (ll, lr, rl, rr) = inspect_4cycle_grouping(&plan).expect("grouping");
3271 assert_eq!(
3273 (ll, lr, rl, rr),
3274 (RelId(1), RelId(2), RelId(3), RelId(4)),
3275 "expected Default grouping"
3276 );
3277 }
3278
3279 #[test]
3292 fn selectivity_pass_4cycle_picks_alt_grouping_when_diagonals_smallest() {
3293 let mut plan = synth_4cycle_plan();
3294 let stats = seed_4cycle_stats(10_000, 10_000, 10, 10);
3295 selectivity_pass::run(&mut plan, &stats, &std::collections::HashMap::new());
3296 let (ll, lr, rl, rr) = inspect_4cycle_grouping(&plan).expect("grouping");
3297 assert_eq!(
3299 (ll, lr, rl, rr),
3300 (RelId(2), RelId(3), RelId(4), RelId(1)),
3301 "expected Alt grouping"
3302 );
3303 }
3304
3305 #[test]
3309 fn selectivity_pass_4cycle_two_snapshots_produce_different_groupings() {
3310 let mut plan_a = synth_4cycle_plan();
3311 let stats_a = seed_4cycle_stats(10, 10_000, 10_000, 10); selectivity_pass::run(&mut plan_a, &stats_a, &std::collections::HashMap::new());
3313 let g_a = inspect_4cycle_grouping(&plan_a).expect("grouping a");
3314
3315 let mut plan_b = synth_4cycle_plan();
3316 let stats_b = seed_4cycle_stats(10_000, 10_000, 10, 10); selectivity_pass::run(&mut plan_b, &stats_b, &std::collections::HashMap::new());
3318 let g_b = inspect_4cycle_grouping(&plan_b).expect("grouping b");
3319
3320 assert_ne!(
3321 g_a, g_b,
3322 "two different stats snapshots must produce different 4-cycle groupings; \
3323 got A = {:?}, B = {:?}",
3324 g_a, g_b
3325 );
3326 }
3327
3328 #[test]
3331 fn selectivity_pass_4cycle_skips_when_card_missing() {
3332 let mut plan = synth_4cycle_plan();
3333 let mut stats = StatsManager::new();
3335 for rid in [RelId(1), RelId(2), RelId(3)] {
3336 stats.register_relation(rid);
3337 stats.update_cardinality(rid, 100);
3338 }
3339 let before = format!("{:?}", plan.rules_by_scc[0][0].body);
3340 selectivity_pass::run(&mut plan, &stats, &std::collections::HashMap::new());
3341 let after = format!("{:?}", plan.rules_by_scc[0][0].body);
3342 assert_eq!(
3343 before, after,
3344 "missing-stats safety floor must leave body unchanged"
3345 );
3346 }
3347}
3348
3349#[cfg(test)]
3350mod tests {
3351 use super::*;
3352 use xlog_core::ScalarType;
3353 use xlog_ir::{ConstValue, ProjectExpr};
3354 use xlog_stats::ColumnStats;
3355
3356 fn make_stats_manager() -> Arc<StatsManager> {
3357 let mut mgr = StatsManager::new();
3358
3359 mgr.register_relation(RelId(1));
3361 mgr.update_cardinality(RelId(1), 10_000);
3362 mgr.update_byte_size(RelId(1), 320_000); mgr.register_relation(RelId(2));
3365 mgr.update_cardinality(RelId(2), 5_000);
3366 mgr.update_byte_size(RelId(2), 160_000);
3367
3368 mgr.register_relation(RelId(3));
3369 mgr.update_cardinality(RelId(3), 1_000);
3370 mgr.update_byte_size(RelId(3), 32_000);
3371
3372 let mut col0 = ColumnStats::new(0, ScalarType::I64);
3374 col0.update_distinct(1000);
3375 col0.update_range(0, 10000);
3376 mgr.add_column_stats(RelId(1), col0);
3377
3378 let mut col1 = ColumnStats::new(1, ScalarType::I64);
3379 col1.update_distinct(100);
3380 mgr.add_column_stats(RelId(1), col1);
3381
3382 Arc::new(mgr)
3383 }
3384
3385 #[test]
3386 fn test_optimizer_new() {
3387 let stats = make_stats_manager();
3388 let optimizer = Optimizer::new(stats);
3389
3390 assert_eq!(optimizer.config().dp_threshold, 10);
3391 assert!(optimizer.config().enable_pushdown);
3392 }
3393
3394 #[test]
3395 fn test_optimizer_with_config() {
3396 let stats = make_stats_manager();
3397 let config = OptimizerConfig {
3398 dp_threshold: 5,
3399 enable_pushdown: false,
3400 ..Default::default()
3401 };
3402 let optimizer = Optimizer::with_config(stats, config);
3403
3404 assert_eq!(optimizer.config().dp_threshold, 5);
3405 assert!(!optimizer.config().enable_pushdown);
3406 }
3407
3408 #[test]
3409 fn test_estimate_scan_cost() {
3410 let stats = make_stats_manager();
3411 let optimizer = Optimizer::new(stats);
3412
3413 let scan = RirNode::Scan { rel: RelId(1) };
3414 let cost = optimizer.estimate_cost(&scan);
3415
3416 assert_eq!(cost.rows, 10_000);
3417 assert!(cost.gpu_mem > 0);
3418 assert_eq!(cost.transfers, 0); }
3420
3421 #[test]
3422 fn test_estimate_scan_cost_unknown_relation() {
3423 let stats = Arc::new(StatsManager::new());
3424 let optimizer = Optimizer::new(stats);
3425
3426 let scan = RirNode::Scan { rel: RelId(999) };
3427 let cost = optimizer.estimate_cost(&scan);
3428
3429 assert_eq!(cost.rows, 1000);
3431 }
3432
3433 #[test]
3434 fn test_estimate_filter_cost() {
3435 let stats = make_stats_manager();
3436 let optimizer = Optimizer::new(stats);
3437
3438 let filter = RirNode::Filter {
3439 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3440 predicate: Expr::Compare {
3441 left: Box::new(Expr::Column(0)),
3442 op: CompareOp::Eq,
3443 right: Box::new(Expr::Const(ConstValue::I64(42))),
3444 },
3445 };
3446
3447 let cost = optimizer.estimate_cost(&filter);
3448
3449 assert!(cost.rows < 10_000);
3451 assert!(cost.rows >= 1);
3452 }
3453
3454 #[test]
3455 fn test_estimate_join_cost() {
3456 let stats = make_stats_manager();
3457 let optimizer = Optimizer::new(stats);
3458
3459 let join = RirNode::Join {
3460 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3461 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3462 left_keys: vec![0],
3463 right_keys: vec![0],
3464 join_type: JoinType::Inner,
3465 };
3466
3467 let cost = optimizer.estimate_cost(&join);
3468
3469 assert!(cost.rows > 0);
3471 assert!(cost.cpu_cost > 0.0);
3472 assert!(cost.gpu_mem > 0);
3473 }
3474
3475 #[test]
3476 fn test_estimate_join_cost_with_selectivity() {
3477 let mut mgr = StatsManager::new();
3478 mgr.register_relation(RelId(1));
3479 mgr.register_relation(RelId(2));
3480 mgr.update_cardinality(RelId(1), 1000);
3481 mgr.update_cardinality(RelId(2), 500);
3482
3483 mgr.record_join_result(RelId(1), RelId(2), vec![0], vec![0], 500_000, 2500);
3485
3486 let optimizer = Optimizer::new(Arc::new(mgr));
3487
3488 let join = RirNode::Join {
3489 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3490 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3491 left_keys: vec![0],
3492 right_keys: vec![0],
3493 join_type: JoinType::Inner,
3494 };
3495
3496 let cost = optimizer.estimate_cost(&join);
3497
3498 assert!(cost.rows > 0);
3500 }
3501
3502 #[test]
3503 fn test_predicate_pushdown_simple_scan() {
3504 let stats = make_stats_manager();
3505 let optimizer = Optimizer::new(stats);
3506
3507 let scan = RirNode::Scan { rel: RelId(1) };
3508 let optimized = optimizer.optimize(scan);
3509
3510 assert!(matches!(optimized, RirNode::Scan { rel: RelId(1) }));
3512 }
3513
3514 #[test]
3515 fn test_predicate_pushdown_filter_on_scan() {
3516 let stats = make_stats_manager();
3517 let optimizer = Optimizer::new(stats);
3518
3519 let filter = RirNode::Filter {
3520 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3521 predicate: Expr::Compare {
3522 left: Box::new(Expr::Column(0)),
3523 op: CompareOp::Eq,
3524 right: Box::new(Expr::Const(ConstValue::I64(42))),
3525 },
3526 };
3527
3528 let optimized = optimizer.optimize(filter);
3529
3530 assert!(matches!(optimized, RirNode::Filter { .. }));
3532 }
3533
3534 #[test]
3535 fn test_predicate_pushdown_merges_filters() {
3536 let stats = make_stats_manager();
3537 let optimizer = Optimizer::new(stats);
3538
3539 let nested_filter = RirNode::Filter {
3540 input: Box::new(RirNode::Filter {
3541 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3542 predicate: Expr::Compare {
3543 left: Box::new(Expr::Column(0)),
3544 op: CompareOp::Gt,
3545 right: Box::new(Expr::Const(ConstValue::I64(0))),
3546 },
3547 }),
3548 predicate: Expr::Compare {
3549 left: Box::new(Expr::Column(0)),
3550 op: CompareOp::Lt,
3551 right: Box::new(Expr::Const(ConstValue::I64(100))),
3552 },
3553 };
3554
3555 let optimized = optimizer.optimize(nested_filter);
3556
3557 if let RirNode::Filter { predicate, .. } = optimized {
3559 assert!(matches!(predicate, Expr::And(_)));
3560 } else {
3561 panic!("Expected Filter node");
3562 }
3563 }
3564
3565 #[test]
3566 fn test_predicate_pushdown_through_project() {
3567 let stats = make_stats_manager();
3568 let optimizer = Optimizer::new(stats);
3569
3570 let plan = RirNode::Filter {
3572 input: Box::new(RirNode::Project {
3573 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3574 columns: vec![ProjectExpr::Column(0), ProjectExpr::Column(1)],
3575 }),
3576 predicate: Expr::Compare {
3577 left: Box::new(Expr::Column(0)),
3578 op: CompareOp::Eq,
3579 right: Box::new(Expr::Const(ConstValue::I64(42))),
3580 },
3581 };
3582
3583 let optimized = optimizer.optimize(plan);
3584
3585 assert!(matches!(optimized, RirNode::Project { .. }));
3587 if let RirNode::Project { input, .. } = optimized {
3588 assert!(matches!(*input, RirNode::Filter { .. }));
3589 }
3590 }
3591
3592 #[test]
3593 fn test_predicate_pushdown_into_join() {
3594 let stats = make_stats_manager();
3595 let optimizer = Optimizer::new(stats);
3596
3597 let plan = RirNode::Filter {
3599 input: Box::new(RirNode::Join {
3600 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3601 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3602 left_keys: vec![0],
3603 right_keys: vec![0],
3604 join_type: JoinType::Inner,
3605 }),
3606 predicate: Expr::Compare {
3607 left: Box::new(Expr::Column(0)), op: CompareOp::Eq,
3609 right: Box::new(Expr::Const(ConstValue::I64(42))),
3610 },
3611 };
3612
3613 let optimized = optimizer.optimize(plan);
3614
3615 if let RirNode::Join { left, .. } = optimized {
3617 assert!(matches!(*left, RirNode::Filter { .. }));
3618 } else {
3619 panic!("Expected Join node");
3620 }
3621 }
3622
3623 #[test]
3624 fn test_plan_cost_total() {
3625 let cost = PlanCost {
3626 rows: 1000,
3627 cpu_cost: 100.0,
3628 gpu_mem: 1_000_000,
3629 transfers: 2,
3630 };
3631
3632 let total = cost.total_cost(100.0);
3633
3634 assert!((total - 1300.0).abs() < 0.001);
3637 }
3638
3639 #[test]
3640 fn test_plan_cost_then() {
3641 let cost1 = PlanCost {
3642 rows: 1000,
3643 cpu_cost: 50.0,
3644 gpu_mem: 500,
3645 transfers: 1,
3646 };
3647
3648 let cost2 = PlanCost {
3649 rows: 500,
3650 cpu_cost: 25.0,
3651 gpu_mem: 800,
3652 transfers: 1,
3653 };
3654
3655 let combined = cost1.then(cost2);
3656
3657 assert_eq!(combined.rows, 500); assert_eq!(combined.cpu_cost, 75.0);
3659 assert_eq!(combined.gpu_mem, 800); assert_eq!(combined.transfers, 2);
3661 }
3662
3663 #[test]
3664 fn test_optimizer_config_default() {
3665 let config = OptimizerConfig::default();
3666
3667 assert_eq!(config.dp_threshold, 10);
3668 assert!((config.index_heat_threshold - 0.7).abs() < 0.001);
3669 assert!(config.enable_pushdown);
3670 assert!((config.default_filter_selectivity - 0.1).abs() < 0.001);
3671 }
3672
3673 #[test]
3674 fn test_should_use_greedy() {
3675 let stats = make_stats_manager();
3676 let config = OptimizerConfig {
3677 dp_threshold: 2,
3678 ..Default::default()
3679 };
3680 let optimizer = Optimizer::with_config(stats, config);
3681
3682 let single = RirNode::Scan { rel: RelId(1) };
3684 assert!(!optimizer.should_use_greedy(&single));
3685
3686 let multi = RirNode::Join {
3688 left: Box::new(RirNode::Join {
3689 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3690 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3691 left_keys: vec![0],
3692 right_keys: vec![0],
3693 join_type: JoinType::Inner,
3694 }),
3695 right: Box::new(RirNode::Scan { rel: RelId(3) }),
3696 left_keys: vec![0],
3697 right_keys: vec![0],
3698 join_type: JoinType::Inner,
3699 };
3700 assert!(optimizer.should_use_greedy(&multi));
3701 }
3702
3703 #[test]
3704 fn test_recommend_indexes() {
3705 let mut mgr = StatsManager::new();
3706 mgr.register_relation(RelId(1));
3707 mgr.register_relation(RelId(2));
3708
3709 for _ in 0..50 {
3711 mgr.record_access(RelId(1));
3712 }
3713
3714 let optimizer = Optimizer::new(Arc::new(mgr));
3715 let recommendations = optimizer.recommend_indexes();
3716
3717 assert!(recommendations.contains(&RelId(1)));
3718 assert!(!recommendations.contains(&RelId(2)));
3719 }
3720
3721 #[test]
3722 fn test_estimate_groupby_cost() {
3723 let stats = make_stats_manager();
3724 let optimizer = Optimizer::new(stats);
3725
3726 let groupby = RirNode::GroupBy {
3727 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3728 key_cols: vec![0],
3729 aggs: vec![(1, xlog_core::AggOp::Sum)],
3730 };
3731
3732 let cost = optimizer.estimate_cost(&groupby);
3733
3734 assert!(cost.rows < 10_000);
3736 assert!(cost.rows >= 1);
3737 }
3738
3739 #[test]
3740 fn test_estimate_union_cost() {
3741 let stats = make_stats_manager();
3742 let optimizer = Optimizer::new(stats);
3743
3744 let union = RirNode::Union {
3745 inputs: vec![
3746 RirNode::Scan { rel: RelId(1) },
3747 RirNode::Scan { rel: RelId(2) },
3748 ],
3749 };
3750
3751 let cost = optimizer.estimate_cost(&union);
3752
3753 assert_eq!(cost.rows, 15_000); }
3756
3757 #[test]
3758 fn test_estimate_distinct_cost() {
3759 let stats = make_stats_manager();
3760 let optimizer = Optimizer::new(stats);
3761
3762 let distinct = RirNode::Distinct {
3763 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3764 key_cols: vec![0],
3765 };
3766
3767 let cost = optimizer.estimate_cost(&distinct);
3768
3769 assert!(cost.rows <= 10_000);
3771 assert!(cost.rows >= 1);
3772 }
3773
3774 #[test]
3775 fn test_estimate_diff_cost() {
3776 let stats = make_stats_manager();
3777 let optimizer = Optimizer::new(stats);
3778
3779 let diff = RirNode::Diff {
3780 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3781 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3782 };
3783
3784 let cost = optimizer.estimate_cost(&diff);
3785
3786 assert!(cost.rows <= 10_000);
3788 assert!(cost.rows >= 1);
3789 }
3790
3791 #[test]
3792 fn test_estimate_fixpoint_cost() {
3793 let stats = make_stats_manager();
3794 let optimizer = Optimizer::new(stats);
3795
3796 let fixpoint = RirNode::Fixpoint {
3797 scc_id: 0,
3798 base: Box::new(RirNode::Scan { rel: RelId(1) }),
3799 recursive: Box::new(RirNode::Scan { rel: RelId(1) }),
3800 delta_rel: RelId(10),
3801 full_rel: RelId(11),
3802 };
3803
3804 let cost = optimizer.estimate_cost(&fixpoint);
3805
3806 assert!(cost.rows >= 10_000);
3808 }
3809
3810 #[test]
3811 fn test_predicate_selectivity_equality() {
3812 let stats = make_stats_manager();
3813 let optimizer = Optimizer::new(stats);
3814
3815 let scan = RirNode::Scan { rel: RelId(1) };
3816
3817 let eq_pred = Expr::Compare {
3819 left: Box::new(Expr::Column(0)),
3820 op: CompareOp::Eq,
3821 right: Box::new(Expr::Const(ConstValue::I64(42))),
3822 };
3823
3824 let selectivity = optimizer.estimate_predicate_selectivity(&eq_pred, &scan);
3825
3826 assert!(selectivity < 0.01);
3828 assert!(selectivity > 0.0);
3829 }
3830
3831 #[test]
3832 fn test_predicate_selectivity_and() {
3833 let stats = make_stats_manager();
3834 let optimizer = Optimizer::new(stats);
3835
3836 let scan = RirNode::Scan { rel: RelId(1) };
3837
3838 let and_pred = Expr::And(vec![
3840 Expr::Compare {
3841 left: Box::new(Expr::Column(0)),
3842 op: CompareOp::Gt,
3843 right: Box::new(Expr::Const(ConstValue::I64(0))),
3844 },
3845 Expr::Compare {
3846 left: Box::new(Expr::Column(0)),
3847 op: CompareOp::Lt,
3848 right: Box::new(Expr::Const(ConstValue::I64(100))),
3849 },
3850 ]);
3851
3852 let selectivity = optimizer.estimate_predicate_selectivity(&and_pred, &scan);
3853
3854 assert!(selectivity < 0.5);
3856 assert!(selectivity > 0.0);
3857 }
3858
3859 #[test]
3860 fn test_predicate_selectivity_not() {
3861 let stats = make_stats_manager();
3862 let optimizer = Optimizer::new(stats);
3863
3864 let scan = RirNode::Scan { rel: RelId(1) };
3865
3866 let not_pred = Expr::Not(Box::new(Expr::Compare {
3868 left: Box::new(Expr::Column(0)),
3869 op: CompareOp::Eq,
3870 right: Box::new(Expr::Const(ConstValue::I64(42))),
3871 }));
3872
3873 let selectivity = optimizer.estimate_predicate_selectivity(¬_pred, &scan);
3874
3875 assert!(selectivity > 0.9);
3877 }
3878
3879 #[test]
3880 fn test_join_type_semi() {
3881 let stats = make_stats_manager();
3882 let optimizer = Optimizer::new(stats);
3883
3884 let semi_join = RirNode::Join {
3885 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3886 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3887 left_keys: vec![0],
3888 right_keys: vec![0],
3889 join_type: JoinType::Semi,
3890 };
3891
3892 let cost = optimizer.estimate_cost(&semi_join);
3893
3894 assert!(cost.rows <= 10_000);
3896 }
3897
3898 #[test]
3899 fn test_join_type_anti() {
3900 let stats = make_stats_manager();
3901 let optimizer = Optimizer::new(stats);
3902
3903 let anti_join = RirNode::Join {
3904 left: Box::new(RirNode::Scan { rel: RelId(1) }),
3905 right: Box::new(RirNode::Scan { rel: RelId(2) }),
3906 left_keys: vec![0],
3907 right_keys: vec![0],
3908 join_type: JoinType::Anti,
3909 };
3910
3911 let cost = optimizer.estimate_cost(&anti_join);
3912
3913 assert!(cost.rows <= 10_000);
3915 }
3916
3917 #[test]
3918 fn test_pushdown_disabled() {
3919 let stats = make_stats_manager();
3920 let config = OptimizerConfig {
3921 enable_pushdown: false,
3922 ..Default::default()
3923 };
3924 let optimizer = Optimizer::with_config(stats, config);
3925
3926 let plan = RirNode::Filter {
3928 input: Box::new(RirNode::Filter {
3929 input: Box::new(RirNode::Scan { rel: RelId(1) }),
3930 predicate: Expr::Compare {
3931 left: Box::new(Expr::Column(0)),
3932 op: CompareOp::Gt,
3933 right: Box::new(Expr::Const(ConstValue::I64(0))),
3934 },
3935 }),
3936 predicate: Expr::Compare {
3937 left: Box::new(Expr::Column(0)),
3938 op: CompareOp::Lt,
3939 right: Box::new(Expr::Const(ConstValue::I64(100))),
3940 },
3941 };
3942
3943 let optimized = optimizer.optimize(plan.clone());
3944
3945 if let RirNode::Filter { input, .. } = optimized {
3948 assert!(matches!(*input, RirNode::Filter { .. }));
3949 } else {
3950 panic!("Expected Filter node");
3951 }
3952 }
3953
3954 #[test]
3955 fn test_collect_columns() {
3956 let expr = Expr::And(vec![
3957 Expr::Compare {
3958 left: Box::new(Expr::Column(0)),
3959 op: CompareOp::Eq,
3960 right: Box::new(Expr::Column(2)),
3961 },
3962 Expr::Compare {
3963 left: Box::new(Expr::Column(1)),
3964 op: CompareOp::Gt,
3965 right: Box::new(Expr::Const(ConstValue::I64(0))),
3966 },
3967 ]);
3968
3969 let cols = Optimizer::collect_columns(&expr);
3970
3971 assert!(cols.contains(&0));
3972 assert!(cols.contains(&1));
3973 assert!(cols.contains(&2));
3974 }
3975
3976 #[test]
3977 fn test_flatten_and() {
3978 let nested = Expr::And(vec![
3979 Expr::And(vec![
3980 Expr::Compare {
3981 left: Box::new(Expr::Column(0)),
3982 op: CompareOp::Eq,
3983 right: Box::new(Expr::Const(ConstValue::I64(1))),
3984 },
3985 Expr::Compare {
3986 left: Box::new(Expr::Column(1)),
3987 op: CompareOp::Eq,
3988 right: Box::new(Expr::Const(ConstValue::I64(2))),
3989 },
3990 ]),
3991 Expr::Compare {
3992 left: Box::new(Expr::Column(2)),
3993 op: CompareOp::Eq,
3994 right: Box::new(Expr::Const(ConstValue::I64(3))),
3995 },
3996 ]);
3997
3998 let flattened = Optimizer::flatten_and(&nested);
3999
4000 assert_eq!(flattened.len(), 3);
4001 }
4002
4003 #[test]
4004 fn test_conjoin_single() {
4005 let single = vec![Expr::Compare {
4006 left: Box::new(Expr::Column(0)),
4007 op: CompareOp::Eq,
4008 right: Box::new(Expr::Const(ConstValue::I64(42))),
4009 }];
4010
4011 let result = Optimizer::conjoin(single);
4012
4013 assert!(matches!(result, Expr::Compare { .. }));
4014 }
4015
4016 #[test]
4017 fn test_conjoin_multiple() {
4018 let multiple = vec![
4019 Expr::Compare {
4020 left: Box::new(Expr::Column(0)),
4021 op: CompareOp::Eq,
4022 right: Box::new(Expr::Const(ConstValue::I64(1))),
4023 },
4024 Expr::Compare {
4025 left: Box::new(Expr::Column(1)),
4026 op: CompareOp::Eq,
4027 right: Box::new(Expr::Const(ConstValue::I64(2))),
4028 },
4029 ];
4030
4031 let result = Optimizer::conjoin(multiple);
4032
4033 assert!(matches!(result, Expr::And(_)));
4034 }
4035
4036 #[test]
4037 fn test_predicate_pushdown_with_schemas() {
4038 let stats = make_stats_manager();
4041 let mut optimizer = Optimizer::new(stats);
4042
4043 let left_schema = Schema::new(vec![
4045 ("c0".to_string(), xlog_core::ScalarType::Symbol),
4046 ("c1".to_string(), xlog_core::ScalarType::Symbol),
4047 ("c2".to_string(), xlog_core::ScalarType::Symbol),
4048 ]);
4049 let right_schema = Schema::new(vec![
4050 ("c0".to_string(), xlog_core::ScalarType::Symbol),
4051 ("c1".to_string(), xlog_core::ScalarType::Symbol),
4052 ("c2".to_string(), xlog_core::ScalarType::U32),
4053 ]);
4054
4055 let mut schemas = HashMap::new();
4056 schemas.insert(RelId(1), left_schema);
4057 schemas.insert(RelId(2), right_schema);
4058 optimizer.set_schemas(schemas);
4059
4060 let plan = RirNode::Filter {
4062 input: Box::new(RirNode::Join {
4063 left: Box::new(RirNode::Scan { rel: RelId(1) }),
4064 right: Box::new(RirNode::Scan { rel: RelId(2) }),
4065 left_keys: vec![0],
4066 right_keys: vec![0],
4067 join_type: JoinType::Inner,
4068 }),
4069 predicate: Expr::Compare {
4070 left: Box::new(Expr::Column(5)), op: CompareOp::Ge,
4072 right: Box::new(Expr::Const(ConstValue::U32(4))),
4073 },
4074 };
4075
4076 let optimized = optimizer.optimize(plan);
4077
4078 if let RirNode::Join { right, .. } = optimized {
4080 if let RirNode::Filter { predicate, .. } = *right {
4081 if let Expr::Compare { left, .. } = predicate {
4082 if let Expr::Column(idx) = *left {
4083 assert_eq!(
4084 idx, 2,
4085 "Column should be remapped to 2 (5 - left_width(3) = 2)"
4086 );
4087 } else {
4088 panic!("Expected Column expression");
4089 }
4090 } else {
4091 panic!("Expected Compare predicate");
4092 }
4093 } else {
4094 panic!("Expected Filter on right side of join");
4095 }
4096 } else {
4097 panic!("Expected Join node");
4098 }
4099 }
4100
4101 fn build_canonical_triangle_multiway() -> RirNode {
4117 let scan_xy = RirNode::Scan { rel: RelId(1) };
4118 let scan_yz = RirNode::Scan { rel: RelId(2) };
4119 let scan_xz = RirNode::Scan { rel: RelId(3) };
4120 let inner_join = RirNode::Join {
4121 left: Box::new(scan_xy.clone()),
4122 right: Box::new(scan_yz.clone()),
4123 left_keys: vec![1],
4124 right_keys: vec![0],
4125 join_type: JoinType::Inner,
4126 };
4127 let outer_join = RirNode::Join {
4128 left: Box::new(inner_join),
4129 right: Box::new(scan_xz.clone()),
4130 left_keys: vec![0, 3],
4131 right_keys: vec![0, 1],
4132 join_type: JoinType::Inner,
4133 };
4134 let fallback = RirNode::Project {
4135 input: Box::new(outer_join),
4136 columns: vec![
4137 ProjectExpr::Column(0),
4138 ProjectExpr::Column(1),
4139 ProjectExpr::Column(3),
4140 ],
4141 };
4142 RirNode::MultiWayJoin {
4143 inputs: vec![scan_xy, scan_yz, scan_xz],
4144 slot_vars: vec![
4145 vec![Some(0), Some(1)],
4146 vec![Some(1), Some(2)],
4147 vec![Some(0), Some(2)],
4148 ],
4149 output_columns: vec![
4150 ProjectExpr::Column(0),
4151 ProjectExpr::Column(1),
4152 ProjectExpr::Column(3),
4153 ],
4154 fallback: Box::new(fallback),
4155 plan: None,
4156 var_order: None,
4157 }
4158 }
4159
4160 fn build_4input_multiway() -> RirNode {
4169 let scans = [RelId(1), RelId(2), RelId(3), RelId(1)]
4170 .map(|rel| RirNode::Scan { rel })
4171 .to_vec();
4172 let slot_vars = vec![
4174 vec![Some(0u32), Some(1)],
4175 vec![Some(1u32), Some(2)],
4176 vec![Some(2u32), Some(3)],
4177 vec![Some(0u32), Some(3)],
4178 ];
4179 let output_columns = vec![
4182 ProjectExpr::Column(0),
4183 ProjectExpr::Column(1),
4184 ProjectExpr::Column(2),
4185 ProjectExpr::Column(3),
4186 ];
4187 let fallback = RirNode::Unit;
4190 RirNode::MultiWayJoin {
4191 inputs: scans,
4192 slot_vars,
4193 output_columns,
4194 fallback: Box::new(fallback),
4195 plan: None,
4196 var_order: None,
4197 }
4198 }
4199
4200 #[test]
4201 fn optimize_returns_multiway_unchanged() {
4202 let optimizer = Optimizer::new(make_stats_manager());
4203 for node in [build_canonical_triangle_multiway(), build_4input_multiway()] {
4204 let optimized = optimizer.optimize(node.clone());
4205 match (&node, &optimized) {
4206 (
4207 RirNode::MultiWayJoin {
4208 inputs: a_in,
4209 output_columns: a_out,
4210 ..
4211 },
4212 RirNode::MultiWayJoin {
4213 inputs: b_in,
4214 output_columns: b_out,
4215 ..
4216 },
4217 ) => {
4218 assert_eq!(a_in.len(), b_in.len());
4219 assert_eq!(a_out.len(), b_out.len());
4220 }
4221 _ => panic!("optimize() must return a MultiWayJoin"),
4222 }
4223 }
4224 }
4225
4226 #[test]
4227 fn estimate_width_uses_output_columns_arity() {
4228 let optimizer = Optimizer::new(make_stats_manager());
4229 assert_eq!(
4231 optimizer.estimate_width(&build_canonical_triangle_multiway()),
4232 3
4233 );
4234 assert_eq!(optimizer.estimate_width(&build_4input_multiway()), 4);
4238 }
4239
4240 #[test]
4241 fn estimate_cost_sums_input_costs() {
4242 let optimizer = Optimizer::new(make_stats_manager());
4243
4244 let cost_tri = optimizer.estimate_cost(&build_canonical_triangle_multiway());
4247 assert!(
4248 cost_tri.rows >= 16_000,
4249 "expected cost.rows >= 16000, got {}",
4250 cost_tri.rows
4251 );
4252
4253 let cost_4 = optimizer.estimate_cost(&build_4input_multiway());
4258 assert!(
4259 cost_4.rows >= 26_000,
4260 "expected 4-input cost.rows >= 26000, got {}",
4261 cost_4.rows
4262 );
4263 assert!(
4264 cost_4.rows > cost_tri.rows,
4265 "4-input cost ({}) must exceed triangle cost ({})",
4266 cost_4.rows,
4267 cost_tri.rows
4268 );
4269 }
4270
4271 #[test]
4272 fn find_column_relation_returns_none_for_multiway() {
4273 let optimizer = Optimizer::new(make_stats_manager());
4274 for node in [build_canonical_triangle_multiway(), build_4input_multiway()] {
4279 for col in 0..node.referenced_relations().len() {
4280 assert!(
4281 optimizer.find_column_relation(&node, col).is_none(),
4282 "find_column_relation must return None for any \
4283 MultiWayJoin column (col={})",
4284 col,
4285 );
4286 }
4287 }
4288 }
4289}