Skip to main content

query/
datafusion.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Planner, QueryEngine implementations based on DataFusion.
16
17mod error;
18mod json_expr_planner;
19mod planner;
20
21use std::any::Any;
22use std::collections::HashMap;
23use std::sync::Arc;
24
25use async_trait::async_trait;
26use common_base::Plugins;
27use common_catalog::consts::is_readonly_table;
28use common_error::ext::BoxedError;
29use common_function::function::FunctionContext;
30use common_function::function_factory::ScalarFunctionFactory;
31use common_query::{Output, OutputData, OutputMeta};
32use common_recordbatch::adapter::{RecordBatchStreamAdapter, RegionQueryStatCounters};
33use common_recordbatch::{EmptyRecordBatchStream, RecordBatch, SendableRecordBatchStream};
34use common_telemetry::tracing;
35use datafusion::catalog::TableFunction;
36use datafusion::dataframe::DataFrame;
37use datafusion::physical_plan::ExecutionPlan;
38use datafusion::physical_plan::analyze::AnalyzeExec;
39use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec;
40use datafusion_common::ResolvedTableReference;
41use datafusion_expr::{
42    AggregateUDF, DmlStatement, LogicalPlan as DfLogicalPlan, LogicalPlan, WindowUDF, WriteOp,
43};
44use datatypes::prelude::VectorRef;
45use datatypes::schema::Schema;
46use futures_util::StreamExt;
47use futures_util::future::try_join;
48use session::context::QueryContextRef;
49use snafu::{OptionExt, ResultExt, ensure};
50use sqlparser::ast::AnalyzeFormat;
51use table::TableRef;
52use table::requests::{DeleteRequest, InsertRequest};
53use table::table::scan::{REGION_SCAN_EXEC_NAME, RegionScanExec};
54use tracing::Span;
55
56use crate::analyze::DistAnalyzeExec;
57pub use crate::datafusion::planner::DfContextProviderAdapter;
58use crate::dist_plan::{
59    DistPlannerOptions, MergeScanLogicalPlan, RemoteDynFilterReceiverInjectorRef,
60};
61use crate::error::{
62    CatalogSnafu, CreateRecordBatchSnafu, MissingTableMutationHandlerSnafu,
63    MissingTimestampColumnSnafu, QueryExecutionSnafu, Result, TableMutationSnafu,
64    TableNotFoundSnafu, TableReadOnlySnafu, UnsupportedExprSnafu,
65};
66use crate::executor::QueryExecutor;
67use crate::metrics::{
68    OnDone, QUERY_STAGE_ELAPSED, maybe_attach_region_watermark_metrics,
69    should_collect_region_watermark_from_query_ctx,
70};
71use crate::options::ScheduledTimeExtension;
72use crate::physical_wrapper::PhysicalPlanWrapperRef;
73use crate::planner::{DfLogicalPlanner, LogicalPlanner};
74use crate::query_engine::{DescribeResult, QueryEngineContext, QueryEngineState};
75use crate::{QueryEngine, metrics};
76
77/// Query parallelism hint key.
78/// This hint can be set in the query context to control the parallelism of the query execution.
79pub const QUERY_PARALLELISM_HINT: &str = "query_parallelism";
80
81/// Whether to fallback to the original plan when failed to push down.
82pub const QUERY_FALLBACK_HINT: &str = "query_fallback";
83
84// An unbounded queue keeps draining source RPCs while mutation RPCs on a shared
85// HTTP/2 connection are pending, trading bounded memory for request liveness.
86async fn forward_record_batches(
87    mut stream: SendableRecordBatchStream,
88    batch_tx: tokio::sync::mpsc::UnboundedSender<Result<RecordBatch>>,
89) -> Result<()> {
90    while let Some(batch) = stream.next().await {
91        match batch.context(CreateRecordBatchSnafu) {
92            Ok(batch) => {
93                if batch_tx.send(Ok(batch)).is_err() {
94                    break;
95                }
96                tokio::task::yield_now().await;
97            }
98            Err(error) => {
99                let _ = batch_tx.send(Err(error));
100                break;
101            }
102        }
103    }
104    Ok(())
105}
106
107fn query_load_region_id(plan: &Arc<dyn ExecutionPlan>) -> Option<u64> {
108    let mut region_id = None;
109    let mut stack = vec![plan.clone()];
110
111    while let Some(plan) = stack.pop() {
112        if plan.name() == REGION_SCAN_EXEC_NAME
113            && let Some(scan) = plan.as_any().downcast_ref::<RegionScanExec>()
114            && let Some(scan_region_id) = scan.query_load_region_id()
115        {
116            match region_id {
117                Some(region_id) if region_id != scan_region_id => return None,
118                Some(_) => {}
119                None => region_id = Some(scan_region_id),
120            }
121        }
122        stack.extend(plan.children().into_iter().cloned());
123    }
124
125    region_id
126}
127
128// Finds the region-owned query statistic counters from the local datanode scan plan.
129//
130// Unlike the Prometheus read-load reporting in `MergeScanExec`, the heartbeat
131// counters must be updated before metrics leave the datanode process. The
132// `RecordBatchStreamAdapter` that resolves `RecordBatchMetrics` does not know
133// the owning `MitoRegion`, so we extract the counters from `RegionScanExec` and
134// pass them to the adapter. If a plan contains scans from different regions,
135// return `None` to avoid charging the whole plan metrics to one region.
136fn query_stat_counters(plan: &Arc<dyn ExecutionPlan>) -> Option<RegionQueryStatCounters> {
137    let mut counters: Option<RegionQueryStatCounters> = None;
138    let mut stack = vec![plan.clone()];
139
140    while let Some(plan) = stack.pop() {
141        if plan.name() == REGION_SCAN_EXEC_NAME
142            && let Some(scan) = plan.as_any().downcast_ref::<RegionScanExec>()
143            && let Some(scan_counters) = scan.query_stat_counters()
144        {
145            match &counters {
146                Some(counters)
147                    if !Arc::ptr_eq(&counters.query_cpu_time, &scan_counters.query_cpu_time)
148                        || !Arc::ptr_eq(
149                            &counters.query_scanned_bytes,
150                            &scan_counters.query_scanned_bytes,
151                        ) =>
152                {
153                    return None;
154                }
155                Some(_) => {}
156                None => counters = Some(scan_counters),
157            }
158        }
159        stack.extend(plan.children().into_iter().cloned());
160    }
161
162    counters
163}
164
165pub struct DatafusionQueryEngine {
166    state: Arc<QueryEngineState>,
167    plugins: Plugins,
168}
169
170impl DatafusionQueryEngine {
171    pub fn new(state: Arc<QueryEngineState>, plugins: Plugins) -> Self {
172        Self { state, plugins }
173    }
174
175    #[tracing::instrument(skip_all)]
176    async fn exec_query_plan(
177        &self,
178        plan: LogicalPlan,
179        query_ctx: QueryContextRef,
180    ) -> Result<Output> {
181        let mut ctx = self.engine_context(query_ctx.clone());
182        let plan = if let Some(receiver_injector) =
183            self.plugins.get::<RemoteDynFilterReceiverInjectorRef>()
184        {
185            receiver_injector.maybe_inject(plan, query_ctx.clone())
186        } else {
187            plan
188        };
189
190        // `create_physical_plan` will optimize logical plan internally
191        let physical_plan = self.create_physical_plan(&mut ctx, &plan).await?;
192        let physical_plan = self.optimize_physical_plan(&mut ctx, physical_plan)?;
193        let physical_plan = if let Some(wrapper) = self.plugins.get::<PhysicalPlanWrapperRef>() {
194            wrapper.wrap(physical_plan, query_ctx)
195        } else {
196            physical_plan
197        };
198
199        let stream = self.execute_stream(&ctx, &physical_plan)?;
200
201        Ok(Output::new(
202            OutputData::Stream(stream),
203            OutputMeta::new_with_plan(physical_plan),
204        ))
205    }
206
207    #[tracing::instrument(skip_all)]
208    async fn exec_dml_statement(
209        &self,
210        dml: DmlStatement,
211        query_ctx: QueryContextRef,
212    ) -> Result<Output> {
213        ensure!(
214            matches!(dml.op, WriteOp::Insert(_) | WriteOp::Delete),
215            UnsupportedExprSnafu {
216                name: format!("DML op {}", dml.op),
217            }
218        );
219
220        let _timer = QUERY_STAGE_ELAPSED
221            .with_label_values(&[dml.op.name()])
222            .start_timer();
223
224        let default_catalog = &query_ctx.current_catalog().to_owned();
225        let default_schema = &query_ctx.current_schema();
226        let table_name = dml.table_name.resolve(default_catalog, default_schema);
227        let table = self.find_table(&table_name, &query_ctx).await?;
228
229        let Output { data, meta } = self
230            .exec_query_plan((*dml.input).clone(), query_ctx.clone())
231            .await?;
232        let stream = match data {
233            OutputData::RecordBatches(batches) => batches.as_stream(),
234            OutputData::Stream(stream) => stream,
235            _ => unreachable!(),
236        };
237
238        let mut affected_rows = 0;
239        let mut insert_cost = 0;
240
241        match dml.op {
242            WriteOp::Insert(_) => {
243                let (batch_tx, batch_rx) = tokio::sync::mpsc::unbounded_channel();
244                let producer = forward_record_batches(stream, batch_tx);
245                let consumer = self.consume_insert_record_batches(
246                    batch_rx,
247                    &table_name,
248                    table.schema(),
249                    query_ctx.clone(),
250                );
251                let ((), (rows, cost)) = try_join(producer, consumer).await?;
252                affected_rows += rows;
253                insert_cost += cost;
254            }
255            WriteOp::Delete => {
256                // Keep DELETE on the same producer/consumer schedule as INSERT so the source
257                // stream can continue draining while mutation RPCs are pending.
258                let (batch_tx, batch_rx) = tokio::sync::mpsc::unbounded_channel();
259                let producer = forward_record_batches(stream, batch_tx);
260                let consumer = self.consume_delete_record_batches(
261                    batch_rx,
262                    &table_name,
263                    &table,
264                    query_ctx.clone(),
265                );
266                let (rows, ()) = try_join(consumer, producer).await?;
267                affected_rows += rows;
268            }
269            _ => unreachable!("guarded by the 'ensure!' at the beginning"),
270        }
271        Ok(Output::new(
272            OutputData::AffectedRows(affected_rows),
273            OutputMeta::new(meta.plan, insert_cost),
274        ))
275    }
276
277    async fn consume_insert_record_batches(
278        &self,
279        mut batch_rx: tokio::sync::mpsc::UnboundedReceiver<Result<RecordBatch>>,
280        table_name: &ResolvedTableReference,
281        table_schema: Arc<Schema>,
282        query_ctx: QueryContextRef,
283    ) -> Result<(usize, usize)> {
284        let mut affected_rows = 0;
285        let mut insert_cost = 0;
286        while let Some(batch) = batch_rx.recv().await {
287            let batch = batch?;
288            let column_vectors = batch
289                .column_vectors(&table_name.to_string(), table_schema.clone())
290                .map_err(BoxedError::new)
291                .context(QueryExecutionSnafu)?;
292            // We ignore the insert op.
293            let output = self
294                .insert(table_name, column_vectors, query_ctx.clone())
295                .await?;
296            let (rows, cost) = output.extract_rows_and_cost();
297            affected_rows += rows;
298            insert_cost += cost;
299        }
300        Ok((affected_rows, insert_cost))
301    }
302
303    async fn consume_delete_record_batches(
304        &self,
305        mut batch_rx: tokio::sync::mpsc::UnboundedReceiver<Result<RecordBatch>>,
306        table_name: &ResolvedTableReference,
307        table: &TableRef,
308        query_ctx: QueryContextRef,
309    ) -> Result<usize> {
310        let mut affected_rows = 0;
311        while let Some(batch) = batch_rx.recv().await {
312            let batch = batch?;
313            let column_vectors = batch
314                .column_vectors(&table_name.to_string(), table.schema())
315                .map_err(BoxedError::new)
316                .context(QueryExecutionSnafu)?;
317            affected_rows += self
318                .delete(table_name, table, column_vectors, query_ctx.clone())
319                .await?;
320        }
321        Ok(affected_rows)
322    }
323
324    #[tracing::instrument(skip_all)]
325    async fn delete(
326        &self,
327        table_name: &ResolvedTableReference,
328        table: &TableRef,
329        column_vectors: HashMap<String, VectorRef>,
330        query_ctx: QueryContextRef,
331    ) -> Result<usize> {
332        let catalog_name = table_name.catalog.to_string();
333        let schema_name = table_name.schema.to_string();
334        let table_name = table_name.table.to_string();
335        let table_schema = table.schema();
336
337        ensure!(
338            !is_readonly_table(&schema_name, &table_name),
339            TableReadOnlySnafu { table: table_name }
340        );
341
342        let ts_column = table_schema
343            .timestamp_column()
344            .map(|x| &x.name)
345            .with_context(|| MissingTimestampColumnSnafu {
346                table_name: table_name.clone(),
347            })?;
348
349        let table_info = table.table_info();
350        let rowkey_columns = table_info
351            .meta
352            .row_key_column_names()
353            .collect::<Vec<&String>>();
354        let column_vectors = column_vectors
355            .into_iter()
356            .filter(|x| &x.0 == ts_column || rowkey_columns.contains(&&x.0))
357            .collect::<HashMap<_, _>>();
358
359        let request = DeleteRequest {
360            catalog_name,
361            schema_name,
362            table_name,
363            key_column_values: column_vectors,
364        };
365
366        self.state
367            .table_mutation_handler()
368            .context(MissingTableMutationHandlerSnafu)?
369            .delete(request, query_ctx)
370            .await
371            .context(TableMutationSnafu)
372    }
373
374    #[tracing::instrument(skip_all)]
375    async fn insert(
376        &self,
377        table_name: &ResolvedTableReference,
378        column_vectors: HashMap<String, VectorRef>,
379        query_ctx: QueryContextRef,
380    ) -> Result<Output> {
381        let catalog_name = table_name.catalog.to_string();
382        let schema_name = table_name.schema.to_string();
383        let table_name = table_name.table.to_string();
384
385        ensure!(
386            !is_readonly_table(&schema_name, &table_name),
387            TableReadOnlySnafu { table: table_name }
388        );
389
390        let request = InsertRequest {
391            catalog_name,
392            schema_name,
393            table_name,
394            columns_values: column_vectors,
395            skip_wal: query_ctx.skip_wal(),
396        };
397
398        self.state
399            .table_mutation_handler()
400            .context(MissingTableMutationHandlerSnafu)?
401            .insert(request, query_ctx)
402            .await
403            .context(TableMutationSnafu)
404    }
405
406    async fn find_table(
407        &self,
408        table_name: &ResolvedTableReference,
409        query_context: &QueryContextRef,
410    ) -> Result<TableRef> {
411        let catalog_name = table_name.catalog.as_ref();
412        let schema_name = table_name.schema.as_ref();
413        let table_name = table_name.table.as_ref();
414
415        self.state
416            .catalog_manager()
417            .table(catalog_name, schema_name, table_name, Some(query_context))
418            .await
419            .context(CatalogSnafu)?
420            .with_context(|| TableNotFoundSnafu { table: table_name })
421    }
422
423    #[tracing::instrument(skip_all)]
424    async fn create_physical_plan(
425        &self,
426        ctx: &mut QueryEngineContext,
427        logical_plan: &LogicalPlan,
428    ) -> Result<Arc<dyn ExecutionPlan>> {
429        /// Only print context on panic, to avoid cluttering logs.
430        ///
431        /// TODO(discord9): remove this once we catch the bug
432        #[derive(Debug)]
433        struct PanicLogger<'a> {
434            input_logical_plan: &'a LogicalPlan,
435            after_analyze: Option<LogicalPlan>,
436            after_optimize: Option<LogicalPlan>,
437            phy_plan: Option<Arc<dyn ExecutionPlan>>,
438        }
439        impl Drop for PanicLogger<'_> {
440            fn drop(&mut self) {
441                if std::thread::panicking() {
442                    common_telemetry::error!(
443                        "Panic while creating physical plan, input logical plan: {:?}, after analyze: {:?}, after optimize: {:?}, final physical plan: {:?}",
444                        self.input_logical_plan,
445                        self.after_analyze,
446                        self.after_optimize,
447                        self.phy_plan
448                    );
449                }
450            }
451        }
452
453        let mut logger = PanicLogger {
454            input_logical_plan: logical_plan,
455            after_analyze: None,
456            after_optimize: None,
457            phy_plan: None,
458        };
459
460        let _timer = metrics::CREATE_PHYSICAL_ELAPSED.start_timer();
461        let state = ctx.state();
462
463        common_telemetry::debug!("Create physical plan, input plan: {logical_plan}");
464
465        // special handle EXPLAIN plan
466        if matches!(logical_plan, DfLogicalPlan::Explain(_)) {
467            return state
468                .create_physical_plan(logical_plan)
469                .await
470                .map_err(Into::into);
471        }
472
473        // analyze first
474        let analyzed_plan = state.analyzer().execute_and_check(
475            logical_plan.clone(),
476            state.config_options(),
477            |_, _| {},
478        )?;
479
480        logger.after_analyze = Some(analyzed_plan.clone());
481
482        common_telemetry::debug!("Create physical plan, analyzed plan: {analyzed_plan}");
483
484        // skip optimize for MergeScan
485        let optimized_plan = if let DfLogicalPlan::Extension(ext) = &analyzed_plan
486            && ext.node.name() == MergeScanLogicalPlan::name()
487        {
488            analyzed_plan.clone()
489        } else {
490            state
491                .optimizer()
492                .optimize(analyzed_plan, state, |_, _| {})?
493        };
494
495        common_telemetry::debug!("Create physical plan, optimized plan: {optimized_plan}");
496        logger.after_optimize = Some(optimized_plan.clone());
497
498        let physical_plan = state
499            .query_planner()
500            .create_physical_plan(&optimized_plan, state)
501            .await?;
502
503        logger.phy_plan = Some(physical_plan.clone());
504        drop(logger);
505        Ok(physical_plan)
506    }
507
508    #[tracing::instrument(skip_all)]
509    fn optimize_physical_plan(
510        &self,
511        ctx: &mut QueryEngineContext,
512        plan: Arc<dyn ExecutionPlan>,
513    ) -> Result<Arc<dyn ExecutionPlan>> {
514        let _timer = metrics::OPTIMIZE_PHYSICAL_ELAPSED.start_timer();
515
516        // TODO(ruihang): `self.create_physical_plan()` already optimize the plan, check
517        // if we need to optimize it again here.
518        // let state = ctx.state();
519        // let config = state.config_options();
520
521        // skip optimize AnalyzeExec plan
522        let optimized_plan = if let Some(analyze_plan) = plan.as_any().downcast_ref::<AnalyzeExec>()
523        {
524            let format = if let Some(format) = ctx.query_ctx().explain_format()
525                && format.to_lowercase() == "json"
526            {
527                AnalyzeFormat::JSON
528            } else {
529                AnalyzeFormat::TEXT
530            };
531            // Sets the verbose flag of the query context.
532            // The MergeScanExec plan uses the verbose flag to determine whether to print the plan in verbose mode.
533            ctx.query_ctx().set_explain_verbose(analyze_plan.verbose());
534
535            Arc::new(DistAnalyzeExec::new(
536                analyze_plan.input().clone(),
537                analyze_plan.verbose(),
538                format,
539            ))
540            // let mut new_plan = analyze_plan.input().clone();
541            // for optimizer in state.physical_optimizers() {
542            //     new_plan = optimizer
543            //         .optimize(new_plan, config)
544            //         .context(DataFusionSnafu)?;
545            // }
546            // Arc::new(DistAnalyzeExec::new(new_plan))
547        } else {
548            plan
549            // let mut new_plan = plan;
550            // for optimizer in state.physical_optimizers() {
551            //     new_plan = optimizer
552            //         .optimize(new_plan, config)
553            //         .context(DataFusionSnafu)?;
554            // }
555            // new_plan
556        };
557
558        Ok(optimized_plan)
559    }
560}
561
562#[async_trait]
563impl QueryEngine for DatafusionQueryEngine {
564    fn as_any(&self) -> &dyn Any {
565        self
566    }
567
568    fn planner(&self) -> Arc<dyn LogicalPlanner> {
569        Arc::new(DfLogicalPlanner::new(self.state.clone()))
570    }
571
572    fn name(&self) -> &str {
573        "datafusion"
574    }
575
576    async fn describe(
577        &self,
578        plan: LogicalPlan,
579        _query_ctx: QueryContextRef,
580    ) -> Result<DescribeResult> {
581        Ok(DescribeResult { logical_plan: plan })
582    }
583
584    async fn execute(&self, plan: LogicalPlan, query_ctx: QueryContextRef) -> Result<Output> {
585        match plan {
586            LogicalPlan::Dml(dml) => self.exec_dml_statement(dml, query_ctx).await,
587            _ => self.exec_query_plan(plan, query_ctx).await,
588        }
589    }
590
591    /// Note in SQL queries, aggregate names are looked up using
592    /// lowercase unless the query uses quotes. For example,
593    ///
594    /// `SELECT MY_UDAF(x)...` will look for an aggregate named `"my_udaf"`
595    /// `SELECT "my_UDAF"(x)` will look for an aggregate named `"my_UDAF"`
596    ///
597    /// So it's better to make UDAF name lowercase when creating one.
598    fn register_aggregate_function(&self, func: AggregateUDF) {
599        self.state.register_aggr_function(func);
600    }
601
602    /// Register an scalar function.
603    /// Will override if the function with same name is already registered.
604    fn register_scalar_function(&self, func: ScalarFunctionFactory) {
605        self.state.register_scalar_function(func);
606    }
607
608    fn register_table_function(&self, func: Arc<TableFunction>) {
609        self.state.register_table_function(func);
610    }
611
612    fn register_window_function(&self, func: WindowUDF) {
613        self.state.register_window_function(func);
614    }
615
616    fn read_table(&self, table: TableRef) -> Result<DataFrame> {
617        self.state.read_table(table).map_err(Into::into)
618    }
619
620    fn engine_context(&self, query_ctx: QueryContextRef) -> QueryEngineContext {
621        let mut state = self.state.session_state();
622        state.config_mut().set_extension(query_ctx.clone());
623        state.config_mut().set_extension(self.state.clone());
624        // note that hints in "x-greptime-hints" is automatically parsed
625        // and set to query context's extension, so we can get it from query context.
626        if let Some(parallelism) = query_ctx.extension(QUERY_PARALLELISM_HINT) {
627            if let Ok(n) = parallelism.parse::<u64>() {
628                if n > 0 {
629                    let new_cfg = state.config().clone().with_target_partitions(n as usize);
630                    *state.config_mut() = new_cfg;
631                }
632            } else {
633                common_telemetry::warn!(
634                    "Failed to parse query_parallelism: {}, using default value",
635                    parallelism
636                );
637            }
638        }
639
640        // configure execution options
641        state.config_mut().options_mut().execution.time_zone =
642            Some(query_ctx.timezone().to_string());
643
644        // usually it's impossible to have both `set variable` set by sql client and
645        // hint in header by grpc client, so only need to deal with them separately
646        if query_ctx.configuration_parameter().allow_query_fallback() {
647            state
648                .config_mut()
649                .options_mut()
650                .extensions
651                .insert(DistPlannerOptions {
652                    allow_query_fallback: true,
653                });
654        } else if let Some(fallback) = query_ctx.extension(QUERY_FALLBACK_HINT) {
655            // also check the query context for fallback hint
656            // if it is set, we will enable the fallback
657            if fallback.to_lowercase().parse::<bool>().unwrap_or(false) {
658                state
659                    .config_mut()
660                    .options_mut()
661                    .extensions
662                    .insert(DistPlannerOptions {
663                        allow_query_fallback: true,
664                    });
665            }
666        }
667
668        state
669            .config_mut()
670            .options_mut()
671            .extensions
672            .insert(FunctionContext {
673                query_ctx: query_ctx.clone(),
674                state: self.engine_state().function_state(),
675            });
676
677        // Carry scheduled Flow time through ConfigOptions.extensions so that
678        // the distributed plan analyzer can read it during expression
679        // simplification (preventing wall-clock constant-folding of `now()`).
680        state
681            .config_mut()
682            .options_mut()
683            .extensions
684            .insert(ScheduledTimeExtension {
685                scheduled_time: crate::options::scheduled_time_from_ctx(&query_ctx),
686            });
687
688        let config_options = state.config_options().clone();
689        let _ = state
690            .execution_props_mut()
691            .config_options
692            .insert(config_options);
693
694        // Apply scheduled time from query context if present, so that `now()` /
695        // `current_timestamp()` functions evaluate against the logical scheduled time
696        // rather than wall-clock.
697        match crate::options::parse_scheduled_time_datetime(&query_ctx.extensions()) {
698            Ok(Some(scheduled_rt)) => {
699                state.execution_props_mut().query_execution_start_time = Some(scheduled_rt);
700            }
701            Ok(None) => {}
702            Err(err) => {
703                common_telemetry::warn!(err; "Ignoring invalid scheduled time query extension");
704            }
705        }
706
707        QueryEngineContext::new(state, query_ctx)
708    }
709
710    fn engine_state(&self) -> &QueryEngineState {
711        &self.state
712    }
713}
714
715impl QueryExecutor for DatafusionQueryEngine {
716    #[tracing::instrument(skip_all)]
717    fn execute_stream(
718        &self,
719        ctx: &QueryEngineContext,
720        plan: &Arc<dyn ExecutionPlan>,
721    ) -> Result<SendableRecordBatchStream> {
722        let query_ctx = ctx.query_ctx();
723        let explain_verbose = query_ctx.explain_verbose();
724        let should_collect_region_watermark =
725            should_collect_region_watermark_from_query_ctx(&query_ctx)?;
726        let output_partitions = plan.properties().output_partitioning().partition_count();
727        if explain_verbose {
728            common_telemetry::info!("Executing query plan, output_partitions: {output_partitions}");
729        }
730
731        let exec_timer = metrics::EXEC_PLAN_ELAPSED.start_timer();
732        let task_ctx = ctx.build_task_ctx();
733        let span = Span::current();
734
735        match plan.properties().output_partitioning().partition_count() {
736            0 => {
737                let schema = Arc::new(
738                    Schema::try_from(plan.schema())
739                        .map_err(BoxedError::new)
740                        .context(QueryExecutionSnafu)?,
741                );
742                Ok(Box::pin(EmptyRecordBatchStream::new(schema)))
743            }
744            1 => {
745                let df_stream = plan.execute(0, task_ctx)?;
746                let mut stream = RecordBatchStreamAdapter::try_new_with_span(df_stream, span)
747                    .context(error::ConvertDfRecordBatchStreamSnafu)
748                    .map_err(BoxedError::new)
749                    .context(QueryExecutionSnafu)?;
750                stream.set_metrics2(plan.clone());
751                stream.set_query_load_region_id(query_load_region_id(plan));
752                stream.set_query_stat_counters(query_stat_counters(plan));
753                stream.set_explain_verbose(explain_verbose);
754                let stream = OnDone::new(Box::pin(stream), move || {
755                    let exec_cost = exec_timer.stop_and_record();
756                    if explain_verbose {
757                        common_telemetry::info!(
758                            "DatafusionQueryEngine execute 1 stream, cost: {:?}s",
759                            exec_cost,
760                        );
761                    }
762                });
763                Ok(maybe_attach_region_watermark_metrics(
764                    Box::pin(stream),
765                    plan.clone(),
766                    should_collect_region_watermark,
767                ))
768            }
769            _ => {
770                // merge into a single partition
771                let merged_plan = CoalescePartitionsExec::new(plan.clone());
772                // CoalescePartitionsExec must produce a single partition
773                assert_eq!(
774                    1,
775                    merged_plan
776                        .properties()
777                        .output_partitioning()
778                        .partition_count()
779                );
780                let df_stream = merged_plan.execute(0, task_ctx)?;
781                let mut stream = RecordBatchStreamAdapter::try_new_with_span(df_stream, span)
782                    .context(error::ConvertDfRecordBatchStreamSnafu)
783                    .map_err(BoxedError::new)
784                    .context(QueryExecutionSnafu)?;
785                stream.set_metrics2(plan.clone());
786                stream.set_query_load_region_id(query_load_region_id(plan));
787                stream.set_query_stat_counters(query_stat_counters(plan));
788                stream.set_explain_verbose(explain_verbose);
789                let stream = OnDone::new(Box::pin(stream), move || {
790                    let exec_cost = exec_timer.stop_and_record();
791                    if explain_verbose {
792                        common_telemetry::info!(
793                            "DatafusionQueryEngine execute {output_partitions} stream, cost: {:?}s",
794                            exec_cost
795                        );
796                    }
797                });
798                Ok(maybe_attach_region_watermark_metrics(
799                    Box::pin(stream),
800                    plan.clone(),
801                    should_collect_region_watermark,
802                ))
803            }
804        }
805    }
806}
807
808#[cfg(test)]
809mod tests {
810    use std::fmt;
811    use std::sync::Arc;
812    use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
813
814    use api::v1::SemanticType;
815    use arrow::array::{ArrayRef, UInt64Array};
816    use arrow_schema::SortOptions;
817    use catalog::RegisterTableRequest;
818    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, NUMBERS_TABLE_ID};
819    use common_error::ext::BoxedError;
820    use common_recordbatch::{EmptyRecordBatchStream, SendableRecordBatchStream, util};
821    use datafusion::physical_plan::display::{DisplayAs, DisplayFormatType};
822    use datafusion::physical_plan::expressions::PhysicalSortExpr;
823    use datafusion::physical_plan::joins::{HashJoinExec, JoinOn, PartitionMode};
824    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
825    use datafusion::physical_plan::{ExecutionPlan, PhysicalExpr};
826    use datafusion::prelude::{col, lit};
827    use datafusion_common::{JoinType, NullEquality};
828    use datafusion_physical_expr::expressions::Column;
829    use datatypes::prelude::ConcreteDataType;
830    use datatypes::schema::{ColumnSchema, SchemaRef};
831    use datatypes::vectors::{Helper, UInt32Vector, VectorRef};
832    use session::context::{QueryContext, QueryContextBuilder};
833    use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder, RegionMetadataRef};
834    use store_api::region_engine::{
835        PartitionRange, PrepareRequest, QueryScanContext, RegionScanner, ScannerProperties,
836    };
837    use store_api::storage::{RegionId, ScanRequest};
838    use table::table::numbers::{NUMBERS_TABLE_NAME, NumbersTable};
839    use table::table::scan::RegionScanExec;
840
841    use super::*;
842    use crate::options::QueryOptions;
843    use crate::parser::QueryLanguageParser;
844    use crate::part_sort::PartSortExec;
845    use crate::query_engine::{QueryEngineFactory, QueryEngineRef};
846
847    #[derive(Debug)]
848    struct RecordingScanner {
849        schema: SchemaRef,
850        metadata: RegionMetadataRef,
851        properties: ScannerProperties,
852        update_calls: Arc<AtomicUsize>,
853        last_filter_len: Arc<AtomicUsize>,
854    }
855
856    impl RecordingScanner {
857        fn new(
858            schema: SchemaRef,
859            metadata: RegionMetadataRef,
860            update_calls: Arc<AtomicUsize>,
861            last_filter_len: Arc<AtomicUsize>,
862        ) -> Self {
863            Self {
864                schema,
865                metadata,
866                properties: ScannerProperties::default(),
867                update_calls,
868                last_filter_len,
869            }
870        }
871    }
872
873    impl RegionScanner for RecordingScanner {
874        fn name(&self) -> &str {
875            "RecordingScanner"
876        }
877
878        fn properties(&self) -> &ScannerProperties {
879            &self.properties
880        }
881
882        fn schema(&self) -> SchemaRef {
883            self.schema.clone()
884        }
885
886        fn metadata(&self) -> RegionMetadataRef {
887            self.metadata.clone()
888        }
889
890        fn prepare(&mut self, request: PrepareRequest) -> std::result::Result<(), BoxedError> {
891            self.properties.prepare(request);
892            Ok(())
893        }
894
895        fn scan_partition(
896            &self,
897            _ctx: &QueryScanContext,
898            _metrics_set: &ExecutionPlanMetricsSet,
899            _partition: usize,
900        ) -> std::result::Result<SendableRecordBatchStream, BoxedError> {
901            Ok(Box::pin(EmptyRecordBatchStream::new(self.schema.clone())))
902        }
903
904        fn has_predicate_without_region(&self) -> bool {
905            true
906        }
907
908        fn add_dyn_filter_to_predicate(
909            &mut self,
910            filter_exprs: Vec<Arc<dyn PhysicalExpr>>,
911        ) -> Vec<bool> {
912            self.update_calls.fetch_add(1, Ordering::Relaxed);
913            self.last_filter_len
914                .store(filter_exprs.len(), Ordering::Relaxed);
915            vec![true; filter_exprs.len()]
916        }
917
918        fn set_logical_region(&mut self, logical_region: bool) {
919            self.properties.set_logical_region(logical_region);
920        }
921
922        fn set_query_load_region_id(&mut self, region_id: store_api::storage::RegionId) {
923            self.properties.set_query_load_region_id(region_id);
924        }
925    }
926
927    impl DisplayAs for RecordingScanner {
928        fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
929            write!(f, "RecordingScanner")
930        }
931    }
932
933    fn build_query_load_region_scan(
934        query_load_region_id: Option<RegionId>,
935    ) -> Arc<dyn ExecutionPlan> {
936        build_region_scan(query_load_region_id, None)
937    }
938
939    fn build_query_stat_counter_region_scan(
940        counters: RegionQueryStatCounters,
941    ) -> Arc<dyn ExecutionPlan> {
942        build_region_scan(None, Some(counters))
943    }
944
945    fn build_region_scan(
946        query_load_region_id: Option<RegionId>,
947        query_stat_counters: Option<RegionQueryStatCounters>,
948    ) -> Arc<dyn ExecutionPlan> {
949        let schema = Arc::new(datatypes::schema::Schema::new(vec![ColumnSchema::new(
950            "ts",
951            ConcreteDataType::timestamp_millisecond_datatype(),
952            false,
953        )]));
954
955        let mut metadata_builder = RegionMetadataBuilder::new(RegionId::new(1024, 1));
956        metadata_builder
957            .push_column_metadata(ColumnMetadata {
958                column_schema: ColumnSchema::new(
959                    "ts",
960                    ConcreteDataType::timestamp_millisecond_datatype(),
961                    false,
962                )
963                .with_time_index(true),
964                semantic_type: SemanticType::Timestamp,
965                column_id: 1,
966            })
967            .primary_key(vec![]);
968        let metadata = Arc::new(metadata_builder.build().unwrap());
969        let mut scanner = RecordingScanner::new(
970            schema,
971            metadata,
972            Arc::new(AtomicUsize::new(0)),
973            Arc::new(AtomicUsize::new(0)),
974        );
975        if let Some(region_id) = query_load_region_id {
976            scanner.set_query_load_region_id(region_id);
977        }
978        if let Some(counters) = query_stat_counters {
979            scanner.properties.set_query_stat_counters(counters);
980        }
981
982        Arc::new(RegionScanExec::new(Box::new(scanner), ScanRequest::default(), None).unwrap())
983    }
984
985    fn query_stat_counters_for_test() -> RegionQueryStatCounters {
986        RegionQueryStatCounters {
987            query_cpu_time: Arc::new(AtomicU64::new(0)),
988            query_scanned_bytes: Arc::new(AtomicU64::new(0)),
989        }
990    }
991
992    #[test]
993    fn query_load_region_id_ignores_scans_without_region_id() {
994        let query_load_region_id = RegionId::new(1024, 42);
995        let scan_without_region_id = build_query_load_region_scan(None);
996        let scan_with_region_id = build_query_load_region_scan(Some(query_load_region_id));
997        let on: JoinOn = vec![(
998            Arc::new(Column::new("ts", 0)) as Arc<dyn PhysicalExpr>,
999            Arc::new(Column::new("ts", 0)) as Arc<dyn PhysicalExpr>,
1000        )];
1001        let plan: Arc<dyn ExecutionPlan> = Arc::new(
1002            HashJoinExec::try_new(
1003                scan_without_region_id,
1004                scan_with_region_id,
1005                on,
1006                None,
1007                &JoinType::Inner,
1008                None,
1009                PartitionMode::CollectLeft,
1010                NullEquality::NullEqualsNull,
1011                false,
1012            )
1013            .unwrap(),
1014        );
1015
1016        assert_eq!(
1017            super::query_load_region_id(&plan),
1018            Some(query_load_region_id.as_u64())
1019        );
1020    }
1021
1022    #[test]
1023    fn query_stat_counters_returns_shared_counter_for_multi_scan_plan() {
1024        let counters = query_stat_counters_for_test();
1025        let left = build_query_stat_counter_region_scan(counters.clone());
1026        let right = build_query_stat_counter_region_scan(counters.clone());
1027        let on: JoinOn = vec![(
1028            Arc::new(Column::new("ts", 0)) as Arc<dyn PhysicalExpr>,
1029            Arc::new(Column::new("ts", 0)) as Arc<dyn PhysicalExpr>,
1030        )];
1031        let plan: Arc<dyn ExecutionPlan> = Arc::new(
1032            HashJoinExec::try_new(
1033                left,
1034                right,
1035                on,
1036                None,
1037                &JoinType::Inner,
1038                None,
1039                PartitionMode::CollectLeft,
1040                NullEquality::NullEqualsNull,
1041                false,
1042            )
1043            .unwrap(),
1044        );
1045
1046        let actual = super::query_stat_counters(&plan).unwrap();
1047        assert!(Arc::ptr_eq(
1048            &actual.query_cpu_time,
1049            &counters.query_cpu_time
1050        ));
1051        assert!(Arc::ptr_eq(
1052            &actual.query_scanned_bytes,
1053            &counters.query_scanned_bytes
1054        ));
1055    }
1056
1057    #[test]
1058    fn query_stat_counters_ignores_mixed_counter_plan() {
1059        let left = build_query_stat_counter_region_scan(query_stat_counters_for_test());
1060        let right = build_query_stat_counter_region_scan(query_stat_counters_for_test());
1061        let on: JoinOn = vec![(
1062            Arc::new(Column::new("ts", 0)) as Arc<dyn PhysicalExpr>,
1063            Arc::new(Column::new("ts", 0)) as Arc<dyn PhysicalExpr>,
1064        )];
1065        let plan: Arc<dyn ExecutionPlan> = Arc::new(
1066            HashJoinExec::try_new(
1067                left,
1068                right,
1069                on,
1070                None,
1071                &JoinType::Inner,
1072                None,
1073                PartitionMode::CollectLeft,
1074                NullEquality::NullEqualsNull,
1075                false,
1076            )
1077            .unwrap(),
1078        );
1079
1080        assert!(super::query_stat_counters(&plan).is_none());
1081    }
1082
1083    async fn create_test_engine() -> QueryEngineRef {
1084        let catalog_manager = catalog::memory::new_memory_catalog_manager().unwrap();
1085        let req = RegisterTableRequest {
1086            catalog: DEFAULT_CATALOG_NAME.to_string(),
1087            schema: DEFAULT_SCHEMA_NAME.to_string(),
1088            table_name: NUMBERS_TABLE_NAME.to_string(),
1089            table_id: NUMBERS_TABLE_ID,
1090            table: NumbersTable::table(NUMBERS_TABLE_ID),
1091        };
1092        catalog_manager.register_table_sync(req).unwrap();
1093
1094        QueryEngineFactory::new(
1095            catalog_manager,
1096            None,
1097            None,
1098            None,
1099            None,
1100            false,
1101            QueryOptions::default(),
1102        )
1103        .query_engine()
1104    }
1105
1106    #[tokio::test]
1107    async fn test_sql_to_plan() {
1108        let engine = create_test_engine().await;
1109        let sql = "select sum(number) from numbers limit 20";
1110
1111        let stmt = QueryLanguageParser::parse_sql(sql, &QueryContext::arc()).unwrap();
1112        let plan = engine
1113            .planner()
1114            .plan(&stmt, QueryContext::arc())
1115            .await
1116            .unwrap();
1117
1118        assert_eq!(
1119            plan.to_string(),
1120            r#"Limit: skip=0, fetch=20
1121  Projection: sum(numbers.number)
1122    Aggregate: groupBy=[[]], aggr=[[sum(numbers.number)]]
1123      TableScan: numbers"#
1124        );
1125    }
1126
1127    #[tokio::test]
1128    async fn test_purge_table_is_not_available_to_select() {
1129        let engine = create_test_engine().await;
1130        let stmt =
1131            QueryLanguageParser::parse_sql("select purge_table('numbers')", &QueryContext::arc())
1132                .unwrap();
1133
1134        assert!(
1135            engine
1136                .planner()
1137                .plan(&stmt, QueryContext::arc())
1138                .await
1139                .is_err()
1140        );
1141    }
1142
1143    #[tokio::test]
1144    async fn test_execute() {
1145        let engine = create_test_engine().await;
1146        let sql = "select sum(number) from numbers limit 20";
1147
1148        let stmt = QueryLanguageParser::parse_sql(sql, &QueryContext::arc()).unwrap();
1149        let plan = engine
1150            .planner()
1151            .plan(&stmt, QueryContext::arc())
1152            .await
1153            .unwrap();
1154
1155        let output = engine.execute(plan, QueryContext::arc()).await.unwrap();
1156
1157        match output.data {
1158            OutputData::Stream(recordbatch) => {
1159                let numbers = util::collect(recordbatch).await.unwrap();
1160                assert_eq!(1, numbers.len());
1161                assert_eq!(numbers[0].num_columns(), 1);
1162                assert_eq!(1, numbers[0].schema.num_columns());
1163                assert_eq!(
1164                    "sum(numbers.number)",
1165                    numbers[0].schema.column_schemas()[0].name
1166                );
1167
1168                let batch = &numbers[0];
1169                assert_eq!(1, batch.num_columns());
1170                assert_eq!(batch.column(0).len(), 1);
1171
1172                let expected = Arc::new(UInt64Array::from_iter_values([4950])) as ArrayRef;
1173                assert_eq!(batch.column(0), &expected);
1174            }
1175            _ => unreachable!(),
1176        }
1177    }
1178
1179    #[tokio::test]
1180    async fn test_read_table() {
1181        let engine = create_test_engine().await;
1182
1183        let engine = engine
1184            .as_any()
1185            .downcast_ref::<DatafusionQueryEngine>()
1186            .unwrap();
1187        let query_ctx = Arc::new(QueryContextBuilder::default().build());
1188        let table = engine
1189            .find_table(
1190                &ResolvedTableReference {
1191                    catalog: "greptime".into(),
1192                    schema: "public".into(),
1193                    table: "numbers".into(),
1194                },
1195                &query_ctx,
1196            )
1197            .await
1198            .unwrap();
1199
1200        let df = engine.read_table(table).unwrap();
1201        let df = df
1202            .select_columns(&["number"])
1203            .unwrap()
1204            .filter(col("number").lt(lit(10)))
1205            .unwrap();
1206        let batches = df.collect().await.unwrap();
1207        assert_eq!(1, batches.len());
1208        let batch = &batches[0];
1209
1210        assert_eq!(1, batch.num_columns());
1211        assert_eq!(batch.column(0).len(), 10);
1212
1213        assert_eq!(
1214            Helper::try_into_vector(batch.column(0)).unwrap(),
1215            Arc::new(UInt32Vector::from_slice([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) as VectorRef
1216        );
1217    }
1218
1219    #[tokio::test]
1220    async fn test_describe() {
1221        let engine = create_test_engine().await;
1222        let sql = "select sum(number) from numbers limit 20";
1223
1224        let stmt = QueryLanguageParser::parse_sql(sql, &QueryContext::arc()).unwrap();
1225
1226        let plan = engine
1227            .planner()
1228            .plan(&stmt, QueryContext::arc())
1229            .await
1230            .unwrap();
1231
1232        let DescribeResult { logical_plan } =
1233            engine.describe(plan, QueryContext::arc()).await.unwrap();
1234
1235        let schema: Schema = logical_plan.schema().clone().try_into().unwrap();
1236
1237        assert_eq!(
1238            schema.column_schemas()[0],
1239            ColumnSchema::new(
1240                "sum(numbers.number)",
1241                ConcreteDataType::uint64_datatype(),
1242                true
1243            )
1244        );
1245        assert_eq!(
1246            "Limit: skip=0, fetch=20\n  Projection: sum(numbers.number)\n    Aggregate: groupBy=[[]], aggr=[[sum(numbers.number)]]\n      TableScan: numbers",
1247            format!("{}", logical_plan.display_indent())
1248        );
1249    }
1250
1251    #[tokio::test]
1252    async fn test_topk_dynamic_filter_pushdown_reaches_region_scan() {
1253        let engine = create_test_engine().await;
1254        let engine = engine
1255            .as_any()
1256            .downcast_ref::<DatafusionQueryEngine>()
1257            .unwrap();
1258        let engine_ctx = engine.engine_context(QueryContext::arc());
1259        let state = engine_ctx.state();
1260
1261        let schema = Arc::new(datatypes::schema::Schema::new(vec![ColumnSchema::new(
1262            "ts",
1263            ConcreteDataType::timestamp_millisecond_datatype(),
1264            false,
1265        )]));
1266
1267        let mut metadata_builder = RegionMetadataBuilder::new(RegionId::new(1024, 1));
1268        metadata_builder
1269            .push_column_metadata(ColumnMetadata {
1270                column_schema: ColumnSchema::new(
1271                    "ts",
1272                    ConcreteDataType::timestamp_millisecond_datatype(),
1273                    false,
1274                )
1275                .with_time_index(true),
1276                semantic_type: SemanticType::Timestamp,
1277                column_id: 1,
1278            })
1279            .primary_key(vec![]);
1280        let metadata = Arc::new(metadata_builder.build().unwrap());
1281
1282        let update_calls = Arc::new(AtomicUsize::new(0));
1283        let last_filter_len = Arc::new(AtomicUsize::new(0));
1284        let scanner = Box::new(RecordingScanner::new(
1285            schema,
1286            metadata,
1287            update_calls.clone(),
1288            last_filter_len.clone(),
1289        ));
1290        let scan = Arc::new(RegionScanExec::new(scanner, ScanRequest::default(), None).unwrap());
1291
1292        let sort_expr = PhysicalSortExpr {
1293            expr: Arc::new(Column::new("ts", 0)),
1294            options: SortOptions {
1295                descending: true,
1296                ..Default::default()
1297            },
1298        };
1299        let partition_ranges: Vec<Vec<PartitionRange>> = vec![vec![]];
1300        let mut plan: Arc<dyn ExecutionPlan> =
1301            Arc::new(PartSortExec::try_new(sort_expr, Some(3), partition_ranges, scan).unwrap());
1302
1303        for optimizer in state.physical_optimizers() {
1304            plan = optimizer.optimize(plan, state.config_options()).unwrap();
1305        }
1306
1307        assert!(update_calls.load(Ordering::Relaxed) > 0);
1308        assert!(last_filter_len.load(Ordering::Relaxed) > 0);
1309    }
1310
1311    #[tokio::test]
1312    async fn test_join_dynamic_filter_pushdown_reaches_region_scan() {
1313        let engine = create_test_engine().await;
1314        let engine = engine
1315            .as_any()
1316            .downcast_ref::<DatafusionQueryEngine>()
1317            .unwrap();
1318        let engine_ctx = engine.engine_context(QueryContext::arc());
1319        let state = engine_ctx.state();
1320
1321        assert!(
1322            state
1323                .config_options()
1324                .optimizer
1325                .enable_join_dynamic_filter_pushdown
1326        );
1327
1328        let schema = Arc::new(datatypes::schema::Schema::new(vec![ColumnSchema::new(
1329            "ts",
1330            ConcreteDataType::timestamp_millisecond_datatype(),
1331            false,
1332        )]));
1333
1334        let mut left_metadata_builder = RegionMetadataBuilder::new(RegionId::new(2048, 1));
1335        left_metadata_builder
1336            .push_column_metadata(ColumnMetadata {
1337                column_schema: ColumnSchema::new(
1338                    "ts",
1339                    ConcreteDataType::timestamp_millisecond_datatype(),
1340                    false,
1341                )
1342                .with_time_index(true),
1343                semantic_type: SemanticType::Timestamp,
1344                column_id: 1,
1345            })
1346            .primary_key(vec![]);
1347        let left_metadata = Arc::new(left_metadata_builder.build().unwrap());
1348
1349        let mut right_metadata_builder = RegionMetadataBuilder::new(RegionId::new(2048, 2));
1350        right_metadata_builder
1351            .push_column_metadata(ColumnMetadata {
1352                column_schema: ColumnSchema::new(
1353                    "ts",
1354                    ConcreteDataType::timestamp_millisecond_datatype(),
1355                    false,
1356                )
1357                .with_time_index(true),
1358                semantic_type: SemanticType::Timestamp,
1359                column_id: 1,
1360            })
1361            .primary_key(vec![]);
1362        let right_metadata = Arc::new(right_metadata_builder.build().unwrap());
1363
1364        let left_update_calls = Arc::new(AtomicUsize::new(0));
1365        let left_last_filter_len = Arc::new(AtomicUsize::new(0));
1366        let right_update_calls = Arc::new(AtomicUsize::new(0));
1367        let right_last_filter_len = Arc::new(AtomicUsize::new(0));
1368
1369        let left_scan = Arc::new(
1370            RegionScanExec::new(
1371                Box::new(RecordingScanner::new(
1372                    schema.clone(),
1373                    left_metadata,
1374                    left_update_calls.clone(),
1375                    left_last_filter_len.clone(),
1376                )),
1377                ScanRequest::default(),
1378                None,
1379            )
1380            .unwrap(),
1381        );
1382        let right_scan = Arc::new(
1383            RegionScanExec::new(
1384                Box::new(RecordingScanner::new(
1385                    schema,
1386                    right_metadata,
1387                    right_update_calls.clone(),
1388                    right_last_filter_len.clone(),
1389                )),
1390                ScanRequest::default(),
1391                None,
1392            )
1393            .unwrap(),
1394        );
1395
1396        let on: JoinOn = vec![(
1397            Arc::new(Column::new("ts", 0)) as Arc<dyn PhysicalExpr>,
1398            Arc::new(Column::new("ts", 0)) as Arc<dyn PhysicalExpr>,
1399        )];
1400
1401        let mut plan: Arc<dyn ExecutionPlan> = Arc::new(
1402            HashJoinExec::try_new(
1403                left_scan,
1404                right_scan,
1405                on,
1406                None,
1407                &JoinType::Inner,
1408                None,
1409                PartitionMode::CollectLeft,
1410                NullEquality::NullEqualsNull,
1411                false,
1412            )
1413            .unwrap(),
1414        );
1415
1416        for optimizer in state.physical_optimizers() {
1417            plan = optimizer.optimize(plan, state.config_options()).unwrap();
1418        }
1419
1420        assert!(left_update_calls.load(Ordering::Relaxed) > 0);
1421        assert_eq!(0, left_last_filter_len.load(Ordering::Relaxed));
1422        assert!(right_update_calls.load(Ordering::Relaxed) > 0);
1423        assert!(right_last_filter_len.load(Ordering::Relaxed) > 0);
1424    }
1425}