Skip to main content

query/dist_plan/
merge_scan.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
15use std::any::Any;
16use std::sync::{Arc, Mutex};
17use std::time::Duration;
18
19use ahash::{HashMap, HashSet};
20use arrow_schema::{Schema as ArrowSchema, SchemaRef as ArrowSchemaRef, SortOptions};
21use async_stream::stream;
22use common_catalog::parse_catalog_and_schema_from_db_string;
23use common_plugins::GREPTIME_EXEC_READ_COST;
24use common_query::request::QueryRequest;
25use common_recordbatch::adapter::{RecordBatchMetrics, region_scan_output_bytes};
26use common_telemetry::tracing_context::TracingContext;
27use datafusion::execution::{SessionState, TaskContext};
28use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
29use datafusion::physical_plan::filter_pushdown::{
30    ChildPushdownResult, FilterPushdownPhase, FilterPushdownPropagation, PushedDown,
31};
32use datafusion::physical_plan::metrics::{
33    Count, ExecutionPlanMetricsSet, Gauge, MetricBuilder, MetricsSet, Time,
34};
35use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
36use datafusion::physical_plan::{
37    DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties,
38    SendableRecordBatchStream,
39};
40use datafusion_common::{Column as ColumnExpr, DataFusionError, Result};
41use datafusion_expr::{Expr, Extension, LogicalPlan, UserDefinedLogicalNodeCore};
42use datafusion_physical_expr::expressions::Column;
43use datafusion_physical_expr::{Distribution, EquivalenceProperties, PhysicalSortExpr};
44use futures_util::StreamExt;
45use greptime_proto::v1::region::RegionRequestHeader;
46use meter_core::data::ReadItem;
47use meter_macros::read_meter;
48use session::context::QueryContextRef;
49use store_api::metrics::{REGION_QUERY_CPU_TIME, REGION_QUERY_SCANNED_BYTES};
50use store_api::storage::RegionId;
51use table::table_name::TableName;
52use tokio::time::Instant;
53use tracing::{Instrument, Span};
54
55use crate::dist_plan::analyzer::AliasMapping;
56use crate::dist_plan::analyzer::utils::patch_batch_timezone;
57use crate::dist_plan::dyn_filter_bridge::{
58    CapturedDynFilter, capture_remote_dyn_filters_for_pushdown,
59    query_context_with_initial_dyn_filter_regs, register_dyn_filters_for_region,
60};
61use crate::dist_plan::{RemoteDynFilterProducerId, RemoteDynFilterRegistryLease};
62use crate::metrics::{MERGE_SCAN_ERRORS_TOTAL, MERGE_SCAN_POLL_ELAPSED, MERGE_SCAN_REGIONS};
63use crate::options::{FlowQueryExtensions, remote_dyn_filter_pushdown_enabled_from_extensions};
64use crate::query_engine::QueryEngineState;
65use crate::region_query::RegionQueryHandlerRef;
66
67fn query_engine_state_from_task_context(context: &TaskContext) -> Option<Arc<QueryEngineState>> {
68    context.session_config().get_extension()
69}
70
71fn remote_dyn_filter_enabled(query_ctx: &QueryContextRef) -> Result<bool> {
72    remote_dyn_filter_pushdown_enabled_from_extensions(&query_ctx.extensions())
73        .map_err(|err| DataFusionError::External(Box::new(err)))
74}
75
76fn acquire_remote_dyn_filter_registry_lease(
77    context: &TaskContext,
78    query_ctx: &QueryContextRef,
79    captured_dyn_filters: &[CapturedDynFilter],
80) -> Option<RemoteDynFilterRegistryLease> {
81    if captured_dyn_filters.is_empty() {
82        return None;
83    }
84
85    let query_id = query_ctx.remote_query_id_value()?;
86    let query_engine_state = query_engine_state_from_task_context(context)?;
87    Some(
88        query_engine_state
89            .dyn_filter_registry_manager()
90            .acquire_lease(query_id),
91    )
92}
93
94fn query_context_for_remote_dyn_filter_region(
95    query_ctx: &QueryContextRef,
96    region_id: RegionId,
97    remote_dyn_filter_registry_lease: Option<&RemoteDynFilterRegistryLease>,
98    captured_dyn_filters: &[CapturedDynFilter],
99) -> session::context::QueryContext {
100    if let Some(remote_dyn_filter_registry_lease) = remote_dyn_filter_registry_lease {
101        register_dyn_filters_for_region(
102            remote_dyn_filter_registry_lease.registry(),
103            region_id,
104            captured_dyn_filters,
105        );
106    }
107
108    query_context_with_initial_dyn_filter_regs(query_ctx, region_id, captured_dyn_filters)
109}
110
111#[derive(Debug, Hash, PartialOrd, PartialEq, Eq, Clone)]
112pub struct MergeScanLogicalPlan {
113    /// In logical plan phase it only contains one input
114    input: LogicalPlan,
115    /// If this plan is a placeholder
116    is_placeholder: bool,
117    partition_cols: AliasMapping,
118    /// Assigned after dist-plan rewriting so rewriters only deal with plan shape.
119    remote_dyn_filter_producer_id: Option<RemoteDynFilterProducerId>,
120}
121
122impl UserDefinedLogicalNodeCore for MergeScanLogicalPlan {
123    fn name(&self) -> &str {
124        Self::name()
125    }
126
127    // Prevent further optimization.
128    // The input can be retrieved by `self.input()`
129    fn inputs(&self) -> Vec<&LogicalPlan> {
130        vec![]
131    }
132
133    fn schema(&self) -> &datafusion_common::DFSchemaRef {
134        self.input.schema()
135    }
136
137    // Prevent further optimization
138    fn expressions(&self) -> Vec<datafusion_expr::Expr> {
139        vec![]
140    }
141
142    fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
143        write!(
144            f,
145            "MergeScan [is_placeholder={}, remote_input=[\n{}\n]]",
146            self.is_placeholder, self.input
147        )
148    }
149
150    fn with_exprs_and_inputs(
151        &self,
152        _exprs: Vec<datafusion::prelude::Expr>,
153        _inputs: Vec<LogicalPlan>,
154    ) -> Result<Self> {
155        Ok(self.clone())
156    }
157}
158
159impl MergeScanLogicalPlan {
160    pub fn new(input: LogicalPlan, is_placeholder: bool, partition_cols: AliasMapping) -> Self {
161        Self {
162            input,
163            is_placeholder,
164            partition_cols,
165            remote_dyn_filter_producer_id: None,
166        }
167    }
168
169    pub(crate) fn with_remote_dyn_filter_producer_id(
170        mut self,
171        remote_dyn_filter_producer_id: RemoteDynFilterProducerId,
172    ) -> Self {
173        self.remote_dyn_filter_producer_id = Some(remote_dyn_filter_producer_id);
174        self
175    }
176
177    pub fn name() -> &'static str {
178        "MergeScan"
179    }
180
181    /// Create a [LogicalPlan::Extension] node from this merge scan plan
182    pub fn into_logical_plan(self) -> LogicalPlan {
183        LogicalPlan::Extension(Extension {
184            node: Arc::new(self),
185        })
186    }
187
188    pub fn is_placeholder(&self) -> bool {
189        self.is_placeholder
190    }
191
192    pub fn input(&self) -> &LogicalPlan {
193        &self.input
194    }
195
196    pub fn partition_cols(&self) -> &AliasMapping {
197        &self.partition_cols
198    }
199
200    pub fn remote_dyn_filter_producer_id(&self) -> Option<RemoteDynFilterProducerId> {
201        self.remote_dyn_filter_producer_id
202    }
203}
204
205#[derive(Clone)]
206pub struct MergeScanExec {
207    table: TableName,
208    regions: Vec<RegionId>,
209    plan: LogicalPlan,
210    arrow_schema: ArrowSchemaRef,
211    region_query_handler: RegionQueryHandlerRef,
212    metric: ExecutionPlanMetricsSet,
213    properties: Arc<PlanProperties>,
214    /// Metrics from sub stages
215    sub_stage_metrics: Arc<Mutex<HashMap<RegionId, RecordBatchMetrics>>>,
216    /// Metrics for each partition
217    partition_metrics: Arc<Mutex<HashMap<usize, PartitionMetrics>>>,
218    query_ctx: QueryContextRef,
219    /// Optional because RDF must fail open: missing ids skip RDF but keep normal query execution.
220    remote_dyn_filter_producer_id: Option<RemoteDynFilterProducerId>,
221    captured_remote_dyn_filters: Arc<Mutex<Vec<CapturedDynFilter>>>,
222    target_partition: usize,
223    partition_cols: AliasMapping,
224    enable_per_region_metrics: bool,
225}
226
227impl std::fmt::Debug for MergeScanExec {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        f.debug_struct("MergeScanExec")
230            .field("table", &self.table)
231            .field("regions", &self.regions)
232            .field("plan", &self.plan)
233            .finish()
234    }
235}
236
237impl MergeScanExec {
238    #[allow(clippy::too_many_arguments)]
239    pub fn new(
240        session_state: &SessionState,
241        table: TableName,
242        regions: Vec<RegionId>,
243        plan: LogicalPlan,
244        arrow_schema: &ArrowSchema,
245        region_query_handler: RegionQueryHandlerRef,
246        query_ctx: QueryContextRef,
247        target_partition: usize,
248        partition_cols: AliasMapping,
249        remote_dyn_filter_producer_id: Option<RemoteDynFilterProducerId>,
250        enable_per_region_metrics: bool,
251    ) -> Result<Self> {
252        // TODO(CookiePieWw): Initially we removed the metadata from the schema in #2000, but we have to
253        // keep it for #4619 to identify json type in src/datatypes/src/schema/column_schema.rs.
254        // Reconsider if it's possible to remove it.
255        let arrow_schema = Arc::new(arrow_schema.clone());
256
257        // States the output ordering of the plan.
258        //
259        // When the input plan is a sort, we can use the sort ordering as the output ordering
260        // if the target partition is greater than the number of regions, which means we won't
261        // break the ordering on merging (of MergeScan).
262        //
263        // Otherwise, we need to use the default ordering.
264        let eq_properties = if let LogicalPlan::Sort(sort) = &plan
265            && target_partition >= regions.len()
266        {
267            let lex_ordering = sort
268                .expr
269                .iter()
270                .map(|sort_expr| {
271                    let physical_expr = session_state
272                        .create_physical_expr(sort_expr.expr.clone(), plan.schema())?;
273                    Ok(PhysicalSortExpr::new(
274                        physical_expr,
275                        SortOptions {
276                            descending: !sort_expr.asc,
277                            nulls_first: sort_expr.nulls_first,
278                        },
279                    ))
280                })
281                .collect::<Result<Vec<_>>>()?;
282            EquivalenceProperties::new_with_orderings(arrow_schema.clone(), vec![lex_ordering])
283        } else {
284            EquivalenceProperties::new(arrow_schema.clone())
285        };
286
287        let partition_exprs = partition_cols
288            .iter()
289            .filter_map(|col| {
290                if let Some(first_alias) = col.1.first() {
291                    session_state
292                        .create_physical_expr(
293                            Expr::Column(ColumnExpr::new_unqualified(
294                                first_alias.name().to_string(),
295                            )),
296                            plan.schema(),
297                        )
298                        .ok()
299                } else {
300                    None
301                }
302            })
303            .collect();
304        let partitioning = Partitioning::Hash(partition_exprs, target_partition);
305
306        let properties = Arc::new(PlanProperties::new(
307            eq_properties,
308            partitioning,
309            EmissionType::Incremental,
310            Boundedness::Bounded,
311        ));
312        Ok(Self {
313            table,
314            regions,
315            plan,
316            arrow_schema,
317            region_query_handler,
318            metric: ExecutionPlanMetricsSet::new(),
319            sub_stage_metrics: Arc::default(),
320            partition_metrics: Arc::default(),
321            properties,
322            query_ctx,
323            remote_dyn_filter_producer_id,
324            captured_remote_dyn_filters: Arc::default(),
325            target_partition,
326            partition_cols,
327            enable_per_region_metrics,
328        })
329    }
330
331    pub fn to_stream(
332        &self,
333        context: Arc<TaskContext>,
334        partition: usize,
335    ) -> Result<SendableRecordBatchStream> {
336        // prepare states to move
337        let regions = self.regions.clone();
338        let region_query_handler = self.region_query_handler.clone();
339        let metric = MergeScanMetric::new(&self.metric);
340        let arrow_schema = self.arrow_schema.clone();
341        let query_ctx = self.query_ctx.clone();
342        let sub_stage_metrics_moved = self.sub_stage_metrics.clone();
343        let partition_metrics_moved = self.partition_metrics.clone();
344        let plan = self.plan.clone();
345        let target_partition = self.target_partition;
346        let remote_dyn_filter_enabled = remote_dyn_filter_enabled(&self.query_ctx)?;
347        let captured_remote_dyn_filters = if remote_dyn_filter_enabled {
348            self.captured_remote_dyn_filters()
349        } else {
350            Vec::new()
351        };
352        let dbname = context.task_id().unwrap_or_default();
353        let tracing_context = TracingContext::from_json(context.session_id().as_str());
354        let current_channel = self.query_ctx.channel();
355        let read_preference = self.query_ctx.read_preference();
356        let explain_verbose = self.query_ctx.explain_verbose();
357        let remote_dyn_filter_registry_lease = acquire_remote_dyn_filter_registry_lease(
358            context.as_ref(),
359            &query_ctx,
360            &captured_remote_dyn_filters,
361        );
362
363        let stream = Box::pin(stream!({
364            let remote_dyn_filter_registry_lease = remote_dyn_filter_registry_lease;
365            // only report metrics once for each MergeScan
366            if partition == 0 {
367                MERGE_SCAN_REGIONS.observe(regions.len() as f64);
368            }
369
370            let _finish_timer = metric.finish_time().timer();
371            let mut ready_timer = metric.ready_time().timer();
372            let mut first_consume_timer = Some(metric.first_consume_time().timer());
373
374            // Per-partition timings, scoped to this partition's stream for `EXPLAIN VERBOSE`.
375            let partition_start = Instant::now();
376            let mut partition_ready_time: Option<Duration> = None;
377            let mut partition_first_consume_time: Option<Duration> = None;
378
379            for region_id in regions
380                .iter()
381                .skip(partition)
382                .step_by(target_partition)
383                .copied()
384            {
385                let region_span = tracing_context.attach(tracing::info_span!(
386                    parent: &Span::current(),
387                    "merge_scan_region",
388                    region_id = %region_id,
389                    partition = partition
390                ));
391                let region_query_ctx = query_context_for_remote_dyn_filter_region(
392                    &query_ctx,
393                    region_id,
394                    remote_dyn_filter_registry_lease.as_ref(),
395                    &captured_remote_dyn_filters,
396                );
397                let request = QueryRequest {
398                    header: Some(RegionRequestHeader {
399                        tracing_context: tracing_context.to_w3c(),
400                        dbname: dbname.clone(),
401                        query_context: Some((&region_query_ctx).into()),
402                    }),
403                    region_id,
404                    plan: plan.clone(),
405                };
406                let region_start = Instant::now();
407                let do_get_start = Instant::now();
408
409                if explain_verbose {
410                    common_telemetry::info!(
411                        "Merge scan one region, partition: {}, region_id: {}",
412                        partition,
413                        region_id
414                    );
415                }
416
417                let mut stream = region_query_handler
418                    .do_get(read_preference, request)
419                    .instrument(region_span.clone())
420                    .await
421                    .map_err(|e| {
422                        MERGE_SCAN_ERRORS_TOTAL.inc();
423                        DataFusionError::External(Box::new(e))
424                    })?;
425                let do_get_cost = do_get_start.elapsed();
426
427                if let Some(remote_dyn_filter_registry_lease) =
428                    remote_dyn_filter_registry_lease.as_ref()
429                {
430                    remote_dyn_filter_registry_lease
431                        .ensure_fanout_task(region_query_handler.clone());
432                }
433
434                ready_timer.stop();
435                if partition_ready_time.is_none() {
436                    partition_ready_time = Some(partition_start.elapsed());
437                }
438
439                let mut poll_duration = Duration::ZERO;
440                let mut poll_timer = Instant::now();
441                while let Some(batch) = stream.next().instrument(region_span.clone()).await {
442                    let poll_elapsed = poll_timer.elapsed();
443                    poll_duration += poll_elapsed;
444
445                    let batch = batch.map_err(|e| DataFusionError::External(Box::new(e)))?;
446                    let batch = patch_batch_timezone(
447                        arrow_schema.clone(),
448                        batch.into_df_record_batch().columns().to_vec(),
449                    )?;
450                    metric.record_output_batch_rows(batch.num_rows());
451                    if let Some(mut first_consume_timer) = first_consume_timer.take() {
452                        first_consume_timer.stop();
453                        partition_first_consume_time = Some(partition_start.elapsed());
454                    }
455
456                    if let Some(metrics) = stream.metrics() {
457                        let mut sub_stage_metrics = sub_stage_metrics_moved.lock().unwrap();
458                        sub_stage_metrics.insert(region_id, metrics);
459                    }
460
461                    yield Ok(batch);
462                    // reset poll timer
463                    poll_timer = Instant::now();
464                }
465                // Also stop on an exhausted stream that yielded no batch. The `take()`
466                // guard ensures it only records once, on the first such region.
467                if let Some(mut first_consume_timer) = first_consume_timer.take() {
468                    first_consume_timer.stop();
469                    partition_first_consume_time = Some(partition_start.elapsed());
470                }
471                let total_cost = region_start.elapsed();
472
473                // Record region metrics and push to global partition_metrics
474                let region_metrics = RegionMetrics {
475                    region_id,
476                    poll_duration,
477                    do_get_cost,
478                    total_cost,
479                };
480
481                // Push RegionMetrics to global partition_metrics immediately after scanning this region
482                {
483                    let mut partition_metrics_guard = partition_metrics_moved.lock().unwrap();
484                    let partition_metrics = partition_metrics_guard
485                        .entry(partition)
486                        .or_insert_with(|| PartitionMetrics::new(partition, explain_verbose));
487                    partition_metrics.add_region_metrics(region_metrics);
488                }
489
490                if explain_verbose {
491                    common_telemetry::info!(
492                        "Merge scan finish one region, partition: {}, region_id: {}, poll_duration: {:?}, first_consume: {}, do_get_cost: {:?}",
493                        partition,
494                        region_id,
495                        poll_duration,
496                        metric.first_consume_time(),
497                        do_get_cost
498                    );
499                }
500
501                // process metrics after all data is drained.
502                if let Some(metrics) = stream.metrics() {
503                    let load = region_scan_load(&metrics);
504                    let (c, s) = parse_catalog_and_schema_from_db_string(&dbname);
505                    let value = read_meter!(c, s, load, current_channel as u8);
506                    metric.record_greptime_exec_cost(value as usize);
507
508                    // record metrics from sub sgates
509                    let mut sub_stage_metrics = sub_stage_metrics_moved.lock().unwrap();
510                    sub_stage_metrics.insert(region_id, metrics);
511                }
512
513                MERGE_SCAN_POLL_ELAPSED.observe(poll_duration.as_secs_f64());
514            }
515
516            // Stop the global timers for partitions with no region, otherwise they keep
517            // running until drop and inflate the shared metrics. No-op otherwise.
518            ready_timer.stop();
519            if let Some(mut first_consume_timer) = first_consume_timer.take() {
520                first_consume_timer.stop();
521            }
522
523            // Finish partition metrics and log results
524            let partition_finish_time = partition_start.elapsed();
525            {
526                let mut partition_metrics_guard = partition_metrics_moved.lock().unwrap();
527                if let Some(partition_metrics) = partition_metrics_guard.get_mut(&partition) {
528                    partition_metrics.set_timings(
529                        partition_ready_time.unwrap_or_default(),
530                        partition_first_consume_time.unwrap_or_default(),
531                        partition_finish_time,
532                    );
533                    partition_metrics.finish();
534                }
535            }
536        }));
537
538        Ok(Box::pin(RecordBatchStreamAdapter::new(
539            self.arrow_schema.clone(),
540            stream,
541        )))
542    }
543
544    pub fn try_with_new_distribution(&self, distribution: Distribution) -> Option<Self> {
545        let Distribution::HashPartitioned(hash_exprs) = distribution else {
546            // not applicable
547            return None;
548        };
549
550        if let Partitioning::Hash(curr_dist, _) = &self.properties.partitioning
551            && curr_dist == &hash_exprs
552        {
553            // No need to change the distribution
554            return None;
555        }
556
557        let all_partition_col_aliases: HashSet<_> = self
558            .partition_cols
559            .values()
560            .flat_map(|aliases| aliases.iter().map(|c| c.name()))
561            .collect();
562        let overlaps: Vec<_> = hash_exprs
563            .iter()
564            .filter(|expr| {
565                expr.as_any()
566                    .downcast_ref::<Column>()
567                    .is_some_and(|col_expr| all_partition_col_aliases.contains(col_expr.name()))
568            })
569            .cloned()
570            .collect();
571
572        if overlaps.is_empty() {
573            return None;
574        }
575
576        Some(Self {
577            table: self.table.clone(),
578            regions: self.regions.clone(),
579            plan: self.plan.clone(),
580            arrow_schema: self.arrow_schema.clone(),
581            region_query_handler: self.region_query_handler.clone(),
582            metric: self.metric.clone(),
583            properties: Arc::new(PlanProperties::new(
584                self.properties.eq_properties.clone(),
585                Partitioning::Hash(overlaps, self.target_partition),
586                self.properties.emission_type,
587                self.properties.boundedness,
588            )),
589            sub_stage_metrics: self.sub_stage_metrics.clone(),
590            partition_metrics: self.partition_metrics.clone(),
591            query_ctx: self.query_ctx.clone(),
592            remote_dyn_filter_producer_id: self.remote_dyn_filter_producer_id,
593            captured_remote_dyn_filters: self.captured_remote_dyn_filters.clone(),
594            target_partition: self.target_partition,
595            partition_cols: self.partition_cols.clone(),
596            enable_per_region_metrics: self.enable_per_region_metrics,
597        })
598    }
599
600    fn captured_remote_dyn_filters(&self) -> Vec<CapturedDynFilter> {
601        self.captured_remote_dyn_filters.lock().unwrap().clone()
602    }
603
604    pub fn sub_stage_metrics(&self) -> Vec<RecordBatchMetrics> {
605        self.sub_stage_metrics
606            .lock()
607            .unwrap()
608            .values()
609            .cloned()
610            .collect()
611    }
612
613    pub fn regions(&self) -> &[RegionId] {
614        &self.regions
615    }
616
617    pub fn is_flow_sink_scan(&self) -> bool {
618        let Some(sink_table_id) =
619            FlowQueryExtensions::parse_flow_extensions(&self.query_ctx.extensions())
620                .ok()
621                .flatten()
622                .and_then(|extensions| extensions.sink_table_id)
623        else {
624            return false;
625        };
626
627        !self.regions.is_empty()
628            && self
629                .regions
630                .iter()
631                .all(|region_id| region_id.table_id() == sink_table_id)
632    }
633
634    pub fn partition_count(&self) -> usize {
635        self.target_partition
636    }
637
638    pub fn region_count(&self) -> usize {
639        self.regions.len()
640    }
641
642    fn partition_metrics(&self) -> Vec<PartitionMetrics> {
643        self.partition_metrics
644            .lock()
645            .unwrap()
646            .values()
647            .cloned()
648            .collect()
649    }
650}
651
652#[cfg(test)]
653impl MergeScanExec {
654    fn remote_dyn_filter_producer_id(&self) -> Option<RemoteDynFilterProducerId> {
655        self.remote_dyn_filter_producer_id
656    }
657}
658
659/// Metrics for a region of a partition.
660#[derive(Debug, Clone)]
661struct RegionMetrics {
662    region_id: RegionId,
663    poll_duration: Duration,
664    do_get_cost: Duration,
665    /// Total cost to scan the region.
666    total_cost: Duration,
667}
668
669/// Metrics for a partition of a MergeScanExec.
670#[derive(Debug, Clone)]
671struct PartitionMetrics {
672    partition: usize,
673    region_metrics: Vec<RegionMetrics>,
674    total_poll_duration: Duration,
675    total_do_get_cost: Duration,
676    total_regions: usize,
677    /// Time until this partition's scan is ready to emit data.
678    ready_time: Duration,
679    /// Time until this partition's first stream poll resolves (a batch or exhausted).
680    first_consume_time: Duration,
681    /// Time until this partition's scan finishes execution.
682    finish_time: Duration,
683    explain_verbose: bool,
684    finished: bool,
685}
686
687impl PartitionMetrics {
688    fn new(partition: usize, explain_verbose: bool) -> Self {
689        Self {
690            partition,
691            region_metrics: Vec::new(),
692            total_poll_duration: Duration::ZERO,
693            total_do_get_cost: Duration::ZERO,
694            total_regions: 0,
695            ready_time: Duration::ZERO,
696            first_consume_time: Duration::ZERO,
697            finish_time: Duration::ZERO,
698            explain_verbose,
699            finished: false,
700        }
701    }
702
703    fn add_region_metrics(&mut self, region_metrics: RegionMetrics) {
704        self.total_poll_duration += region_metrics.poll_duration;
705        self.total_do_get_cost += region_metrics.do_get_cost;
706        self.total_regions += 1;
707        self.region_metrics.push(region_metrics);
708    }
709
710    /// Set the per-partition timings captured during streaming.
711    fn set_timings(
712        &mut self,
713        ready_time: Duration,
714        first_consume_time: Duration,
715        finish_time: Duration,
716    ) {
717        self.ready_time = ready_time;
718        self.first_consume_time = first_consume_time;
719        self.finish_time = finish_time;
720    }
721
722    /// Finish the partition metrics and log the results.
723    fn finish(&mut self) {
724        if self.finished {
725            return;
726        }
727        self.finished = true;
728        self.log_metrics();
729    }
730
731    /// Log partition metrics based on explain_verbose level.
732    fn log_metrics(&self) {
733        if self.explain_verbose {
734            common_telemetry::info!(
735                "MergeScan partition {} finished: {} regions, total_poll_duration: {:?}, total_do_get_cost: {:?}, ready_time: {:?}, first_consume_time: {:?}, finish_time: {:?}",
736                self.partition,
737                self.total_regions,
738                self.total_poll_duration,
739                self.total_do_get_cost,
740                self.ready_time,
741                self.first_consume_time,
742                self.finish_time
743            );
744        } else {
745            common_telemetry::debug!(
746                "MergeScan partition {} finished: {} regions, total_poll_duration: {:?}, total_do_get_cost: {:?}, ready_time: {:?}, first_consume_time: {:?}, finish_time: {:?}",
747                self.partition,
748                self.total_regions,
749                self.total_poll_duration,
750                self.total_do_get_cost,
751                self.ready_time,
752                self.first_consume_time,
753                self.finish_time
754            );
755        }
756    }
757}
758
759impl Drop for PartitionMetrics {
760    fn drop(&mut self) {
761        if !self.finished {
762            self.log_metrics();
763        }
764    }
765}
766
767impl ExecutionPlan for MergeScanExec {
768    fn as_any(&self) -> &dyn Any {
769        self
770    }
771
772    fn schema(&self) -> ArrowSchemaRef {
773        self.arrow_schema.clone()
774    }
775
776    fn properties(&self) -> &Arc<PlanProperties> {
777        &self.properties
778    }
779
780    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
781        vec![]
782    }
783
784    // DataFusion will swap children unconditionally.
785    // But since this node is leaf node, it's safe to just return self.
786    fn with_new_children(
787        self: Arc<Self>,
788        _children: Vec<Arc<dyn ExecutionPlan>>,
789    ) -> Result<Arc<dyn ExecutionPlan>> {
790        Ok(self.clone())
791    }
792
793    fn handle_child_pushdown_result(
794        &self,
795        _phase: FilterPushdownPhase,
796        child_pushdown_result: ChildPushdownResult,
797        _config: &datafusion::config::ConfigOptions,
798    ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
799        let parent_filters = child_pushdown_result
800            .parent_filters
801            .into_iter()
802            .map(|filter| filter.filter)
803            .collect::<Vec<_>>();
804
805        if !remote_dyn_filter_enabled(&self.query_ctx)? {
806            // Reject remote pushdown instead of pretending success: this keeps
807            // DataFusion/local dynamic filter semantics intact while disabling
808            // only FE -> DN remote dynamic filter propagation.
809            self.captured_remote_dyn_filters.lock().unwrap().clear();
810            let new_self = Arc::new(self.clone());
811
812            return Ok(FilterPushdownPropagation {
813                filters: parent_filters.into_iter().map(|_| PushedDown::No).collect(),
814                updated_node: Some(new_self),
815            });
816        }
817
818        let Some(remote_dyn_filter_producer_id) = self.remote_dyn_filter_producer_id else {
819            // Missing RDF identity disables only RDF, not normal execution.
820            common_telemetry::warn!(
821                "MergeScan remote dynamic filter producer id is not assigned; skipping remote dynamic filter pushdown"
822            );
823            self.captured_remote_dyn_filters.lock().unwrap().clear();
824            let new_self = Arc::new(self.clone());
825
826            return Ok(FilterPushdownPropagation {
827                filters: parent_filters.into_iter().map(|_| PushedDown::No).collect(),
828                updated_node: Some(new_self),
829            });
830        };
831        let remote_dyn_filter_pushdown =
832            capture_remote_dyn_filters_for_pushdown(remote_dyn_filter_producer_id, parent_filters);
833        *self.captured_remote_dyn_filters.lock().unwrap() =
834            remote_dyn_filter_pushdown.captured_dyn_filters;
835        let new_self = Arc::new(self.clone());
836
837        Ok(FilterPushdownPropagation {
838            filters: remote_dyn_filter_pushdown
839                .pushed_down
840                .into_iter()
841                .map(|pushdown_ready| {
842                    if pushdown_ready {
843                        PushedDown::Yes
844                    } else {
845                        PushedDown::No
846                    }
847                })
848                .collect(),
849            updated_node: Some(new_self),
850        })
851    }
852
853    fn execute(
854        &self,
855        partition: usize,
856        context: Arc<TaskContext>,
857    ) -> Result<SendableRecordBatchStream> {
858        self.to_stream(context, partition)
859    }
860
861    fn metrics(&self) -> Option<MetricsSet> {
862        Some(self.metric.clone_inner())
863    }
864
865    fn name(&self) -> &str {
866        "MergeScanExec"
867    }
868}
869
870impl DisplayAs for MergeScanExec {
871    fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
872        write!(f, "MergeScanExec: peers=[")?;
873        for region_id in self.regions.iter() {
874            write!(f, "{}, ", region_id)?;
875        }
876        write!(f, "]")?;
877
878        if matches!(t, DisplayFormatType::Verbose) {
879            let partition_metrics = self.partition_metrics();
880            if !partition_metrics.is_empty() {
881                write!(f, ", metrics={{")?;
882                for (i, pm) in partition_metrics.iter().enumerate() {
883                    if i > 0 {
884                        write!(f, ", ")?;
885                    }
886                    write!(
887                        f,
888                        "\"partition_{}\":{{\"regions\":{},\"total_poll_duration\":\"{:?}\",\"total_do_get_cost\":\"{:?}\",\"ready_time\":\"{:?}\",\"first_consume_time\":\"{:?}\",\"finish_time\":\"{:?}\",\"region_metrics\":[",
889                        pm.partition,
890                        pm.total_regions,
891                        pm.total_poll_duration,
892                        pm.total_do_get_cost,
893                        pm.ready_time,
894                        pm.first_consume_time,
895                        pm.finish_time
896                    )?;
897                    for (j, rm) in pm.region_metrics.iter().enumerate() {
898                        if j > 0 {
899                            write!(f, ",")?;
900                        }
901                        write!(
902                            f,
903                            "{{\"region_id\":\"{}\",\"poll_duration\":\"{:?}\",\"do_get_cost\":\"{:?}\",\"total_cost\":\"{:?}\"}}",
904                            rm.region_id, rm.poll_duration, rm.do_get_cost, rm.total_cost
905                        )?;
906                    }
907                    write!(f, "]}}")?;
908                }
909                write!(f, "}}")?;
910            }
911        }
912
913        Ok(())
914    }
915}
916
917fn region_scan_load(metrics: &RecordBatchMetrics) -> ReadItem {
918    ReadItem {
919        cpu_time: metrics.elapsed_compute as u64,
920        table_scan: region_scan_output_bytes(metrics) as u64,
921    }
922}
923
924fn report_region_query_load(region_id: RegionId, load: &ReadItem) {
925    let region_id = region_id.to_string();
926    REGION_QUERY_CPU_TIME
927        .with_label_values(&[&region_id])
928        .inc_by(load.cpu_time);
929    REGION_QUERY_SCANNED_BYTES
930        .with_label_values(&[&region_id])
931        .inc_by(load.table_scan);
932}
933
934fn query_load_region_id(default_region_id: RegionId, metrics: &RecordBatchMetrics) -> RegionId {
935    metrics
936        .query_load_region_id
937        .map(RegionId::from_u64)
938        .unwrap_or(default_region_id)
939}
940
941impl Drop for MergeScanExec {
942    fn drop(&mut self) {
943        // Per-region Prometheus metrics can have high cardinality, so they are
944        // controlled by `enable_per_region_metrics`. Region-owned counters for
945        // heartbeat reporting are updated on datanodes when query metrics resolve.
946        if !self.enable_per_region_metrics {
947            return;
948        }
949
950        let metrics = self.sub_stage_metrics.lock().unwrap();
951        for (region_id, metrics) in metrics.iter() {
952            let load = region_scan_load(metrics);
953            report_region_query_load(query_load_region_id(*region_id, metrics), &load);
954        }
955    }
956}
957
958#[derive(Debug, Clone)]
959struct MergeScanMetric {
960    /// Nanosecond elapsed till the scan operator is ready to emit data
961    ready_time: Time,
962    /// Nanosecond elapsed till the first record batch emitted from the scan operator gets consumed
963    first_consume_time: Time,
964    /// Nanosecond elapsed till the scan operator finished execution
965    finish_time: Time,
966    /// Count of rows fetched from remote
967    output_rows: Count,
968
969    /// Gauge for greptime plan execution cost metrics for output
970    greptime_exec_cost: Gauge,
971}
972
973impl MergeScanMetric {
974    pub fn new(metric: &ExecutionPlanMetricsSet) -> Self {
975        Self {
976            ready_time: MetricBuilder::new(metric).subset_time("ready_time", 1),
977            first_consume_time: MetricBuilder::new(metric).subset_time("first_consume_time", 1),
978            finish_time: MetricBuilder::new(metric).subset_time("finish_time", 1),
979            output_rows: MetricBuilder::new(metric).output_rows(1),
980            greptime_exec_cost: MetricBuilder::new(metric).gauge(GREPTIME_EXEC_READ_COST, 1),
981        }
982    }
983
984    pub fn ready_time(&self) -> &Time {
985        &self.ready_time
986    }
987
988    pub fn first_consume_time(&self) -> &Time {
989        &self.first_consume_time
990    }
991
992    pub fn finish_time(&self) -> &Time {
993        &self.finish_time
994    }
995
996    pub fn record_output_batch_rows(&self, num_rows: usize) {
997        self.output_rows.add(num_rows);
998    }
999
1000    pub fn record_greptime_exec_cost(&self, metrics: usize) {
1001        self.greptime_exec_cost.add(metrics);
1002    }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007    use std::collections::BTreeSet;
1008
1009    use async_trait::async_trait;
1010    use common_query::request::INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY;
1011    use common_recordbatch::adapter::{PlanMetrics, RecordBatchMetrics};
1012    use datafusion::config::ConfigOptions;
1013    use datafusion::execution::SessionStateBuilder;
1014    use datafusion::physical_plan::filter_pushdown::ChildFilterPushdownResult;
1015    use datafusion_common::TableReference;
1016    use datafusion_expr::{LogicalPlanBuilder, lit};
1017    use datafusion_physical_expr::Distribution;
1018    use datafusion_physical_expr::expressions::{
1019        Column, DynamicFilterPhysicalExpr, lit as physical_lit,
1020    };
1021    use session::ReadPreference;
1022    use session::context::QueryContext;
1023    use session::query_id::QueryId;
1024    use table::table::scan::REGION_SCAN_EXEC_NAME;
1025    use table::table_name::TableName;
1026    use uuid::Uuid;
1027
1028    use super::*;
1029    use crate::dist_plan::{DynFilterRegistryManager, Subscriber};
1030    use crate::region_query::RegionQueryHandler;
1031
1032    fn test_query_id(value: u128) -> QueryId {
1033        QueryId::from(Uuid::from_u128(value))
1034    }
1035
1036    #[test]
1037    fn remote_dyn_filter_region_query_context_registers_before_do_get() {
1038        let registry_manager = Arc::new(DynFilterRegistryManager::default());
1039        let query_ctx = QueryContext::arc();
1040        let query_id = query_ctx
1041            .remote_query_id_value()
1042            .expect("query context must have remote query id");
1043        let lease = registry_manager.acquire_lease(query_id);
1044        let region_id = RegionId::new(1024, 7);
1045        let dyn_filter = Arc::new(DynamicFilterPhysicalExpr::new(
1046            vec![Arc::new(Column::new("host", 0)) as Arc<_>],
1047            physical_lit(true) as _,
1048        )) as Arc<dyn datafusion_physical_expr::PhysicalExpr>;
1049        let captured = capture_remote_dyn_filters_for_pushdown(
1050            RemoteDynFilterProducerId::new(42),
1051            vec![dyn_filter],
1052        );
1053        assert_eq!(captured.captured_dyn_filters.len(), 1);
1054
1055        let region_query_ctx = query_context_for_remote_dyn_filter_region(
1056            &query_ctx,
1057            region_id,
1058            Some(&lease),
1059            &captured.captured_dyn_filters,
1060        );
1061
1062        let entries = lease.registry().entries();
1063        assert_eq!(entries.len(), 1);
1064        assert_eq!(entries[0].subscribers(), vec![Subscriber::new(region_id)]);
1065        assert!(
1066            !entries[0].fanout_started_for_test(),
1067            "fanout must start only after do_get succeeds"
1068        );
1069        assert!(
1070            region_query_ctx
1071                .extension(INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY)
1072                .is_some(),
1073            "initial RDF registrations must be present in the do_get query context"
1074        );
1075    }
1076
1077    #[test]
1078    fn remote_dyn_filter_registry_cleanup_waits_for_last_query_scoped_stream_drop() {
1079        let registry_manager = Arc::new(DynFilterRegistryManager::default());
1080        let query_id = test_query_id(1);
1081
1082        let first = registry_manager.acquire_lease(query_id);
1083        let second = registry_manager.acquire_lease(query_id);
1084
1085        drop(first);
1086        assert_eq!(registry_manager.registry_count(), 1);
1087
1088        drop(second);
1089        assert_eq!(registry_manager.registry_count(), 0);
1090    }
1091
1092    #[test]
1093    fn remote_dyn_filter_registry_cleanup_shares_query_scope_across_independent_leases() {
1094        let registry_manager = Arc::new(DynFilterRegistryManager::default());
1095        let query_id = test_query_id(1);
1096
1097        let first_exec_like_lease = registry_manager.acquire_lease(query_id);
1098        let second_exec_like_lease = registry_manager.acquire_lease(query_id);
1099
1100        drop(first_exec_like_lease);
1101        assert_eq!(registry_manager.registry_count(), 1);
1102
1103        drop(second_exec_like_lease);
1104        assert_eq!(registry_manager.registry_count(), 0);
1105    }
1106
1107    struct TestRegionQueryHandler;
1108
1109    #[async_trait]
1110    impl RegionQueryHandler for TestRegionQueryHandler {
1111        async fn do_get(
1112            &self,
1113            _read_preference: ReadPreference,
1114            _request: common_query::request::QueryRequest,
1115        ) -> crate::error::Result<common_recordbatch::SendableRecordBatchStream> {
1116            unimplemented!("test only")
1117        }
1118
1119        async fn handle_remote_dyn_filter_update(
1120            &self,
1121            _region_id: RegionId,
1122            _query_id: String,
1123            _update: api::v1::region::RemoteDynFilterUpdate,
1124        ) -> crate::error::Result<()> {
1125            unimplemented!("test only")
1126        }
1127
1128        async fn handle_remote_dyn_filter_unregister(
1129            &self,
1130            _region_id: RegionId,
1131            _query_id: String,
1132            _unregister: api::v1::region::RemoteDynFilterUnregister,
1133        ) -> crate::error::Result<()> {
1134            unimplemented!("test only")
1135        }
1136    }
1137
1138    #[test]
1139    fn try_with_new_distribution_preserves_remote_dyn_filter_producer_id() {
1140        let remote_dyn_filter_producer_id = RemoteDynFilterProducerId::new(42);
1141
1142        // Build a plan whose schema contains "col1"
1143        let plan = LogicalPlanBuilder::empty(true)
1144            .project(vec![lit(1i32).alias("col1")])
1145            .unwrap()
1146            .build()
1147            .unwrap();
1148
1149        let schema = plan.schema().as_arrow().clone();
1150        let table = TableName::new("catalog", "schema", "table");
1151        let regions = vec![RegionId::new(1024, 1)];
1152        let query_ctx = QueryContext::arc();
1153
1154        // Non-empty partition_cols so try_with_new_distribution can detect an overlap
1155        let mut partition_cols = AliasMapping::new();
1156        partition_cols.insert(
1157            "col1".to_string(),
1158            BTreeSet::from([ColumnExpr::new(Some(TableReference::bare("table")), "col1")]),
1159        );
1160
1161        let session_state = SessionStateBuilder::new().build();
1162
1163        let handler = Arc::new(TestRegionQueryHandler);
1164        let target_partition = 2;
1165
1166        let exec = MergeScanExec::new(
1167            &session_state,
1168            table,
1169            regions,
1170            plan,
1171            &schema,
1172            handler,
1173            query_ctx,
1174            target_partition,
1175            partition_cols,
1176            Some(remote_dyn_filter_producer_id),
1177            false,
1178        )
1179        .unwrap();
1180
1181        assert_eq!(
1182            exec.remote_dyn_filter_producer_id(),
1183            Some(remote_dyn_filter_producer_id)
1184        );
1185
1186        // A distribution that differs from the current partitioning but shares a
1187        // column name present in partition_cols, so try_with_new_distribution
1188        // produces a clone instead of returning None.
1189        let new_dist = Distribution::HashPartitioned(vec![
1190            Arc::new(Column::new("col1", 0)),
1191            Arc::new(Column::new("col2", 1)),
1192        ]);
1193
1194        let cloned = exec
1195            .try_with_new_distribution(new_dist)
1196            .expect("expected a cloned exec with overlapping partition col");
1197
1198        assert_eq!(
1199            cloned.remote_dyn_filter_producer_id(),
1200            Some(remote_dyn_filter_producer_id),
1201            "try_with_new_distribution must preserve remote dynamic filter producer id"
1202        );
1203    }
1204
1205    #[test]
1206    fn remote_dyn_filter_preflight_removes_parent_filter_after_dn_runtime_is_ready() {
1207        let remote_dyn_filter_producer_id = RemoteDynFilterProducerId::new(42);
1208        let plan = LogicalPlanBuilder::empty(true)
1209            .project(vec![lit(1i32).alias("col1")])
1210            .unwrap()
1211            .build()
1212            .unwrap();
1213
1214        let schema = plan.schema().as_arrow().clone();
1215        let table = TableName::new("catalog", "schema", "table");
1216        let regions = vec![RegionId::new(1024, 1)];
1217        let query_ctx = QueryContext::arc();
1218        let session_state = SessionStateBuilder::new().build();
1219        let handler = Arc::new(TestRegionQueryHandler);
1220        let exec = MergeScanExec::new(
1221            &session_state,
1222            table,
1223            regions,
1224            plan,
1225            &schema,
1226            handler,
1227            query_ctx,
1228            1,
1229            AliasMapping::new(),
1230            Some(remote_dyn_filter_producer_id),
1231            false,
1232        )
1233        .unwrap();
1234        let dyn_filter = Arc::new(DynamicFilterPhysicalExpr::new(
1235            vec![Arc::new(Column::new("host", 0)) as Arc<_>],
1236            physical_lit(true) as _,
1237        )) as Arc<dyn datafusion_physical_expr::PhysicalExpr>;
1238
1239        let propagation = exec
1240            .handle_child_pushdown_result(
1241                FilterPushdownPhase::Post,
1242                ChildPushdownResult {
1243                    parent_filters: vec![ChildFilterPushdownResult {
1244                        filter: dyn_filter,
1245                        child_results: vec![PushedDown::Yes],
1246                    }],
1247                    self_filters: Vec::new(),
1248                },
1249                &ConfigOptions::new(),
1250            )
1251            .unwrap();
1252
1253        assert_eq!(exec.captured_remote_dyn_filters().len(), 1);
1254        assert!(matches!(propagation.filters.as_slice(), [PushedDown::Yes]));
1255    }
1256
1257    #[test]
1258    fn scan_output_bytes_uses_plan_name() {
1259        let metrics = RecordBatchMetrics {
1260            plan_metrics: vec![PlanMetrics {
1261                plan: "SeqScan: region=1".to_string(),
1262                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
1263                level: 0,
1264                metrics: vec![("output_bytes".to_string(), 42)],
1265            }],
1266            ..Default::default()
1267        };
1268
1269        assert_eq!(region_scan_output_bytes(&metrics), 42);
1270    }
1271
1272    #[test]
1273    fn scan_output_bytes_defaults_to_zero_without_region_scan() {
1274        let metrics = RecordBatchMetrics {
1275            plan_metrics: vec![PlanMetrics {
1276                plan: "ProjectionExec".to_string(),
1277                plan_name: "ProjectionExec".to_string(),
1278                level: 0,
1279                metrics: vec![("output_bytes".to_string(), 42)],
1280            }],
1281            ..Default::default()
1282        };
1283
1284        assert_eq!(region_scan_output_bytes(&metrics), 0);
1285    }
1286
1287    #[test]
1288    fn scan_output_bytes_sums_multiple_region_scans() {
1289        let metrics = RecordBatchMetrics {
1290            plan_metrics: vec![
1291                PlanMetrics {
1292                    plan: "RegionScanExec: region=1".to_string(),
1293                    plan_name: REGION_SCAN_EXEC_NAME.to_string(),
1294                    level: 0,
1295                    metrics: vec![("output_bytes".to_string(), 42)],
1296                },
1297                PlanMetrics {
1298                    plan: "RegionScanExec: region=2".to_string(),
1299                    plan_name: REGION_SCAN_EXEC_NAME.to_string(),
1300                    level: 0,
1301                    metrics: vec![("output_bytes".to_string(), 18)],
1302                },
1303            ],
1304            ..Default::default()
1305        };
1306
1307        assert_eq!(region_scan_output_bytes(&metrics), 60);
1308    }
1309
1310    #[test]
1311    fn merge_scan_reports_region_query_load_on_drop() {
1312        use store_api::metrics::{REGION_QUERY_CPU_TIME, REGION_QUERY_SCANNED_BYTES};
1313
1314        let region_id = RegionId::new(1024, 10002);
1315        let region_id_label = region_id.to_string();
1316        let labels = [&region_id_label];
1317        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&labels);
1318        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&labels);
1319
1320        let plan = LogicalPlanBuilder::empty(true)
1321            .project(vec![lit(1i32).alias("col1")])
1322            .unwrap()
1323            .build()
1324            .unwrap();
1325        let schema = plan.schema().as_arrow().clone();
1326        let exec = MergeScanExec::new(
1327            &SessionStateBuilder::new().build(),
1328            TableName::new("catalog", "schema", "table"),
1329            vec![region_id],
1330            plan,
1331            &schema,
1332            Arc::new(TestRegionQueryHandler),
1333            QueryContext::arc(),
1334            1,
1335            AliasMapping::new(),
1336            None,
1337            true,
1338        )
1339        .unwrap();
1340
1341        let metrics = RecordBatchMetrics {
1342            elapsed_compute: 42,
1343            plan_metrics: vec![PlanMetrics {
1344                plan: "RegionScanExec: region=1".to_string(),
1345                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
1346                level: 0,
1347                metrics: vec![("output_bytes".to_string(), 24)],
1348            }],
1349            ..Default::default()
1350        };
1351        exec.sub_stage_metrics
1352            .lock()
1353            .unwrap()
1354            .insert(region_id, metrics);
1355
1356        assert_eq!(REGION_QUERY_CPU_TIME.with_label_values(&labels).get(), 0);
1357        assert_eq!(
1358            REGION_QUERY_SCANNED_BYTES.with_label_values(&labels).get(),
1359            0
1360        );
1361
1362        drop(exec);
1363
1364        assert_eq!(REGION_QUERY_CPU_TIME.with_label_values(&labels).get(), 42);
1365        assert_eq!(
1366            REGION_QUERY_SCANNED_BYTES.with_label_values(&labels).get(),
1367            24
1368        );
1369
1370        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&labels);
1371        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&labels);
1372    }
1373
1374    #[test]
1375    fn merge_scan_reports_query_load_with_metrics_region_id() {
1376        use store_api::metrics::{REGION_QUERY_CPU_TIME, REGION_QUERY_SCANNED_BYTES};
1377
1378        let logical_region_id = RegionId::new(1024, 10002);
1379        let physical_region_id = RegionId::new(1024, 1);
1380        let logical_region_id_label = logical_region_id.to_string();
1381        let physical_region_id_label = physical_region_id.to_string();
1382        let logical_labels = [&logical_region_id_label];
1383        let physical_labels = [&physical_region_id_label];
1384        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&logical_labels);
1385        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&logical_labels);
1386        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&physical_labels);
1387        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&physical_labels);
1388
1389        let plan = LogicalPlanBuilder::empty(true)
1390            .project(vec![lit(1i32).alias("col1")])
1391            .unwrap()
1392            .build()
1393            .unwrap();
1394        let schema = plan.schema().as_arrow().clone();
1395        let exec = MergeScanExec::new(
1396            &SessionStateBuilder::new().build(),
1397            TableName::new("catalog", "schema", "table"),
1398            vec![logical_region_id],
1399            plan,
1400            &schema,
1401            Arc::new(TestRegionQueryHandler),
1402            QueryContext::arc(),
1403            1,
1404            AliasMapping::new(),
1405            None,
1406            true,
1407        )
1408        .unwrap();
1409
1410        let metrics = RecordBatchMetrics {
1411            elapsed_compute: 42,
1412            query_load_region_id: Some(physical_region_id.as_u64()),
1413            plan_metrics: vec![PlanMetrics {
1414                plan: "RegionScanExec: region=1".to_string(),
1415                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
1416                level: 0,
1417                metrics: vec![("output_bytes".to_string(), 24)],
1418            }],
1419            ..Default::default()
1420        };
1421        exec.sub_stage_metrics
1422            .lock()
1423            .unwrap()
1424            .insert(logical_region_id, metrics);
1425
1426        drop(exec);
1427
1428        assert_eq!(
1429            REGION_QUERY_CPU_TIME
1430                .with_label_values(&logical_labels)
1431                .get(),
1432            0
1433        );
1434        assert_eq!(
1435            REGION_QUERY_SCANNED_BYTES
1436                .with_label_values(&logical_labels)
1437                .get(),
1438            0
1439        );
1440        assert_eq!(
1441            REGION_QUERY_CPU_TIME
1442                .with_label_values(&physical_labels)
1443                .get(),
1444            42
1445        );
1446        assert_eq!(
1447            REGION_QUERY_SCANNED_BYTES
1448                .with_label_values(&physical_labels)
1449                .get(),
1450            24
1451        );
1452
1453        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&logical_labels);
1454        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&logical_labels);
1455        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&physical_labels);
1456        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&physical_labels);
1457    }
1458}