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            range: None,
683            reverse_order: false,
684            unique_point: false,
685            covering: false,
686            requires_row_fetch: false,
687            estimated_rows: rows,
688        })
689    }
690
691    fn probe(name: &str, required: BindingId, rows: usize) -> RelationalJoinAccessPath {
692        RelationalJoinAccessPath::probe(
693            RelationalAccessPathDescriptor {
694                kind: RelationalAccessPathKind::Index,
695                name: name.to_string(),
696                index_columns: vec!["id".to_string()],
697                access_columns: BTreeSet::from(["id".to_string()]),
698                equality_prefix_len: 1,
699                order_prefix_len: 0,
700                exclusive_range: false,
701                range: None,
702                reverse_order: false,
703                unique_point: rows == 1,
704                covering: false,
705                requires_row_fetch: true,
706                estimated_rows: rows,
707            },
708            required.into(),
709        )
710    }
711
712    fn relation(
713        binding: BindingId,
714        rows: usize,
715        probes: Vec<RelationalJoinAccessPath>,
716    ) -> RelationalJoinRelation {
717        let mut access_paths = vec![scan(rows)];
718        access_paths.extend(probes);
719        RelationalJoinRelation {
720            binding,
721            access_paths,
722        }
723    }
724
725    fn left_then_inner_tree(top_left: BindingId) -> RelationalJoinTree {
726        RelationalJoinTree::join(
727            operator(2, RelationalJoinOperatorKind::Inner, top_left, C),
728            RelationalJoinTree::join(
729                operator(1, RelationalJoinOperatorKind::LeftOuter, A, B),
730                RelationalJoinTree::Relation(A),
731                RelationalJoinTree::Relation(B),
732            ),
733            RelationalJoinTree::Relation(C),
734        )
735    }
736
737    fn problem(
738        tree: RelationalJoinTree,
739        filter: Option<BoundPredicate>,
740        top_left: BindingId,
741    ) -> RelationalJoinRewriteProblem {
742        let mut a_probes = vec![probe("a_by_b", B, 1)];
743        let mut b_probes = vec![probe("b_by_a", A, 1)];
744        let c_probes = if top_left == A {
745            a_probes.push(probe("a_by_c", C, 1));
746            vec![probe("c_by_a", A, 1)]
747        } else {
748            b_probes.push(probe("b_by_c", C, 1));
749            vec![probe("c_by_b", B, 1)]
750        };
751        RelationalJoinRewriteProblem {
752            relations: vec![
753                relation(A, 1_000, a_probes),
754                relation(B, 100, b_probes),
755                relation(C, 1, c_probes),
756            ],
757            initial_tree: tree,
758            post_join_filter: filter,
759        }
760    }
761
762    #[test]
763    fn cdc_blocks_non_associative_left_outer_rewrite_before_memo_insertion() {
764        let problem = problem(left_then_inner_tree(B), None, B);
765        let result = enumerate_relational_join_rewrites(
766            &problem,
767            &RequiredProperties::default(),
768            RelationalJoinEnumerationConfig::default(),
769        )
770        .unwrap();
771
772        assert_eq!(result.plan.binding_order(), [A, B, C]);
773        let top = result
774            .conflict_analysis
775            .descriptor(RelationalJoinOperatorId::new(2))
776            .unwrap();
777        assert_eq!(top.left_total_eligibility, BindingSet::from([A, B]));
778        assert!(top.conflict_rules.is_empty());
779        assert!(!top.is_applicable(&B.into(), &C.into()));
780    }
781
782    #[test]
783    fn cdc_allows_left_asscom_when_the_top_predicate_uses_the_preserved_side() {
784        let problem = problem(left_then_inner_tree(A), None, A);
785        let result = enumerate_relational_join_rewrites(
786            &problem,
787            &RequiredProperties::default(),
788            RelationalJoinEnumerationConfig::default(),
789        )
790        .unwrap();
791
792        assert_eq!(result.plan.binding_order(), [C, A, B]);
793        assert_eq!(
794            result.plan.cost(),
795            PlanCost {
796                estimated_rows: 1,
797                cost: 21,
798            }
799        );
800        assert_eq!(
801            result.plan.steps[1].operator_kind,
802            RelationalJoinOperatorKind::LeftOuter
803        );
804    }
805
806    #[test]
807    fn proven_post_filter_converts_left_outer_before_conflict_detection() {
808        let filter = BoundPredicate::IsNotNull(value(B));
809        let problem = problem(left_then_inner_tree(B), Some(filter), B);
810        let result = enumerate_relational_join_rewrites(
811            &problem,
812            &RequiredProperties::default(),
813            RelationalJoinEnumerationConfig::default(),
814        )
815        .unwrap();
816
817        let lower = result
818            .conflict_analysis
819            .descriptor(RelationalJoinOperatorId::new(1))
820            .unwrap();
821        assert!(lower.was_null_rejection_simplified());
822        assert_eq!(result.plan.binding_order(), [C, B, A]);
823        assert!(result
824            .plan
825            .steps
826            .iter()
827            .all(|step| step.operator_kind == RelationalJoinOperatorKind::Inner));
828    }
829
830    #[test]
831    fn opaque_post_filter_cannot_authorize_left_outer_conversion() {
832        let filter = BoundPredicate::Opaque {
833            referenced_bindings: B.into(),
834        };
835        let problem = problem(left_then_inner_tree(B), Some(filter), B);
836        let result = enumerate_relational_join_rewrites(
837            &problem,
838            &RequiredProperties::default(),
839            RelationalJoinEnumerationConfig::default(),
840        )
841        .unwrap();
842
843        let lower = result
844            .conflict_analysis
845            .descriptor(RelationalJoinOperatorId::new(1))
846            .unwrap();
847        assert!(!lower.was_null_rejection_simplified());
848        assert_eq!(result.plan.binding_order(), [A, B, C]);
849    }
850
851    #[test]
852    fn null_rejection_uses_the_whole_nullable_subtree_binding_set() {
853        let right = RelationalJoinTree::join(
854            operator(1, RelationalJoinOperatorKind::Inner, B, C),
855            RelationalJoinTree::Relation(B),
856            RelationalJoinTree::Relation(C),
857        );
858        let tree = RelationalJoinTree::join(
859            RelationalJoinOperator {
860                id: RelationalJoinOperatorId::new(2),
861                kind: RelationalJoinOperatorKind::LeftOuter,
862                predicate_ids: vec![RelationalJoinPredicateId::new(2)],
863                predicate: comparison(A, B),
864            },
865            RelationalJoinTree::Relation(A),
866            right,
867        );
868        let filter = BoundPredicate::And(vec![
869            BoundPredicate::IsNotNull(value(B)),
870            BoundPredicate::IsNotNull(value(C)),
871        ]);
872        let analysis = analyze_relational_join_conflicts(&tree, Some(&filter)).unwrap();
873
874        let outer = analysis
875            .descriptor(RelationalJoinOperatorId::new(2))
876            .unwrap();
877        assert_eq!(outer.initial_right_bindings, BindingSet::from([B, C]));
878        assert!(outer.was_null_rejection_simplified());
879    }
880}