1use 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#[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 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 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 pub fn join_type(&self) -> JoinType {
166 self.core.join_type
167 }
168
169 pub fn eq_indexes(&self) -> Vec<(usize, usize)> {
171 self.core.eq_indexes()
172 }
173
174 pub fn output_indices(&self) -> &Vec<usize> {
176 &self.core.output_indices
177 }
178
179 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 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 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 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 let right = self.right();
283 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 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 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 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 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 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 let order_col_ids = table.order_column_ids();
401 let dist_key = table.distribution_key.clone();
402 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 let shortest_prefix_len = dist_key_in_order_key_pos
417 .iter()
418 .max()
419 .map_or(1, |pos| pos + 1);
420
421 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 let (new_scan, scan_predicate, project_expr) = logical_scan.predicate_pull_up();
432 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 if !new_predicate.has_eq() {
469 return Ok(None);
470 }
471
472 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 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 if !matches!(self.join_type(), JoinType::Inner | JoinType::LeftSemi) {
526 return None;
527 }
528
529 if !self.right().max_one_row() || self.right().schema().len() != 1 {
531 return None;
532 }
533
534 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 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 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 if !self.output_indices().iter().all(|i| *i < left_len) {
557 return None;
558 }
559
560 Some((left_ref.index, comparator))
561 }
562
563 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 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 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
737fn 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 return None;
783 }
784 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
811struct 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
830struct 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 fn predicate_pushdown(
865 &self,
866 predicate: Condition,
867 ctx: &mut PredicatePushdownContext,
868 ) -> PlanRef {
869 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 let eq_condition = EqJoinPredicate::create(left_col_num, right_col_num, new_on.clone());
903
904 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 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 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 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 let mut result_plan: Result<StreamTemporalJoin> =
1150 self.to_stream_temporal_join(logical_scan, predicate.clone(), ctx);
1151 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 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 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 let (new_scan, scan_predicate, project_expr) = logical_scan.predicate_pull_up();
1205 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 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 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 let shortest_prefix_len = dist_key_in_order_key_pos
1294 .iter()
1295 .max()
1296 .map_or(0, |pos| pos + 1);
1297
1298 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 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 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 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 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 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 if self
1474 .output_indices()
1475 .iter()
1476 .copied()
1477 .ne(0..self.left().schema().len())
1478 {
1479 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 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 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 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 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 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 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 Ok((plan, out_col_change))
1852 }
1853
1854 fn try_better_locality(&self, columns: &[usize]) -> Option<PlanRef> {
1855 if !self.temporal_filter_candidate() {
1857 return None;
1858 }
1859
1860 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 #[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 let required_cols = vec![2, 3];
1946 let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
1947
1948 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 #[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 let required_cols = vec![0];
2016 let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
2018 let as_plan = plan.as_logical_join().unwrap();
2019 assert_eq!(as_plan.schema().fields().len(), 1);
2021 assert_eq!(as_plan.schema().fields()[0], fields[offset]);
2022
2023 let required_cols = vec![0, 1, 2];
2025 let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
2027 let as_plan = plan.as_logical_join().unwrap();
2028 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 #[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 let required_cols = vec![1, 3];
2090 let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
2091
2092 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 #[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 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 let result = logical_join.to_batch().unwrap();
2179
2180 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 #[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 let required_cols = vec![3, 2];
2252 let plan = join.prune_col(&required_cols, &mut ColumnPruningContext::new(join.clone()));
2253
2254 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 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 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 values
2320 .base
2321 .functional_dependency_mut()
2322 .add_functional_dependency_by_column_indices(&[0], &[1, 2]);
2323 values
2324 };
2325 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 FunctionalDependency::with_indices(5, &[0], &[1]),
2357 FunctionalDependency::with_indices(5, &[2], &[3, 4]),
2359 FunctionalDependency::with_indices(5, &[], &[0]),
2361 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 FunctionalDependency::with_indices(5, &[2], &[3, 4]),
2374 ]
2375 .into_iter()
2376 .collect::<HashSet<_>>(),
2377 ),
2378 (
2379 JoinType::LeftOuter,
2380 [
2381 FunctionalDependency::with_indices(5, &[0], &[1]),
2383 ]
2384 .into_iter()
2385 .collect::<HashSet<_>>(),
2386 ),
2387 (
2388 JoinType::LeftSemi,
2389 [
2390 FunctionalDependency::with_indices(2, &[0], &[1]),
2392 ]
2393 .into_iter()
2394 .collect::<HashSet<_>>(),
2395 ),
2396 (
2397 JoinType::LeftAnti,
2398 [
2399 FunctionalDependency::with_indices(2, &[0], &[1]),
2401 ]
2402 .into_iter()
2403 .collect::<HashSet<_>>(),
2404 ),
2405 (
2406 JoinType::RightSemi,
2407 [
2408 FunctionalDependency::with_indices(3, &[0], &[1, 2]),
2410 ]
2411 .into_iter()
2412 .collect::<HashSet<_>>(),
2413 ),
2414 (
2415 JoinType::RightAnti,
2416 [
2417 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}