Skip to main content

hawdb_optimizer_relational/
relational_join_rewrite.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! CD-C legality analysis and memo-backed join rewrite enumeration.
16
17use 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}