Skip to main content

risingwave_frontend/optimizer/plan_node/
logical_join.rs

1// Copyright 2022 RisingWave Labs
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
15use std::collections::HashMap;
16use std::ops::Deref;
17
18use fixedbitset::FixedBitSet;
19use itertools::{EitherOrBoth, Itertools};
20use pretty_xmlish::{Pretty, XmlNode};
21use risingwave_common::catalog::ColumnId;
22use risingwave_expr::bail;
23use risingwave_pb::expr::expr_node::PbType;
24use risingwave_pb::plan_common::{AsOfJoinDesc, JoinType, PbAsOfJoinInequalityType};
25use risingwave_sqlparser::ast::AsOf;
26
27use super::generic::{
28    GenericPlanNode, GenericPlanRef, push_down_into_join, push_down_join_condition,
29};
30use super::utils::{Distill, childless_record};
31use super::{
32    BackfillType, BatchPlanRef, ColPrunable, ExprRewritable, Logical, LogicalPlanRef as PlanRef,
33    PlanBase, PlanTreeNodeBinary, PredicatePushdown, StreamHashJoin, StreamPlanRef, StreamProject,
34    ToBatch, ToStream, generic, try_enforce_locality_requirement,
35};
36use crate::error::{ErrorCode, Result, RwError};
37use crate::expr::{CollectInputRef, Expr, ExprImpl, ExprRewriter, ExprType, ExprVisitor, InputRef};
38use crate::optimizer::plan_node::expr_visitable::ExprVisitable;
39use crate::optimizer::plan_node::generic::DynamicFilter;
40use crate::optimizer::plan_node::stream_asof_join::StreamAsOfJoin;
41use crate::optimizer::plan_node::utils::IndicesDisplay;
42use crate::optimizer::plan_node::{
43    BatchHashJoin, BatchLookupJoin, BatchNestedLoopJoin, ColumnPruningContext, EqJoinPredicate,
44    LogicalFilter, LogicalScan, PredicatePushdownContext, RewriteStreamContext,
45    StreamDynamicFilter, StreamFilter, StreamTableScan, StreamTemporalJoin, ToStreamContext,
46};
47use crate::optimizer::plan_visitor::LogicalCardinalityExt;
48use crate::optimizer::property::{Distribution, RequiredDist};
49use crate::utils::{ColIndexMapping, ColIndexMappingRewriteExt, Condition, ConditionDisplay};
50
51/// `LogicalJoin` combines two relations according to some condition.
52///
53/// Each output row has fields from the left and right inputs. The set of output rows is a subset
54/// of the cartesian product of the two inputs; precisely which subset depends on the join
55/// condition. In addition, the output columns are a subset of the columns of the left and
56/// right columns, dependent on the output indices provided. A repeat output index is illegal.
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct LogicalJoin {
59    pub base: PlanBase<Logical>,
60    core: generic::Join<PlanRef>,
61}
62
63impl Distill for LogicalJoin {
64    fn distill<'a>(&self) -> XmlNode<'a> {
65        let verbose = self.base.ctx().is_explain_verbose();
66        let mut vec = Vec::with_capacity(if verbose { 3 } else { 2 });
67        vec.push(("type", Pretty::debug(&self.join_type())));
68
69        let concat_schema = self.core.concat_schema();
70        let cond = Pretty::debug(&ConditionDisplay {
71            condition: self.on(),
72            input_schema: &concat_schema,
73        });
74        vec.push(("on", cond));
75
76        if verbose {
77            let data = IndicesDisplay::from_join(&self.core, &concat_schema);
78            vec.push(("output", data));
79        }
80
81        childless_record("LogicalJoin", vec)
82    }
83}
84
85impl LogicalJoin {
86    pub(crate) fn new(left: PlanRef, right: PlanRef, join_type: JoinType, on: Condition) -> Self {
87        let core = generic::Join::with_full_output(left, right, join_type, on);
88        Self::with_core(core)
89    }
90
91    pub(crate) fn with_output_indices(
92        left: PlanRef,
93        right: PlanRef,
94        join_type: JoinType,
95        on: Condition,
96        output_indices: Vec<usize>,
97    ) -> Self {
98        let core = generic::Join::new(left, right, on, join_type, output_indices);
99        Self::with_core(core)
100    }
101
102    pub fn with_core(core: generic::Join<PlanRef>) -> Self {
103        let base = PlanBase::new_logical_with_core(&core);
104        LogicalJoin { base, core }
105    }
106
107    pub fn create(
108        left: PlanRef,
109        right: PlanRef,
110        join_type: JoinType,
111        on_clause: ExprImpl,
112    ) -> PlanRef {
113        Self::new(left, right, join_type, Condition::with_expr(on_clause)).into()
114    }
115
116    pub fn internal_column_num(&self) -> usize {
117        self.core.internal_column_num()
118    }
119
120    pub fn i2l_col_mapping_ignore_join_type(&self) -> ColIndexMapping {
121        self.core.i2l_col_mapping_ignore_join_type()
122    }
123
124    pub fn i2r_col_mapping_ignore_join_type(&self) -> ColIndexMapping {
125        self.core.i2r_col_mapping_ignore_join_type()
126    }
127
128    /// Get a reference to the logical join's on.
129    pub fn on(&self) -> &Condition {
130        self.core
131            .on
132            .as_condition_ref()
133            .expect("logical join should store predicate as Condition")
134    }
135
136    pub fn core(&self) -> &generic::Join<PlanRef> {
137        &self.core
138    }
139
140    /// Collect all input ref in the on condition. And separate them into left and right.
141    pub fn input_idx_on_condition(&self) -> (Vec<usize>, Vec<usize>) {
142        let input_refs = self
143            .core
144            .on
145            .as_condition_ref()
146            .expect("logical join should store predicate as Condition")
147            .collect_input_refs(self.core.left.schema().len() + self.core.right.schema().len());
148        let index_group = input_refs
149            .ones()
150            .chunk_by(|i| *i < self.core.left.schema().len());
151        let left_index = index_group
152            .into_iter()
153            .next()
154            .map_or(vec![], |group| group.1.collect_vec());
155        let right_index = index_group.into_iter().next().map_or(vec![], |group| {
156            group
157                .1
158                .map(|i| i - self.core.left.schema().len())
159                .collect_vec()
160        });
161        (left_index, right_index)
162    }
163
164    /// Get the join type of the logical join.
165    pub fn join_type(&self) -> JoinType {
166        self.core.join_type
167    }
168
169    /// Get the eq join key of the logical join.
170    pub fn eq_indexes(&self) -> Vec<(usize, usize)> {
171        self.core.eq_indexes()
172    }
173
174    /// Get the output indices of the logical join.
175    pub fn output_indices(&self) -> &Vec<usize> {
176        &self.core.output_indices
177    }
178
179    /// Clone with new output indices
180    pub fn clone_with_output_indices(&self, output_indices: Vec<usize>) -> Self {
181        Self::with_core(generic::Join {
182            output_indices,
183            ..self.core.clone()
184        })
185    }
186
187    /// Clone with new `on` condition
188    pub fn clone_with_cond(&self, on: Condition) -> Self {
189        Self::with_core(generic::Join {
190            on: generic::JoinOn::Condition(on),
191            ..self.core.clone()
192        })
193    }
194
195    pub fn is_left_join(&self) -> bool {
196        matches!(self.join_type(), JoinType::LeftSemi | JoinType::LeftAnti)
197    }
198
199    pub fn is_right_join(&self) -> bool {
200        matches!(self.join_type(), JoinType::RightSemi | JoinType::RightAnti)
201    }
202
203    pub fn is_full_out(&self) -> bool {
204        self.core.is_full_out()
205    }
206
207    pub fn is_asof_join(&self) -> bool {
208        self.join_type() == JoinType::AsofInner || self.join_type() == JoinType::AsofLeftOuter
209    }
210
211    pub fn output_indices_are_trivial(&self) -> bool {
212        itertools::equal(
213            self.output_indices().iter().cloned(),
214            0..self.internal_column_num(),
215        )
216    }
217
218    /// Try to simplify the outer join with the predicate on the top of the join
219    ///
220    /// now it is just a naive implementation for comparison expression, we can give a more general
221    /// implementation with constant folding in future
222    fn simplify_outer(predicate: &Condition, left_col_num: usize, join_type: JoinType) -> JoinType {
223        let (mut gen_null_in_left, mut gen_null_in_right) = match join_type {
224            JoinType::LeftOuter => (false, true),
225            JoinType::RightOuter => (true, false),
226            JoinType::FullOuter => (true, true),
227            _ => return join_type,
228        };
229
230        for expr in &predicate.conjunctions {
231            if let ExprImpl::FunctionCall(func) = expr {
232                match func.func_type() {
233                    ExprType::Equal
234                    | ExprType::NotEqual
235                    | ExprType::LessThan
236                    | ExprType::LessThanOrEqual
237                    | ExprType::GreaterThan
238                    | ExprType::GreaterThanOrEqual => {
239                        for input in func.inputs() {
240                            if let ExprImpl::InputRef(input) = input {
241                                let idx = input.index;
242                                if idx < left_col_num {
243                                    gen_null_in_left = false;
244                                } else {
245                                    gen_null_in_right = false;
246                                }
247                            }
248                        }
249                    }
250                    _ => {}
251                };
252            }
253        }
254
255        match (gen_null_in_left, gen_null_in_right) {
256            (true, true) => JoinType::FullOuter,
257            (true, false) => JoinType::RightOuter,
258            (false, true) => JoinType::LeftOuter,
259            (false, false) => JoinType::Inner,
260        }
261    }
262
263    /// Index Join:
264    /// Try to convert logical join into batch lookup join and meanwhile it will do
265    /// the index selection for the lookup table so that we can benefit from indexes.
266    fn to_batch_lookup_join_with_index_selection(
267        &self,
268        predicate: EqJoinPredicate,
269        batch_join: generic::Join<BatchPlanRef>,
270    ) -> Result<Option<BatchLookupJoin>> {
271        match batch_join.join_type {
272            JoinType::Inner
273            | JoinType::LeftOuter
274            | JoinType::LeftSemi
275            | JoinType::LeftAnti
276            | JoinType::AsofInner
277            | JoinType::AsofLeftOuter => {}
278            _ => return Ok(None),
279        };
280
281        // Index selection for index join.
282        let right = self.right();
283        // Lookup Join only supports basic tables on the join's right side.
284        let logical_scan: &LogicalScan = if let Some(logical_scan) = right.as_logical_scan() {
285            logical_scan
286        } else {
287            return Ok(None);
288        };
289
290        let mut result_plan = None;
291        // Lookup primary table.
292        if let Some(lookup_join) =
293            self.to_batch_lookup_join(predicate.clone(), batch_join.clone())?
294        {
295            result_plan = Some(lookup_join);
296        }
297
298        if self
299            .core
300            .ctx()
301            .session_ctx()
302            .config()
303            .enable_index_selection()
304        {
305            let indexes = logical_scan.table_indexes();
306            for index in indexes {
307                if let Some(index_scan) = logical_scan.to_index_scan_if_index_covered(index) {
308                    let index_scan: PlanRef = index_scan.into();
309                    let that = self.clone_with_left_right(self.left(), index_scan.clone());
310                    let mut new_batch_join = batch_join.clone();
311                    new_batch_join.right =
312                        index_scan.to_batch().expect("index scan failed to batch");
313
314                    // Lookup covered index.
315                    if let Some(lookup_join) =
316                        that.to_batch_lookup_join(predicate.clone(), new_batch_join)?
317                    {
318                        match &result_plan {
319                            None => result_plan = Some(lookup_join),
320                            Some(prev_lookup_join) => {
321                                // Prefer to choose lookup join with longer lookup prefix len.
322                                if prev_lookup_join.lookup_prefix_len()
323                                    < lookup_join.lookup_prefix_len()
324                                {
325                                    result_plan = Some(lookup_join)
326                                }
327                            }
328                        }
329                    }
330                }
331            }
332        }
333
334        Ok(result_plan)
335    }
336
337    /// Try to convert logical join into batch lookup join.
338    fn to_batch_lookup_join(
339        &self,
340        predicate: EqJoinPredicate,
341        logical_join: generic::Join<BatchPlanRef>,
342    ) -> Result<Option<BatchLookupJoin>> {
343        let logical_scan: &LogicalScan =
344            if let Some(logical_scan) = self.core.right.as_logical_scan() {
345                logical_scan
346            } else {
347                return Ok(None);
348            };
349        Self::gen_batch_lookup_join(logical_scan, predicate, logical_join, self.is_asof_join())
350    }
351
352    /// Decides the lookup prefix, as a reordering of the eq keys, by matching the lookup
353    /// table's order-key columns. The executor binds eq keys to the order key positionally,
354    /// so a repeated order-key column (e.g. an MV with `ORDER BY a, a`) ends the prefix.
355    fn lookup_prefix_reorder_idx(
356        predicate: &EqJoinPredicate,
357        output_column_ids: &[ColumnId],
358        order_col_ids: &[ColumnId],
359    ) -> Vec<usize> {
360        let mut reorder_idx = Vec::with_capacity(order_col_ids.len());
361        for order_col_id in order_col_ids {
362            let mut found = false;
363            for (i, eq_idx) in predicate.right_eq_indexes().into_iter().enumerate() {
364                if *order_col_id == output_column_ids[eq_idx] {
365                    if !reorder_idx.contains(&i) {
366                        reorder_idx.push(i);
367                        found = true;
368                    }
369                    break;
370                }
371            }
372            if !found {
373                break;
374            }
375        }
376        reorder_idx
377    }
378
379    pub fn gen_batch_lookup_join(
380        logical_scan: &LogicalScan,
381        predicate: EqJoinPredicate,
382        logical_join: generic::Join<BatchPlanRef>,
383        is_as_of: bool,
384    ) -> Result<Option<BatchLookupJoin>> {
385        match logical_join.join_type {
386            JoinType::Inner
387            | JoinType::LeftOuter
388            | JoinType::LeftSemi
389            | JoinType::LeftAnti
390            | JoinType::AsofInner
391            | JoinType::AsofLeftOuter => {}
392            _ => return Ok(None),
393        };
394
395        let table = logical_scan.table();
396        let output_column_ids = logical_scan.output_column_ids();
397
398        // Verify that the right join key columns are the the prefix of the primary key and
399        // also contain the distribution key.
400        let order_col_ids = table.order_column_ids();
401        let dist_key = table.distribution_key.clone();
402        // The at least prefix of order key that contains distribution key.
403        let mut dist_key_in_order_key_pos = vec![];
404        for d in dist_key {
405            let pos = table
406                .order_column_indices()
407                .position(|x| x == d)
408                .expect("dist_key must in order_key");
409            dist_key_in_order_key_pos.push(pos);
410        }
411        // The shortest prefix of order key that contains distribution key.
412        //
413        // If the lookup table has a singleton distribution (i.e. an empty distribution key),
414        // any non-empty prefix of the order key can be used for the lookup, so we require a
415        // prefix of length at least 1.
416        let shortest_prefix_len = dist_key_in_order_key_pos
417            .iter()
418            .max()
419            .map_or(1, |pos| pos + 1);
420
421        // Reorder the join equal predicate to match the order key.
422        let reorder_idx =
423            Self::lookup_prefix_reorder_idx(&predicate, &output_column_ids, &order_col_ids);
424        if reorder_idx.len() < shortest_prefix_len {
425            return Ok(None);
426        }
427        let lookup_prefix_len = reorder_idx.len();
428        let predicate = predicate.reorder(&reorder_idx);
429
430        // Extract the predicate from logical scan. Only pure scan is supported.
431        let (new_scan, scan_predicate, project_expr) = logical_scan.predicate_pull_up();
432        // Construct output column to require column mapping
433        let o2r = if let Some(project_expr) = project_expr {
434            project_expr
435                .into_iter()
436                .map(|x| x.as_input_ref().unwrap().index)
437                .collect_vec()
438        } else {
439            (0..logical_scan.output_col_idx().len()).collect_vec()
440        };
441        let left_schema_len = logical_join.left.schema().len();
442
443        let mut join_predicate_rewriter = LookupJoinPredicateRewriter {
444            offset: left_schema_len,
445            mapping: o2r.clone(),
446        };
447
448        let new_eq_cond = predicate
449            .eq_cond()
450            .rewrite_expr(&mut join_predicate_rewriter);
451
452        let mut scan_predicate_rewriter = LookupJoinScanPredicateRewriter {
453            offset: left_schema_len,
454        };
455
456        let new_other_cond = predicate
457            .other_cond()
458            .clone()
459            .rewrite_expr(&mut join_predicate_rewriter)
460            .and(scan_predicate.rewrite_expr(&mut scan_predicate_rewriter));
461
462        let new_join_on = new_eq_cond.and(new_other_cond);
463        let new_predicate =
464            EqJoinPredicate::create(left_schema_len, new_scan.schema().len(), new_join_on);
465
466        // We discovered that we cannot use a lookup join after pulling up the predicate
467        // from one side and simplifying the condition. Let's use some other join instead.
468        if !new_predicate.has_eq() {
469            return Ok(None);
470        }
471
472        // Rewrite the join output indices and all output indices referred to the old scan need to
473        // rewrite.
474        let new_join_output_indices = logical_join
475            .output_indices
476            .iter()
477            .map(|&x| {
478                if x < left_schema_len {
479                    x
480                } else {
481                    o2r[x - left_schema_len] + left_schema_len
482                }
483            })
484            .collect_vec();
485
486        let new_scan_output_column_ids = new_scan.output_column_ids();
487        let as_of = new_scan.as_of.clone();
488        let new_logical_scan: LogicalScan = new_scan.into();
489
490        // Construct a new logical join, because we have change its RHS.
491        let new_logical_join = generic::Join::new_with_eq_predicate(
492            logical_join.left,
493            new_logical_scan.to_batch()?,
494            new_predicate,
495            logical_join.join_type,
496            new_join_output_indices,
497        );
498
499        let asof_desc = is_as_of
500            .then(|| {
501                Self::get_inequality_desc_from_predicate(
502                    predicate.other_cond().clone(),
503                    left_schema_len,
504                )
505            })
506            .transpose()?;
507
508        Ok(Some(BatchLookupJoin::new(
509            new_logical_join,
510            table.clone(),
511            new_scan_output_column_ids,
512            lookup_prefix_len,
513            false,
514            as_of,
515            asof_desc,
516        )))
517    }
518
519    pub fn decompose(self) -> (PlanRef, PlanRef, Condition, JoinType, Vec<usize>) {
520        self.core.decompose()
521    }
522
523    fn dynamic_filter_candidate(&self, predicate: &Condition) -> Option<(usize, PbType)> {
524        // Dynamic filter only supports `Inner`/`LeftSemi`.
525        if !matches!(self.join_type(), JoinType::Inner | JoinType::LeftSemi) {
526            return None;
527        }
528
529        // Dynamic filter requires right side to be a scalar with one column.
530        if !self.right().max_one_row() || self.right().schema().len() != 1 {
531            return None;
532        }
533
534        // Dynamic filter only supports a single comparison predicate.
535        if predicate.conjunctions.len() > 1 {
536            return None;
537        }
538        let expr: ExprImpl = predicate.clone().into();
539        let (left_ref, comparator, right_ref) = expr.as_comparison_cond()?;
540
541        // Comparison must cross inputs: left input ref vs right scalar input ref.
542        let left_len = self.left().schema().len();
543        let condition_cross_inputs = left_ref.index < left_len && right_ref.index == left_len;
544        if !condition_cross_inputs {
545            return None;
546        }
547
548        // Comparison keys must be type aligned.
549        if self.left().schema().fields()[left_ref.index].data_type
550            != self.right().schema().fields()[0].data_type
551        {
552            return None;
553        }
554
555        // Dynamic filter output can only come from the left side.
556        if !self.output_indices().iter().all(|i| *i < left_len) {
557            return None;
558        }
559
560        Some((left_ref.index, comparator))
561    }
562
563    /// Check whether this join can be treated as a temporal filter for locality optimization.
564    ///
565    /// This is intentionally stricter than dynamic filter:
566    /// - right side must be a `LogicalNow`,
567    /// - and all dynamic filter preconditions must hold.
568    fn temporal_filter_candidate(&self) -> bool {
569        self.right().as_logical_now().is_some()
570            && self.dynamic_filter_candidate(self.on()).is_some()
571    }
572}
573
574impl PlanTreeNodeBinary<Logical> for LogicalJoin {
575    fn left(&self) -> PlanRef {
576        self.core.left.clone()
577    }
578
579    fn right(&self) -> PlanRef {
580        self.core.right.clone()
581    }
582
583    fn clone_with_left_right(&self, left: PlanRef, right: PlanRef) -> Self {
584        Self::with_core(generic::Join {
585            left,
586            right,
587            ..self.core.clone()
588        })
589    }
590
591    fn rewrite_with_left_right(
592        &self,
593        left: PlanRef,
594        left_col_change: ColIndexMapping,
595        right: PlanRef,
596        right_col_change: ColIndexMapping,
597    ) -> (Self, ColIndexMapping) {
598        let (new_on, new_output_indices) = {
599            let (mut map, _) = left_col_change.clone().into_parts();
600            let (mut right_map, _) = right_col_change.clone().into_parts();
601            for i in right_map.iter_mut().flatten() {
602                *i += left.schema().len();
603            }
604            map.append(&mut right_map);
605            let mut mapping = ColIndexMapping::new(map, left.schema().len() + right.schema().len());
606
607            let new_output_indices = self
608                .output_indices()
609                .iter()
610                .map(|&i| mapping.map(i))
611                .collect::<Vec<_>>();
612            let new_on = self.on().clone().rewrite_expr(&mut mapping);
613            (new_on, new_output_indices)
614        };
615
616        let join = Self::with_output_indices(
617            left,
618            right,
619            self.join_type(),
620            new_on,
621            new_output_indices.clone(),
622        );
623
624        let new_i2o = ColIndexMapping::with_remaining_columns(
625            &new_output_indices,
626            join.internal_column_num(),
627        );
628
629        let old_o2i = self.core.o2i_col_mapping();
630
631        let old_o2l = old_o2i
632            .composite(&self.core.i2l_col_mapping())
633            .composite(&left_col_change);
634        let old_o2r = old_o2i
635            .composite(&self.core.i2r_col_mapping())
636            .composite(&right_col_change);
637        let new_l2o = join.core.l2i_col_mapping().composite(&new_i2o);
638        let new_r2o = join.core.r2i_col_mapping().composite(&new_i2o);
639
640        let out_col_change = old_o2l
641            .composite(&new_l2o)
642            .union(&old_o2r.composite(&new_r2o));
643        (join, out_col_change)
644    }
645}
646
647impl_plan_tree_node_for_binary! { Logical, LogicalJoin }
648
649impl ColPrunable for LogicalJoin {
650    fn prune_col(&self, required_cols: &[usize], ctx: &mut ColumnPruningContext) -> PlanRef {
651        // make `required_cols` point to internal table instead of output schema.
652        let required_cols = required_cols
653            .iter()
654            .map(|i| self.output_indices()[*i])
655            .collect_vec();
656        let left_len = self.left().schema().fields.len();
657
658        let total_len = self.left().schema().len() + self.right().schema().len();
659        let mut resized_required_cols = FixedBitSet::with_capacity(total_len);
660
661        required_cols.iter().for_each(|&i| {
662            if self.is_right_join() {
663                resized_required_cols.insert(left_len + i);
664            } else {
665                resized_required_cols.insert(i);
666            }
667        });
668
669        // add those columns which are required in the join condition to
670        // to those that are required in the output
671        let mut visitor = CollectInputRef::new(resized_required_cols);
672        self.on().visit_expr(&mut visitor);
673        let left_right_required_cols = FixedBitSet::from(visitor).ones().collect_vec();
674
675        let mut left_required_cols = Vec::new();
676        let mut right_required_cols = Vec::new();
677        left_right_required_cols.iter().for_each(|&i| {
678            if i < left_len {
679                left_required_cols.push(i);
680            } else {
681                right_required_cols.push(i - left_len);
682            }
683        });
684
685        let mut on = self.on().clone();
686        let mut mapping =
687            ColIndexMapping::with_remaining_columns(&left_right_required_cols, total_len);
688        on = on.rewrite_expr(&mut mapping);
689
690        let new_output_indices = {
691            let required_inputs_in_output = if self.is_left_join() {
692                &left_required_cols
693            } else if self.is_right_join() {
694                &right_required_cols
695            } else {
696                &left_right_required_cols
697            };
698
699            let mapping =
700                ColIndexMapping::with_remaining_columns(required_inputs_in_output, total_len);
701            required_cols.iter().map(|&i| mapping.map(i)).collect_vec()
702        };
703
704        LogicalJoin::with_output_indices(
705            self.left().prune_col(&left_required_cols, ctx),
706            self.right().prune_col(&right_required_cols, ctx),
707            self.join_type(),
708            on,
709            new_output_indices,
710        )
711        .into()
712    }
713}
714
715impl ExprRewritable<Logical> for LogicalJoin {
716    fn has_rewritable_expr(&self) -> bool {
717        true
718    }
719
720    fn rewrite_exprs(&self, r: &mut dyn ExprRewriter) -> PlanRef {
721        let mut core = self.core.clone();
722        core.rewrite_exprs(r);
723        Self {
724            base: self.base.clone_with_new_plan_id(),
725            core,
726        }
727        .into()
728    }
729}
730
731impl ExprVisitable for LogicalJoin {
732    fn visit_exprs(&self, v: &mut dyn ExprVisitor) {
733        self.core.visit_exprs(v);
734    }
735}
736
737/// We are trying to derive a predicate to apply to the other side of a join if all
738/// the `InputRef`s in the predicate are eq condition columns, and can hence be substituted
739/// with the corresponding eq condition columns of the other side.
740///
741/// Strategy:
742/// 1. If the function is pure except for any `InputRef` (which may refer to impure computation),
743///    then we proceed. Else abort.
744/// 2. Then, we collect `InputRef`s in the conjunction.
745/// 3. If they are all columns in the given side of join eq condition, then we proceed. Else abort.
746/// 4. We then rewrite the `ExprImpl`, by replacing `InputRef` column indices with the equivalent in
747///    the other side.
748///
749/// # Arguments
750///
751/// Suppose we derive a predicate from the left side to be pushed to the right side.
752/// * `expr`: An expr from the left side.
753/// * `col_num`: The number of columns in the left side.
754fn derive_predicate_from_eq_condition(
755    expr: &ExprImpl,
756    eq_condition: &EqJoinPredicate,
757    col_num: usize,
758    expr_is_left: bool,
759) -> Option<ExprImpl> {
760    if expr.is_impure() {
761        return None;
762    }
763    let eq_indices = eq_condition
764        .eq_indexes_typed()
765        .iter()
766        .filter_map(|(l, r)| {
767            if l.return_type() != r.return_type() {
768                None
769            } else if expr_is_left {
770                Some(l.index())
771            } else {
772                Some(r.index())
773            }
774        })
775        .collect_vec();
776    if expr
777        .collect_input_refs(col_num)
778        .ones()
779        .any(|index| !eq_indices.contains(&index))
780    {
781        // expr contains an InputRef not in eq_condition
782        return None;
783    }
784    // The function is pure except for `InputRef` and all `InputRef`s are `eq_condition` indices.
785    // Hence, we can substitute those `InputRef`s with indices from the other side.
786    let other_side_mapping = if expr_is_left {
787        eq_condition.eq_indexes_typed().into_iter().collect()
788    } else {
789        eq_condition
790            .eq_indexes_typed()
791            .into_iter()
792            .map(|(x, y)| (y, x))
793            .collect()
794    };
795    struct InputRefsRewriter {
796        mapping: HashMap<InputRef, InputRef>,
797    }
798    impl ExprRewriter for InputRefsRewriter {
799        fn rewrite_input_ref(&mut self, input_ref: InputRef) -> ExprImpl {
800            self.mapping[&input_ref].clone().into()
801        }
802    }
803    Some(
804        InputRefsRewriter {
805            mapping: other_side_mapping,
806        }
807        .rewrite_expr(expr.clone()),
808    )
809}
810
811/// Rewrite the join predicate and all columns referred to the scan side need to rewrite.
812struct LookupJoinPredicateRewriter {
813    offset: usize,
814    mapping: Vec<usize>,
815}
816impl ExprRewriter for LookupJoinPredicateRewriter {
817    fn rewrite_input_ref(&mut self, input_ref: InputRef) -> ExprImpl {
818        if input_ref.index() < self.offset {
819            input_ref.into()
820        } else {
821            InputRef::new(
822                self.mapping[input_ref.index() - self.offset] + self.offset,
823                input_ref.return_type(),
824            )
825            .into()
826        }
827    }
828}
829
830/// Rewrite the scan predicate so we can add it to the join predicate.
831struct LookupJoinScanPredicateRewriter {
832    offset: usize,
833}
834impl ExprRewriter for LookupJoinScanPredicateRewriter {
835    fn rewrite_input_ref(&mut self, input_ref: InputRef) -> ExprImpl {
836        InputRef::new(input_ref.index() + self.offset, input_ref.return_type()).into()
837    }
838}
839
840impl PredicatePushdown for LogicalJoin {
841    /// Pushes predicates above and within a join node into the join node and/or its children nodes.
842    ///
843    /// # Which predicates can be pushed
844    ///
845    /// For inner join, we can do all kinds of pushdown.
846    ///
847    /// For left/right semi join, we can push filter to left/right and on-clause,
848    /// and push on-clause to left/right.
849    ///
850    /// For left/right anti join, we can push filter to left/right, but on-clause can not be pushed
851    ///
852    /// ## Outer Join
853    ///
854    /// Preserved Row table
855    /// : The table in an Outer Join that must return all rows.
856    ///
857    /// Null Supplying table
858    /// : This is the table that has nulls filled in for its columns in unmatched rows.
859    ///
860    /// |                          | Preserved Row table | Null Supplying table |
861    /// |--------------------------|---------------------|----------------------|
862    /// | Join predicate (on)      | Not Pushed          | Pushed               |
863    /// | Where predicate (filter) | Pushed              | Not Pushed           |
864    fn predicate_pushdown(
865        &self,
866        predicate: Condition,
867        ctx: &mut PredicatePushdownContext,
868    ) -> PlanRef {
869        // rewrite output col referencing indices as internal cols
870        let mut predicate = {
871            let mut mapping = self.core.o2i_col_mapping();
872            predicate.rewrite_expr(&mut mapping)
873        };
874
875        let left_col_num = self.left().schema().len();
876        let right_col_num = self.right().schema().len();
877        let join_type = LogicalJoin::simplify_outer(&predicate, left_col_num, self.join_type());
878
879        let push_down_temporal_predicate = self.temporal_join_on().is_none();
880
881        let (left_from_filter, right_from_filter, on) = push_down_into_join(
882            &mut predicate,
883            left_col_num,
884            right_col_num,
885            join_type,
886            push_down_temporal_predicate,
887        );
888
889        let mut new_on = self.on().clone().and(on);
890        let (left_from_on, right_from_on) = push_down_join_condition(
891            &mut new_on,
892            left_col_num,
893            right_col_num,
894            join_type,
895            push_down_temporal_predicate,
896        );
897
898        let left_predicate = left_from_filter.and(left_from_on);
899        let right_predicate = right_from_filter.and(right_from_on);
900
901        // Derive conditions to push to the other side based on eq condition columns
902        let eq_condition = EqJoinPredicate::create(left_col_num, right_col_num, new_on.clone());
903
904        // Only push to RHS if RHS is inner side of a join (RHS requires match on LHS)
905        let right_from_left = if matches!(
906            join_type,
907            JoinType::Inner | JoinType::LeftOuter | JoinType::RightSemi | JoinType::LeftSemi
908        ) {
909            Condition {
910                conjunctions: left_predicate
911                    .conjunctions
912                    .iter()
913                    .filter_map(|expr| {
914                        derive_predicate_from_eq_condition(expr, &eq_condition, left_col_num, true)
915                    })
916                    .collect(),
917            }
918        } else {
919            Condition::true_cond()
920        };
921
922        // Only push to LHS if LHS is inner side of a join (LHS requires match on RHS)
923        let left_from_right = if matches!(
924            join_type,
925            JoinType::Inner | JoinType::RightOuter | JoinType::LeftSemi | JoinType::RightSemi
926        ) {
927            Condition {
928                conjunctions: right_predicate
929                    .conjunctions
930                    .iter()
931                    .filter_map(|expr| {
932                        derive_predicate_from_eq_condition(
933                            expr,
934                            &eq_condition,
935                            right_col_num,
936                            false,
937                        )
938                    })
939                    .collect(),
940            }
941        } else {
942            Condition::true_cond()
943        };
944
945        let left_predicate = left_predicate.and(left_from_right);
946        let right_predicate = right_predicate.and(right_from_left);
947
948        let new_left = self.left().predicate_pushdown(left_predicate, ctx);
949        let new_right = self.right().predicate_pushdown(right_predicate, ctx);
950        let new_join = LogicalJoin::with_output_indices(
951            new_left,
952            new_right,
953            join_type,
954            new_on,
955            self.output_indices().clone(),
956        );
957
958        let mut mapping = self.core.i2o_col_mapping();
959        predicate = predicate.rewrite_expr(&mut mapping);
960        LogicalFilter::create(new_join.into(), predicate)
961    }
962}
963
964#[derive(Clone, Copy)]
965struct TemporalJoinScan<'a>(&'a LogicalScan);
966
967impl<'a> Deref for TemporalJoinScan<'a> {
968    type Target = LogicalScan;
969
970    fn deref(&self) -> &Self::Target {
971        self.0
972    }
973}
974
975impl LogicalJoin {
976    fn get_stream_input_for_hash_join(
977        &self,
978        predicate: &EqJoinPredicate,
979        ctx: &mut ToStreamContext,
980    ) -> Result<(StreamPlanRef, StreamPlanRef)> {
981        use super::stream::prelude::*;
982
983        let mut right = self.right().to_stream_with_dist_required(
984            &RequiredDist::shard_by_key(self.right().schema().len(), &predicate.right_eq_indexes()),
985            ctx,
986        )?;
987        let r2l =
988            predicate.r2l_eq_columns_mapping(self.left().schema().len(), right.schema().len());
989        let l2r =
990            predicate.l2r_eq_columns_mapping(self.left().schema().len(), right.schema().len());
991        let mut left;
992        let right_dist = right.distribution();
993        match right_dist {
994            Distribution::HashShard(_) => {
995                let left_dist = r2l
996                    .rewrite_required_distribution(&RequiredDist::PhysicalDist(right_dist.clone()));
997                left = self.left().to_stream_with_dist_required(&left_dist, ctx)?;
998            }
999            Distribution::UpstreamHashShard(_, _) => {
1000                left = self.left().to_stream_with_dist_required(
1001                    &RequiredDist::shard_by_key(
1002                        self.left().schema().len(),
1003                        &predicate.left_eq_indexes(),
1004                    ),
1005                    ctx,
1006                )?;
1007                let left_dist = left.distribution();
1008                match left_dist {
1009                    Distribution::HashShard(_) => {
1010                        let right_dist = l2r.rewrite_required_distribution(
1011                            &RequiredDist::PhysicalDist(left_dist.clone()),
1012                        );
1013                        right = right_dist.streaming_enforce_if_not_satisfies(right)?
1014                    }
1015                    Distribution::UpstreamHashShard(_, _) => {
1016                        left = RequiredDist::hash_shard(&predicate.left_eq_indexes())
1017                            .streaming_enforce_if_not_satisfies(left)?;
1018                        right = RequiredDist::hash_shard(&predicate.right_eq_indexes())
1019                            .streaming_enforce_if_not_satisfies(right)?;
1020                    }
1021                    _ => unreachable!(),
1022                }
1023            }
1024            _ => unreachable!(),
1025        }
1026        Ok((left, right))
1027    }
1028
1029    fn to_stream_hash_join(
1030        &self,
1031        predicate: EqJoinPredicate,
1032        ctx: &mut ToStreamContext,
1033    ) -> Result<StreamPlanRef> {
1034        use super::stream::prelude::*;
1035
1036        assert!(predicate.has_eq());
1037        let (left, right) = self.get_stream_input_for_hash_join(&predicate, ctx)?;
1038
1039        let mut core = self.core.clone_with_inputs(left, right);
1040        core.on = generic::JoinOn::EqPredicate(predicate);
1041
1042        // Convert to Hash Join for equal joins
1043        // For inner joins, pull non-equal conditions to a filter operator on top of it by default.
1044        // We do so as the filter operator can apply the non-equal condition batch-wise (vectorized)
1045        // as opposed to the HashJoin, which applies the condition row-wise.
1046        // However, the default behavior of pulling up non-equal conditions can be overridden by the
1047        // session variable `streaming_force_filter_inside_join` as it can save unnecessary
1048        // materialization of rows only to be filtered later.
1049
1050        let stream_hash_join = StreamHashJoin::new(core.clone())?;
1051        let predicate = stream_hash_join.eq_join_predicate().clone();
1052
1053        let force_filter_inside_join = self
1054            .base
1055            .ctx()
1056            .session_ctx()
1057            .config()
1058            .streaming_force_filter_inside_join();
1059
1060        let pull_filter = self.join_type() == JoinType::Inner
1061            && stream_hash_join.eq_join_predicate().has_non_eq()
1062            && stream_hash_join.inequality_pairs().is_empty()
1063            && (!force_filter_inside_join);
1064        if pull_filter {
1065            let default_indices = (0..self.internal_column_num()).collect::<Vec<_>>();
1066
1067            let mut core = core;
1068            core.output_indices = default_indices.clone();
1069            // Temporarily remove output indices.
1070            let eq_cond = EqJoinPredicate::new(
1071                Condition::true_cond(),
1072                predicate.eq_keys().to_vec(),
1073                self.left().schema().len(),
1074                self.right().schema().len(),
1075            );
1076            core.on = generic::JoinOn::EqPredicate(eq_cond);
1077            let hash_join = StreamHashJoin::new(core)?.into();
1078            let logical_filter = generic::Filter::new(predicate.non_eq_cond(), hash_join);
1079            let plan = StreamFilter::new(logical_filter).into();
1080            if self.output_indices() != &default_indices {
1081                let logical_project = generic::Project::with_mapping(
1082                    plan,
1083                    ColIndexMapping::with_remaining_columns(
1084                        self.output_indices(),
1085                        self.internal_column_num(),
1086                    ),
1087                );
1088                Ok(StreamProject::new(logical_project).into())
1089            } else {
1090                Ok(plan)
1091            }
1092        } else {
1093            Ok(stream_hash_join.into())
1094        }
1095    }
1096
1097    pub fn should_be_temporal_join(&self) -> bool {
1098        self.temporal_join_on().is_some()
1099    }
1100
1101    fn temporal_join_on(&self) -> Option<TemporalJoinScan<'_>> {
1102        if let Some(logical_scan) = self.core.right.as_logical_scan() {
1103            matches!(
1104                logical_scan.as_of(),
1105                Some(AsOf::ProcessTime | AsOf::ProcessTimeBroadcast)
1106            )
1107            .then_some(TemporalJoinScan(logical_scan))
1108        } else {
1109            None
1110        }
1111    }
1112
1113    fn should_be_stream_temporal_join<'a>(
1114        &'a self,
1115        ctx: &ToStreamContext,
1116    ) -> Result<Option<TemporalJoinScan<'a>>> {
1117        Ok(if let Some(scan) = self.temporal_join_on() {
1118            if !matches!(self.join_type(), JoinType::Inner | JoinType::LeftOuter) {
1119                return Err(RwError::from(ErrorCode::NotSupported(
1120                    format!("temporal join with {:?} join type", self.join_type()),
1121                    "Temporal join only supports inner join and left outer join".into(),
1122                )));
1123            }
1124            if ctx.backfill_type().is_snapshot_backfill() {
1125                return Err(RwError::from(ErrorCode::NotSupported(
1126                    "Temporal join with snapshot backfill not supported".into(),
1127                    "Please use arrangement backfill".into(),
1128                )));
1129            }
1130            if scan.cross_database() {
1131                return Err(RwError::from(ErrorCode::NotSupported(
1132                        "Temporal join requires the lookup table to be in the same database as the stream source table".into(),
1133                        "Please ensure both tables are in the same database".into(),
1134                    )));
1135            }
1136            Some(scan)
1137        } else {
1138            None
1139        })
1140    }
1141
1142    fn to_stream_temporal_join_with_index_selection(
1143        &self,
1144        logical_scan: TemporalJoinScan<'_>,
1145        predicate: EqJoinPredicate,
1146        ctx: &mut ToStreamContext,
1147    ) -> Result<StreamPlanRef> {
1148        // Use primary table.
1149        let mut result_plan: Result<StreamTemporalJoin> =
1150            self.to_stream_temporal_join(logical_scan, predicate.clone(), ctx);
1151        // Return directly if this temporal join can match the pk of its right table.
1152        if let Ok(temporal_join) = &result_plan
1153            && temporal_join.eq_join_predicate().eq_indexes().len()
1154                == logical_scan.primary_key().len()
1155        {
1156            return result_plan.map(|x| x.into());
1157        }
1158        if self
1159            .core
1160            .ctx()
1161            .session_ctx()
1162            .config()
1163            .enable_index_selection()
1164        {
1165            let indexes = logical_scan.table_indexes();
1166            for index in indexes {
1167                // Use index table
1168                if let Some(index_scan) = logical_scan.to_index_scan_if_index_covered(index) {
1169                    let index_scan: PlanRef = index_scan.into();
1170                    let that = self.clone_with_left_right(self.left(), index_scan.clone());
1171                    if let Ok(temporal_join) = that.to_stream_temporal_join(
1172                        that.temporal_join_on().expect(
1173                            "index scan created from temporal join scan must also be temporal join",
1174                        ),
1175                        predicate.clone(),
1176                        ctx,
1177                    ) {
1178                        match &result_plan {
1179                            Err(_) => result_plan = Ok(temporal_join),
1180                            Ok(prev_temporal_join) => {
1181                                // Prefer to the temporal join with a longer lookup prefix len.
1182                                if prev_temporal_join.eq_join_predicate().eq_indexes().len()
1183                                    < temporal_join.eq_join_predicate().eq_indexes().len()
1184                                {
1185                                    result_plan = Ok(temporal_join)
1186                                }
1187                            }
1188                        }
1189                    }
1190                }
1191            }
1192        }
1193
1194        result_plan.map(|x| x.into())
1195    }
1196
1197    fn temporal_join_scan_predicate_pull_up(
1198        logical_scan: TemporalJoinScan<'_>,
1199        predicate: EqJoinPredicate,
1200        output_indices: &[usize],
1201        left_schema_len: usize,
1202    ) -> Result<(StreamTableScan, EqJoinPredicate, Condition, Vec<usize>)> {
1203        // Extract the predicate from logical scan. Only pure scan is supported.
1204        let (new_scan, scan_predicate, project_expr) = logical_scan.predicate_pull_up();
1205        // Construct output column to require column mapping
1206        let o2r = if let Some(project_expr) = project_expr {
1207            project_expr
1208                .into_iter()
1209                .map(|x| x.as_input_ref().unwrap().index)
1210                .collect_vec()
1211        } else {
1212            (0..logical_scan.output_col_idx().len()).collect_vec()
1213        };
1214        let mut join_predicate_rewriter = LookupJoinPredicateRewriter {
1215            offset: left_schema_len,
1216            mapping: o2r.clone(),
1217        };
1218
1219        let new_eq_cond = predicate
1220            .eq_cond()
1221            .rewrite_expr(&mut join_predicate_rewriter);
1222
1223        let mut scan_predicate_rewriter = LookupJoinScanPredicateRewriter {
1224            offset: left_schema_len,
1225        };
1226
1227        let new_other_cond = predicate
1228            .other_cond()
1229            .clone()
1230            .rewrite_expr(&mut join_predicate_rewriter)
1231            .and(scan_predicate.rewrite_expr(&mut scan_predicate_rewriter));
1232
1233        let new_join_on = new_eq_cond.and(new_other_cond);
1234
1235        let new_predicate = EqJoinPredicate::create(
1236            left_schema_len,
1237            new_scan.schema().len(),
1238            new_join_on.clone(),
1239        );
1240
1241        // Rewrite the join output indices and all output indices referred to the old scan need to
1242        // rewrite.
1243        let new_join_output_indices = output_indices
1244            .iter()
1245            .map(|&x| {
1246                if x < left_schema_len {
1247                    x
1248                } else {
1249                    o2r[x - left_schema_len] + left_schema_len
1250                }
1251            })
1252            .collect_vec();
1253
1254        let new_stream_table_scan =
1255            StreamTableScan::new_with_backfill_type(new_scan, BackfillType::Replicated);
1256        Ok((
1257            new_stream_table_scan,
1258            new_predicate,
1259            new_join_on,
1260            new_join_output_indices,
1261        ))
1262    }
1263
1264    fn to_stream_temporal_join(
1265        &self,
1266        logical_scan: TemporalJoinScan<'_>,
1267        predicate: EqJoinPredicate,
1268        ctx: &mut ToStreamContext,
1269    ) -> Result<StreamTemporalJoin> {
1270        use super::stream::prelude::*;
1271
1272        assert!(predicate.has_eq());
1273
1274        let is_broadcast = matches!(logical_scan.as_of(), Some(AsOf::ProcessTimeBroadcast));
1275
1276        let table = logical_scan.table();
1277        let output_column_ids = logical_scan.output_column_ids();
1278
1279        // Verify that the right join key columns are the the prefix of the primary key and
1280        // also contain the distribution key.
1281        let order_col_ids = table.order_column_ids();
1282        let dist_key = table.distribution_key.clone();
1283
1284        let mut dist_key_in_order_key_pos = vec![];
1285        for d in dist_key {
1286            let pos = table
1287                .order_column_indices()
1288                .position(|x| x == d)
1289                .expect("dist_key must in order_key");
1290            dist_key_in_order_key_pos.push(pos);
1291        }
1292        // The shortest prefix of order key that contains distribution key.
1293        let shortest_prefix_len = dist_key_in_order_key_pos
1294            .iter()
1295            .max()
1296            .map_or(0, |pos| pos + 1);
1297
1298        // Reorder the join equal predicate to match the order key.
1299        let reorder_idx =
1300            Self::lookup_prefix_reorder_idx(&predicate, &output_column_ids, &order_col_ids);
1301        if reorder_idx.len() < shortest_prefix_len {
1302            return Err(RwError::from(ErrorCode::NotSupported(
1303                "Temporal join requires the equivalence join condition includes the key columns that form the distribution key of the lookup table".into(),
1304                concat!(
1305                    "Use DESCRIBE <table_name> to view the table's key information.\n",
1306                    "You can create an index on the lookup table to facilitate the temporal join if necessary."
1307                ).into(),
1308            )));
1309        }
1310        let lookup_prefix_len = reorder_idx.len();
1311        let predicate = predicate.reorder(&reorder_idx);
1312
1313        let required_dist = if dist_key_in_order_key_pos.is_empty() {
1314            RequiredDist::single()
1315        } else {
1316            let left_eq_indexes = predicate.left_eq_indexes();
1317            let left_dist_key = dist_key_in_order_key_pos
1318                .iter()
1319                .map(|pos| left_eq_indexes[*pos])
1320                .collect_vec();
1321
1322            RequiredDist::hash_shard(&left_dist_key)
1323        };
1324
1325        let left = self.left().to_stream(ctx)?;
1326        let left = if is_broadcast {
1327            // Always shuffle the LHS by its stream key. The point of a broadcast temporal join is
1328            // to make the join fragment independent: without an exchange here, the join would be
1329            // fused into the upstream fragment whenever the LHS already declares a concrete
1330            // distribution (e.g. a source with `HashShard(_row_id)` or a table scan with
1331            // `UpstreamHashShard`), which ties the join parallelism to the upstream and inherits
1332            // its key skew. One extra hash exchange buys an independently scalable join fragment
1333            // and evenly spread rows. A singleton LHS cannot be sharded, so keep it as is.
1334            match left.distribution() {
1335                Distribution::Single => left,
1336                _ => RequiredDist::shard_by_key(left.schema().len(), left.expect_stream_key())
1337                    .stream_enforce(left),
1338            }
1339        } else {
1340            // Enforce a shuffle for the temporal join LHS to let the scheduler be able to schedule
1341            // the join fragment together with the RHS with a `no_shuffle` exchange.
1342            required_dist.stream_enforce(left)
1343        };
1344
1345        let (new_stream_table_scan, new_predicate, new_join_on, new_join_output_indices) =
1346            Self::temporal_join_scan_predicate_pull_up(
1347                logical_scan,
1348                predicate,
1349                self.output_indices(),
1350                self.left().schema().len(),
1351            )?;
1352
1353        let right = if is_broadcast {
1354            RequiredDist::PhysicalDist(Distribution::Broadcast)
1355                .streaming_enforce_if_not_satisfies(new_stream_table_scan.into())?
1356        } else {
1357            RequiredDist::no_shuffle(new_stream_table_scan.into())
1358        };
1359        if !new_predicate.has_eq() {
1360            return Err(RwError::from(ErrorCode::NotSupported(
1361                "Temporal join requires a non trivial join condition".into(),
1362                "Please remove the false condition of the join".into(),
1363            )));
1364        }
1365
1366        // Construct a new logical join, because we have change its RHS.
1367        let new_logical_join = generic::Join::new(
1368            left,
1369            right,
1370            new_join_on,
1371            self.join_type(),
1372            new_join_output_indices,
1373        );
1374
1375        let new_predicate = new_predicate.retain_prefix_eq_key(lookup_prefix_len);
1376
1377        let mut new_logical_join = new_logical_join;
1378        new_logical_join.on = generic::JoinOn::EqPredicate(new_predicate);
1379        StreamTemporalJoin::new(new_logical_join, false)
1380    }
1381
1382    fn to_stream_nested_loop_temporal_join(
1383        &self,
1384        logical_scan: TemporalJoinScan<'_>,
1385        predicate: EqJoinPredicate,
1386        ctx: &mut ToStreamContext,
1387    ) -> Result<StreamPlanRef> {
1388        use super::stream::prelude::*;
1389        assert!(!predicate.has_eq());
1390
1391        if matches!(logical_scan.as_of(), Some(AsOf::ProcessTimeBroadcast)) {
1392            return Err(RwError::from(ErrorCode::NotSupported(
1393                "Broadcast temporal join requires an equality condition".into(),
1394                "Please add an equality condition over a lookup-table key".into(),
1395            )));
1396        }
1397
1398        let left = self.left().to_stream_with_dist_required(
1399            &RequiredDist::PhysicalDist(Distribution::Broadcast),
1400            ctx,
1401        )?;
1402        assert!(left.as_stream_exchange().is_some());
1403
1404        if self.join_type() != JoinType::Inner {
1405            return Err(RwError::from(ErrorCode::NotSupported(
1406                "Temporal join requires an inner join".into(),
1407                "Please use an inner join".into(),
1408            )));
1409        }
1410
1411        if !left.append_only() {
1412            return Err(RwError::from(ErrorCode::NotSupported(
1413                "Nested-loop Temporal join requires the left hash side to be append only".into(),
1414                "Please ensure the left hash side is append only".into(),
1415            )));
1416        }
1417
1418        let (new_stream_table_scan, new_predicate, new_join_on, new_join_output_indices) =
1419            Self::temporal_join_scan_predicate_pull_up(
1420                logical_scan,
1421                predicate,
1422                self.output_indices(),
1423                self.left().schema().len(),
1424            )?;
1425
1426        let right = RequiredDist::no_shuffle(new_stream_table_scan.into());
1427
1428        // Construct a new logical join, because we have change its RHS.
1429        let new_logical_join = generic::Join::new(
1430            left,
1431            right,
1432            new_join_on,
1433            self.join_type(),
1434            new_join_output_indices,
1435        );
1436
1437        let mut new_logical_join = new_logical_join;
1438        new_logical_join.on = generic::JoinOn::EqPredicate(new_predicate);
1439        Ok(StreamTemporalJoin::new(new_logical_join, true)?.into())
1440    }
1441
1442    fn to_stream_dynamic_filter(
1443        &self,
1444        predicate: Condition,
1445        ctx: &mut ToStreamContext,
1446    ) -> Result<Option<StreamPlanRef>> {
1447        use super::stream::prelude::*;
1448
1449        // If there is exactly one predicate, it is a comparison (<, <=, >, >=), and the
1450        // join is a `Inner` or `LeftSemi` join, we can convert the scalar subquery into a
1451        // `StreamDynamicFilter`
1452        let Some((left_key_idx, comparator)) = self.dynamic_filter_candidate(&predicate) else {
1453            return Ok(None);
1454        };
1455
1456        let left = self.left().to_stream(ctx)?.enforce_concrete_distribution();
1457        let right = self.right().to_stream_with_dist_required(
1458            &RequiredDist::PhysicalDist(Distribution::Broadcast),
1459            ctx,
1460        )?;
1461
1462        assert!(right.as_stream_exchange().is_some());
1463        assert_eq!(
1464            *Itertools::exactly_one(right.inputs().iter())
1465                .unwrap()
1466                .distribution(),
1467            Distribution::Single
1468        );
1469
1470        let core = DynamicFilter::new(comparator, left_key_idx, left, right);
1471        let plan = StreamDynamicFilter::new(core)?.into();
1472        // TODO: `DynamicFilterExecutor` should support `output_indices` in `ChunkBuilder`
1473        if self
1474            .output_indices()
1475            .iter()
1476            .copied()
1477            .ne(0..self.left().schema().len())
1478        {
1479            // The schema of dynamic filter is always the same as the left side now, and we have
1480            // checked that all output columns are from the left side before.
1481            let logical_project = generic::Project::with_mapping(
1482                plan,
1483                ColIndexMapping::with_remaining_columns(
1484                    self.output_indices(),
1485                    self.left().schema().len(),
1486                ),
1487            );
1488            Ok(Some(StreamProject::new(logical_project).into()))
1489        } else {
1490            Ok(Some(plan))
1491        }
1492    }
1493
1494    pub fn index_lookup_join_to_batch_lookup_join(&self) -> Result<Option<BatchPlanRef>> {
1495        let predicate = EqJoinPredicate::create(
1496            self.left().schema().len(),
1497            self.right().schema().len(),
1498            self.on().clone(),
1499        );
1500        assert!(predicate.has_eq());
1501
1502        let join = self
1503            .core
1504            .clone_with_inputs(self.core.left.to_batch()?, self.core.right.to_batch()?);
1505
1506        Ok(self.to_batch_lookup_join(predicate, join)?.map(Into::into))
1507    }
1508
1509    fn to_stream_asof_join(
1510        &self,
1511        predicate: EqJoinPredicate,
1512        ctx: &mut ToStreamContext,
1513    ) -> Result<StreamPlanRef> {
1514        use super::stream::prelude::*;
1515
1516        if predicate.eq_keys().is_empty() {
1517            return Err(ErrorCode::InvalidInputSyntax(
1518                "AsOf join requires at least 1 equal condition".to_owned(),
1519            )
1520            .into());
1521        }
1522
1523        let (left, right) = self.get_stream_input_for_hash_join(&predicate, ctx)?;
1524        let left_len = left.schema().len();
1525        let mut core = self.core.clone_with_inputs(left, right);
1526        core.on = generic::JoinOn::EqPredicate(predicate);
1527
1528        let inequality_desc = Self::get_inequality_desc_from_predicate(
1529            core.on
1530                .as_eq_predicate_ref()
1531                .expect("core predicate must exist")
1532                .other_cond()
1533                .clone(),
1534            left_len,
1535        )?;
1536
1537        Ok(StreamAsOfJoin::new(core, inequality_desc)?.into())
1538    }
1539
1540    /// Convert the logical join to a Hash join.
1541    fn to_batch_hash_join(
1542        &self,
1543        logical_join: generic::Join<BatchPlanRef>,
1544        predicate: EqJoinPredicate,
1545    ) -> Result<BatchPlanRef> {
1546        use super::batch::prelude::*;
1547
1548        let left_schema_len = logical_join.left.schema().len();
1549        let asof_desc = self
1550            .is_asof_join()
1551            .then(|| {
1552                Self::get_inequality_desc_from_predicate(
1553                    predicate.other_cond().clone(),
1554                    left_schema_len,
1555                )
1556            })
1557            .transpose()?;
1558
1559        let logical_join = generic::Join {
1560            on: generic::JoinOn::EqPredicate(predicate),
1561            ..logical_join
1562        };
1563        let batch_join = BatchHashJoin::new(logical_join, asof_desc);
1564        Ok(batch_join.into())
1565    }
1566
1567    pub fn get_inequality_desc_from_predicate(
1568        predicate: Condition,
1569        left_input_len: usize,
1570    ) -> Result<AsOfJoinDesc> {
1571        let expr: ExprImpl = predicate.into();
1572        if let Some((left_input_ref, expr_type, right_input_ref)) = expr.as_comparison_cond() {
1573            if left_input_ref.index() < left_input_len && right_input_ref.index() >= left_input_len
1574            {
1575                Ok(AsOfJoinDesc {
1576                    left_idx: left_input_ref.index() as u32,
1577                    right_idx: (right_input_ref.index() - left_input_len) as u32,
1578                    inequality_type: Self::expr_type_to_comparison_type(expr_type)?.into(),
1579                })
1580            } else {
1581                bail!("inequal condition from the same side should be push down in optimizer");
1582            }
1583        } else {
1584            Err(ErrorCode::InvalidInputSyntax(
1585                "AsOf join requires exactly 1 ineuquality condition".to_owned(),
1586            )
1587            .into())
1588        }
1589    }
1590
1591    fn expr_type_to_comparison_type(expr_type: PbType) -> Result<PbAsOfJoinInequalityType> {
1592        match expr_type {
1593            PbType::LessThan => Ok(PbAsOfJoinInequalityType::AsOfInequalityTypeLt),
1594            PbType::LessThanOrEqual => Ok(PbAsOfJoinInequalityType::AsOfInequalityTypeLe),
1595            PbType::GreaterThan => Ok(PbAsOfJoinInequalityType::AsOfInequalityTypeGt),
1596            PbType::GreaterThanOrEqual => Ok(PbAsOfJoinInequalityType::AsOfInequalityTypeGe),
1597            _ => Err(ErrorCode::InvalidInputSyntax(format!(
1598                "Invalid comparison type: {}",
1599                expr_type.as_str_name()
1600            ))
1601            .into()),
1602        }
1603    }
1604}
1605
1606impl ToBatch for LogicalJoin {
1607    fn to_batch(&self) -> Result<crate::optimizer::plan_node::BatchPlanRef> {
1608        let predicate = EqJoinPredicate::create(
1609            self.left().schema().len(),
1610            self.right().schema().len(),
1611            self.on().clone(),
1612        );
1613
1614        let batch_join = self
1615            .core
1616            .clone_with_inputs(self.core.left.to_batch()?, self.core.right.to_batch()?);
1617
1618        let ctx = self.base.ctx();
1619        let config = ctx.session_ctx().config();
1620
1621        if predicate.has_eq() {
1622            if !predicate.eq_keys_are_type_aligned() {
1623                return Err(ErrorCode::InternalError(format!(
1624                    "Join eq keys are not aligned for predicate: {predicate:?}"
1625                ))
1626                .into());
1627            }
1628            if config.batch_enable_lookup_join()
1629                && let Some(lookup_join) = self.to_batch_lookup_join_with_index_selection(
1630                    predicate.clone(),
1631                    batch_join.clone(),
1632                )?
1633            {
1634                return Ok(lookup_join.into());
1635            }
1636            self.to_batch_hash_join(batch_join, predicate)
1637        } else if self.is_asof_join() {
1638            Err(ErrorCode::InvalidInputSyntax(
1639                "AsOf join requires at least 1 equal condition".to_owned(),
1640            )
1641            .into())
1642        } else {
1643            // Convert to Nested-loop Join for non-equal joins
1644            Ok(BatchNestedLoopJoin::new(batch_join).into())
1645        }
1646    }
1647}
1648
1649impl ToStream for LogicalJoin {
1650    fn to_stream(
1651        &self,
1652        ctx: &mut ToStreamContext,
1653    ) -> Result<crate::optimizer::plan_node::StreamPlanRef> {
1654        if self
1655            .on()
1656            .conjunctions
1657            .iter()
1658            .any(|cond| cond.count_nows() > 0)
1659        {
1660            return Err(ErrorCode::NotSupported(
1661                "optimizer has tried to separate the temporal predicate(with now() expression) from the on condition, but it still reminded in on join's condition. Considering move it into WHERE clause?".to_owned(),
1662                 "please refer to https://docs.risingwave.com/processing/sql/temporal-filters for more information".to_owned()).into());
1663        }
1664
1665        let predicate = EqJoinPredicate::create(
1666            self.left().schema().len(),
1667            self.right().schema().len(),
1668            self.on().clone(),
1669        );
1670
1671        if self.join_type() == JoinType::AsofInner || self.join_type() == JoinType::AsofLeftOuter {
1672            self.to_stream_asof_join(predicate, ctx)
1673        } else if predicate.has_eq() {
1674            if !predicate.eq_keys_are_type_aligned() {
1675                return Err(ErrorCode::InternalError(format!(
1676                    "Join eq keys are not aligned for predicate: {predicate:?}"
1677                ))
1678                .into());
1679            }
1680
1681            if let Some(scan) = self.should_be_stream_temporal_join(ctx)? {
1682                self.to_stream_temporal_join_with_index_selection(scan, predicate, ctx)
1683            } else {
1684                self.to_stream_hash_join(predicate, ctx)
1685            }
1686        } else if let Some(scan) = self.should_be_stream_temporal_join(ctx)? {
1687            self.to_stream_nested_loop_temporal_join(scan, predicate, ctx)
1688        } else if let Some(dynamic_filter) =
1689            self.to_stream_dynamic_filter(self.on().clone(), ctx)?
1690        {
1691            Ok(dynamic_filter)
1692        } else {
1693            Err(RwError::from(ErrorCode::NotSupported(
1694                "streaming nested-loop join".to_owned(),
1695                "The non-equal join in the query requires a nested-loop join executor, which could be very expensive to run. \
1696                 Consider rewriting the query to use dynamic filter as a substitute if possible.\n\
1697                 See also: https://docs.risingwave.com/processing/sql/dynamic-filters".to_owned(),
1698            )))
1699        }
1700    }
1701
1702    fn logical_rewrite_for_stream(
1703        &self,
1704        ctx: &mut RewriteStreamContext,
1705    ) -> Result<(PlanRef, ColIndexMapping)> {
1706        let eq_indexes = self.eq_indexes();
1707        let (logical_left, logical_right) = if eq_indexes.is_empty() {
1708            (self.left(), self.right())
1709        } else {
1710            let lhs_join_key_idx = eq_indexes.iter().map(|(l, _)| *l).collect_vec();
1711            if self.should_be_temporal_join() {
1712                (
1713                    try_enforce_locality_requirement(
1714                        self.left(),
1715                        &lhs_join_key_idx,
1716                        ctx.locality_backfill_enabled(),
1717                    ),
1718                    self.right(),
1719                )
1720            } else {
1721                let rhs_join_key_idx = eq_indexes.iter().map(|(_, r)| *r).collect_vec();
1722                (
1723                    try_enforce_locality_requirement(
1724                        self.left(),
1725                        &lhs_join_key_idx,
1726                        ctx.locality_backfill_enabled(),
1727                    ),
1728                    try_enforce_locality_requirement(
1729                        self.right(),
1730                        &rhs_join_key_idx,
1731                        ctx.locality_backfill_enabled(),
1732                    ),
1733                )
1734            }
1735        };
1736
1737        let (left, left_col_change) = logical_left.logical_rewrite_for_stream(ctx)?;
1738        let left_len = left.schema().len();
1739        let (right, right_col_change) = logical_right.logical_rewrite_for_stream(ctx)?;
1740        let (join, out_col_change) = self.rewrite_with_left_right(
1741            left.clone(),
1742            left_col_change,
1743            right.clone(),
1744            right_col_change,
1745        );
1746
1747        let mapping = ColIndexMapping::with_remaining_columns(
1748            join.output_indices(),
1749            join.internal_column_num(),
1750        );
1751
1752        let l2o = join.core.l2i_col_mapping().composite(&mapping);
1753        let r2o = join.core.r2i_col_mapping().composite(&mapping);
1754
1755        // Add missing pk indices to the logical join
1756        let mut left_to_add = left
1757            .expect_stream_key()
1758            .iter()
1759            .cloned()
1760            .filter(|i| l2o.try_map(*i).is_none())
1761            .collect_vec();
1762
1763        let mut right_to_add = right
1764            .expect_stream_key()
1765            .iter()
1766            .filter(|&&i| r2o.try_map(i).is_none())
1767            .map(|&i| i + left_len)
1768            .collect_vec();
1769
1770        // NOTE(st1page): add join keys in the pk_indices a work around before we really have stream
1771        // key.
1772        let right_len = right.schema().len();
1773        let eq_predicate = EqJoinPredicate::create(left_len, right_len, join.on().clone());
1774
1775        let either_or_both = self.core.add_which_join_key_to_pk();
1776
1777        for (lk, rk) in eq_predicate.eq_indexes() {
1778            match either_or_both {
1779                EitherOrBoth::Left(_) => {
1780                    if l2o.try_map(lk).is_none() {
1781                        left_to_add.push(lk);
1782                    }
1783                }
1784                EitherOrBoth::Right(_) => {
1785                    if r2o.try_map(rk).is_none() {
1786                        right_to_add.push(rk + left_len)
1787                    }
1788                }
1789                EitherOrBoth::Both(_, _) => {
1790                    if l2o.try_map(lk).is_none() {
1791                        left_to_add.push(lk);
1792                    }
1793                    if r2o.try_map(rk).is_none() {
1794                        right_to_add.push(rk + left_len)
1795                    }
1796                }
1797            };
1798        }
1799        let left_to_add = left_to_add.into_iter().unique();
1800        let right_to_add = right_to_add.into_iter().unique();
1801        // NOTE(st1page) over
1802
1803        let mut new_output_indices = join.output_indices().clone();
1804        if !join.is_right_join() {
1805            new_output_indices.extend(left_to_add);
1806        }
1807        if !join.is_left_join() {
1808            new_output_indices.extend(right_to_add);
1809        }
1810
1811        let join_with_pk = join.clone_with_output_indices(new_output_indices);
1812
1813        let plan = if join_with_pk.join_type() == JoinType::FullOuter {
1814            // ignore the all NULL to maintain the stream key's uniqueness, see https://github.com/risingwavelabs/risingwave/issues/8084 for more information
1815
1816            let l2o = join_with_pk
1817                .core
1818                .l2i_col_mapping()
1819                .composite(&join_with_pk.core.i2o_col_mapping());
1820            let r2o = join_with_pk
1821                .core
1822                .r2i_col_mapping()
1823                .composite(&join_with_pk.core.i2o_col_mapping());
1824            let mut left_right_keys = join_with_pk
1825                .left()
1826                .expect_stream_key()
1827                .iter()
1828                .map(|i| l2o.map(*i))
1829                .collect_vec();
1830            left_right_keys.extend(
1831                join_with_pk
1832                    .right()
1833                    .expect_stream_key()
1834                    .iter()
1835                    .map(|i| r2o.map(*i)),
1836            );
1837            left_right_keys.extend(
1838                eq_predicate
1839                    .eq_indexes()
1840                    .iter()
1841                    .flat_map(|(lk, rk)| [l2o.map(*lk), r2o.map(*rk)]),
1842            );
1843            let left_right_keys = left_right_keys.into_iter().unique().collect_vec();
1844            let plan: PlanRef = join_with_pk.into();
1845            LogicalFilter::filter_out_all_null_keys(plan, &left_right_keys)
1846        } else {
1847            join_with_pk.into()
1848        };
1849
1850        // the added columns is at the end, so it will not change the exists column index
1851        Ok((plan, out_col_change))
1852    }
1853
1854    fn try_better_locality(&self, columns: &[usize]) -> Option<PlanRef> {
1855        // Only propagate locality for temporal-filter.
1856        if !self.temporal_filter_candidate() {
1857            return None;
1858        }
1859
1860        // Temporal filter only outputs columns from left input, so mapping is safe.
1861        let o2i_mapping = self.core.o2i_col_mapping();
1862        let left_input_columns = columns
1863            .iter()
1864            .map(|&col| o2i_mapping.try_map(col))
1865            .collect::<Option<Vec<usize>>>()?;
1866        if let Some(better_left_plan) = self.left().try_better_locality(&left_input_columns) {
1867            return Some(
1868                self.clone_with_left_right(better_left_plan, self.right())
1869                    .into(),
1870            );
1871        }
1872        None
1873    }
1874}
1875
1876#[cfg(test)]
1877mod tests {
1878
1879    use std::collections::HashSet;
1880
1881    use risingwave_common::catalog::{Field, Schema};
1882    use risingwave_common::types::{DataType, Datum};
1883    use risingwave_pb::expr::expr_node::Type;
1884
1885    use super::*;
1886    use crate::expr::{FunctionCall, Literal, assert_eq_input_ref};
1887    use crate::optimizer::optimizer_context::OptimizerContext;
1888    use crate::optimizer::plan_node::LogicalValues;
1889    use crate::optimizer::property::FunctionalDependency;
1890
1891    /// Pruning
1892    /// ```text
1893    /// Join(on: input_ref(1)=input_ref(3))
1894    ///   TableScan(v1, v2, v3)
1895    ///   TableScan(v4, v5, v6)
1896    /// ```
1897    /// with required columns [2,3] will result in
1898    /// ```text
1899    /// Project(input_ref(1), input_ref(2))
1900    ///   Join(on: input_ref(0)=input_ref(2))
1901    ///     TableScan(v2, v3)
1902    ///     TableScan(v4)
1903    /// ```
1904    #[tokio::test]
1905    async fn test_prune_join() {
1906        let ty = DataType::Int32;
1907        let ctx = OptimizerContext::mock();
1908        let fields: Vec<Field> = (1..7)
1909            .map(|i| Field::with_name(ty.clone(), format!("v{}", i)))
1910            .collect();
1911        let left = LogicalValues::new(
1912            vec![],
1913            Schema {
1914                fields: fields[0..3].to_vec(),
1915            },
1916            ctx.clone(),
1917        );
1918        let right = LogicalValues::new(
1919            vec![],
1920            Schema {
1921                fields: fields[3..6].to_vec(),
1922            },
1923            ctx,
1924        );
1925        let on: ExprImpl = ExprImpl::FunctionCall(Box::new(
1926            FunctionCall::new(
1927                Type::Equal,
1928                vec![
1929                    ExprImpl::InputRef(Box::new(InputRef::new(1, ty.clone()))),
1930                    ExprImpl::InputRef(Box::new(InputRef::new(3, ty))),
1931                ],
1932            )
1933            .unwrap(),
1934        ));
1935        let join_type = JoinType::Inner;
1936        let join: PlanRef = LogicalJoin::new(
1937            left.into(),
1938            right.into(),
1939            join_type,
1940            Condition::with_expr(on),
1941        )
1942        .into();
1943
1944        // Perform the prune
1945        let required_cols = vec![2, 3];
1946        let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
1947
1948        // Check the result
1949        let join = plan.as_logical_join().unwrap();
1950        assert_eq!(join.schema().fields().len(), 2);
1951        assert_eq!(join.schema().fields()[0], fields[2]);
1952        assert_eq!(join.schema().fields()[1], fields[3]);
1953
1954        let expr: ExprImpl = join.on().clone().into();
1955        let call = expr.as_function_call().unwrap();
1956        assert_eq_input_ref!(&call.inputs()[0], 0);
1957        assert_eq_input_ref!(&call.inputs()[1], 2);
1958
1959        let left = join.left();
1960        let left = left.as_logical_values().unwrap();
1961        assert_eq!(left.schema().fields(), &fields[1..3]);
1962        let right = join.right();
1963        let right = right.as_logical_values().unwrap();
1964        assert_eq!(right.schema().fields(), &fields[3..4]);
1965    }
1966
1967    /// Semi join panicked previously at `prune_col`. Add test to prevent regression.
1968    #[tokio::test]
1969    async fn test_prune_semi_join() {
1970        let ty = DataType::Int32;
1971        let ctx = OptimizerContext::mock();
1972        let fields: Vec<Field> = (1..7)
1973            .map(|i| Field::with_name(ty.clone(), format!("v{}", i)))
1974            .collect();
1975        let left = LogicalValues::new(
1976            vec![],
1977            Schema {
1978                fields: fields[0..3].to_vec(),
1979            },
1980            ctx.clone(),
1981        );
1982        let right = LogicalValues::new(
1983            vec![],
1984            Schema {
1985                fields: fields[3..6].to_vec(),
1986            },
1987            ctx,
1988        );
1989        let on: ExprImpl = ExprImpl::FunctionCall(Box::new(
1990            FunctionCall::new(
1991                Type::Equal,
1992                vec![
1993                    ExprImpl::InputRef(Box::new(InputRef::new(1, ty.clone()))),
1994                    ExprImpl::InputRef(Box::new(InputRef::new(4, ty))),
1995                ],
1996            )
1997            .unwrap(),
1998        ));
1999        for join_type in [
2000            JoinType::LeftSemi,
2001            JoinType::RightSemi,
2002            JoinType::LeftAnti,
2003            JoinType::RightAnti,
2004        ] {
2005            let join = LogicalJoin::new(
2006                left.clone().into(),
2007                right.clone().into(),
2008                join_type,
2009                Condition::with_expr(on.clone()),
2010            );
2011
2012            let offset = if join.is_right_join() { 3 } else { 0 };
2013            let join: PlanRef = join.into();
2014            // Perform the prune
2015            let required_cols = vec![0];
2016            // key 0 is never used in the join (always key 1)
2017            let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
2018            let as_plan = plan.as_logical_join().unwrap();
2019            // Check the result
2020            assert_eq!(as_plan.schema().fields().len(), 1);
2021            assert_eq!(as_plan.schema().fields()[0], fields[offset]);
2022
2023            // Perform the prune
2024            let required_cols = vec![0, 1, 2];
2025            // should not panic here
2026            let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
2027            let as_plan = plan.as_logical_join().unwrap();
2028            // Check the result
2029            assert_eq!(as_plan.schema().fields().len(), 3);
2030            assert_eq!(as_plan.schema().fields()[0], fields[offset]);
2031            assert_eq!(as_plan.schema().fields()[1], fields[offset + 1]);
2032            assert_eq!(as_plan.schema().fields()[2], fields[offset + 2]);
2033        }
2034    }
2035
2036    /// Pruning
2037    /// ```text
2038    /// Join(on: input_ref(1)=input_ref(3))
2039    ///   TableScan(v1, v2, v3)
2040    ///   TableScan(v4, v5, v6)
2041    /// ```
2042    /// with required columns [1, 3] will result in
2043    /// ```text
2044    /// Join(on: input_ref(0)=input_ref(1))
2045    ///   TableScan(v2)
2046    ///   TableScan(v4)
2047    /// ```
2048    #[tokio::test]
2049    async fn test_prune_join_no_project() {
2050        let ty = DataType::Int32;
2051        let ctx = OptimizerContext::mock();
2052        let fields: Vec<Field> = (1..7)
2053            .map(|i| Field::with_name(ty.clone(), format!("v{}", i)))
2054            .collect();
2055        let left = LogicalValues::new(
2056            vec![],
2057            Schema {
2058                fields: fields[0..3].to_vec(),
2059            },
2060            ctx.clone(),
2061        );
2062        let right = LogicalValues::new(
2063            vec![],
2064            Schema {
2065                fields: fields[3..6].to_vec(),
2066            },
2067            ctx,
2068        );
2069        let on: ExprImpl = ExprImpl::FunctionCall(Box::new(
2070            FunctionCall::new(
2071                Type::Equal,
2072                vec![
2073                    ExprImpl::InputRef(Box::new(InputRef::new(1, ty.clone()))),
2074                    ExprImpl::InputRef(Box::new(InputRef::new(3, ty))),
2075                ],
2076            )
2077            .unwrap(),
2078        ));
2079        let join_type = JoinType::Inner;
2080        let join: PlanRef = LogicalJoin::new(
2081            left.into(),
2082            right.into(),
2083            join_type,
2084            Condition::with_expr(on),
2085        )
2086        .into();
2087
2088        // Perform the prune
2089        let required_cols = vec![1, 3];
2090        let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
2091
2092        // Check the result
2093        let join = plan.as_logical_join().unwrap();
2094        assert_eq!(join.schema().fields().len(), 2);
2095        assert_eq!(join.schema().fields()[0], fields[1]);
2096        assert_eq!(join.schema().fields()[1], fields[3]);
2097
2098        let expr: ExprImpl = join.on().clone().into();
2099        let call = expr.as_function_call().unwrap();
2100        assert_eq_input_ref!(&call.inputs()[0], 0);
2101        assert_eq_input_ref!(&call.inputs()[1], 1);
2102
2103        let left = join.left();
2104        let left = left.as_logical_values().unwrap();
2105        assert_eq!(left.schema().fields(), &fields[1..2]);
2106        let right = join.right();
2107        let right = right.as_logical_values().unwrap();
2108        assert_eq!(right.schema().fields(), &fields[3..4]);
2109    }
2110
2111    /// Convert
2112    /// ```text
2113    /// Join(on: ($1 = $3) AND ($2 == 42))
2114    ///   TableScan(v1, v2, v3)
2115    ///   TableScan(v4, v5, v6)
2116    /// ```
2117    /// to
2118    /// ```text
2119    /// Filter($2 == 42)
2120    ///   HashJoin(on: $1 = $3)
2121    ///     TableScan(v1, v2, v3)
2122    ///     TableScan(v4, v5, v6)
2123    /// ```
2124    #[tokio::test]
2125    async fn test_join_to_batch() {
2126        let ctx = OptimizerContext::mock();
2127        let fields: Vec<Field> = (1..7)
2128            .map(|i| Field::with_name(DataType::Int32, format!("v{}", i)))
2129            .collect();
2130        let left = LogicalValues::new(
2131            vec![],
2132            Schema {
2133                fields: fields[0..3].to_vec(),
2134            },
2135            ctx.clone(),
2136        );
2137        let right = LogicalValues::new(
2138            vec![],
2139            Schema {
2140                fields: fields[3..6].to_vec(),
2141            },
2142            ctx,
2143        );
2144
2145        fn input_ref(i: usize) -> ExprImpl {
2146            ExprImpl::InputRef(Box::new(InputRef::new(i, DataType::Int32)))
2147        }
2148        let eq_cond = ExprImpl::FunctionCall(Box::new(
2149            FunctionCall::new(Type::Equal, vec![input_ref(1), input_ref(3)]).unwrap(),
2150        ));
2151        let non_eq_cond = ExprImpl::FunctionCall(Box::new(
2152            FunctionCall::new(
2153                Type::Equal,
2154                vec![
2155                    input_ref(2),
2156                    ExprImpl::Literal(Box::new(Literal::new(
2157                        Datum::Some(42_i32.into()),
2158                        DataType::Int32,
2159                    ))),
2160                ],
2161            )
2162            .unwrap(),
2163        ));
2164        // Condition: ($1 = $3) AND ($2 == 42)
2165        let on_cond = ExprImpl::FunctionCall(Box::new(
2166            FunctionCall::new(Type::And, vec![eq_cond.clone(), non_eq_cond.clone()]).unwrap(),
2167        ));
2168
2169        let join_type = JoinType::Inner;
2170        let logical_join = LogicalJoin::new(
2171            left.into(),
2172            right.into(),
2173            join_type,
2174            Condition::with_expr(on_cond),
2175        );
2176
2177        // Perform `to_batch`
2178        let result = logical_join.to_batch().unwrap();
2179
2180        // Expected plan:  HashJoin($1 = $3 AND $2 == 42)
2181        let hash_join = result.as_batch_hash_join().unwrap();
2182        assert_eq!(
2183            ExprImpl::from(hash_join.eq_join_predicate().eq_cond()),
2184            eq_cond
2185        );
2186        assert_eq!(
2187            *hash_join
2188                .eq_join_predicate()
2189                .non_eq_cond()
2190                .conjunctions
2191                .first()
2192                .unwrap(),
2193            non_eq_cond
2194        );
2195    }
2196
2197    /// Pruning
2198    /// ```text
2199    /// Join(on: input_ref(1)=input_ref(3))
2200    ///   TableScan(v1, v2, v3)
2201    ///   TableScan(v4, v5, v6)
2202    /// ```
2203    /// with required columns [3, 2] will result in
2204    /// ```text
2205    /// Project(input_ref(2), input_ref(1))
2206    ///   Join(on: input_ref(0)=input_ref(2))
2207    ///     TableScan(v2, v3)
2208    ///     TableScan(v4)
2209    /// ```
2210    #[tokio::test]
2211    async fn test_join_column_prune_with_order_required() {
2212        let ty = DataType::Int32;
2213        let ctx = OptimizerContext::mock();
2214        let fields: Vec<Field> = (1..7)
2215            .map(|i| Field::with_name(ty.clone(), format!("v{}", i)))
2216            .collect();
2217        let left = LogicalValues::new(
2218            vec![],
2219            Schema {
2220                fields: fields[0..3].to_vec(),
2221            },
2222            ctx.clone(),
2223        );
2224        let right = LogicalValues::new(
2225            vec![],
2226            Schema {
2227                fields: fields[3..6].to_vec(),
2228            },
2229            ctx,
2230        );
2231        let on: ExprImpl = ExprImpl::FunctionCall(Box::new(
2232            FunctionCall::new(
2233                Type::Equal,
2234                vec![
2235                    ExprImpl::InputRef(Box::new(InputRef::new(1, ty.clone()))),
2236                    ExprImpl::InputRef(Box::new(InputRef::new(3, ty))),
2237                ],
2238            )
2239            .unwrap(),
2240        ));
2241        let join_type = JoinType::Inner;
2242        let join: PlanRef = LogicalJoin::new(
2243            left.into(),
2244            right.into(),
2245            join_type,
2246            Condition::with_expr(on),
2247        )
2248        .into();
2249
2250        // Perform the prune
2251        let required_cols = vec![3, 2];
2252        let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
2253
2254        // Check the result
2255        let join = plan.as_logical_join().unwrap();
2256        assert_eq!(join.schema().fields().len(), 2);
2257        assert_eq!(join.schema().fields()[0], fields[3]);
2258        assert_eq!(join.schema().fields()[1], fields[2]);
2259
2260        let expr: ExprImpl = join.on().clone().into();
2261        let call = expr.as_function_call().unwrap();
2262        assert_eq_input_ref!(&call.inputs()[0], 0);
2263        assert_eq_input_ref!(&call.inputs()[1], 2);
2264
2265        let left = join.left();
2266        let left = left.as_logical_values().unwrap();
2267        assert_eq!(left.schema().fields(), &fields[1..3]);
2268        let right = join.right();
2269        let right = right.as_logical_values().unwrap();
2270        assert_eq!(right.schema().fields(), &fields[3..4]);
2271    }
2272
2273    #[tokio::test]
2274    async fn fd_derivation_inner_outer_join() {
2275        // left: [l0, l1], right: [r0, r1, r2]
2276        // FD: l0 --> l1, r0 --> { r1, r2 }
2277        // On: l0 = 0 AND l1 = r1
2278        //
2279        // Inner Join:
2280        //  Schema: [l0, l1, r0, r1, r2]
2281        //  FD: l0 --> l1, r0 --> { r1, r2 }, {} --> l0, l1 --> r1, r1 --> l1
2282        // Left Outer Join:
2283        //  Schema: [l0, l1, r0, r1, r2]
2284        //  FD: l0 --> l1
2285        // Right Outer Join:
2286        //  Schema: [l0, l1, r0, r1, r2]
2287        //  FD: r0 --> { r1, r2 }
2288        // Full Outer Join:
2289        //  Schema: [l0, l1, r0, r1, r2]
2290        //  FD: empty
2291        // Left Semi/Anti Join:
2292        //  Schema: [l0, l1]
2293        //  FD: l0 --> l1
2294        // Right Semi/Anti Join:
2295        //  Schema: [r0, r1, r2]
2296        //  FD: r0 --> {r1, r2}
2297        let ctx = OptimizerContext::mock();
2298        let left = {
2299            let fields: Vec<Field> = vec![
2300                Field::with_name(DataType::Int32, "l0"),
2301                Field::with_name(DataType::Int32, "l1"),
2302            ];
2303            let mut values = LogicalValues::new(vec![], Schema { fields }, ctx.clone());
2304            // 0 --> 1
2305            values
2306                .base
2307                .functional_dependency_mut()
2308                .add_functional_dependency_by_column_indices(&[0], &[1]);
2309            values
2310        };
2311        let right = {
2312            let fields: Vec<Field> = vec![
2313                Field::with_name(DataType::Int32, "r0"),
2314                Field::with_name(DataType::Int32, "r1"),
2315                Field::with_name(DataType::Int32, "r2"),
2316            ];
2317            let mut values = LogicalValues::new(vec![], Schema { fields }, ctx);
2318            // 0 --> 1, 2
2319            values
2320                .base
2321                .functional_dependency_mut()
2322                .add_functional_dependency_by_column_indices(&[0], &[1, 2]);
2323            values
2324        };
2325        // l0 = 0 AND l1 = r1
2326        let on: ExprImpl = FunctionCall::new(
2327            Type::And,
2328            vec![
2329                FunctionCall::new(
2330                    Type::Equal,
2331                    vec![
2332                        InputRef::new(0, DataType::Int32).into(),
2333                        ExprImpl::literal_int(0),
2334                    ],
2335                )
2336                .unwrap()
2337                .into(),
2338                FunctionCall::new(
2339                    Type::Equal,
2340                    vec![
2341                        InputRef::new(1, DataType::Int32).into(),
2342                        InputRef::new(3, DataType::Int32).into(),
2343                    ],
2344                )
2345                .unwrap()
2346                .into(),
2347            ],
2348        )
2349        .unwrap()
2350        .into();
2351        let expected_fd_set = [
2352            (
2353                JoinType::Inner,
2354                [
2355                    // inherit from left
2356                    FunctionalDependency::with_indices(5, &[0], &[1]),
2357                    // inherit from right
2358                    FunctionalDependency::with_indices(5, &[2], &[3, 4]),
2359                    // constant column in join condition
2360                    FunctionalDependency::with_indices(5, &[], &[0]),
2361                    // eq column in join condition
2362                    FunctionalDependency::with_indices(5, &[1], &[3]),
2363                    FunctionalDependency::with_indices(5, &[3], &[1]),
2364                ]
2365                .into_iter()
2366                .collect::<HashSet<_>>(),
2367            ),
2368            (JoinType::FullOuter, HashSet::new()),
2369            (
2370                JoinType::RightOuter,
2371                [
2372                    // inherit from right
2373                    FunctionalDependency::with_indices(5, &[2], &[3, 4]),
2374                ]
2375                .into_iter()
2376                .collect::<HashSet<_>>(),
2377            ),
2378            (
2379                JoinType::LeftOuter,
2380                [
2381                    // inherit from left
2382                    FunctionalDependency::with_indices(5, &[0], &[1]),
2383                ]
2384                .into_iter()
2385                .collect::<HashSet<_>>(),
2386            ),
2387            (
2388                JoinType::LeftSemi,
2389                [
2390                    // inherit from left
2391                    FunctionalDependency::with_indices(2, &[0], &[1]),
2392                ]
2393                .into_iter()
2394                .collect::<HashSet<_>>(),
2395            ),
2396            (
2397                JoinType::LeftAnti,
2398                [
2399                    // inherit from left
2400                    FunctionalDependency::with_indices(2, &[0], &[1]),
2401                ]
2402                .into_iter()
2403                .collect::<HashSet<_>>(),
2404            ),
2405            (
2406                JoinType::RightSemi,
2407                [
2408                    // inherit from right
2409                    FunctionalDependency::with_indices(3, &[0], &[1, 2]),
2410                ]
2411                .into_iter()
2412                .collect::<HashSet<_>>(),
2413            ),
2414            (
2415                JoinType::RightAnti,
2416                [
2417                    // inherit from right
2418                    FunctionalDependency::with_indices(3, &[0], &[1, 2]),
2419                ]
2420                .into_iter()
2421                .collect::<HashSet<_>>(),
2422            ),
2423        ];
2424
2425        for (join_type, expected_res) in expected_fd_set {
2426            let join = LogicalJoin::new(
2427                left.clone().into(),
2428                right.clone().into(),
2429                join_type,
2430                Condition::with_expr(on.clone()),
2431            );
2432            let fd_set = join
2433                .functional_dependency()
2434                .as_dependencies()
2435                .iter()
2436                .cloned()
2437                .collect::<HashSet<_>>();
2438            assert_eq!(fd_set, expected_res);
2439        }
2440    }
2441}