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