1use crate::{
18 PhysicalProperties, PlanCost, PlanCostBreakdown, RelationalJoinAccessPath,
19 RelationalJoinEnumerationConfig, RelationalJoinEnumerationError, RelationalJoinPredicateId,
20 RelationalJoinRelation, RequiredProperties,
21};
22use hawdb_expression::{prove_null_rejecting, BindingId, BindingSet, BoundPredicate};
23use std::{
24 collections::{BTreeMap, BTreeSet},
25 error::Error,
26 fmt,
27};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct RelationalJoinOperatorId(u32);
31
32impl RelationalJoinOperatorId {
33 pub const fn new(value: u32) -> Self {
34 Self(value)
35 }
36
37 pub const fn get(self) -> u32 {
38 self.0
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum RelationalJoinOperatorKind {
44 Inner,
45 LeftOuter,
46}
47
48impl RelationalJoinOperatorKind {
49 pub const fn is_commutative(self) -> bool {
50 matches!(self, Self::Inner)
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct RelationalJoinOperator {
56 pub id: RelationalJoinOperatorId,
57 pub kind: RelationalJoinOperatorKind,
58 pub predicate_ids: Vec<RelationalJoinPredicateId>,
59 pub predicate: BoundPredicate,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum RelationalJoinTree {
64 Relation(BindingId),
65 Join {
66 operator: RelationalJoinOperator,
67 left: Box<Self>,
68 right: Box<Self>,
69 },
70}
71
72impl RelationalJoinTree {
73 pub fn join(
74 operator: RelationalJoinOperator,
75 left: RelationalJoinTree,
76 right: RelationalJoinTree,
77 ) -> Self {
78 Self::Join {
79 operator,
80 left: Box::new(left),
81 right: Box::new(right),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct RelationalJoinConflictRule {
88 pub trigger: BindingSet,
89 pub required: BindingSet,
90}
91
92impl RelationalJoinConflictRule {
93 pub fn is_obeyed_by(&self, bindings: &BindingSet) -> bool {
94 !self.trigger.intersects(bindings) || self.required.is_subset(bindings)
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct RelationalJoinConflictDescriptor {
100 pub operator_id: RelationalJoinOperatorId,
101 pub declared_kind: RelationalJoinOperatorKind,
102 pub effective_kind: RelationalJoinOperatorKind,
103 pub initial_left_bindings: BindingSet,
104 pub initial_right_bindings: BindingSet,
105 pub predicate_bindings: BindingSet,
106 pub left_total_eligibility: BindingSet,
107 pub right_total_eligibility: BindingSet,
108 pub conflict_rules: Vec<RelationalJoinConflictRule>,
109 pub predicate_ids: Vec<RelationalJoinPredicateId>,
110}
111
112impl RelationalJoinConflictDescriptor {
113 pub fn was_null_rejection_simplified(&self) -> bool {
114 self.declared_kind == RelationalJoinOperatorKind::LeftOuter
115 && self.effective_kind == RelationalJoinOperatorKind::Inner
116 }
117
118 pub fn is_applicable(&self, left: &BindingSet, right: &BindingSet) -> bool {
119 if left.intersects(right)
120 || !self.left_total_eligibility.is_subset(left)
121 || !self.right_total_eligibility.is_subset(right)
122 {
123 return false;
124 }
125 let available = binding_union(left, right);
126 self.conflict_rules
127 .iter()
128 .all(|rule| rule.is_obeyed_by(&available))
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct RelationalJoinConflictAnalysis {
134 pub root_bindings: BindingSet,
135 pub descriptors: BTreeMap<RelationalJoinOperatorId, RelationalJoinConflictDescriptor>,
136}
137
138impl RelationalJoinConflictAnalysis {
139 pub fn descriptor(
140 &self,
141 operator: RelationalJoinOperatorId,
142 ) -> Option<&RelationalJoinConflictDescriptor> {
143 self.descriptors.get(&operator)
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct RelationalJoinRewriteProblem {
149 pub relations: Vec<RelationalJoinRelation>,
150 pub initial_tree: RelationalJoinTree,
151 pub post_join_filter: Option<BoundPredicate>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct RelationalJoinRewriteStep {
156 pub operator_id: RelationalJoinOperatorId,
157 pub operator_kind: RelationalJoinOperatorKind,
158 pub binding: BindingId,
159 pub access_path: RelationalJoinAccessPath,
160 pub predicate_ids: Vec<RelationalJoinPredicateId>,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct RelationalJoinRewritePlan {
165 pub base_binding: BindingId,
166 pub base_access_path: RelationalJoinAccessPath,
167 pub steps: Vec<RelationalJoinRewriteStep>,
168 pub cost_breakdown: PlanCostBreakdown,
169 pub properties: PhysicalProperties,
170}
171
172impl RelationalJoinRewritePlan {
173 pub fn cost(&self) -> PlanCost {
174 self.cost_breakdown.as_plan_cost()
175 }
176
177 pub fn binding_order(&self) -> Vec<BindingId> {
178 std::iter::once(self.base_binding)
179 .chain(self.steps.iter().map(|step| step.binding))
180 .collect()
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct RelationalJoinRewriteEnumeration {
186 pub plan: RelationalJoinRewritePlan,
187 pub conflict_analysis: RelationalJoinConflictAnalysis,
188 pub memo_groups: usize,
189 pub memo_expressions: usize,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub enum RelationalJoinRewriteError {
194 Enumeration(RelationalJoinEnumerationError),
195 DuplicateTreeBinding(BindingId),
196 DuplicateOperator(RelationalJoinOperatorId),
197 PredicateOutsideOperatorSubtree {
198 operator: RelationalJoinOperatorId,
199 binding: BindingId,
200 },
201 RelationSetMismatch,
202 OperatorCountMismatch {
203 relations: usize,
204 operators: usize,
205 },
206 NoLegalRewrite,
207}
208
209impl fmt::Display for RelationalJoinRewriteError {
210 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Self::Enumeration(error) => error.fmt(formatter),
213 Self::DuplicateTreeBinding(binding) => {
214 write!(formatter, "duplicate tree binding {}", binding.get())
215 }
216 Self::DuplicateOperator(operator) => {
217 write!(formatter, "duplicate join operator {}", operator.get())
218 }
219 Self::PredicateOutsideOperatorSubtree { operator, binding } => write!(
220 formatter,
221 "join operator {} predicate references binding {} outside its subtree",
222 operator.get(),
223 binding.get()
224 ),
225 Self::RelationSetMismatch => write!(
226 formatter,
227 "join rewrite relation list does not match initial tree bindings"
228 ),
229 Self::OperatorCountMismatch {
230 relations,
231 operators,
232 } => write!(
233 formatter,
234 "join tree has {relations} relations but {operators} operators"
235 ),
236 Self::NoLegalRewrite => write!(formatter, "join rewrite has no legal complete plan"),
237 }
238 }
239}
240
241impl Error for RelationalJoinRewriteError {}
242
243impl From<RelationalJoinEnumerationError> for RelationalJoinRewriteError {
244 fn from(error: RelationalJoinEnumerationError) -> Self {
245 Self::Enumeration(error)
246 }
247}
248
249#[derive(Debug, Clone)]
250struct OperatorFacts {
251 kind: RelationalJoinOperatorKind,
252 left_bindings: BindingSet,
253 right_bindings: BindingSet,
254 predicate_bindings: BindingSet,
255}
256
257struct AnalyzedTree {
258 bindings: BindingSet,
259 operators: Vec<OperatorFacts>,
260}
261
262pub fn analyze_relational_join_conflicts(
263 tree: &RelationalJoinTree,
264 post_join_filter: Option<&BoundPredicate>,
265) -> Result<RelationalJoinConflictAnalysis, RelationalJoinRewriteError> {
266 let mut descriptors = BTreeMap::new();
267 let mut seen_bindings = BindingSet::new();
268 let mut seen_operators = BTreeSet::new();
269 let analyzed = analyze_tree(
270 tree,
271 post_join_filter,
272 &mut descriptors,
273 &mut seen_bindings,
274 &mut seen_operators,
275 )?;
276 if analyzed.operators.len() + 1 != analyzed.bindings.len() {
277 return Err(RelationalJoinRewriteError::OperatorCountMismatch {
278 relations: analyzed.bindings.len(),
279 operators: analyzed.operators.len(),
280 });
281 }
282 Ok(RelationalJoinConflictAnalysis {
283 root_bindings: analyzed.bindings,
284 descriptors,
285 })
286}
287
288fn analyze_tree(
289 tree: &RelationalJoinTree,
290 post_join_filter: Option<&BoundPredicate>,
291 descriptors: &mut BTreeMap<RelationalJoinOperatorId, RelationalJoinConflictDescriptor>,
292 seen_bindings: &mut BindingSet,
293 seen_operators: &mut BTreeSet<RelationalJoinOperatorId>,
294) -> Result<AnalyzedTree, RelationalJoinRewriteError> {
295 match tree {
296 RelationalJoinTree::Relation(binding) => {
297 if !seen_bindings.insert(*binding) {
298 return Err(RelationalJoinRewriteError::DuplicateTreeBinding(*binding));
299 }
300 Ok(AnalyzedTree {
301 bindings: (*binding).into(),
302 operators: Vec::new(),
303 })
304 }
305 RelationalJoinTree::Join {
306 operator,
307 left,
308 right,
309 } => {
310 if !seen_operators.insert(operator.id) {
311 return Err(RelationalJoinRewriteError::DuplicateOperator(operator.id));
312 }
313 let left = analyze_tree(
314 left,
315 post_join_filter,
316 descriptors,
317 seen_bindings,
318 seen_operators,
319 )?;
320 let right = analyze_tree(
321 right,
322 post_join_filter,
323 descriptors,
324 seen_bindings,
325 seen_operators,
326 )?;
327 let bindings = binding_union(&left.bindings, &right.bindings);
328 let predicate_bindings = operator.predicate.referenced_bindings();
329 if let Some(binding) = predicate_bindings
330 .iter()
331 .find(|binding| !bindings.contains(*binding))
332 {
333 return Err(
334 RelationalJoinRewriteError::PredicateOutsideOperatorSubtree {
335 operator: operator.id,
336 binding,
337 },
338 );
339 }
340 let effective_kind =
341 effective_operator_kind(operator, &right.bindings, post_join_filter);
342 let mut rules =
343 calculate_conflict_rules(effective_kind, &left.operators, &right.operators);
344 rules.sort_by(|left, right| {
345 left.trigger
346 .cmp(&right.trigger)
347 .then_with(|| left.required.cmp(&right.required))
348 });
349 rules.dedup();
350
351 let left_predicate_bindings = binding_intersection(&predicate_bindings, &left.bindings);
352 let right_predicate_bindings =
353 binding_intersection(&predicate_bindings, &right.bindings);
354 let degenerate =
355 left_predicate_bindings.is_empty() || right_predicate_bindings.is_empty();
356 let initial_total_eligibility = if degenerate {
357 bindings.clone()
358 } else {
359 predicate_bindings.clone()
360 };
361 let (total_eligibility, remaining_rules) =
362 absorb_conflict_rules(initial_total_eligibility, rules);
363 let descriptor = RelationalJoinConflictDescriptor {
364 operator_id: operator.id,
365 declared_kind: operator.kind,
366 effective_kind,
367 initial_left_bindings: left.bindings.clone(),
368 initial_right_bindings: right.bindings.clone(),
369 predicate_bindings: predicate_bindings.clone(),
370 left_total_eligibility: binding_intersection(&total_eligibility, &left.bindings),
371 right_total_eligibility: binding_intersection(&total_eligibility, &right.bindings),
372 conflict_rules: remaining_rules,
373 predicate_ids: operator.predicate_ids.clone(),
374 };
375 descriptors.insert(operator.id, descriptor);
376
377 let mut operators = left.operators;
378 operators.extend(right.operators);
379 operators.push(OperatorFacts {
380 kind: effective_kind,
381 left_bindings: left.bindings,
382 right_bindings: right.bindings,
383 predicate_bindings,
384 });
385 Ok(AnalyzedTree {
386 bindings,
387 operators,
388 })
389 }
390 }
391}
392
393fn effective_operator_kind(
394 operator: &RelationalJoinOperator,
395 right_bindings: &BindingSet,
396 post_join_filter: Option<&BoundPredicate>,
397) -> RelationalJoinOperatorKind {
398 if operator.kind == RelationalJoinOperatorKind::LeftOuter
399 && post_join_filter
400 .is_some_and(|filter| prove_null_rejecting(filter, right_bindings).is_proven())
401 {
402 RelationalJoinOperatorKind::Inner
403 } else {
404 operator.kind
405 }
406}
407
408fn calculate_conflict_rules(
409 current: RelationalJoinOperatorKind,
410 left_operators: &[OperatorFacts],
411 right_operators: &[OperatorFacts],
412) -> Vec<RelationalJoinConflictRule> {
413 let mut rules = Vec::new();
414 for nested in left_operators {
415 if !is_associative(nested.kind, current) {
416 rules.push(RelationalJoinConflictRule {
417 trigger: nested.right_bindings.clone(),
418 required: predicate_side_or_subtree(
419 &nested.left_bindings,
420 &nested.predicate_bindings,
421 ),
422 });
423 }
424 if !is_left_asscom(nested.kind, current) {
425 rules.push(RelationalJoinConflictRule {
426 trigger: nested.left_bindings.clone(),
427 required: predicate_side_or_subtree(
428 &nested.right_bindings,
429 &nested.predicate_bindings,
430 ),
431 });
432 }
433 }
434 for nested in right_operators {
435 if !is_associative(current, nested.kind) {
436 rules.push(RelationalJoinConflictRule {
437 trigger: nested.left_bindings.clone(),
438 required: predicate_side_or_subtree(
439 &nested.right_bindings,
440 &nested.predicate_bindings,
441 ),
442 });
443 }
444 if !is_right_asscom(current, nested.kind) {
445 rules.push(RelationalJoinConflictRule {
446 trigger: nested.right_bindings.clone(),
447 required: predicate_side_or_subtree(
448 &nested.left_bindings,
449 &nested.predicate_bindings,
450 ),
451 });
452 }
453 }
454 rules
455}
456
457fn predicate_side_or_subtree(subtree: &BindingSet, predicate_bindings: &BindingSet) -> BindingSet {
458 let used = binding_intersection(subtree, predicate_bindings);
459 if used.is_empty() {
460 subtree.clone()
461 } else {
462 used
463 }
464}
465
466fn absorb_conflict_rules(
467 mut total_eligibility: BindingSet,
468 mut rules: Vec<RelationalJoinConflictRule>,
469) -> (BindingSet, Vec<RelationalJoinConflictRule>) {
470 loop {
471 let expanded = rules
472 .iter()
473 .filter(|rule| rule.trigger.intersects(&total_eligibility))
474 .fold(total_eligibility.clone(), |bindings, rule| {
475 binding_union(&bindings, &rule.required)
476 });
477 if expanded == total_eligibility {
478 break;
479 }
480 total_eligibility = expanded;
481 }
482 rules.retain(|rule| !rule.required.is_subset(&total_eligibility));
483 (total_eligibility, rules)
484}
485
486const fn is_associative(
487 lower: RelationalJoinOperatorKind,
488 upper: RelationalJoinOperatorKind,
489) -> bool {
490 matches!(lower, RelationalJoinOperatorKind::Inner)
491 && matches!(
492 upper,
493 RelationalJoinOperatorKind::Inner | RelationalJoinOperatorKind::LeftOuter
494 )
495}
496
497const fn is_left_asscom(
498 _lower: RelationalJoinOperatorKind,
499 _upper: RelationalJoinOperatorKind,
500) -> bool {
501 true
502}
503
504const fn is_right_asscom(
505 lower: RelationalJoinOperatorKind,
506 upper: RelationalJoinOperatorKind,
507) -> bool {
508 matches!(lower, RelationalJoinOperatorKind::Inner)
509 && matches!(upper, RelationalJoinOperatorKind::Inner)
510}
511
512pub fn enumerate_relational_join_rewrites(
513 problem: &RelationalJoinRewriteProblem,
514 required_properties: &RequiredProperties,
515 config: RelationalJoinEnumerationConfig,
516) -> Result<RelationalJoinRewriteEnumeration, RelationalJoinRewriteError> {
517 enumerate_relational_join_rewrites_with_cost_contexts(
518 problem,
519 required_properties,
520 config,
521 &crate::RelationalJoinCostContexts::default(),
522 )
523}
524
525pub fn enumerate_relational_join_rewrites_with_cost_contexts(
526 problem: &RelationalJoinRewriteProblem,
527 required_properties: &RequiredProperties,
528 config: RelationalJoinEnumerationConfig,
529 cost_contexts: &crate::RelationalJoinCostContexts,
530) -> Result<RelationalJoinRewriteEnumeration, RelationalJoinRewriteError> {
531 let analysis = analyze_relational_join_conflicts(
532 &problem.initial_tree,
533 problem.post_join_filter.as_ref(),
534 )?;
535 validate_problem_relations(problem, &analysis)?;
536 crate::relational_join_hypergraph::enumerate_left_deep_rewrites(
537 problem,
538 analysis,
539 required_properties,
540 config,
541 cost_contexts,
542 )
543}
544
545pub(crate) fn validate_problem_relations(
546 problem: &RelationalJoinRewriteProblem,
547 analysis: &RelationalJoinConflictAnalysis,
548) -> Result<(), RelationalJoinRewriteError> {
549 if problem.relations.is_empty() {
550 return Err(RelationalJoinEnumerationError::EmptyGraph.into());
551 }
552 let relation_bindings: BindingSet = problem
553 .relations
554 .iter()
555 .map(|relation| relation.binding)
556 .collect();
557 if relation_bindings.len() != problem.relations.len() {
558 let mut seen = BindingSet::new();
559 let duplicate = problem
560 .relations
561 .iter()
562 .map(|relation| relation.binding)
563 .find(|binding| !seen.insert(*binding))
564 .expect("binding count mismatch requires a duplicate");
565 return Err(RelationalJoinEnumerationError::DuplicateBinding(duplicate).into());
566 }
567 if relation_bindings != analysis.root_bindings {
568 return Err(RelationalJoinRewriteError::RelationSetMismatch);
569 }
570 for relation in &problem.relations {
571 if !relation
572 .access_paths
573 .iter()
574 .any(|access| access.supports_base())
575 {
576 return Err(RelationalJoinEnumerationError::MissingBaseAccess(relation.binding).into());
577 }
578 for access in &relation.access_paths {
579 access.descriptor.validate().map_err(|reason| {
580 RelationalJoinEnumerationError::InvalidAccessPath {
581 binding: relation.binding,
582 reason,
583 }
584 })?;
585 access.validate_usage().map_err(|reason| {
586 RelationalJoinEnumerationError::InvalidAccessPath {
587 binding: relation.binding,
588 reason,
589 }
590 })?;
591 if access.required_bindings.contains(relation.binding) {
592 return Err(
593 RelationalJoinEnumerationError::SelfDependentAccess(relation.binding).into(),
594 );
595 }
596 if let Some(binding) = access
597 .required_bindings
598 .iter()
599 .find(|binding| !relation_bindings.contains(*binding))
600 {
601 return Err(RelationalJoinEnumerationError::UnknownAccessBinding {
602 relation: relation.binding,
603 binding,
604 }
605 .into());
606 }
607 if let Some(binding) = access.required_bindings.iter().find(|binding| {
608 !analysis.descriptors.values().any(|descriptor| {
609 descriptor.predicate_bindings.contains(relation.binding)
610 && descriptor.predicate_bindings.contains(*binding)
611 })
612 }) {
613 return Err(RelationalJoinEnumerationError::AccessWithoutPredicate {
614 relation: relation.binding,
615 binding,
616 }
617 .into());
618 }
619 }
620 }
621 Ok(())
622}
623
624fn binding_union(left: &BindingSet, right: &BindingSet) -> BindingSet {
625 left.iter().chain(right.iter()).collect()
626}
627
628fn binding_intersection(left: &BindingSet, right: &BindingSet) -> BindingSet {
629 left.iter()
630 .filter(|binding| right.contains(*binding))
631 .collect()
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::{RelationalAccessPathDescriptor, RelationalAccessPathKind};
638 use hawdb_expression::{BoundScalarExpression, ScalarNullability};
639 use std::collections::BTreeSet;
640
641 const A: BindingId = BindingId::new(1);
642 const B: BindingId = BindingId::new(2);
643 const C: BindingId = BindingId::new(3);
644
645 fn value(binding: BindingId) -> BoundScalarExpression {
646 BoundScalarExpression::BindingValue {
647 binding,
648 nullability: ScalarNullability::MaybeNull,
649 }
650 }
651
652 fn comparison(left: BindingId, right: BindingId) -> BoundPredicate {
653 BoundPredicate::Comparison {
654 left: value(left),
655 right: value(right),
656 }
657 }
658
659 fn operator(
660 id: u32,
661 kind: RelationalJoinOperatorKind,
662 left: BindingId,
663 right: BindingId,
664 ) -> RelationalJoinOperator {
665 RelationalJoinOperator {
666 id: RelationalJoinOperatorId::new(id),
667 kind,
668 predicate_ids: vec![RelationalJoinPredicateId::new(id)],
669 predicate: comparison(left, right),
670 }
671 }
672
673 fn scan(rows: usize) -> RelationalJoinAccessPath {
674 RelationalJoinAccessPath::base_and_probe(RelationalAccessPathDescriptor {
675 kind: RelationalAccessPathKind::FullScan,
676 name: "__full_scan".to_string(),
677 index_columns: Vec::new(),
678 access_columns: BTreeSet::new(),
679 equality_prefix_len: 0,
680 order_prefix_len: 0,
681 exclusive_range: false,
682 reverse_order: false,
683 unique_point: false,
684 covering: false,
685 requires_row_fetch: false,
686 estimated_rows: rows,
687 })
688 }
689
690 fn probe(name: &str, required: BindingId, rows: usize) -> RelationalJoinAccessPath {
691 RelationalJoinAccessPath::probe(
692 RelationalAccessPathDescriptor {
693 kind: RelationalAccessPathKind::Index,
694 name: name.to_string(),
695 index_columns: vec!["id".to_string()],
696 access_columns: BTreeSet::from(["id".to_string()]),
697 equality_prefix_len: 1,
698 order_prefix_len: 0,
699 exclusive_range: false,
700 reverse_order: false,
701 unique_point: rows == 1,
702 covering: false,
703 requires_row_fetch: true,
704 estimated_rows: rows,
705 },
706 required.into(),
707 )
708 }
709
710 fn relation(
711 binding: BindingId,
712 rows: usize,
713 probes: Vec<RelationalJoinAccessPath>,
714 ) -> RelationalJoinRelation {
715 let mut access_paths = vec![scan(rows)];
716 access_paths.extend(probes);
717 RelationalJoinRelation {
718 binding,
719 access_paths,
720 }
721 }
722
723 fn left_then_inner_tree(top_left: BindingId) -> RelationalJoinTree {
724 RelationalJoinTree::join(
725 operator(2, RelationalJoinOperatorKind::Inner, top_left, C),
726 RelationalJoinTree::join(
727 operator(1, RelationalJoinOperatorKind::LeftOuter, A, B),
728 RelationalJoinTree::Relation(A),
729 RelationalJoinTree::Relation(B),
730 ),
731 RelationalJoinTree::Relation(C),
732 )
733 }
734
735 fn problem(
736 tree: RelationalJoinTree,
737 filter: Option<BoundPredicate>,
738 top_left: BindingId,
739 ) -> RelationalJoinRewriteProblem {
740 let mut a_probes = vec![probe("a_by_b", B, 1)];
741 let mut b_probes = vec![probe("b_by_a", A, 1)];
742 let c_probes = if top_left == A {
743 a_probes.push(probe("a_by_c", C, 1));
744 vec![probe("c_by_a", A, 1)]
745 } else {
746 b_probes.push(probe("b_by_c", C, 1));
747 vec![probe("c_by_b", B, 1)]
748 };
749 RelationalJoinRewriteProblem {
750 relations: vec![
751 relation(A, 1_000, a_probes),
752 relation(B, 100, b_probes),
753 relation(C, 1, c_probes),
754 ],
755 initial_tree: tree,
756 post_join_filter: filter,
757 }
758 }
759
760 #[test]
761 fn cdc_blocks_non_associative_left_outer_rewrite_before_memo_insertion() {
762 let problem = problem(left_then_inner_tree(B), None, B);
763 let result = enumerate_relational_join_rewrites(
764 &problem,
765 &RequiredProperties::default(),
766 RelationalJoinEnumerationConfig::default(),
767 )
768 .unwrap();
769
770 assert_eq!(result.plan.binding_order(), [A, B, C]);
771 let top = result
772 .conflict_analysis
773 .descriptor(RelationalJoinOperatorId::new(2))
774 .unwrap();
775 assert_eq!(top.left_total_eligibility, BindingSet::from([A, B]));
776 assert!(top.conflict_rules.is_empty());
777 assert!(!top.is_applicable(&B.into(), &C.into()));
778 }
779
780 #[test]
781 fn cdc_allows_left_asscom_when_the_top_predicate_uses_the_preserved_side() {
782 let problem = problem(left_then_inner_tree(A), None, A);
783 let result = enumerate_relational_join_rewrites(
784 &problem,
785 &RequiredProperties::default(),
786 RelationalJoinEnumerationConfig::default(),
787 )
788 .unwrap();
789
790 assert_eq!(result.plan.binding_order(), [C, A, B]);
791 assert_eq!(
792 result.plan.cost(),
793 PlanCost {
794 estimated_rows: 1,
795 cost: 21,
796 }
797 );
798 assert_eq!(
799 result.plan.steps[1].operator_kind,
800 RelationalJoinOperatorKind::LeftOuter
801 );
802 }
803
804 #[test]
805 fn proven_post_filter_converts_left_outer_before_conflict_detection() {
806 let filter = BoundPredicate::IsNotNull(value(B));
807 let problem = problem(left_then_inner_tree(B), Some(filter), B);
808 let result = enumerate_relational_join_rewrites(
809 &problem,
810 &RequiredProperties::default(),
811 RelationalJoinEnumerationConfig::default(),
812 )
813 .unwrap();
814
815 let lower = result
816 .conflict_analysis
817 .descriptor(RelationalJoinOperatorId::new(1))
818 .unwrap();
819 assert!(lower.was_null_rejection_simplified());
820 assert_eq!(result.plan.binding_order(), [C, B, A]);
821 assert!(result
822 .plan
823 .steps
824 .iter()
825 .all(|step| step.operator_kind == RelationalJoinOperatorKind::Inner));
826 }
827
828 #[test]
829 fn opaque_post_filter_cannot_authorize_left_outer_conversion() {
830 let filter = BoundPredicate::Opaque {
831 referenced_bindings: B.into(),
832 };
833 let problem = problem(left_then_inner_tree(B), Some(filter), B);
834 let result = enumerate_relational_join_rewrites(
835 &problem,
836 &RequiredProperties::default(),
837 RelationalJoinEnumerationConfig::default(),
838 )
839 .unwrap();
840
841 let lower = result
842 .conflict_analysis
843 .descriptor(RelationalJoinOperatorId::new(1))
844 .unwrap();
845 assert!(!lower.was_null_rejection_simplified());
846 assert_eq!(result.plan.binding_order(), [A, B, C]);
847 }
848
849 #[test]
850 fn null_rejection_uses_the_whole_nullable_subtree_binding_set() {
851 let right = RelationalJoinTree::join(
852 operator(1, RelationalJoinOperatorKind::Inner, B, C),
853 RelationalJoinTree::Relation(B),
854 RelationalJoinTree::Relation(C),
855 );
856 let tree = RelationalJoinTree::join(
857 RelationalJoinOperator {
858 id: RelationalJoinOperatorId::new(2),
859 kind: RelationalJoinOperatorKind::LeftOuter,
860 predicate_ids: vec![RelationalJoinPredicateId::new(2)],
861 predicate: comparison(A, B),
862 },
863 RelationalJoinTree::Relation(A),
864 right,
865 );
866 let filter = BoundPredicate::And(vec![
867 BoundPredicate::IsNotNull(value(B)),
868 BoundPredicate::IsNotNull(value(C)),
869 ]);
870 let analysis = analyze_relational_join_conflicts(&tree, Some(&filter)).unwrap();
871
872 let outer = analysis
873 .descriptor(RelationalJoinOperatorId::new(2))
874 .unwrap();
875 assert_eq!(outer.initial_right_bindings, BindingSet::from([B, C]));
876 assert!(outer.was_null_rejection_simplified());
877 }
878}