Skip to main content

risingwave_batch_executors/executor/join/
local_lookup_join.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::marker::PhantomData;
17use std::sync::Arc;
18
19use anyhow::{Context, anyhow};
20use itertools::Itertools;
21use risingwave_common::bitmap::{Bitmap, BitmapBuilder};
22use risingwave_common::catalog::{ColumnDesc, Field, Schema};
23use risingwave_common::hash::table_distribution::TableDistribution;
24use risingwave_common::hash::{
25    ExpandedWorkerSlotMapping, HashKey, HashKeyDispatcher, VirtualNode, VnodeCountCompat,
26    WorkerSlotId,
27};
28use risingwave_common::memory::MemoryContext;
29use risingwave_common::types::{DataType, Datum};
30use risingwave_common::util::chunk_coalesce::DataChunkBuilder;
31use risingwave_common::util::iter_util::ZipEqFast;
32use risingwave_common::util::scan_range::ScanRange;
33use risingwave_common::util::tracing::TracingContext;
34use risingwave_expr::expr::{BoxedExpression, build_from_prost};
35use risingwave_pb::batch_plan::exchange_info::DistributionMode;
36use risingwave_pb::batch_plan::exchange_source::LocalExecutePlan::Plan;
37use risingwave_pb::batch_plan::plan_node::NodeBody;
38use risingwave_pb::batch_plan::{
39    ExchangeInfo, ExchangeNode, LocalExecutePlan, PbExchangeSource, PbTaskId, PlanFragment,
40    PlanNode, RowSeqScanNode, TaskOutputId,
41};
42use risingwave_pb::common::{BatchQueryEpoch, WorkerNode};
43use risingwave_pb::plan_common::StorageTableDesc;
44
45use super::AsOfDesc;
46use crate::error::Result;
47use crate::executor::{
48    BoxedDataChunkStream, BoxedExecutor, BoxedExecutorBuilder, DummyExecutor, Executor,
49    ExecutorBuilder, JoinType, LookupJoinBase,
50};
51use crate::task::{BatchTaskContext, ShutdownToken, TaskId};
52
53/// Inner side executor builder for the `LocalLookupJoinExecutor`
54struct InnerSideExecutorBuilder {
55    table_desc: StorageTableDesc,
56    table_distribution: TableDistribution,
57    vnode_mapping: ExpandedWorkerSlotMapping,
58    outer_side_key_types: Vec<DataType>,
59    inner_side_schema: Schema,
60    inner_side_column_ids: Vec<i32>,
61    inner_side_key_types: Vec<DataType>,
62    lookup_prefix_len: usize,
63    context: Arc<dyn BatchTaskContext>,
64    task_id: TaskId,
65    query_epoch: BatchQueryEpoch,
66    worker_slot_mapping: HashMap<WorkerSlotId, WorkerNode>,
67    worker_slot_to_scan_range_mapping: HashMap<WorkerSlotId, Vec<(ScanRange, VirtualNode)>>,
68    #[expect(dead_code)]
69    chunk_size: usize,
70    shutdown_rx: ShutdownToken,
71    next_stage_id: usize,
72}
73
74/// Used to build the executor for the inner side
75pub trait LookupExecutorBuilder: Send + 'static {
76    fn reset(&mut self);
77
78    fn add_scan_range(
79        &mut self,
80        key_datums: Vec<Datum>,
81    ) -> impl Future<Output = Result<()>> + Send + '_;
82
83    fn build_executor(&mut self) -> impl Future<Output = Result<BoxedExecutor>> + Send + '_;
84}
85
86impl InnerSideExecutorBuilder {
87    /// Gets the virtual node based on the given `scan_range`
88    fn get_virtual_node(&self, scan_range: &ScanRange) -> Result<VirtualNode> {
89        let virtual_node = scan_range
90            .try_compute_vnode(&self.table_distribution)
91            .context("Could not compute vnode for lookup join")?;
92        Ok(virtual_node)
93    }
94
95    /// Creates the `RowSeqScanNode` that will be used for scanning the inner side table
96    /// based on the passed `scan_range` and virtual node.
97    fn create_row_seq_scan_node(&self, id: &WorkerSlotId) -> Result<NodeBody> {
98        let list = self.worker_slot_to_scan_range_mapping.get(id).unwrap();
99        let mut scan_ranges = vec![];
100        let mut vnode_bitmap = BitmapBuilder::zeroed(self.vnode_mapping.len());
101
102        list.iter().for_each(|(scan_range, vnode)| {
103            scan_ranges.push(scan_range.to_protobuf());
104            vnode_bitmap.set(vnode.to_index(), true);
105        });
106
107        let row_seq_scan_node = NodeBody::RowSeqScan(RowSeqScanNode {
108            table_desc: Some(self.table_desc.clone()),
109            column_ids: self.inner_side_column_ids.clone(),
110            scan_ranges,
111            ordered: false,
112            vnode_bitmap: Some(vnode_bitmap.finish().to_protobuf()),
113            limit: None,
114            query_epoch: Some(self.query_epoch),
115        });
116
117        Ok(row_seq_scan_node)
118    }
119
120    /// Creates the `PbExchangeSource` using the given `id`.
121    fn build_prost_exchange_source(&self, id: &WorkerSlotId) -> Result<PbExchangeSource> {
122        let worker = self
123            .worker_slot_mapping
124            .get(id)
125            .context("No worker node found for the given worker slot id.")?;
126
127        let local_execute_plan = LocalExecutePlan {
128            plan: Some(PlanFragment {
129                root: Some(PlanNode {
130                    children: vec![],
131                    identity: "SeqScan".to_owned(),
132                    node_body: Some(self.create_row_seq_scan_node(id)?),
133                }),
134                exchange_info: Some(ExchangeInfo {
135                    mode: DistributionMode::Single as i32,
136                    ..Default::default()
137                }),
138            }),
139            tracing_context: TracingContext::from_current_span().to_protobuf(),
140        };
141
142        let prost_exchange_source = PbExchangeSource {
143            task_output_id: Some(TaskOutputId {
144                task_id: Some(PbTaskId {
145                    query_id: self.task_id.query_id.clone(),
146                    // Offset the stage id so that it does not conflict with the stages of the query.
147                    stage_id: self.task_id.stage_id + 10000 + self.next_stage_id as u32,
148                    task_id: (*id).into(),
149                }),
150                output_id: 0,
151            }),
152            host: Some(worker.host.as_ref().unwrap().clone()),
153            local_execute_plan: Some(Plan(local_execute_plan)),
154        };
155
156        Ok(prost_exchange_source)
157    }
158}
159
160impl LookupExecutorBuilder for InnerSideExecutorBuilder {
161    fn reset(&mut self) {
162        self.worker_slot_to_scan_range_mapping = HashMap::new();
163    }
164
165    /// Adds the scan range made from the given `kwy_scalar_impls` into the worker slot id
166    /// hash map, along with the scan range's virtual node.
167    async fn add_scan_range(&mut self, key_datums: Vec<Datum>) -> Result<()> {
168        let mut scan_range = ScanRange::full_table_scan();
169
170        for ((datum, outer_type), inner_type) in key_datums
171            .into_iter()
172            .zip_eq_fast(
173                self.outer_side_key_types
174                    .iter()
175                    .take(self.lookup_prefix_len),
176            )
177            .zip_eq_fast(
178                self.inner_side_key_types
179                    .iter()
180                    .take(self.lookup_prefix_len),
181            )
182        {
183            let datum = if inner_type == outer_type {
184                datum
185            } else {
186                bail!("Join key types are not aligned: LHS: {outer_type:?}, RHS: {inner_type:?}");
187            };
188
189            scan_range.eq_conds.push(datum);
190        }
191
192        let vnode = self.get_virtual_node(&scan_range)?;
193        let worker_slot_id = self.vnode_mapping[vnode.to_index()];
194
195        let list = self
196            .worker_slot_to_scan_range_mapping
197            .entry(worker_slot_id)
198            .or_default();
199        list.push((scan_range, vnode));
200
201        Ok(())
202    }
203
204    /// Builds and returns the `ExchangeExecutor` used for the inner side of the
205    /// `LocalLookupJoinExecutor`.
206    async fn build_executor(&mut self) -> Result<BoxedExecutor> {
207        self.next_stage_id += 1;
208        let mut sources = vec![];
209        for id in self.worker_slot_to_scan_range_mapping.keys() {
210            sources.push(self.build_prost_exchange_source(id)?);
211        }
212
213        if sources.is_empty() {
214            return Ok(Box::new(DummyExecutor {
215                schema: Schema::default(),
216            }));
217        }
218
219        let exchange_node = NodeBody::Exchange(ExchangeNode {
220            sources,
221            sequential: true,
222            input_schema: self.inner_side_schema.to_prost(),
223        });
224
225        let plan_node = PlanNode {
226            children: vec![],
227            identity: "LocalLookupJoinExchangeExecutor".to_owned(),
228            node_body: Some(exchange_node),
229        };
230
231        let task_id = self.task_id.clone();
232
233        let executor_builder = ExecutorBuilder::new(
234            &plan_node,
235            &task_id,
236            self.context.clone(),
237            self.shutdown_rx.clone(),
238        );
239
240        executor_builder.build().await
241    }
242}
243
244/// Local Lookup Join Executor.
245/// High level Execution flow:
246/// Repeat 1-3:
247///   1. Read N rows from outer side input and send keys to inner side builder after deduplication.
248///   2. Inner side input lookups inner side table with keys and builds hash map.
249///   3. Outer side rows join each inner side rows by probing the hash map.
250///
251/// Furthermore, we also want to minimize the number of RPC requests we send through the
252/// `ExchangeExecutors`. This is done by grouping rows with the same key datums together, and also
253/// by grouping together scan ranges that point to the same partition (and can thus be easily
254/// scanned by the same worker node).
255pub struct LocalLookupJoinExecutor<K, B: LookupExecutorBuilder> {
256    base: LookupJoinBase<K, B>,
257}
258
259impl<K: HashKey, B: LookupExecutorBuilder> Executor for LocalLookupJoinExecutor<K, B> {
260    fn schema(&self) -> &Schema {
261        &self.base.schema
262    }
263
264    fn identity(&self) -> &str {
265        &self.base.identity
266    }
267
268    fn execute(self: Box<Self>) -> BoxedDataChunkStream {
269        Box::new(self.base).do_execute()
270    }
271}
272
273impl<K, B: LookupExecutorBuilder> LocalLookupJoinExecutor<K, B> {
274    pub fn new(base: LookupJoinBase<K, B>) -> Self {
275        Self { base }
276    }
277}
278
279pub struct LocalLookupJoinExecutorBuilder {}
280
281impl BoxedExecutorBuilder for LocalLookupJoinExecutorBuilder {
282    async fn new_boxed_executor(
283        source: &ExecutorBuilder<'_>,
284        inputs: Vec<BoxedExecutor>,
285    ) -> Result<BoxedExecutor> {
286        let [outer_side_input]: [_; 1] = inputs.try_into().unwrap();
287
288        let lookup_join_node = try_match_expand!(
289            source.plan_node().get_node_body().unwrap(),
290            NodeBody::LocalLookupJoin
291        )?;
292
293        let join_type = JoinType::from_prost(lookup_join_node.get_join_type()?);
294        let condition = match lookup_join_node.get_condition() {
295            Ok(cond_prost) => Some(build_from_prost(cond_prost)?),
296            Err(_) => None,
297        };
298
299        let output_indices: Vec<usize> = lookup_join_node
300            .get_output_indices()
301            .iter()
302            .map(|&x| x as usize)
303            .collect();
304
305        let outer_side_data_types = outer_side_input.schema().data_types();
306
307        let table_desc = lookup_join_node.get_inner_side_table_desc()?;
308        let inner_side_column_ids = lookup_join_node.get_inner_side_column_ids().clone();
309
310        let inner_side_schema = Schema {
311            fields: inner_side_column_ids
312                .iter()
313                .map(|&id| {
314                    let column = table_desc
315                        .columns
316                        .iter()
317                        .find(|c| c.column_id == id)
318                        .unwrap();
319                    Field::from(&ColumnDesc::from(column))
320                })
321                .collect_vec(),
322        };
323
324        let fields = if join_type == JoinType::LeftSemi || join_type == JoinType::LeftAnti {
325            outer_side_input.schema().fields.clone()
326        } else {
327            [
328                outer_side_input.schema().fields.clone(),
329                inner_side_schema.fields.clone(),
330            ]
331            .concat()
332        };
333
334        let original_schema = Schema { fields };
335        let actual_schema = output_indices
336            .iter()
337            .map(|&idx| original_schema[idx].clone())
338            .collect();
339
340        let mut outer_side_key_idxs = vec![];
341        for outer_side_key in lookup_join_node.get_outer_side_key() {
342            outer_side_key_idxs.push(*outer_side_key as usize)
343        }
344
345        let outer_side_key_types: Vec<DataType> = outer_side_key_idxs
346            .iter()
347            .map(|&i| outer_side_data_types[i].clone())
348            .collect_vec();
349
350        let lookup_prefix_len: usize = lookup_join_node.get_lookup_prefix_len() as usize;
351
352        let mut inner_side_key_idxs = vec![];
353        for inner_side_key in lookup_join_node.get_inner_side_key() {
354            inner_side_key_idxs.push(*inner_side_key as usize)
355        }
356
357        let inner_side_key_types = inner_side_key_idxs
358            .iter()
359            .map(|&i| inner_side_schema.fields[i].data_type.clone())
360            .collect_vec();
361
362        let null_safe = lookup_join_node.get_null_safe().clone();
363
364        let vnode_mapping = lookup_join_node
365            .get_inner_side_vnode_mapping()
366            .iter()
367            .copied()
368            .map(WorkerSlotId::from)
369            .collect_vec();
370
371        assert!(!vnode_mapping.is_empty());
372
373        let chunk_size = source.context().get_config().developer.chunk_size;
374
375        let asof_desc = lookup_join_node
376            .asof_desc
377            .map(|desc| AsOfDesc::from_protobuf(&desc))
378            .transpose()?;
379
380        let worker_nodes = lookup_join_node.get_worker_nodes();
381        let worker_slot_mapping: HashMap<WorkerSlotId, WorkerNode> = worker_nodes
382            .iter()
383            .flat_map(|worker| {
384                (0..(worker.compute_node_parallelism()))
385                    .map(|i| (WorkerSlotId::new(worker.id, i), worker.clone()))
386            })
387            .collect();
388
389        let vnodes = Some(Bitmap::ones(table_desc.vnode_count()).into());
390
391        let inner_side_builder = InnerSideExecutorBuilder {
392            table_desc: table_desc.clone(),
393            table_distribution: TableDistribution::new_from_storage_table_desc(vnodes, table_desc),
394            vnode_mapping,
395            outer_side_key_types,
396            inner_side_schema,
397            inner_side_column_ids,
398            inner_side_key_types: inner_side_key_types.clone(),
399            lookup_prefix_len,
400            context: source.context().clone(),
401            task_id: source.task_id.clone(),
402            query_epoch: lookup_join_node
403                .query_epoch
404                .ok_or_else(|| anyhow!("query_epoch not set in local lookup join"))?,
405            worker_slot_to_scan_range_mapping: HashMap::new(),
406            chunk_size,
407            shutdown_rx: source.shutdown_rx().clone(),
408            next_stage_id: 0,
409            worker_slot_mapping,
410        };
411
412        let identity = source.plan_node().get_identity().clone();
413        Ok(LocalLookupJoinExecutorArgs {
414            join_type,
415            condition,
416            outer_side_input,
417            outer_side_data_types,
418            outer_side_key_idxs,
419            inner_side_builder,
420            inner_side_key_types,
421            inner_side_key_idxs,
422            null_safe,
423            lookup_prefix_len,
424            chunk_builder: DataChunkBuilder::new(original_schema.data_types(), chunk_size),
425            schema: actual_schema,
426            output_indices,
427            chunk_size,
428            asof_desc,
429            identity: identity.clone(),
430            shutdown_rx: source.shutdown_rx().clone(),
431            mem_ctx: source.context().create_executor_mem_context(&identity),
432        }
433        .dispatch())
434    }
435}
436
437struct LocalLookupJoinExecutorArgs<B: LookupExecutorBuilder> {
438    join_type: JoinType,
439    condition: Option<BoxedExpression>,
440    outer_side_input: BoxedExecutor,
441    outer_side_data_types: Vec<DataType>,
442    outer_side_key_idxs: Vec<usize>,
443    inner_side_builder: B,
444    inner_side_key_types: Vec<DataType>,
445    inner_side_key_idxs: Vec<usize>,
446    null_safe: Vec<bool>,
447    lookup_prefix_len: usize,
448    chunk_builder: DataChunkBuilder,
449    schema: Schema,
450    output_indices: Vec<usize>,
451    chunk_size: usize,
452    asof_desc: Option<AsOfDesc>,
453    identity: String,
454    shutdown_rx: ShutdownToken,
455    mem_ctx: MemoryContext,
456}
457
458impl<B: LookupExecutorBuilder> HashKeyDispatcher for LocalLookupJoinExecutorArgs<B> {
459    type Output = BoxedExecutor;
460
461    fn dispatch_impl<K: HashKey>(self) -> Self::Output {
462        Box::new(LocalLookupJoinExecutor::<K, B>::new(LookupJoinBase {
463            join_type: self.join_type,
464            condition: self.condition,
465            outer_side_input: self.outer_side_input,
466            outer_side_data_types: self.outer_side_data_types,
467            outer_side_key_idxs: self.outer_side_key_idxs,
468            inner_side_builder: self.inner_side_builder,
469            inner_side_key_types: self.inner_side_key_types,
470            inner_side_key_idxs: self.inner_side_key_idxs,
471            null_safe: self.null_safe,
472            lookup_prefix_len: self.lookup_prefix_len,
473            chunk_builder: self.chunk_builder,
474            schema: self.schema,
475            output_indices: self.output_indices,
476            chunk_size: self.chunk_size,
477            asof_desc: self.asof_desc,
478            identity: self.identity,
479            shutdown_rx: self.shutdown_rx,
480            mem_ctx: self.mem_ctx,
481            _phantom: PhantomData,
482        }))
483    }
484
485    fn data_types(&self) -> &[DataType] {
486        &self.inner_side_key_types
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use std::sync::Arc;
493
494    use risingwave_common::array::{DataChunk, DataChunkTestExt};
495    use risingwave_common::catalog::{Field, Schema};
496    use risingwave_common::hash::HashKeyDispatcher;
497    use risingwave_common::memory::MemoryContext;
498    use risingwave_common::types::DataType;
499    use risingwave_common::util::chunk_coalesce::DataChunkBuilder;
500    use risingwave_common::util::sort_util::{ColumnOrder, OrderType};
501    use risingwave_expr::expr::{BoxedExpression, build_from_pretty};
502
503    use crate::executor::join::JoinType;
504    use crate::executor::test_utils::{
505        FakeInnerSideExecutorBuilder, MockExecutor, diff_executor_output,
506    };
507    use crate::executor::{BoxedExecutor, SortExecutor};
508    use crate::local_lookup_join::LocalLookupJoinExecutorArgs;
509    use crate::monitor::BatchSpillMetrics;
510    use crate::task::ShutdownToken;
511
512    const CHUNK_SIZE: usize = 1024;
513
514    fn create_outer_side_input() -> BoxedExecutor {
515        let schema = Schema {
516            fields: vec![
517                Field::unnamed(DataType::Int32),
518                Field::unnamed(DataType::Float32),
519            ],
520        };
521        let mut executor = MockExecutor::new(schema);
522
523        executor.add(DataChunk::from_pretty(
524            "i f
525             1 6.1
526             2 8.4
527             3 3.9",
528        ));
529
530        executor.add(DataChunk::from_pretty(
531            "i f
532             2 5.5
533             5 4.1
534             5 9.1
535             . .",
536        ));
537
538        Box::new(executor)
539    }
540
541    fn create_lookup_join_executor(
542        join_type: JoinType,
543        condition: Option<BoxedExpression>,
544        null_safe: bool,
545    ) -> BoxedExecutor {
546        let outer_side_input = create_outer_side_input();
547
548        let fields = if join_type == JoinType::LeftSemi || join_type == JoinType::LeftAnti {
549            outer_side_input.schema().fields.clone()
550        } else {
551            [
552                outer_side_input.schema().fields.clone(),
553                outer_side_input.schema().fields.clone(),
554            ]
555            .concat()
556        };
557        let original_schema = Schema { fields };
558
559        let inner_side_schema = Schema {
560            fields: outer_side_input.schema().fields.clone(),
561        };
562
563        let inner_side_data_types = inner_side_schema.data_types();
564        let outer_side_data_types = outer_side_input.schema().data_types();
565
566        LocalLookupJoinExecutorArgs {
567            join_type,
568            condition,
569            outer_side_input,
570            outer_side_data_types,
571            outer_side_key_idxs: vec![0],
572            inner_side_builder: FakeInnerSideExecutorBuilder::new(inner_side_schema),
573            inner_side_key_types: vec![inner_side_data_types[0].clone()],
574            inner_side_key_idxs: vec![0],
575            null_safe: vec![null_safe],
576            lookup_prefix_len: 1,
577            chunk_builder: DataChunkBuilder::new(original_schema.data_types(), CHUNK_SIZE),
578            schema: original_schema.clone(),
579            output_indices: (0..original_schema.len()).collect(),
580            chunk_size: CHUNK_SIZE,
581            asof_desc: None,
582            identity: "TestLookupJoinExecutor".to_owned(),
583            shutdown_rx: ShutdownToken::empty(),
584            mem_ctx: MemoryContext::none(),
585        }
586        .dispatch()
587    }
588
589    fn create_order_by_executor(child: BoxedExecutor) -> BoxedExecutor {
590        let column_orders = vec![
591            ColumnOrder {
592                column_index: 0,
593                order_type: OrderType::ascending(),
594            },
595            ColumnOrder {
596                column_index: 1,
597                order_type: OrderType::ascending(),
598            },
599        ];
600
601        Box::new(SortExecutor::new(
602            child,
603            Arc::new(column_orders),
604            "SortExecutor".into(),
605            CHUNK_SIZE,
606            MemoryContext::none(),
607            None,
608            BatchSpillMetrics::for_test(),
609        ))
610    }
611
612    async fn do_test(
613        join_type: JoinType,
614        condition: Option<BoxedExpression>,
615        null_safe: bool,
616        expected: DataChunk,
617    ) {
618        let lookup_join_executor = create_lookup_join_executor(join_type, condition, null_safe);
619        let order_by_executor = create_order_by_executor(lookup_join_executor);
620        let mut expected_mock_exec = MockExecutor::new(order_by_executor.schema().clone());
621        expected_mock_exec.add(expected);
622        diff_executor_output(order_by_executor, Box::new(expected_mock_exec)).await;
623    }
624
625    #[tokio::test]
626    async fn test_inner_join() {
627        let expected = DataChunk::from_pretty(
628            "i f   i f
629             1 6.1 1 9.2
630             2 5.5 2 5.5
631             2 5.5 2 4.4
632             2 8.4 2 5.5
633             2 8.4 2 4.4
634             5 4.1 5 2.3
635             5 4.1 5 3.7
636             5 9.1 5 2.3
637             5 9.1 5 3.7",
638        );
639
640        do_test(JoinType::Inner, None, false, expected).await;
641    }
642
643    #[tokio::test]
644    async fn test_null_safe_inner_join() {
645        let expected = DataChunk::from_pretty(
646            "i f   i f
647             1 6.1 1 9.2
648             2 5.5 2 5.5
649             2 5.5 2 4.4
650             2 8.4 2 5.5
651             2 8.4 2 4.4
652             5 4.1 5 2.3
653             5 4.1 5 3.7
654             5 9.1 5 2.3
655             5 9.1 5 3.7
656             .  .  .  .",
657        );
658
659        do_test(JoinType::Inner, None, true, expected).await;
660    }
661
662    #[tokio::test]
663    async fn test_left_outer_join() {
664        let expected = DataChunk::from_pretty(
665            "i f   i f
666             1 6.1 1 9.2
667             2 5.5 2 5.5
668             2 5.5 2 4.4
669             2 8.4 2 5.5
670             2 8.4 2 4.4
671             3 3.9 . .
672             5 4.1 5 2.3
673             5 4.1 5 3.7
674             5 9.1 5 2.3
675             5 9.1 5 3.7
676             . .   . .",
677        );
678
679        do_test(JoinType::LeftOuter, None, false, expected).await;
680    }
681
682    #[tokio::test]
683    async fn test_left_semi_join() {
684        let expected = DataChunk::from_pretty(
685            "i f
686             1 6.1
687             2 5.5
688             2 8.4
689             5 4.1
690             5 9.1",
691        );
692
693        do_test(JoinType::LeftSemi, None, false, expected).await;
694    }
695
696    #[tokio::test]
697    async fn test_left_anti_join() {
698        let expected = DataChunk::from_pretty(
699            "i f
700             3 3.9
701             . .",
702        );
703
704        do_test(JoinType::LeftAnti, None, false, expected).await;
705    }
706
707    #[tokio::test]
708    async fn test_inner_join_with_condition() {
709        let expected = DataChunk::from_pretty(
710            "i f   i f
711             1 6.1 1 9.2
712             2 5.5 2 5.5
713             2 8.4 2 5.5",
714        );
715        let condition = build_from_pretty("(less_than:boolean 5:float4 $3:float4)");
716
717        do_test(JoinType::Inner, Some(condition), false, expected).await;
718    }
719
720    #[tokio::test]
721    async fn test_left_outer_join_with_condition() {
722        let expected = DataChunk::from_pretty(
723            "i f   i f
724             1 6.1 1 9.2
725             2 5.5 2 5.5
726             2 8.4 2 5.5
727             3 3.9 . .
728             5 4.1 . .
729             5 9.1 . .
730             . .   . .",
731        );
732        let condition = build_from_pretty("(less_than:boolean 5:float4 $3:float4)");
733
734        do_test(JoinType::LeftOuter, Some(condition), false, expected).await;
735    }
736
737    #[tokio::test]
738    async fn test_left_semi_join_with_condition() {
739        let expected = DataChunk::from_pretty(
740            "i f
741             1 6.1
742             2 5.5
743             2 8.4",
744        );
745        let condition = build_from_pretty("(less_than:boolean 5:float4 $3:float4)");
746
747        do_test(JoinType::LeftSemi, Some(condition), false, expected).await;
748    }
749
750    #[tokio::test]
751    async fn test_left_anti_join_with_condition() {
752        let expected = DataChunk::from_pretty(
753            "i f
754            3 3.9
755            5 4.1
756            5 9.1
757            . .",
758        );
759        let condition = build_from_pretty("(less_than:boolean 5:float4 $3:float4)");
760
761        do_test(JoinType::LeftAnti, Some(condition), false, expected).await;
762    }
763}