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 let analysis = analyze_relational_join_conflicts(
518 &problem.initial_tree,
519 problem.post_join_filter.as_ref(),
520 )?;
521 validate_problem_relations(problem, &analysis)?;
522 crate::relational_join_hypergraph::enumerate_left_deep_rewrites(
523 problem,
524 analysis,
525 required_properties,
526 config,
527 )
528}
529
530pub(crate) fn validate_problem_relations(
531 problem: &RelationalJoinRewriteProblem,
532 analysis: &RelationalJoinConflictAnalysis,
533) -> Result<(), RelationalJoinRewriteError> {
534 if problem.relations.is_empty() {
535 return Err(RelationalJoinEnumerationError::EmptyGraph.into());
536 }
537 let relation_bindings: BindingSet = problem
538 .relations
539 .iter()
540 .map(|relation| relation.binding)
541 .collect();
542 if relation_bindings.len() != problem.relations.len() {
543 let mut seen = BindingSet::new();
544 let duplicate = problem
545 .relations
546 .iter()
547 .map(|relation| relation.binding)
548 .find(|binding| !seen.insert(*binding))
549 .expect("binding count mismatch requires a duplicate");
550 return Err(RelationalJoinEnumerationError::DuplicateBinding(duplicate).into());
551 }
552 if relation_bindings != analysis.root_bindings {
553 return Err(RelationalJoinRewriteError::RelationSetMismatch);
554 }
555 for relation in &problem.relations {
556 if !relation
557 .access_paths
558 .iter()
559 .any(|access| access.supports_base())
560 {
561 return Err(RelationalJoinEnumerationError::MissingBaseAccess(relation.binding).into());
562 }
563 for access in &relation.access_paths {
564 access.descriptor.validate().map_err(|reason| {
565 RelationalJoinEnumerationError::InvalidAccessPath {
566 binding: relation.binding,
567 reason,
568 }
569 })?;
570 access.validate_usage().map_err(|reason| {
571 RelationalJoinEnumerationError::InvalidAccessPath {
572 binding: relation.binding,
573 reason,
574 }
575 })?;
576 if access.required_bindings.contains(relation.binding) {
577 return Err(
578 RelationalJoinEnumerationError::SelfDependentAccess(relation.binding).into(),
579 );
580 }
581 if let Some(binding) = access
582 .required_bindings
583 .iter()
584 .find(|binding| !relation_bindings.contains(*binding))
585 {
586 return Err(RelationalJoinEnumerationError::UnknownAccessBinding {
587 relation: relation.binding,
588 binding,
589 }
590 .into());
591 }
592 if let Some(binding) = access.required_bindings.iter().find(|binding| {
593 !analysis.descriptors.values().any(|descriptor| {
594 descriptor.predicate_bindings.contains(relation.binding)
595 && descriptor.predicate_bindings.contains(*binding)
596 })
597 }) {
598 return Err(RelationalJoinEnumerationError::AccessWithoutPredicate {
599 relation: relation.binding,
600 binding,
601 }
602 .into());
603 }
604 }
605 }
606 Ok(())
607}
608
609fn binding_union(left: &BindingSet, right: &BindingSet) -> BindingSet {
610 left.iter().chain(right.iter()).collect()
611}
612
613fn binding_intersection(left: &BindingSet, right: &BindingSet) -> BindingSet {
614 left.iter()
615 .filter(|binding| right.contains(*binding))
616 .collect()
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622 use crate::{RelationalAccessPathDescriptor, RelationalAccessPathKind};
623 use hawdb_expression::{BoundScalarExpression, ScalarNullability};
624 use std::collections::BTreeSet;
625
626 const A: BindingId = BindingId::new(1);
627 const B: BindingId = BindingId::new(2);
628 const C: BindingId = BindingId::new(3);
629
630 fn value(binding: BindingId) -> BoundScalarExpression {
631 BoundScalarExpression::BindingValue {
632 binding,
633 nullability: ScalarNullability::MaybeNull,
634 }
635 }
636
637 fn comparison(left: BindingId, right: BindingId) -> BoundPredicate {
638 BoundPredicate::Comparison {
639 left: value(left),
640 right: value(right),
641 }
642 }
643
644 fn operator(
645 id: u32,
646 kind: RelationalJoinOperatorKind,
647 left: BindingId,
648 right: BindingId,
649 ) -> RelationalJoinOperator {
650 RelationalJoinOperator {
651 id: RelationalJoinOperatorId::new(id),
652 kind,
653 predicate_ids: vec![RelationalJoinPredicateId::new(id)],
654 predicate: comparison(left, right),
655 }
656 }
657
658 fn scan(rows: usize) -> RelationalJoinAccessPath {
659 RelationalJoinAccessPath::base_and_probe(RelationalAccessPathDescriptor {
660 kind: RelationalAccessPathKind::FullScan,
661 name: "__full_scan".to_string(),
662 index_columns: Vec::new(),
663 access_columns: BTreeSet::new(),
664 equality_prefix_len: 0,
665 order_prefix_len: 0,
666 exclusive_range: false,
667 reverse_order: false,
668 unique_point: false,
669 covering: false,
670 requires_row_fetch: false,
671 estimated_rows: rows,
672 })
673 }
674
675 fn probe(name: &str, required: BindingId, rows: usize) -> RelationalJoinAccessPath {
676 RelationalJoinAccessPath::probe(
677 RelationalAccessPathDescriptor {
678 kind: RelationalAccessPathKind::Index,
679 name: name.to_string(),
680 index_columns: vec!["id".to_string()],
681 access_columns: BTreeSet::from(["id".to_string()]),
682 equality_prefix_len: 1,
683 order_prefix_len: 0,
684 exclusive_range: false,
685 reverse_order: false,
686 unique_point: rows == 1,
687 covering: false,
688 requires_row_fetch: true,
689 estimated_rows: rows,
690 },
691 required.into(),
692 )
693 }
694
695 fn relation(
696 binding: BindingId,
697 rows: usize,
698 probes: Vec<RelationalJoinAccessPath>,
699 ) -> RelationalJoinRelation {
700 let mut access_paths = vec![scan(rows)];
701 access_paths.extend(probes);
702 RelationalJoinRelation {
703 binding,
704 access_paths,
705 }
706 }
707
708 fn left_then_inner_tree(top_left: BindingId) -> RelationalJoinTree {
709 RelationalJoinTree::join(
710 operator(2, RelationalJoinOperatorKind::Inner, top_left, C),
711 RelationalJoinTree::join(
712 operator(1, RelationalJoinOperatorKind::LeftOuter, A, B),
713 RelationalJoinTree::Relation(A),
714 RelationalJoinTree::Relation(B),
715 ),
716 RelationalJoinTree::Relation(C),
717 )
718 }
719
720 fn problem(
721 tree: RelationalJoinTree,
722 filter: Option<BoundPredicate>,
723 top_left: BindingId,
724 ) -> RelationalJoinRewriteProblem {
725 let mut a_probes = vec![probe("a_by_b", B, 1)];
726 let mut b_probes = vec![probe("b_by_a", A, 1)];
727 let c_probes = if top_left == A {
728 a_probes.push(probe("a_by_c", C, 1));
729 vec![probe("c_by_a", A, 1)]
730 } else {
731 b_probes.push(probe("b_by_c", C, 1));
732 vec![probe("c_by_b", B, 1)]
733 };
734 RelationalJoinRewriteProblem {
735 relations: vec![
736 relation(A, 1_000, a_probes),
737 relation(B, 100, b_probes),
738 relation(C, 1, c_probes),
739 ],
740 initial_tree: tree,
741 post_join_filter: filter,
742 }
743 }
744
745 #[test]
746 fn cdc_blocks_non_associative_left_outer_rewrite_before_memo_insertion() {
747 let problem = problem(left_then_inner_tree(B), None, B);
748 let result = enumerate_relational_join_rewrites(
749 &problem,
750 &RequiredProperties::default(),
751 RelationalJoinEnumerationConfig::default(),
752 )
753 .unwrap();
754
755 assert_eq!(result.plan.binding_order(), [A, B, C]);
756 let top = result
757 .conflict_analysis
758 .descriptor(RelationalJoinOperatorId::new(2))
759 .unwrap();
760 assert_eq!(top.left_total_eligibility, BindingSet::from([A, B]));
761 assert!(top.conflict_rules.is_empty());
762 assert!(!top.is_applicable(&B.into(), &C.into()));
763 }
764
765 #[test]
766 fn cdc_allows_left_asscom_when_the_top_predicate_uses_the_preserved_side() {
767 let problem = problem(left_then_inner_tree(A), None, A);
768 let result = enumerate_relational_join_rewrites(
769 &problem,
770 &RequiredProperties::default(),
771 RelationalJoinEnumerationConfig::default(),
772 )
773 .unwrap();
774
775 assert_eq!(result.plan.binding_order(), [C, A, B]);
776 assert_eq!(
777 result.plan.cost(),
778 PlanCost {
779 estimated_rows: 1,
780 cost: 21,
781 }
782 );
783 assert_eq!(
784 result.plan.steps[1].operator_kind,
785 RelationalJoinOperatorKind::LeftOuter
786 );
787 }
788
789 #[test]
790 fn proven_post_filter_converts_left_outer_before_conflict_detection() {
791 let filter = BoundPredicate::IsNotNull(value(B));
792 let problem = problem(left_then_inner_tree(B), Some(filter), B);
793 let result = enumerate_relational_join_rewrites(
794 &problem,
795 &RequiredProperties::default(),
796 RelationalJoinEnumerationConfig::default(),
797 )
798 .unwrap();
799
800 let lower = result
801 .conflict_analysis
802 .descriptor(RelationalJoinOperatorId::new(1))
803 .unwrap();
804 assert!(lower.was_null_rejection_simplified());
805 assert_eq!(result.plan.binding_order(), [C, B, A]);
806 assert!(result
807 .plan
808 .steps
809 .iter()
810 .all(|step| step.operator_kind == RelationalJoinOperatorKind::Inner));
811 }
812
813 #[test]
814 fn opaque_post_filter_cannot_authorize_left_outer_conversion() {
815 let filter = BoundPredicate::Opaque {
816 referenced_bindings: B.into(),
817 };
818 let problem = problem(left_then_inner_tree(B), Some(filter), B);
819 let result = enumerate_relational_join_rewrites(
820 &problem,
821 &RequiredProperties::default(),
822 RelationalJoinEnumerationConfig::default(),
823 )
824 .unwrap();
825
826 let lower = result
827 .conflict_analysis
828 .descriptor(RelationalJoinOperatorId::new(1))
829 .unwrap();
830 assert!(!lower.was_null_rejection_simplified());
831 assert_eq!(result.plan.binding_order(), [A, B, C]);
832 }
833
834 #[test]
835 fn null_rejection_uses_the_whole_nullable_subtree_binding_set() {
836 let right = RelationalJoinTree::join(
837 operator(1, RelationalJoinOperatorKind::Inner, B, C),
838 RelationalJoinTree::Relation(B),
839 RelationalJoinTree::Relation(C),
840 );
841 let tree = RelationalJoinTree::join(
842 RelationalJoinOperator {
843 id: RelationalJoinOperatorId::new(2),
844 kind: RelationalJoinOperatorKind::LeftOuter,
845 predicate_ids: vec![RelationalJoinPredicateId::new(2)],
846 predicate: comparison(A, B),
847 },
848 RelationalJoinTree::Relation(A),
849 right,
850 );
851 let filter = BoundPredicate::And(vec![
852 BoundPredicate::IsNotNull(value(B)),
853 BoundPredicate::IsNotNull(value(C)),
854 ]);
855 let analysis = analyze_relational_join_conflicts(&tree, Some(&filter)).unwrap();
856
857 let outer = analysis
858 .descriptor(RelationalJoinOperatorId::new(2))
859 .unwrap();
860 assert_eq!(outer.initial_right_bindings, BindingSet::from([B, C]));
861 assert!(outer.was_null_rejection_simplified());
862 }
863}