1use std::collections::HashMap;
80use xlog_core::RelId;
81use xlog_ir::rir::{
82 ColumnSwap, CostPredictionRecord as RirCostPredictionRecord, KCliqueVariableOrder,
83 MultiwayPlan, PlannedHashReason, ProjectExpr, SortedLayoutSpec, StreamGroupId, VariableOrder,
84 K_CLIQUE_MAX_EDGES, K_CLIQUE_MAX_K,
85};
86use xlog_ir::{ExecutionPlan, JoinType, RirNode};
87use xlog_stats::{StatsManager, StatsSnapshot};
88
89use crate::compiler_config::CompilerConfig;
90use crate::hypergraph::var_order::{
91 plan_kclique_var_order, FullVariableOrder, KCliqueEdge, KCliqueShape,
92};
93use crate::hypergraph::VertexId;
94use crate::wcoj_var_ordering::{wcoj_cost_gate_predicts_wcoj, WcojVariableOrderingModel};
95
96pub fn promote_multiway(
116 plan: &mut ExecutionPlan,
117 _rel_ids: &HashMap<String, RelId>,
118 stats: &StatsManager,
119 config: &CompilerConfig,
120) {
121 let rel_arities = plan.rel_arities.clone();
126 for (scc_id, rules) in plan.rules_by_scc.iter_mut().enumerate() {
127 if plan.sccs.get(scc_id).is_none() {
128 continue;
129 }
130 for rule in rules.iter_mut() {
131 if try_promote_triangle_inside_aggregate(&mut rule.body, stats, config) {
158 continue;
159 }
160 if try_promote_4cycle_inside_aggregate(&mut rule.body, stats, config) {
165 continue;
166 }
167 if try_promote_clique_inside_aggregate(&mut rule.body, stats) {
173 continue;
174 }
175 if try_promote_general_multiway_inside_aggregate(&mut rule.body, &rel_arities) {
183 continue;
184 }
185 if let Some(promoted) = try_promote_chain(&rule.body) {
186 rule.body = promoted;
187 continue;
188 }
189 let normalized_tri = normalize_triangle_to_left_deep(&rule.body);
199 let body_for_tri = normalized_tri.as_ref().unwrap_or(&rule.body);
200 if let Some(promoted) = try_promote_triangle(body_for_tri, stats, config) {
201 rule.body = promoted;
202 continue;
203 }
204 let normalized_4c = normalize_4cycle_to_bushy(&rule.body);
205 let body_for_4c = normalized_4c.as_ref().unwrap_or(&rule.body);
206 if let Some(promoted) = try_promote_4cycle(body_for_4c, stats, config) {
207 rule.body = promoted;
208 continue;
209 }
210 if let Some(promoted) = try_promote_clique_k(&rule.body, 5, stats)
218 .or_else(|| try_promote_clique_k(&rule.body, 6, stats))
219 .or_else(|| try_promote_clique_k(&rule.body, 7, stats))
220 .or_else(|| try_promote_clique_k(&rule.body, 8, stats))
221 {
222 rule.body = promoted;
223 continue;
224 }
225 if let Some(promoted) = try_promote_general_multiway(&rule.body, &rel_arities) {
234 rule.body = promoted;
235 continue;
236 }
237 }
238 }
239}
240
241fn ac_idx(atom_idx: u8, col_idx: u8) -> u8 {
245 debug_assert!(atom_idx < 3);
246 debug_assert!(col_idx < 2);
247 atom_idx * 2 + col_idx
248}
249
250fn inner_output_ac(k: usize) -> Option<(u8, u8)> {
254 match k {
255 0 => Some((0, 0)),
256 1 => Some((0, 1)),
257 2 => Some((1, 0)),
258 3 => Some((1, 1)),
259 _ => None,
260 }
261}
262
263fn outer_output_ac(k: usize) -> Option<(u8, u8)> {
267 match k {
268 0..=3 => inner_output_ac(k),
269 4 => Some((2, 0)),
270 5 => Some((2, 1)),
271 _ => None,
272 }
273}
274
275fn uf_find(parent: &mut [u8; 6], x: u8) -> u8 {
278 let mut root = x;
279 while parent[root as usize] != root {
280 root = parent[root as usize];
281 }
282 let mut cur = x;
283 while parent[cur as usize] != root {
284 let next = parent[cur as usize];
285 parent[cur as usize] = root;
286 cur = next;
287 }
288 root
289}
290
291fn uf_union(parent: &mut [u8; 6], a: u8, b: u8) {
292 let ra = uf_find(parent, a);
293 let rb = uf_find(parent, b);
294 if ra != rb {
295 parent[rb as usize] = ra;
296 }
297}
298
299#[expect(
317 clippy::too_many_arguments,
318 reason = "triangle inference validates three relation slots and four lowered key vectors without manufacturing an aggregate input type"
319)]
320fn infer_triangle_semantics(
321 inner_left_rel: RelId,
322 inner_right_rel: RelId,
323 outer_third_rel: RelId,
324 lk2: &[usize],
325 rk2: &[usize],
326 lk1: &[usize],
327 rk1: &[usize],
328 project_cols: &[ProjectExpr],
329) -> Option<(RelId, RelId, RelId)> {
330 if lk2.len() != 1 || rk2.len() != 1 {
331 return None;
332 }
333 if lk1.len() != 2 || rk1.len() != 2 {
334 return None;
335 }
336 if project_cols.len() != 3 {
337 return None;
338 }
339 if lk2[0] >= 2 || rk2[0] >= 2 {
340 return None;
341 }
342 if lk1.iter().any(|k| *k >= 4) || rk1.iter().any(|k| *k >= 2) {
343 return None;
344 }
345
346 let mut parent = [0u8, 1, 2, 3, 4, 5];
349
350 uf_union(
352 &mut parent,
353 ac_idx(0, lk2[0] as u8),
354 ac_idx(1, rk2[0] as u8),
355 );
356
357 for i in 0..2 {
359 let (inner_atom, inner_col) = inner_output_ac(lk1[i])?;
360 uf_union(
361 &mut parent,
362 ac_idx(inner_atom, inner_col),
363 ac_idx(2, rk1[i] as u8),
364 );
365 }
366
367 let roots: [u8; 6] = std::array::from_fn(|i| uf_find(&mut parent, i as u8));
370 let mut counts: HashMap<u8, u8> = HashMap::new();
371 for r in &roots {
372 *counts.entry(*r).or_insert(0) += 1;
373 }
374 if counts.len() != 3 || counts.values().any(|c| *c != 2) {
375 return None;
376 }
377
378 let mut head_classes: [u8; 3] = [0; 3];
381 for (i, pc) in project_cols.iter().enumerate() {
382 let ProjectExpr::Column(k) = pc else {
383 return None;
384 };
385 let (atom, col) = outer_output_ac(*k)?;
386 head_classes[i] = uf_find(&mut parent, ac_idx(atom, col));
387 }
388 if head_classes[0] == head_classes[1]
390 || head_classes[0] == head_classes[2]
391 || head_classes[1] == head_classes[2]
392 {
393 return None;
394 }
395 let x_class = head_classes[0];
396 let y_class = head_classes[1];
397 let z_class = head_classes[2];
398
399 let atom_classes = |atom_idx: u8| -> (u8, u8) {
402 (
403 roots[ac_idx(atom_idx, 0) as usize],
404 roots[ac_idx(atom_idx, 1) as usize],
405 )
406 };
407
408 let atom_rels = [inner_left_rel, inner_right_rel, outer_third_rel];
409 let mut rel_xy: Option<RelId> = None;
410 let mut rel_yz: Option<RelId> = None;
411 let mut rel_xz: Option<RelId> = None;
412
413 for atom_idx in 0..3u8 {
414 let (c0, c1) = atom_classes(atom_idx);
415 let binds_x = c0 == x_class || c1 == x_class;
416 let binds_y = c0 == y_class || c1 == y_class;
417 let binds_z = c0 == z_class || c1 == z_class;
418 match (binds_x, binds_y, binds_z) {
419 (true, true, false) => rel_xy = Some(atom_rels[atom_idx as usize]),
420 (false, true, true) => rel_yz = Some(atom_rels[atom_idx as usize]),
421 (true, false, true) => rel_xz = Some(atom_rels[atom_idx as usize]),
422 _ => return None,
423 }
424 }
425
426 Some((rel_xy?, rel_yz?, rel_xz?))
427}
428
429fn normalize_triangle_to_left_deep(node: &RirNode) -> Option<RirNode> {
449 let RirNode::Project {
450 input: outer_input,
451 columns,
452 } = node
453 else {
454 return None;
455 };
456 let RirNode::Join {
457 left: outer_l,
458 right: outer_r,
459 left_keys: outer_lk,
460 right_keys: outer_rk,
461 join_type: outer_jt,
462 } = outer_input.as_ref()
463 else {
464 return None;
465 };
466 if !matches!(outer_jt, JoinType::Inner) {
467 return None;
468 }
469 let RirNode::Scan { rel: _ } = outer_l.as_ref() else {
471 return None;
472 };
473 let RirNode::Join { .. } = outer_r.as_ref() else {
474 return None;
475 };
476 let RirNode::Join {
479 left: inner_l,
480 right: inner_r,
481 ..
482 } = outer_r.as_ref()
483 else {
484 return None;
485 };
486 if !matches!(inner_l.as_ref(), RirNode::Scan { .. })
487 || !matches!(inner_r.as_ref(), RirNode::Scan { .. })
488 {
489 return None;
490 }
491 let new_outer = RirNode::Join {
496 left: outer_r.clone(),
497 right: outer_l.clone(),
498 left_keys: outer_rk.clone(),
499 right_keys: outer_lk.clone(),
500 join_type: JoinType::Inner,
501 };
502 let new_columns: Vec<ProjectExpr> = columns
504 .iter()
505 .map(|expr| match expr {
506 ProjectExpr::Column(k) => ProjectExpr::Column((*k + 4) % 6),
507 other => other.clone(),
508 })
509 .collect();
510 Some(RirNode::Project {
511 input: Box::new(new_outer),
512 columns: new_columns,
513 })
514}
515
516fn normalize_4cycle_to_bushy(node: &RirNode) -> Option<RirNode> {
533 let RirNode::Project {
534 input: outer_input,
535 columns,
536 } = node
537 else {
538 return None;
539 };
540 let RirNode::Join {
541 left: outer_l,
542 right: outer_r,
543 left_keys: outer_lk,
544 right_keys: outer_rk,
545 join_type: outer_jt,
546 } = outer_input.as_ref()
547 else {
548 return None;
549 };
550 if !matches!(outer_jt, JoinType::Inner) {
551 return None;
552 }
553 let RirNode::Scan { rel: r0 } = outer_l.as_ref() else {
556 return None;
557 };
558 let RirNode::Join {
559 left: middle_l,
560 right: middle_r,
561 left_keys: middle_lk,
562 right_keys: middle_rk,
563 join_type: middle_jt,
564 } = outer_r.as_ref()
565 else {
566 return None;
567 };
568 if !matches!(middle_jt, JoinType::Inner) {
569 return None;
570 }
571 let RirNode::Scan { rel: r1 } = middle_l.as_ref() else {
572 return None;
573 };
574 let RirNode::Join {
575 left: deep_l,
576 right: deep_r,
577 left_keys: deep_lk,
578 right_keys: deep_rk,
579 join_type: deep_jt,
580 } = middle_r.as_ref()
581 else {
582 return None;
583 };
584 if !matches!(deep_jt, JoinType::Inner) {
585 return None;
586 }
587 let RirNode::Scan { rel: r2 } = deep_l.as_ref() else {
588 return None;
589 };
590 let RirNode::Scan { rel: r3 } = deep_r.as_ref() else {
591 return None;
592 };
593 if outer_lk.as_slice() != [0, 1] || outer_rk.as_slice() != [5, 0] {
595 return None;
596 }
597 if middle_lk.as_slice() != [1] || middle_rk.as_slice() != [0] {
598 return None;
599 }
600 if deep_lk.as_slice() != [1] || deep_rk.as_slice() != [0] {
601 return None;
602 }
603 let inner_left = RirNode::Join {
611 left: Box::new(RirNode::Scan { rel: *r0 }),
612 right: Box::new(RirNode::Scan { rel: *r1 }),
613 left_keys: vec![1],
614 right_keys: vec![0],
615 join_type: JoinType::Inner,
616 };
617 let inner_right = RirNode::Join {
618 left: Box::new(RirNode::Scan { rel: *r2 }),
619 right: Box::new(RirNode::Scan { rel: *r3 }),
620 left_keys: vec![1],
621 right_keys: vec![0],
622 join_type: JoinType::Inner,
623 };
624 let new_outer = RirNode::Join {
625 left: Box::new(inner_left),
626 right: Box::new(inner_right),
627 left_keys: vec![3, 0],
628 right_keys: vec![0, 3],
629 join_type: JoinType::Inner,
630 };
631 Some(RirNode::Project {
632 input: Box::new(new_outer),
633 columns: columns.clone(),
634 })
635}
636
637fn try_promote_triangle_inside_aggregate(
649 body: &mut RirNode,
650 stats: &StatsManager,
651 config: &CompilerConfig,
652) -> bool {
653 let RirNode::Project { input: gb, .. } = body else {
654 return false;
655 };
656 let RirNode::GroupBy {
657 input: group_input, ..
658 } = gb.as_mut()
659 else {
660 return false;
661 };
662 let RirNode::Project {
663 input: inner,
664 columns: group_cols,
665 } = group_input.as_mut()
666 else {
667 return false;
668 };
669 let canonical = RirNode::Project {
672 input: inner.clone(),
673 columns: vec![
674 ProjectExpr::Column(0),
675 ProjectExpr::Column(1),
676 ProjectExpr::Column(3),
677 ],
678 };
679 let normalized = normalize_triangle_to_left_deep(&canonical);
680 let candidate = normalized.as_ref().unwrap_or(&canonical);
681 let Some(promoted) = try_promote_triangle(candidate, stats, config) else {
682 return false;
683 };
684 let RirNode::MultiWayJoin { output_columns, .. } = &promoted else {
685 return false;
686 };
687 let mut remapped: Vec<ProjectExpr> = Vec::with_capacity(group_cols.len());
688 for col in group_cols.iter() {
689 let ProjectExpr::Column(c) = col else {
690 return false;
691 };
692 let Some(pos) = output_columns
693 .iter()
694 .position(|oc| matches!(oc, ProjectExpr::Column(x) if x == c))
695 else {
696 return false;
697 };
698 remapped.push(ProjectExpr::Column(pos));
699 }
700 *group_cols = remapped;
701 **inner = promoted;
702 true
703}
704
705fn try_promote_4cycle_inside_aggregate(
718 body: &mut RirNode,
719 stats: &StatsManager,
720 config: &CompilerConfig,
721) -> bool {
722 let RirNode::Project { input: gb, .. } = body else {
723 return false;
724 };
725 let RirNode::GroupBy {
726 input: group_input, ..
727 } = gb.as_mut()
728 else {
729 return false;
730 };
731 let RirNode::Project {
732 input: inner,
733 columns: group_cols,
734 } = group_input.as_mut()
735 else {
736 return false;
737 };
738 let canonical = RirNode::Project {
741 input: inner.clone(),
742 columns: vec![
743 ProjectExpr::Column(0),
744 ProjectExpr::Column(1),
745 ProjectExpr::Column(3),
746 ProjectExpr::Column(5),
747 ],
748 };
749 let normalized = normalize_4cycle_to_bushy(&canonical);
750 let candidate = normalized.as_ref().unwrap_or(&canonical);
751 let Some(promoted) = try_promote_4cycle(candidate, stats, config) else {
752 return false;
753 };
754 let RirNode::MultiWayJoin { output_columns, .. } = &promoted else {
755 return false;
756 };
757 let mut remapped: Vec<ProjectExpr> = Vec::with_capacity(group_cols.len());
758 for col in group_cols.iter() {
759 let ProjectExpr::Column(c) = col else {
760 return false;
761 };
762 let Some(pos) = output_columns
763 .iter()
764 .position(|oc| matches!(oc, ProjectExpr::Column(x) if x == c))
765 else {
766 return false;
767 };
768 remapped.push(ProjectExpr::Column(pos));
769 }
770 *group_cols = remapped;
771 **inner = promoted;
772 true
773}
774
775fn try_promote_clique_inside_aggregate(body: &mut RirNode, stats: &StatsManager) -> bool {
794 let RirNode::Project { input: gb, .. } = body else {
795 return false;
796 };
797 let RirNode::GroupBy {
798 input: group_input, ..
799 } = gb.as_mut()
800 else {
801 return false;
802 };
803 let RirNode::Project {
804 input: inner,
805 columns: group_cols,
806 } = group_input.as_mut()
807 else {
808 return false;
809 };
810 let mut scans: Vec<RelId> = Vec::new();
813 let mut key_pairs: Vec<(usize, usize)> = Vec::new();
814 if walk_clique_node(inner, &mut scans, &mut key_pairs).is_none() {
815 return false;
816 }
817 let k = match scans.len() {
818 10 => 5,
819 15 => 6,
820 _ => return false,
821 };
822 let n_slots = 2 * scans.len();
825 let mut parent: Vec<usize> = (0..n_slots).collect();
826 for (a, b) in &key_pairs {
827 if *a >= n_slots || *b >= n_slots {
828 return false;
829 }
830 uf_union_clique(&mut parent, *a, *b);
831 }
832 let mut class_roots: Vec<usize> = Vec::new();
833 let mut first_slot_of_class: HashMap<usize, usize> = HashMap::new();
834 for slot in 0..n_slots {
835 let root = uf_find_clique(&mut parent, slot);
836 if let std::collections::hash_map::Entry::Vacant(entry) = first_slot_of_class.entry(root) {
837 entry.insert(slot);
838 class_roots.push(root);
839 }
840 }
841 if class_roots.len() != k {
842 return false;
843 }
844 let class_idx: HashMap<usize, usize> = class_roots
855 .iter()
856 .enumerate()
857 .map(|(idx, root)| (*root, idx))
858 .collect();
859 let mut in_degree = vec![0usize; k];
860 for atom in 0..scans.len() {
861 let cls_a = uf_find_clique(&mut parent, 2 * atom);
862 let cls_b = uf_find_clique(&mut parent, 2 * atom + 1);
863 if cls_a == cls_b {
864 return false;
865 }
866 in_degree[class_idx[&cls_b]] += 1;
867 }
868 let mut class_pos_by_head = vec![usize::MAX; k];
869 for (pos, deg) in in_degree.iter().enumerate() {
870 if *deg >= k || class_pos_by_head[*deg] != usize::MAX {
871 return false;
872 }
873 class_pos_by_head[*deg] = pos;
874 }
875 let representative_slots: Vec<usize> = class_pos_by_head
876 .iter()
877 .map(|pos| first_slot_of_class[&class_roots[*pos]])
878 .collect();
879 let class_to_head: HashMap<usize, usize> = class_pos_by_head
880 .iter()
881 .enumerate()
882 .map(|(head_idx, pos)| (class_roots[*pos], head_idx))
883 .collect();
884 let canonical = RirNode::Project {
885 input: inner.clone(),
886 columns: representative_slots
887 .iter()
888 .map(|slot| ProjectExpr::Column(*slot))
889 .collect(),
890 };
891 let Some(promoted) = try_promote_clique_k(&canonical, k, stats) else {
892 return false;
893 };
894 let mut remapped: Vec<ProjectExpr> = Vec::with_capacity(group_cols.len());
897 for col in group_cols.iter() {
898 let ProjectExpr::Column(c) = col else {
899 return false;
900 };
901 if *c >= n_slots {
902 return false;
903 }
904 let root = uf_find_clique(&mut parent, *c);
905 let Some(head_idx) = class_to_head.get(&root) else {
906 return false;
907 };
908 remapped.push(ProjectExpr::Column(*head_idx));
909 }
910 *group_cols = remapped;
911 **inner = promoted;
912 true
913}
914
915fn try_promote_triangle(
918 node: &RirNode,
919 stats: &StatsManager,
920 config: &CompilerConfig,
921) -> Option<RirNode> {
922 let RirNode::Project {
923 input: outer_input,
924 columns,
925 } = node
926 else {
927 return None;
928 };
929 let RirNode::Join {
930 left: l1,
931 right: r1,
932 left_keys: lk1,
933 right_keys: rk1,
934 join_type: jt1,
935 } = outer_input.as_ref()
936 else {
937 return None;
938 };
939 if !matches!(jt1, JoinType::Inner) {
940 return None;
941 }
942 let RirNode::Scan { rel: rel_third } = r1.as_ref() else {
943 return None;
944 };
945 let RirNode::Join {
946 left: l2,
947 right: r2,
948 left_keys: lk2,
949 right_keys: rk2,
950 join_type: jt2,
951 } = l1.as_ref()
952 else {
953 return None;
954 };
955 if !matches!(jt2, JoinType::Inner) {
956 return None;
957 }
958 let RirNode::Scan { rel: rel_inner_l } = l2.as_ref() else {
959 return None;
960 };
961 let RirNode::Scan { rel: rel_inner_r } = r2.as_ref() else {
962 return None;
963 };
964
965 let (rel_xy, rel_yz, rel_xz) = infer_triangle_semantics(
967 *rel_inner_l,
968 *rel_inner_r,
969 *rel_third,
970 lk2,
971 rk2,
972 lk1,
973 rk1,
974 columns,
975 )?;
976
977 let inputs = vec![
978 RirNode::Scan { rel: rel_xy },
979 RirNode::Scan { rel: rel_yz },
980 RirNode::Scan { rel: rel_xz },
981 ];
982 let slot_vars = vec![
986 vec![Some(0u32), Some(1)],
987 vec![Some(1u32), Some(2)],
988 vec![Some(0u32), Some(2)],
989 ];
990 let output_columns = columns.clone();
991 let fallback = Box::new(node.clone());
992 use crate::compiler_config::WcojVarOrderingKind;
997 use crate::wcoj_var_ordering::{
998 build_triangle_var_order, HeatAwareLeaderModel, LeaderCardinalityModel,
999 };
1000 let leader_idx = match config.wcoj_variable_ordering {
1001 WcojVarOrderingKind::Disabled => None,
1002 WcojVarOrderingKind::LeaderCardinality => {
1003 LeaderCardinalityModel.pick_triangle_leader([rel_xy, rel_yz, rel_xz], stats, config)
1004 }
1005 WcojVarOrderingKind::HeatAware => {
1006 HeatAwareLeaderModel.pick_triangle_leader([rel_xy, rel_yz, rel_xz], stats, config)
1007 }
1008 };
1009 let var_order = leader_idx.map(build_triangle_var_order);
1010 Some(RirNode::MultiWayJoin {
1011 inputs,
1012 slot_vars,
1013 output_columns,
1014 fallback,
1015 plan: None,
1016 var_order,
1017 })
1018}
1019
1020fn try_promote_chain(node: &RirNode) -> Option<RirNode> {
1024 let RirNode::Project { input, columns } = node else {
1025 return None;
1026 };
1027 let RirNode::Join {
1028 left,
1029 right,
1030 left_keys,
1031 right_keys,
1032 join_type,
1033 } = input.as_ref()
1034 else {
1035 return None;
1036 };
1037 if !matches!(join_type, JoinType::Inner) {
1038 return None;
1039 }
1040 if left_keys.len() != 1 || right_keys.len() != 1 {
1041 return None;
1042 }
1043 let left_key = left_keys[0];
1044 let right_key = right_keys[0];
1045 if left_key >= 2 || right_key >= 2 {
1046 return None;
1047 }
1048 let RirNode::Scan { rel: rel_left } = left.as_ref() else {
1049 return None;
1050 };
1051 let RirNode::Scan { rel: rel_right } = right.as_ref() else {
1052 return None;
1053 };
1054
1055 Some(RirNode::ChainJoin {
1056 left: Box::new(RirNode::Scan { rel: *rel_left }),
1057 right: Box::new(RirNode::Scan { rel: *rel_right }),
1058 left_key,
1059 right_key,
1060 output_columns: columns.clone(),
1061 fallback: Box::new(node.clone()),
1062 })
1063}
1064
1065fn ac_idx_4(atom_idx: u8, col_idx: u8) -> u8 {
1070 debug_assert!(atom_idx < 4);
1071 debug_assert!(col_idx < 2);
1072 atom_idx * 2 + col_idx
1073}
1074
1075fn outer_left_inner_output_ac(k: usize) -> Option<(u8, u8)> {
1076 match k {
1077 0 => Some((0, 0)),
1078 1 => Some((0, 1)),
1079 2 => Some((1, 0)),
1080 3 => Some((1, 1)),
1081 _ => None,
1082 }
1083}
1084
1085fn outer_right_inner_output_ac(k: usize) -> Option<(u8, u8)> {
1086 match k {
1087 0 => Some((2, 0)),
1088 1 => Some((2, 1)),
1089 2 => Some((3, 0)),
1090 3 => Some((3, 1)),
1091 _ => None,
1092 }
1093}
1094
1095fn outer_4cycle_output_ac(k: usize) -> Option<(u8, u8)> {
1096 match k {
1097 0..=3 => outer_left_inner_output_ac(k),
1098 4..=7 => outer_right_inner_output_ac(k - 4),
1099 _ => None,
1100 }
1101}
1102
1103fn uf_find_8(parent: &mut [u8; 8], x: u8) -> u8 {
1104 let mut root = x;
1105 while parent[root as usize] != root {
1106 root = parent[root as usize];
1107 }
1108 let mut cur = x;
1109 while parent[cur as usize] != root {
1110 let next = parent[cur as usize];
1111 parent[cur as usize] = root;
1112 cur = next;
1113 }
1114 root
1115}
1116
1117fn uf_union_8(parent: &mut [u8; 8], a: u8, b: u8) {
1118 let ra = uf_find_8(parent, a);
1119 let rb = uf_find_8(parent, b);
1120 if ra != rb {
1121 parent[rb as usize] = ra;
1122 }
1123}
1124
1125#[expect(
1132 clippy::too_many_arguments,
1133 reason = "four-cycle inference validates four relation slots and six lowered key vectors as one fixed physical shape"
1134)]
1135fn infer_4cycle_semantics(
1136 rel_ll: RelId,
1137 rel_lr: RelId,
1138 rel_rl: RelId,
1139 rel_rr: RelId,
1140 ilk_l: &[usize],
1141 irk_l: &[usize],
1142 ilk_r: &[usize],
1143 irk_r: &[usize],
1144 olk: &[usize],
1145 ork: &[usize],
1146 project_cols: &[ProjectExpr],
1147) -> Option<(RelId, RelId, RelId, RelId)> {
1148 if ilk_l.len() != 1 || irk_l.len() != 1 {
1149 return None;
1150 }
1151 if ilk_r.len() != 1 || irk_r.len() != 1 {
1152 return None;
1153 }
1154 if olk.len() != 2 || ork.len() != 2 {
1155 return None;
1156 }
1157 if project_cols.len() != 4 {
1158 return None;
1159 }
1160 if ilk_l[0] >= 2 || irk_l[0] >= 2 || ilk_r[0] >= 2 || irk_r[0] >= 2 {
1161 return None;
1162 }
1163 if olk.iter().any(|k| *k >= 4) || ork.iter().any(|k| *k >= 4) {
1164 return None;
1165 }
1166
1167 let mut parent = [0u8, 1, 2, 3, 4, 5, 6, 7];
1168
1169 uf_union_8(
1171 &mut parent,
1172 ac_idx_4(0, ilk_l[0] as u8),
1173 ac_idx_4(1, irk_l[0] as u8),
1174 );
1175
1176 uf_union_8(
1178 &mut parent,
1179 ac_idx_4(2, ilk_r[0] as u8),
1180 ac_idx_4(3, irk_r[0] as u8),
1181 );
1182
1183 for i in 0..2 {
1187 let (la, lc) = outer_left_inner_output_ac(olk[i])?;
1188 let (ra, rc) = outer_right_inner_output_ac(ork[i])?;
1189 uf_union_8(&mut parent, ac_idx_4(la, lc), ac_idx_4(ra, rc));
1190 }
1191
1192 let roots: [u8; 8] = std::array::from_fn(|i| uf_find_8(&mut parent, i as u8));
1193 let mut counts: HashMap<u8, u8> = HashMap::new();
1194 for r in &roots {
1195 *counts.entry(*r).or_insert(0) += 1;
1196 }
1197 if counts.len() != 4 || counts.values().any(|c| *c != 2) {
1198 return None;
1199 }
1200
1201 let mut head_classes: [u8; 4] = [0; 4];
1203 for (i, pc) in project_cols.iter().enumerate() {
1204 let ProjectExpr::Column(k) = pc else {
1205 return None;
1206 };
1207 let (atom, col) = outer_4cycle_output_ac(*k)?;
1208 head_classes[i] = uf_find_8(&mut parent, ac_idx_4(atom, col));
1209 }
1210 for i in 0..4 {
1212 for j in (i + 1)..4 {
1213 if head_classes[i] == head_classes[j] {
1214 return None;
1215 }
1216 }
1217 }
1218 let w_class = head_classes[0];
1219 let x_class = head_classes[1];
1220 let y_class = head_classes[2];
1221 let z_class = head_classes[3];
1222
1223 let atom_classes = |atom_idx: u8| -> (u8, u8) {
1224 (
1225 roots[ac_idx_4(atom_idx, 0) as usize],
1226 roots[ac_idx_4(atom_idx, 1) as usize],
1227 )
1228 };
1229
1230 let atom_rels = [rel_ll, rel_lr, rel_rl, rel_rr];
1231 let mut rel_wx: Option<RelId> = None;
1232 let mut rel_xy: Option<RelId> = None;
1233 let mut rel_yz: Option<RelId> = None;
1234 let mut rel_zw: Option<RelId> = None;
1235
1236 for atom_idx in 0..4u8 {
1237 let (c0, c1) = atom_classes(atom_idx);
1238 let binds_w = c0 == w_class || c1 == w_class;
1239 let binds_x = c0 == x_class || c1 == x_class;
1240 let binds_y = c0 == y_class || c1 == y_class;
1241 let binds_z = c0 == z_class || c1 == z_class;
1242 match (binds_w, binds_x, binds_y, binds_z) {
1243 (true, true, false, false) => rel_wx = Some(atom_rels[atom_idx as usize]),
1244 (false, true, true, false) => rel_xy = Some(atom_rels[atom_idx as usize]),
1245 (false, false, true, true) => rel_yz = Some(atom_rels[atom_idx as usize]),
1246 (true, false, false, true) => rel_zw = Some(atom_rels[atom_idx as usize]),
1247 _ => return None,
1248 }
1249 }
1250
1251 Some((rel_wx?, rel_xy?, rel_yz?, rel_zw?))
1252}
1253
1254fn try_promote_4cycle(
1274 node: &RirNode,
1275 stats: &StatsManager,
1276 config: &CompilerConfig,
1277) -> Option<RirNode> {
1278 let RirNode::Project {
1279 input: outer_input,
1280 columns,
1281 } = node
1282 else {
1283 return None;
1284 };
1285 let RirNode::Join {
1286 left: outer_l,
1287 right: outer_r,
1288 left_keys: olk,
1289 right_keys: ork,
1290 join_type: ojt,
1291 } = outer_input.as_ref()
1292 else {
1293 return None;
1294 };
1295 if !matches!(ojt, JoinType::Inner) {
1296 return None;
1297 }
1298 let RirNode::Join {
1299 left: ll,
1300 right: lr,
1301 left_keys: ilk_l,
1302 right_keys: irk_l,
1303 join_type: ijt_l,
1304 } = outer_l.as_ref()
1305 else {
1306 return None;
1307 };
1308 if !matches!(ijt_l, JoinType::Inner) {
1309 return None;
1310 }
1311 let RirNode::Scan { rel: rel_ll } = ll.as_ref() else {
1312 return None;
1313 };
1314 let RirNode::Scan { rel: rel_lr } = lr.as_ref() else {
1315 return None;
1316 };
1317 let RirNode::Join {
1318 left: rl,
1319 right: rr,
1320 left_keys: ilk_r,
1321 right_keys: irk_r,
1322 join_type: ijt_r,
1323 } = outer_r.as_ref()
1324 else {
1325 return None;
1326 };
1327 if !matches!(ijt_r, JoinType::Inner) {
1328 return None;
1329 }
1330 let RirNode::Scan { rel: rel_rl } = rl.as_ref() else {
1331 return None;
1332 };
1333 let RirNode::Scan { rel: rel_rr } = rr.as_ref() else {
1334 return None;
1335 };
1336
1337 let (rel_wx, rel_xy, rel_yz, rel_zw) = infer_4cycle_semantics(
1340 *rel_ll, *rel_lr, *rel_rl, *rel_rr, ilk_l, irk_l, ilk_r, irk_r, olk, ork, columns,
1341 )?;
1342
1343 let inputs = vec![
1344 RirNode::Scan { rel: rel_wx },
1345 RirNode::Scan { rel: rel_xy },
1346 RirNode::Scan { rel: rel_yz },
1347 RirNode::Scan { rel: rel_zw },
1348 ];
1349 let slot_vars = vec![
1354 vec![Some(0u32), Some(1)],
1355 vec![Some(1u32), Some(2)],
1356 vec![Some(2u32), Some(3)],
1357 vec![Some(3u32), Some(0)],
1358 ];
1359 let output_columns = columns.clone();
1360 let fallback = Box::new(node.clone());
1361 use crate::compiler_config::WcojVarOrderingKind;
1365 use crate::wcoj_var_ordering::{
1366 build_cycle4_var_order, HeatAwareLeaderModel, LeaderCardinalityModel,
1367 };
1368 let leader_idx_4 = match config.wcoj_variable_ordering {
1369 WcojVarOrderingKind::Disabled => None,
1370 WcojVarOrderingKind::LeaderCardinality => LeaderCardinalityModel.pick_4cycle_leader(
1371 [rel_wx, rel_xy, rel_yz, rel_zw],
1372 stats,
1373 config,
1374 ),
1375 WcojVarOrderingKind::HeatAware => {
1376 HeatAwareLeaderModel.pick_4cycle_leader([rel_wx, rel_xy, rel_yz, rel_zw], stats, config)
1377 }
1378 };
1379 let var_order = leader_idx_4.map(build_cycle4_var_order);
1380 Some(RirNode::MultiWayJoin {
1381 inputs,
1382 slot_vars,
1383 output_columns,
1384 fallback,
1385 plan: None,
1386 var_order,
1387 })
1388}
1389
1390fn clique_edge_idx(i: usize, j: usize, k: usize) -> usize {
1409 debug_assert!(i < j && j < k);
1410 i * (2 * k - i - 1) / 2 + (j - i - 1)
1411}
1412
1413fn uf_find_clique(parent: &mut [usize], mut x: usize) -> usize {
1415 while parent[x] != x {
1416 parent[x] = parent[parent[x]];
1417 x = parent[x];
1418 }
1419 x
1420}
1421
1422fn uf_union_clique(parent: &mut [usize], a: usize, b: usize) {
1423 let ra = uf_find_clique(parent, a);
1424 let rb = uf_find_clique(parent, b);
1425 if ra != rb {
1426 parent[rb] = ra;
1427 }
1428}
1429
1430type FlattenedCliqueBody = (Vec<RelId>, Vec<(usize, usize)>, Vec<usize>);
1446
1447fn flatten_clique_body(body: &RirNode) -> Option<FlattenedCliqueBody> {
1448 let RirNode::Project { input, columns } = body else {
1449 return None;
1450 };
1451 let mut scans: Vec<RelId> = Vec::new();
1452 let mut key_pairs: Vec<(usize, usize)> = Vec::new();
1453 let _width = walk_clique_node(input, &mut scans, &mut key_pairs)?;
1454 let mut project_globals: Vec<usize> = Vec::with_capacity(columns.len());
1455 for c in columns {
1456 let xlog_ir::rir::ProjectExpr::Column(k) = c else {
1457 return None;
1458 };
1459 project_globals.push(*k);
1460 }
1461 Some((scans, key_pairs, project_globals))
1462}
1463
1464fn walk_clique_node(
1467 node: &RirNode,
1468 scans: &mut Vec<RelId>,
1469 key_pairs: &mut Vec<(usize, usize)>,
1470) -> Option<usize> {
1471 match node {
1472 RirNode::Scan { rel } => {
1473 scans.push(*rel);
1474 Some(2)
1475 }
1476 RirNode::Join {
1477 left,
1478 right,
1479 left_keys,
1480 right_keys,
1481 join_type,
1482 } => {
1483 if !matches!(join_type, JoinType::Inner) {
1484 return None;
1485 }
1486 let left_offset = scans.len() * 2;
1487 let left_width = walk_clique_node(left, scans, key_pairs)?;
1488 let right_offset = left_offset + left_width;
1489 let right_width = walk_clique_node(right, scans, key_pairs)?;
1490 if left_keys.len() != right_keys.len() {
1491 return None;
1492 }
1493 for (lk, rk) in left_keys.iter().zip(right_keys.iter()) {
1494 if *lk >= left_width || *rk >= right_width {
1495 return None;
1496 }
1497 key_pairs.push((left_offset + *lk, right_offset + *rk));
1498 }
1499 Some(left_width + right_width)
1500 }
1501 _ => None,
1505 }
1506}
1507
1508fn walk_general_node(
1517 node: &RirNode,
1518 arities: &HashMap<RelId, usize>,
1519 scans: &mut Vec<RelId>,
1520 widths: &mut Vec<usize>,
1521 key_pairs: &mut Vec<(usize, usize)>,
1522) -> Option<usize> {
1523 match node {
1524 RirNode::Scan { rel } => {
1525 let width = *arities.get(rel)?;
1526 if width == 0 {
1527 return None;
1528 }
1529 scans.push(*rel);
1530 widths.push(width);
1531 Some(width)
1532 }
1533 RirNode::Join {
1534 left,
1535 right,
1536 left_keys,
1537 right_keys,
1538 join_type,
1539 } => {
1540 if !matches!(join_type, JoinType::Inner) {
1541 return None;
1542 }
1543 let left_offset: usize = widths.iter().sum();
1546 let left_width = walk_general_node(left, arities, scans, widths, key_pairs)?;
1547 let right_offset = left_offset + left_width;
1548 let right_width = walk_general_node(right, arities, scans, widths, key_pairs)?;
1549 if left_keys.len() != right_keys.len() || left_keys.is_empty() {
1550 return None;
1551 }
1552 for (lk, rk) in left_keys.iter().zip(right_keys.iter()) {
1553 if *lk >= left_width || *rk >= right_width {
1554 return None;
1555 }
1556 key_pairs.push((left_offset + *lk, right_offset + *rk));
1557 }
1558 Some(left_width + right_width)
1559 }
1560 _ => None,
1561 }
1562}
1563
1564fn try_promote_general_multiway(
1578 body: &RirNode,
1579 arities: &HashMap<RelId, usize>,
1580) -> Option<RirNode> {
1581 let RirNode::Project { input, columns } = body else {
1582 return None;
1583 };
1584 let mut scans: Vec<RelId> = Vec::new();
1585 let mut widths: Vec<usize> = Vec::new();
1586 let mut key_pairs: Vec<(usize, usize)> = Vec::new();
1587 let total = walk_general_node(input, arities, &mut scans, &mut widths, &mut key_pairs)?;
1588 if scans.len() < 3 {
1589 return None;
1590 }
1591
1592 let mut parent: Vec<usize> = (0..total).collect();
1597 for (a, b) in &key_pairs {
1598 if *a >= total || *b >= total {
1599 return None;
1600 }
1601 uf_union_clique(&mut parent, *a, *b);
1602 }
1603 let mut class_of_root: HashMap<usize, u32> = HashMap::new();
1604 let mut slot_class: Vec<u32> = Vec::with_capacity(total);
1605 for slot in 0..total {
1606 let root = uf_find_clique(&mut parent, slot);
1607 let next = class_of_root.len() as u32;
1608 let cls = *class_of_root.entry(root).or_insert(next);
1609 slot_class.push(cls);
1610 }
1611
1612 for c in columns {
1615 let ProjectExpr::Column(k) = c else {
1616 return None;
1617 };
1618 if *k >= total {
1619 return None;
1620 }
1621 }
1622
1623 let inputs: Vec<RirNode> = scans
1624 .iter()
1625 .map(|rel| RirNode::Scan { rel: *rel })
1626 .collect();
1627 let mut slot_vars: Vec<Vec<Option<u32>>> = Vec::with_capacity(scans.len());
1628 let mut offset = 0usize;
1629 for &w in &widths {
1630 slot_vars.push((offset..offset + w).map(|s| Some(slot_class[s])).collect());
1631 offset += w;
1632 }
1633
1634 Some(RirNode::MultiWayJoin {
1635 inputs,
1636 slot_vars,
1637 output_columns: columns.clone(),
1638 fallback: Box::new(body.clone()),
1639 plan: Some(MultiwayPlan::FreeJoin),
1645 var_order: None,
1646 })
1647}
1648
1649fn try_promote_general_multiway_inside_aggregate(
1664 body: &mut RirNode,
1665 arities: &HashMap<RelId, usize>,
1666) -> bool {
1667 let RirNode::Project { input: gb, .. } = body else {
1668 return false;
1669 };
1670 let RirNode::GroupBy {
1671 input: group_input, ..
1672 } = gb.as_mut()
1673 else {
1674 return false;
1675 };
1676 let RirNode::Project {
1677 input: inner,
1678 columns: group_cols,
1679 } = group_input.as_mut()
1680 else {
1681 return false;
1682 };
1683 let candidate = RirNode::Project {
1684 input: inner.clone(),
1685 columns: group_cols.clone(),
1686 };
1687 let Some(promoted) = try_promote_general_multiway(&candidate, arities) else {
1688 return false;
1689 };
1690 *group_cols = (0..group_cols.len()).map(ProjectExpr::Column).collect();
1693 **inner = promoted;
1694 true
1695}
1696
1697fn try_promote_clique_k(body: &RirNode, k: usize, stats: &StatsManager) -> Option<RirNode> {
1705 if !(5..=8).contains(&k) {
1706 return None;
1707 }
1708 let expected_edges = k * (k - 1) / 2;
1709
1710 let (scans, key_pairs, project_globals) = flatten_clique_body(body)?;
1712
1713 if scans.len() != expected_edges {
1715 return None;
1716 }
1717
1718 if project_globals.len() != k {
1720 return None;
1721 }
1722
1723 let n_slots = 2 * expected_edges;
1726 let mut parent: Vec<usize> = (0..n_slots).collect();
1727 for (a, b) in &key_pairs {
1728 if *a >= n_slots || *b >= n_slots {
1729 return None;
1730 }
1731 uf_union_clique(&mut parent, *a, *b);
1732 }
1733
1734 let mut head_class: Vec<usize> = Vec::with_capacity(k);
1738 for col in &project_globals {
1739 if *col >= n_slots {
1740 return None;
1741 }
1742 head_class.push(uf_find_clique(&mut parent, *col));
1743 }
1744 let mut sorted_head_classes = head_class.clone();
1746 sorted_head_classes.sort();
1747 sorted_head_classes.dedup();
1748 if sorted_head_classes.len() != k {
1749 return None;
1750 }
1751
1752 let mut all_class_count: HashMap<usize, usize> = HashMap::new();
1756 for slot in 0..n_slots {
1757 let root = uf_find_clique(&mut parent, slot);
1758 *all_class_count.entry(root).or_insert(0) += 1;
1759 }
1760 if all_class_count.len() != k {
1761 return None;
1762 }
1763 for &count in all_class_count.values() {
1766 if count != k - 1 {
1767 return None;
1768 }
1769 }
1770
1771 let mut class_to_head_idx: HashMap<usize, usize> = HashMap::new();
1774 for (head_idx, cls) in head_class.iter().enumerate() {
1775 class_to_head_idx.insert(*cls, head_idx);
1776 }
1777
1778 let mut atom_pairs: Vec<(usize, usize)> = Vec::with_capacity(expected_edges);
1784 let mut canonical_to_scan_idx: HashMap<(usize, usize), usize> = HashMap::new();
1785 for (atom_i, _rel) in scans.iter().enumerate() {
1786 let slot_a = 2 * atom_i;
1787 let slot_b = 2 * atom_i + 1;
1788 let cls_a = uf_find_clique(&mut parent, slot_a);
1789 let cls_b = uf_find_clique(&mut parent, slot_b);
1790 if cls_a == cls_b {
1791 return None;
1793 }
1794 let head_a = class_to_head_idx.get(&cls_a)?;
1795 let head_b = class_to_head_idx.get(&cls_b)?;
1796 if *head_a > *head_b {
1801 return None;
1802 }
1803 let (lo, hi) = (*head_a, *head_b);
1804 atom_pairs.push((lo, hi));
1805 if canonical_to_scan_idx.insert((lo, hi), atom_i).is_some() {
1807 return None;
1808 }
1809 }
1810
1811 if canonical_to_scan_idx.len() != expected_edges {
1814 return None;
1815 }
1816 for i in 0..k {
1817 for j in (i + 1)..k {
1818 if !canonical_to_scan_idx.contains_key(&(i, j)) {
1819 return None;
1820 }
1821 }
1822 }
1823
1824 let mut reordered_scans: Vec<RelId> = Vec::with_capacity(expected_edges);
1826 for i in 0..k {
1827 for j in (i + 1)..k {
1828 let scan_idx = canonical_to_scan_idx[&(i, j)];
1829 reordered_scans.push(scans[scan_idx]);
1830 }
1831 }
1832
1833 let inputs: Vec<RirNode> = reordered_scans
1838 .iter()
1839 .map(|rel| RirNode::Scan { rel: *rel })
1840 .collect();
1841 let mut slot_vars: Vec<Vec<Option<u32>>> = Vec::with_capacity(expected_edges);
1842 for i in 0..k {
1843 for j in (i + 1)..k {
1844 let _ = clique_edge_idx(i, j, k); slot_vars.push(vec![Some(i as u32), Some(j as u32)]);
1846 }
1847 }
1848 let RirNode::Project { columns, .. } = body else {
1852 return None;
1853 };
1854 let output_columns = columns.clone();
1855 let fallback = Box::new(body.clone());
1856 let shape = build_kclique_shape(k, &reordered_scans)?;
1857 let planner_stats = kclique_planner_stats(stats);
1858
1859 let (plan, var_order) = match plan_kclique_var_order(&shape, &planner_stats) {
1864 Some(full_order) => {
1865 let evidence = rir_cost_prediction(&full_order);
1866 if wcoj_cost_gate_predicts_wcoj(evidence.wcoj_cost, evidence.hash_cost) {
1867 let kclique_order = kclique_variable_order_from_plan(&shape, &full_order)?;
1868 (
1869 MultiwayPlan::WcojWithPlan(kclique_order.clone()),
1870 Some(VariableOrder::kclique(kclique_order)),
1871 )
1872 } else {
1873 (
1874 MultiwayPlan::PlannedHashRoute {
1875 reason: PlannedHashReason::PlannerPredictsHashWins,
1876 planner_evidence: evidence,
1877 },
1878 None,
1879 )
1880 }
1881 }
1882 None => (
1883 MultiwayPlan::PlannedHashRoute {
1884 reason: PlannedHashReason::IncompleteStatsSafeDefault,
1885 planner_evidence: RirCostPredictionRecord::empty(),
1886 },
1887 None,
1888 ),
1889 };
1890
1891 Some(RirNode::MultiWayJoin {
1892 inputs,
1893 slot_vars,
1894 output_columns,
1895 fallback,
1896 plan: Some(plan),
1897 var_order,
1898 })
1899}
1900
1901fn build_kclique_shape(k: usize, rels: &[RelId]) -> Option<KCliqueShape> {
1902 let mut edges = Vec::with_capacity(rels.len());
1903 let mut idx = 0usize;
1904 for i in 0..k {
1905 for j in (i + 1)..k {
1906 let rel_id = *rels.get(idx)?;
1907 edges.push(KCliqueEdge {
1908 rel_id,
1909 left: VertexId(i),
1910 right: VertexId(j),
1911 left_col: 0,
1912 right_col: 1,
1913 });
1914 idx += 1;
1915 }
1916 }
1917 KCliqueShape::from_edges(k as u8, edges)
1918}
1919
1920fn kclique_planner_stats(stats: &StatsManager) -> StatsSnapshot {
1921 stats.snapshot()
1922}
1923
1924fn rir_cost_prediction(plan: &FullVariableOrder) -> RirCostPredictionRecord {
1925 RirCostPredictionRecord {
1926 wcoj_cost: plan.cost_prediction.wcoj_cost,
1927 hash_cost: plan.cost_prediction.hash_cost,
1928 }
1929}
1930
1931fn kclique_variable_order_from_plan(
1932 shape: &KCliqueShape,
1933 plan: &FullVariableOrder,
1934) -> Option<KCliqueVariableOrder> {
1935 let k = shape.variable_count();
1936 let expected_edges = usize::from(k) * usize::from(k - 1) / 2;
1937 if plan.variable_order.len() != usize::from(k) || plan.edge_permutation.len() != expected_edges
1938 {
1939 return None;
1940 }
1941
1942 let mut variable_positions = [u8::MAX; K_CLIQUE_MAX_K];
1943 for (position, variable) in plan.variable_order.iter().enumerate() {
1944 if variable.0 >= usize::from(k) {
1945 return None;
1946 }
1947 variable_positions[variable.0] = position as u8;
1948 }
1949
1950 let mut edge_permutation = [u8::MAX; K_CLIQUE_MAX_EDGES];
1951 let mut column_swaps = Vec::new();
1952 let mut leader_slot = None;
1953 for (slot, edge_idx) in plan.edge_permutation.iter().copied().enumerate() {
1954 let edge = shape.edges().get(edge_idx)?;
1955 let left_pos = variable_positions[edge.left.0];
1956 let right_pos = variable_positions[edge.right.0];
1957 if left_pos == u8::MAX || right_pos == u8::MAX {
1958 return None;
1959 }
1960 edge_permutation[slot] = edge_idx as u8;
1961 if left_pos > right_pos {
1962 column_swaps.push(ColumnSwap {
1963 edge_slot: slot as u8,
1964 swap_cols: true,
1965 });
1966 }
1967 if [left_pos, right_pos].into_iter().min() == Some(0)
1968 && [left_pos, right_pos].into_iter().max() == Some(1)
1969 {
1970 leader_slot = Some(slot as u8);
1971 }
1972 }
1973
1974 let sorted_edge_slots = vec![leader_slot.unwrap_or(0)];
1975 let sorted_layout_requirements = SortedLayoutSpec {
1976 edge_slots: sorted_edge_slots,
1977 key_columns: vec![vec![0, 1]],
1978 };
1979
1980 let helper_split_specs = plan.helper_split_specs.clone();
1982 Some(KCliqueVariableOrder::new(
1983 k,
1984 variable_positions,
1985 edge_permutation,
1986 column_swaps,
1987 sorted_layout_requirements,
1988 helper_split_specs,
1989 StreamGroupId(0),
1990 ))
1991}
1992
1993#[cfg(test)]
1994mod tests {
1995 use super::*;
1996 use xlog_core::RelId;
1997 use xlog_ir::{CompiledRule, ExecutionPlan, PlanBuilder, Scc};
1998
1999 fn canonical_triangle_tree() -> RirNode {
2000 let inner = RirNode::Join {
2001 left: Box::new(RirNode::Scan { rel: RelId(1) }),
2002 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2003 left_keys: vec![1],
2004 right_keys: vec![0],
2005 join_type: JoinType::Inner,
2006 };
2007 let outer = RirNode::Join {
2008 left: Box::new(inner),
2009 right: Box::new(RirNode::Scan { rel: RelId(3) }),
2010 left_keys: vec![0, 3],
2011 right_keys: vec![0, 1],
2012 join_type: JoinType::Inner,
2013 };
2014 RirNode::Project {
2015 input: Box::new(outer),
2016 columns: vec![
2017 ProjectExpr::Column(0),
2018 ProjectExpr::Column(1),
2019 ProjectExpr::Column(3),
2020 ],
2021 }
2022 }
2023
2024 fn plan_with_body(body: RirNode) -> ExecutionPlan {
2025 let mut builder = PlanBuilder::new();
2026 builder.add_scc(Scc {
2027 id: 0,
2028 predicates: vec!["t".to_string()],
2029 is_recursive: false,
2030 });
2031 builder.add_rule(
2032 0,
2033 CompiledRule {
2034 head: "t".to_string(),
2035 body,
2036 meta: Default::default(),
2037 },
2038 );
2039 builder.build()
2040 }
2041
2042 fn canonical_chain_tree() -> RirNode {
2043 let join = RirNode::Join {
2044 left: Box::new(RirNode::Scan { rel: RelId(1) }),
2045 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2046 left_keys: vec![1],
2047 right_keys: vec![0],
2048 join_type: JoinType::Inner,
2049 };
2050 RirNode::Project {
2051 input: Box::new(join),
2052 columns: vec![ProjectExpr::Column(0), ProjectExpr::Column(3)],
2053 }
2054 }
2055
2056 #[test]
2057 fn promotes_canonical_chain() {
2058 let mut plan = plan_with_body(canonical_chain_tree());
2059 promote_multiway(
2060 &mut plan,
2061 &HashMap::new(),
2062 &StatsManager::new(),
2063 &CompilerConfig::default(),
2064 );
2065 let body = &plan.rules_by_scc[0][0].body;
2066 match body {
2067 RirNode::ChainJoin {
2068 left,
2069 right,
2070 left_key,
2071 right_key,
2072 output_columns,
2073 fallback,
2074 } => {
2075 assert!(matches!(left.as_ref(), RirNode::Scan { rel: RelId(1) }));
2076 assert!(matches!(right.as_ref(), RirNode::Scan { rel: RelId(2) }));
2077 assert_eq!(*left_key, 1);
2078 assert_eq!(*right_key, 0);
2079 assert_eq!(
2080 output_columns,
2081 &vec![ProjectExpr::Column(0), ProjectExpr::Column(3)]
2082 );
2083 assert!(matches!(fallback.as_ref(), RirNode::Project { .. }));
2084 }
2085 other => panic!("expected ChainJoin, got {:?}", other),
2086 }
2087 }
2088
2089 #[test]
2090 fn chain_promotion_rejects_non_inner_join() {
2091 let mut body = canonical_chain_tree();
2092 if let RirNode::Project { input, .. } = &mut body {
2093 if let RirNode::Join { join_type, .. } = input.as_mut() {
2094 *join_type = JoinType::LeftOuter;
2095 }
2096 }
2097 assert!(try_promote_chain(&body).is_none());
2098 }
2099
2100 #[test]
2101 fn chain_promotion_rejects_multi_key_join() {
2102 let mut body = canonical_chain_tree();
2103 if let RirNode::Project { input, .. } = &mut body {
2104 if let RirNode::Join {
2105 left_keys,
2106 right_keys,
2107 ..
2108 } = input.as_mut()
2109 {
2110 *left_keys = vec![0, 1];
2111 *right_keys = vec![0, 1];
2112 }
2113 }
2114 assert!(try_promote_chain(&body).is_none());
2115 }
2116
2117 #[test]
2118 fn promotes_canonical_triangle() {
2119 let mut plan = plan_with_body(canonical_triangle_tree());
2120 promote_multiway(
2121 &mut plan,
2122 &HashMap::new(),
2123 &StatsManager::new(),
2124 &CompilerConfig::default(),
2125 );
2126 let body = &plan.rules_by_scc[0][0].body;
2127 match body {
2128 RirNode::MultiWayJoin {
2129 inputs,
2130 slot_vars,
2131 output_columns,
2132 fallback,
2133 ..
2134 } => {
2135 assert_eq!(inputs.len(), 3);
2136 assert!(matches!(inputs[0], RirNode::Scan { rel: RelId(1) }));
2137 assert!(matches!(inputs[1], RirNode::Scan { rel: RelId(2) }));
2138 assert!(matches!(inputs[2], RirNode::Scan { rel: RelId(3) }));
2139 assert_eq!(
2140 slot_vars,
2141 &vec![
2142 vec![Some(0u32), Some(1)],
2143 vec![Some(1u32), Some(2)],
2144 vec![Some(0u32), Some(2)],
2145 ]
2146 );
2147 assert_eq!(
2148 output_columns,
2149 &vec![
2150 ProjectExpr::Column(0),
2151 ProjectExpr::Column(1),
2152 ProjectExpr::Column(3),
2153 ]
2154 );
2155 assert!(matches!(fallback.as_ref(), RirNode::Project { .. }));
2157 }
2158 other => panic!("expected MultiWayJoin, got {:?}", other),
2159 }
2160 }
2161
2162 #[test]
2163 fn fallback_is_structurally_equal_to_input() {
2164 let pre = canonical_triangle_tree();
2165 let mut plan = plan_with_body(pre.clone());
2166 promote_multiway(
2167 &mut plan,
2168 &HashMap::new(),
2169 &StatsManager::new(),
2170 &CompilerConfig::default(),
2171 );
2172 let body = &plan.rules_by_scc[0][0].body;
2173 let RirNode::MultiWayJoin { fallback, .. } = body else {
2174 panic!("expected MultiWayJoin");
2175 };
2176 assert_eq!(format!("{:?}", fallback.as_ref()), format!("{:?}", pre));
2180 }
2181
2182 #[test]
2183 fn idempotent_under_repeat_calls() {
2184 let mut plan = plan_with_body(canonical_triangle_tree());
2185 promote_multiway(
2186 &mut plan,
2187 &HashMap::new(),
2188 &StatsManager::new(),
2189 &CompilerConfig::default(),
2190 );
2191 let first = format!("{:?}", plan.rules_by_scc[0][0].body);
2192 promote_multiway(
2193 &mut plan,
2194 &HashMap::new(),
2195 &StatsManager::new(),
2196 &CompilerConfig::default(),
2197 );
2198 let second = format!("{:?}", plan.rules_by_scc[0][0].body);
2199 assert_eq!(first, second);
2200 }
2201
2202 #[test]
2208 fn promotes_triangle_with_x_shared_inner_pair() {
2209 let inner = RirNode::Join {
2210 left: Box::new(RirNode::Scan { rel: RelId(1) }), right: Box::new(RirNode::Scan { rel: RelId(2) }), left_keys: vec![0],
2213 right_keys: vec![0],
2214 join_type: JoinType::Inner,
2215 };
2216 let outer = RirNode::Join {
2217 left: Box::new(inner),
2218 right: Box::new(RirNode::Scan { rel: RelId(3) }), left_keys: vec![1, 3],
2220 right_keys: vec![0, 1],
2221 join_type: JoinType::Inner,
2222 };
2223 let body = RirNode::Project {
2224 input: Box::new(outer),
2225 columns: vec![
2226 ProjectExpr::Column(0),
2227 ProjectExpr::Column(1),
2228 ProjectExpr::Column(3),
2229 ],
2230 };
2231 let mut plan = plan_with_body(body);
2232 promote_multiway(
2233 &mut plan,
2234 &HashMap::new(),
2235 &StatsManager::new(),
2236 &CompilerConfig::default(),
2237 );
2238 let RirNode::MultiWayJoin {
2239 inputs, slot_vars, ..
2240 } = &plan.rules_by_scc[0][0].body
2241 else {
2242 panic!("expected MultiWayJoin after promotion");
2243 };
2244 let scan_rels: Vec<RelId> = inputs
2246 .iter()
2247 .map(|n| match n {
2248 RirNode::Scan { rel } => *rel,
2249 _ => panic!("expected Scan in MultiWayJoin inputs"),
2250 })
2251 .collect();
2252 assert_eq!(scan_rels, vec![RelId(1), RelId(3), RelId(2)]);
2253 assert_eq!(
2255 slot_vars,
2256 &vec![
2257 vec![Some(0u32), Some(1)],
2258 vec![Some(1u32), Some(2)],
2259 vec![Some(0u32), Some(2)],
2260 ]
2261 );
2262 }
2263
2264 #[test]
2270 fn promotes_triangle_with_z_shared_inner_pair() {
2271 let inner = RirNode::Join {
2272 left: Box::new(RirNode::Scan { rel: RelId(1) }), right: Box::new(RirNode::Scan { rel: RelId(2) }), left_keys: vec![1],
2275 right_keys: vec![1],
2276 join_type: JoinType::Inner,
2277 };
2278 let outer = RirNode::Join {
2279 left: Box::new(inner),
2280 right: Box::new(RirNode::Scan { rel: RelId(3) }), left_keys: vec![0, 2],
2282 right_keys: vec![0, 1],
2283 join_type: JoinType::Inner,
2284 };
2285 let body = RirNode::Project {
2286 input: Box::new(outer),
2287 columns: vec![
2290 ProjectExpr::Column(0),
2291 ProjectExpr::Column(2),
2292 ProjectExpr::Column(3),
2293 ],
2294 };
2295 let mut plan = plan_with_body(body);
2296 promote_multiway(
2297 &mut plan,
2298 &HashMap::new(),
2299 &StatsManager::new(),
2300 &CompilerConfig::default(),
2301 );
2302 let RirNode::MultiWayJoin {
2303 inputs, slot_vars, ..
2304 } = &plan.rules_by_scc[0][0].body
2305 else {
2306 panic!("expected MultiWayJoin after promotion");
2307 };
2308 let scan_rels: Vec<RelId> = inputs
2309 .iter()
2310 .map(|n| match n {
2311 RirNode::Scan { rel } => *rel,
2312 _ => panic!("expected Scan in MultiWayJoin inputs"),
2313 })
2314 .collect();
2315 assert_eq!(scan_rels, vec![RelId(3), RelId(2), RelId(1)]);
2316 assert_eq!(
2317 slot_vars,
2318 &vec![
2319 vec![Some(0u32), Some(1)],
2320 vec![Some(1u32), Some(2)],
2321 vec![Some(0u32), Some(2)],
2322 ]
2323 );
2324 }
2325
2326 #[test]
2327 fn promotes_triangle_with_rotated_projection_columns() {
2328 let inner = RirNode::Join {
2340 left: Box::new(RirNode::Scan { rel: RelId(1) }),
2341 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2342 left_keys: vec![1],
2343 right_keys: vec![0],
2344 join_type: JoinType::Inner,
2345 };
2346 let outer = RirNode::Join {
2347 left: Box::new(inner),
2348 right: Box::new(RirNode::Scan { rel: RelId(3) }),
2349 left_keys: vec![0, 3],
2350 right_keys: vec![0, 1],
2351 join_type: JoinType::Inner,
2352 };
2353 let body = RirNode::Project {
2354 input: Box::new(outer),
2355 columns: vec![
2358 ProjectExpr::Column(1),
2359 ProjectExpr::Column(0),
2360 ProjectExpr::Column(3),
2361 ],
2362 };
2363 let mut plan = plan_with_body(body);
2364 promote_multiway(
2365 &mut plan,
2366 &HashMap::new(),
2367 &StatsManager::new(),
2368 &CompilerConfig::default(),
2369 );
2370 let RirNode::MultiWayJoin {
2372 slot_vars,
2373 output_columns,
2374 ..
2375 } = &plan.rules_by_scc[0][0].body
2376 else {
2377 panic!("expected MultiWayJoin after promotion");
2378 };
2379 assert_eq!(
2381 slot_vars,
2382 &vec![
2383 vec![Some(0u32), Some(1)],
2384 vec![Some(1u32), Some(2)],
2385 vec![Some(0u32), Some(2)],
2386 ]
2387 );
2388 assert_eq!(
2390 output_columns,
2391 &vec![
2392 ProjectExpr::Column(1),
2393 ProjectExpr::Column(0),
2394 ProjectExpr::Column(3),
2395 ]
2396 );
2397 }
2398
2399 #[test]
2400 fn rejects_non_inner_join() {
2401 let inner = RirNode::Join {
2402 left: Box::new(RirNode::Scan { rel: RelId(1) }),
2403 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2404 left_keys: vec![1],
2405 right_keys: vec![0],
2406 join_type: JoinType::LeftOuter,
2407 };
2408 let outer = RirNode::Join {
2409 left: Box::new(inner),
2410 right: Box::new(RirNode::Scan { rel: RelId(3) }),
2411 left_keys: vec![0, 3],
2412 right_keys: vec![0, 1],
2413 join_type: JoinType::Inner,
2414 };
2415 let body = RirNode::Project {
2416 input: Box::new(outer),
2417 columns: vec![
2418 ProjectExpr::Column(0),
2419 ProjectExpr::Column(1),
2420 ProjectExpr::Column(3),
2421 ],
2422 };
2423 let mut plan = plan_with_body(body);
2424 promote_multiway(
2425 &mut plan,
2426 &HashMap::new(),
2427 &StatsManager::new(),
2428 &CompilerConfig::default(),
2429 );
2430 assert!(matches!(
2431 &plan.rules_by_scc[0][0].body,
2432 RirNode::Project { .. }
2433 ));
2434 }
2435
2436 #[test]
2437 fn rejects_filter_above_outer_join() {
2438 let inner = RirNode::Join {
2441 left: Box::new(RirNode::Scan { rel: RelId(1) }),
2442 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2443 left_keys: vec![1],
2444 right_keys: vec![0],
2445 join_type: JoinType::Inner,
2446 };
2447 let outer = RirNode::Join {
2448 left: Box::new(inner),
2449 right: Box::new(RirNode::Scan { rel: RelId(3) }),
2450 left_keys: vec![0, 3],
2451 right_keys: vec![0, 1],
2452 join_type: JoinType::Inner,
2453 };
2454 let filtered = RirNode::Filter {
2455 input: Box::new(outer),
2456 predicate: xlog_ir::Expr::Column(0),
2457 };
2458 let body = RirNode::Project {
2459 input: Box::new(filtered),
2460 columns: vec![
2461 ProjectExpr::Column(0),
2462 ProjectExpr::Column(1),
2463 ProjectExpr::Column(3),
2464 ],
2465 };
2466 let mut plan = plan_with_body(body);
2467 promote_multiway(
2468 &mut plan,
2469 &HashMap::new(),
2470 &StatsManager::new(),
2471 &CompilerConfig::default(),
2472 );
2473 assert!(matches!(
2474 &plan.rules_by_scc[0][0].body,
2475 RirNode::Project { .. }
2476 ));
2477 }
2478
2479 #[test]
2480 fn meta_preserved_byte_for_byte() {
2481 use xlog_core::Schema;
2482 use xlog_ir::metadata::RirMeta;
2483
2484 let schema = Schema::new(vec![
2485 ("x".to_string(), xlog_core::ScalarType::U32),
2486 ("y".to_string(), xlog_core::ScalarType::U32),
2487 ("z".to_string(), xlog_core::ScalarType::U32),
2488 ]);
2489 let meta_pre = RirMeta::with_schema(schema).with_rows(100, 250);
2490
2491 let mut builder = PlanBuilder::new();
2492 builder.add_scc(Scc {
2493 id: 0,
2494 predicates: vec!["t".to_string()],
2495 is_recursive: false,
2496 });
2497 builder.add_rule(
2498 0,
2499 CompiledRule {
2500 head: "t".to_string(),
2501 body: canonical_triangle_tree(),
2502 meta: meta_pre.clone(),
2503 },
2504 );
2505 let mut plan = builder.build();
2506 promote_multiway(
2507 &mut plan,
2508 &HashMap::new(),
2509 &StatsManager::new(),
2510 &CompilerConfig::default(),
2511 );
2512 assert_eq!(
2513 format!("{:?}", plan.rules_by_scc[0][0].meta),
2514 format!("{:?}", meta_pre),
2515 );
2516 }
2517
2518 #[test]
2528 fn promotes_stable_triangle_in_recursive_scc() {
2529 let mut builder = PlanBuilder::new();
2530 builder.add_scc(Scc {
2531 id: 0,
2532 predicates: vec!["tri".to_string()],
2533 is_recursive: true,
2534 });
2535 builder.add_rule(
2536 0,
2537 CompiledRule {
2538 head: "tri".to_string(),
2539 body: canonical_triangle_tree(),
2540 meta: Default::default(),
2541 },
2542 );
2543 let mut plan = builder.build();
2544 promote_multiway(
2547 &mut plan,
2548 &HashMap::new(),
2549 &StatsManager::new(),
2550 &CompilerConfig::default(),
2551 );
2552 assert!(matches!(
2553 &plan.rules_by_scc[0][0].body,
2554 RirNode::MultiWayJoin { .. }
2555 ));
2556 }
2557
2558 #[test]
2563 fn promotes_linear_recursive_triangle() {
2564 let mut builder = PlanBuilder::new();
2565 builder.add_scc(Scc {
2566 id: 0,
2567 predicates: vec!["tri".to_string()],
2568 is_recursive: true,
2569 });
2570 builder.add_rule(
2571 0,
2572 CompiledRule {
2573 head: "tri".to_string(),
2574 body: canonical_triangle_tree(),
2575 meta: Default::default(),
2576 },
2577 );
2578 let mut plan = builder.build();
2579 let mut rel_ids = HashMap::new();
2580 rel_ids.insert("tri".to_string(), RelId(2)); promote_multiway(
2582 &mut plan,
2583 &rel_ids,
2584 &StatsManager::new(),
2585 &CompilerConfig::default(),
2586 );
2587 assert!(matches!(
2588 &plan.rules_by_scc[0][0].body,
2589 RirNode::MultiWayJoin { .. }
2590 ));
2591 }
2592
2593 #[test]
2599 fn promotes_multirec_triangle_in_recursive_scc() {
2600 let mut builder = PlanBuilder::new();
2601 builder.add_scc(Scc {
2602 id: 0,
2603 predicates: vec!["tri_a".to_string(), "tri_b".to_string()],
2604 is_recursive: true,
2605 });
2606 builder.add_rule(
2607 0,
2608 CompiledRule {
2609 head: "tri_a".to_string(),
2610 body: canonical_triangle_tree(),
2611 meta: Default::default(),
2612 },
2613 );
2614 let mut plan = builder.build();
2615 let mut rel_ids = HashMap::new();
2616 rel_ids.insert("tri_a".to_string(), RelId(1));
2617 rel_ids.insert("tri_b".to_string(), RelId(2));
2618 promote_multiway(
2620 &mut plan,
2621 &rel_ids,
2622 &StatsManager::new(),
2623 &CompilerConfig::default(),
2624 );
2625 assert!(matches!(
2626 &plan.rules_by_scc[0][0].body,
2627 RirNode::MultiWayJoin { .. }
2628 ));
2629 }
2630
2631 #[test]
2635 fn promotes_linear_rec_and_non_rec_sccs_in_mixed_plan() {
2636 let mut builder = PlanBuilder::new();
2637 builder.add_scc(Scc {
2638 id: 0,
2639 predicates: vec!["rec".to_string()],
2640 is_recursive: true,
2641 });
2642 builder.add_rule(
2643 0,
2644 CompiledRule {
2645 head: "rec".to_string(),
2646 body: canonical_triangle_tree(),
2647 meta: Default::default(),
2648 },
2649 );
2650 builder.add_scc(Scc {
2651 id: 1,
2652 predicates: vec!["nonrec".to_string()],
2653 is_recursive: false,
2654 });
2655 builder.add_rule(
2656 1,
2657 CompiledRule {
2658 head: "nonrec".to_string(),
2659 body: canonical_triangle_tree(),
2660 meta: Default::default(),
2661 },
2662 );
2663 let mut plan = builder.build();
2664 let mut rel_ids = HashMap::new();
2665 rel_ids.insert("rec".to_string(), RelId(1)); promote_multiway(
2669 &mut plan,
2670 &rel_ids,
2671 &StatsManager::new(),
2672 &CompilerConfig::default(),
2673 );
2674 assert!(matches!(
2675 &plan.rules_by_scc[0][0].body,
2676 RirNode::MultiWayJoin { .. }
2677 ));
2678 assert!(matches!(
2679 &plan.rules_by_scc[1][0].body,
2680 RirNode::MultiWayJoin { .. }
2681 ));
2682 }
2683
2684 fn canonical_4cycle_tree() -> RirNode {
2694 let inner_l = RirNode::Join {
2695 left: Box::new(RirNode::Scan { rel: RelId(1) }),
2696 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2697 left_keys: vec![1],
2698 right_keys: vec![0],
2699 join_type: JoinType::Inner,
2700 };
2701 let inner_r = RirNode::Join {
2702 left: Box::new(RirNode::Scan { rel: RelId(3) }),
2703 right: Box::new(RirNode::Scan { rel: RelId(4) }),
2704 left_keys: vec![1],
2705 right_keys: vec![0],
2706 join_type: JoinType::Inner,
2707 };
2708 let outer = RirNode::Join {
2709 left: Box::new(inner_l),
2710 right: Box::new(inner_r),
2711 left_keys: vec![0, 3],
2712 right_keys: vec![3, 0],
2713 join_type: JoinType::Inner,
2714 };
2715 RirNode::Project {
2716 input: Box::new(outer),
2717 columns: vec![
2718 ProjectExpr::Column(0),
2719 ProjectExpr::Column(1),
2720 ProjectExpr::Column(3),
2721 ProjectExpr::Column(5),
2722 ],
2723 }
2724 }
2725
2726 fn plan_with_4cycle_body(body: RirNode) -> ExecutionPlan {
2727 let mut builder = PlanBuilder::new();
2728 builder.add_scc(Scc {
2729 id: 0,
2730 predicates: vec!["cycle4".to_string()],
2731 is_recursive: false,
2732 });
2733 builder.add_rule(
2734 0,
2735 CompiledRule {
2736 head: "cycle4".to_string(),
2737 body,
2738 meta: Default::default(),
2739 },
2740 );
2741 builder.build()
2742 }
2743
2744 #[test]
2745 fn promotes_canonical_4cycle() {
2746 let mut plan = plan_with_4cycle_body(canonical_4cycle_tree());
2747 promote_multiway(
2748 &mut plan,
2749 &HashMap::new(),
2750 &StatsManager::new(),
2751 &CompilerConfig::default(),
2752 );
2753 let body = &plan.rules_by_scc[0][0].body;
2754 match body {
2755 RirNode::MultiWayJoin {
2756 inputs,
2757 slot_vars,
2758 output_columns,
2759 fallback,
2760 ..
2761 } => {
2762 assert_eq!(inputs.len(), 4);
2763 assert!(matches!(inputs[0], RirNode::Scan { rel: RelId(1) }));
2764 assert!(matches!(inputs[1], RirNode::Scan { rel: RelId(2) }));
2765 assert!(matches!(inputs[2], RirNode::Scan { rel: RelId(3) }));
2766 assert!(matches!(inputs[3], RirNode::Scan { rel: RelId(4) }));
2767 assert_eq!(
2768 slot_vars,
2769 &vec![
2770 vec![Some(0u32), Some(1)],
2771 vec![Some(1u32), Some(2)],
2772 vec![Some(2u32), Some(3)],
2773 vec![Some(3u32), Some(0)],
2774 ]
2775 );
2776 assert_eq!(
2777 output_columns,
2778 &vec![
2779 ProjectExpr::Column(0),
2780 ProjectExpr::Column(1),
2781 ProjectExpr::Column(3),
2782 ProjectExpr::Column(5),
2783 ]
2784 );
2785 assert!(matches!(fallback.as_ref(), RirNode::Project { .. }));
2786 }
2787 other => panic!("expected MultiWayJoin, got {:?}", other),
2788 }
2789 }
2790
2791 #[test]
2792 fn fallback_4cycle_is_structurally_equal_to_input() {
2793 let pre = canonical_4cycle_tree();
2794 let mut plan = plan_with_4cycle_body(pre.clone());
2795 promote_multiway(
2796 &mut plan,
2797 &HashMap::new(),
2798 &StatsManager::new(),
2799 &CompilerConfig::default(),
2800 );
2801 let body = &plan.rules_by_scc[0][0].body;
2802 let RirNode::MultiWayJoin { fallback, .. } = body else {
2803 panic!("expected MultiWayJoin");
2804 };
2805 assert_eq!(format!("{:?}", fallback.as_ref()), format!("{:?}", pre));
2806 }
2807
2808 #[test]
2809 fn idempotent_4cycle_under_repeat_calls() {
2810 let mut plan = plan_with_4cycle_body(canonical_4cycle_tree());
2811 promote_multiway(
2812 &mut plan,
2813 &HashMap::new(),
2814 &StatsManager::new(),
2815 &CompilerConfig::default(),
2816 );
2817 let first = format!("{:?}", plan.rules_by_scc[0][0].body);
2818 promote_multiway(
2819 &mut plan,
2820 &HashMap::new(),
2821 &StatsManager::new(),
2822 &CompilerConfig::default(),
2823 );
2824 let second = format!("{:?}", plan.rules_by_scc[0][0].body);
2825 assert_eq!(first, second);
2826 }
2827
2828 #[test]
2829 fn rejects_4cycle_with_left_deep_shape() {
2830 let inner = RirNode::Join {
2837 left: Box::new(RirNode::Scan { rel: RelId(1) }),
2838 right: Box::new(RirNode::Scan { rel: RelId(2) }),
2839 left_keys: vec![1],
2840 right_keys: vec![0],
2841 join_type: JoinType::Inner,
2842 };
2843 let outer = RirNode::Join {
2844 left: Box::new(inner),
2845 right: Box::new(RirNode::Scan { rel: RelId(3) }),
2846 left_keys: vec![0, 3],
2847 right_keys: vec![0, 1],
2848 join_type: JoinType::Inner,
2849 };
2850 let body = RirNode::Project {
2851 input: Box::new(outer),
2852 columns: vec![
2853 ProjectExpr::Column(0),
2854 ProjectExpr::Column(1),
2855 ProjectExpr::Column(3),
2856 ProjectExpr::Column(5),
2857 ],
2858 };
2859 assert!(
2864 try_promote_4cycle(&body, &StatsManager::new(), &CompilerConfig::default()).is_none()
2865 );
2866 }
2867
2868 #[test]
2876 fn promotes_4cycle_with_alternative_inner_grouping() {
2877 let inner_left = RirNode::Join {
2880 left: Box::new(RirNode::Scan { rel: RelId(2) }),
2881 right: Box::new(RirNode::Scan { rel: RelId(3) }),
2882 left_keys: vec![1],
2883 right_keys: vec![0],
2884 join_type: JoinType::Inner,
2885 };
2886 let inner_right = RirNode::Join {
2887 left: Box::new(RirNode::Scan { rel: RelId(4) }),
2888 right: Box::new(RirNode::Scan { rel: RelId(1) }),
2889 left_keys: vec![1],
2890 right_keys: vec![0],
2891 join_type: JoinType::Inner,
2892 };
2893 let outer = RirNode::Join {
2894 left: Box::new(inner_left),
2895 right: Box::new(inner_right),
2896 left_keys: vec![0, 3],
2899 right_keys: vec![3, 0],
2900 join_type: JoinType::Inner,
2901 };
2902 let body = RirNode::Project {
2905 input: Box::new(outer),
2906 columns: vec![
2907 ProjectExpr::Column(5),
2908 ProjectExpr::Column(0),
2909 ProjectExpr::Column(1),
2910 ProjectExpr::Column(3),
2911 ],
2912 };
2913 let mut plan = plan_with_body(body);
2914 promote_multiway(
2915 &mut plan,
2916 &HashMap::new(),
2917 &StatsManager::new(),
2918 &CompilerConfig::default(),
2919 );
2920 let RirNode::MultiWayJoin {
2921 inputs, slot_vars, ..
2922 } = &plan.rules_by_scc[0][0].body
2923 else {
2924 panic!("expected MultiWayJoin after promotion");
2925 };
2926 let scan_rels: Vec<RelId> = inputs
2927 .iter()
2928 .map(|n| match n {
2929 RirNode::Scan { rel } => *rel,
2930 _ => panic!("expected Scan in MultiWayJoin inputs"),
2931 })
2932 .collect();
2933 assert_eq!(
2936 scan_rels,
2937 vec![RelId(1), RelId(2), RelId(3), RelId(4)],
2938 "inputs must be in semantic order regardless of positional layout"
2939 );
2940 assert_eq!(
2942 slot_vars,
2943 &vec![
2944 vec![Some(0u32), Some(1)],
2945 vec![Some(1u32), Some(2)],
2946 vec![Some(2u32), Some(3)],
2947 vec![Some(3u32), Some(0)],
2948 ]
2949 );
2950 }
2951
2952 #[test]
2953 fn rejects_4cycle_with_rotated_columns() {
2954 let mut body = canonical_4cycle_tree();
2955 if let RirNode::Project { columns, .. } = &mut body {
2956 columns.swap(0, 1);
2958 }
2959 assert!(
2960 try_promote_4cycle(&body, &StatsManager::new(), &CompilerConfig::default()).is_none()
2961 );
2962 }
2963
2964 #[test]
2965 fn rejects_4cycle_with_non_inner_outer_join() {
2966 let mut body = canonical_4cycle_tree();
2967 if let RirNode::Project { input, .. } = &mut body {
2968 if let RirNode::Join { join_type, .. } = input.as_mut() {
2969 *join_type = JoinType::LeftOuter;
2970 }
2971 }
2972 assert!(
2973 try_promote_4cycle(&body, &StatsManager::new(), &CompilerConfig::default()).is_none()
2974 );
2975 }
2976
2977 #[test]
2978 fn rejects_4cycle_with_wrong_outer_keys() {
2979 let mut body = canonical_4cycle_tree();
2980 if let RirNode::Project { input, .. } = &mut body {
2981 if let RirNode::Join { left_keys, .. } = input.as_mut() {
2982 *left_keys = vec![0, 4]; }
2984 }
2985 assert!(
2986 try_promote_4cycle(&body, &StatsManager::new(), &CompilerConfig::default()).is_none()
2987 );
2988 }
2989
2990 #[test]
2993 fn promotes_stable_4cycle_in_recursive_scc() {
2994 let mut builder = PlanBuilder::new();
2995 builder.add_scc(Scc {
2996 id: 0,
2997 predicates: vec!["rec_cycle".to_string()],
2998 is_recursive: true,
2999 });
3000 builder.add_rule(
3001 0,
3002 CompiledRule {
3003 head: "rec_cycle".to_string(),
3004 body: canonical_4cycle_tree(),
3005 meta: Default::default(),
3006 },
3007 );
3008 let mut plan = builder.build();
3009 promote_multiway(
3011 &mut plan,
3012 &HashMap::new(),
3013 &StatsManager::new(),
3014 &CompilerConfig::default(),
3015 );
3016 assert!(matches!(
3017 &plan.rules_by_scc[0][0].body,
3018 RirNode::MultiWayJoin { .. }
3019 ));
3020 }
3021
3022 #[test]
3025 fn promotes_linear_recursive_4cycle() {
3026 let mut builder = PlanBuilder::new();
3027 builder.add_scc(Scc {
3028 id: 0,
3029 predicates: vec!["rec_cycle".to_string()],
3030 is_recursive: true,
3031 });
3032 builder.add_rule(
3033 0,
3034 CompiledRule {
3035 head: "rec_cycle".to_string(),
3036 body: canonical_4cycle_tree(),
3037 meta: Default::default(),
3038 },
3039 );
3040 let mut plan = builder.build();
3041 let mut rel_ids = HashMap::new();
3042 rel_ids.insert("rec_cycle".to_string(), RelId(2));
3045 promote_multiway(
3046 &mut plan,
3047 &rel_ids,
3048 &StatsManager::new(),
3049 &CompilerConfig::default(),
3050 );
3051 assert!(matches!(
3052 &plan.rules_by_scc[0][0].body,
3053 RirNode::MultiWayJoin { .. }
3054 ));
3055 }
3056
3057 #[test]
3060 fn promotes_multirec_4cycle_in_recursive_scc() {
3061 let mut builder = PlanBuilder::new();
3062 builder.add_scc(Scc {
3063 id: 0,
3064 predicates: vec!["rc_a".to_string(), "rc_b".to_string()],
3065 is_recursive: true,
3066 });
3067 builder.add_rule(
3068 0,
3069 CompiledRule {
3070 head: "rc_a".to_string(),
3071 body: canonical_4cycle_tree(),
3072 meta: Default::default(),
3073 },
3074 );
3075 let mut plan = builder.build();
3076 let mut rel_ids = HashMap::new();
3077 rel_ids.insert("rc_a".to_string(), RelId(1));
3078 rel_ids.insert("rc_b".to_string(), RelId(2));
3079 promote_multiway(
3080 &mut plan,
3081 &rel_ids,
3082 &StatsManager::new(),
3083 &CompilerConfig::default(),
3084 );
3085 assert!(matches!(
3086 &plan.rules_by_scc[0][0].body,
3087 RirNode::MultiWayJoin { .. }
3088 ));
3089 }
3090
3091 #[test]
3092 fn triangle_does_not_match_4cycle_promoter() {
3093 let triangle = canonical_triangle_tree();
3097 assert!(
3098 try_promote_4cycle(&triangle, &StatsManager::new(), &CompilerConfig::default())
3099 .is_none()
3100 );
3101 let four_cycle = canonical_4cycle_tree();
3102 assert!(try_promote_triangle(
3103 &four_cycle,
3104 &StatsManager::new(),
3105 &CompilerConfig::default()
3106 )
3107 .is_none());
3108 }
3109}