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;
16#[cfg(test)]
17use std::cell::Cell;
18use std::sync::{Arc, Mutex};
19use std::time::Duration;
20
21use ahash::{HashMap, HashSet};
22use arrow_schema::{
23    ArrowError, DataType, DataType as ArrowDataType, Field, Schema as ArrowSchema,
24    SchemaRef as ArrowSchemaRef, SortOptions,
25};
26use async_stream::stream;
27use common_catalog::parse_catalog_and_schema_from_db_string;
28use common_plugins::GREPTIME_EXEC_READ_COST;
29use common_query::request::QueryRequest;
30use common_recordbatch::adapter::{RecordBatchMetrics, region_scan_output_bytes};
31use common_telemetry::tracing_context::TracingContext;
32use datafusion::execution::{SessionState, TaskContext};
33use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
34use datafusion::physical_plan::filter_pushdown::{
35    ChildPushdownResult, FilterPushdownPhase, FilterPushdownPropagation, PushedDown,
36};
37use datafusion::physical_plan::metrics::{
38    Count, ExecutionPlanMetricsSet, Gauge, MetricBuilder, MetricsSet, Time,
39};
40use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
41use datafusion::physical_plan::{
42    DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties,
43    SendableRecordBatchStream,
44};
45use datafusion_common::{Column as ColumnExpr, DataFusionError, Result};
46use datafusion_expr::{Expr, Extension, LogicalPlan, UserDefinedLogicalNodeCore};
47use datafusion_physical_expr::expressions::Column;
48use datafusion_physical_expr::{Distribution, EquivalenceProperties, PhysicalSortExpr};
49use datatypes::extension::json::{
50    Json2ExtensionType, is_any_json_extension_type, is_json2_extension_type,
51    is_legacy_json2_extension_type,
52};
53use futures_util::StreamExt;
54use greptime_proto::v1::region::RegionRequestHeader;
55use meter_core::data::ReadItem;
56use meter_macros::read_meter;
57use session::context::{
58    FLIGHT_METRICS_HEARTBEAT_INTERVAL, QueryContextRef,
59    SUPPORT_FLIGHT_METRICS_BEFORE_BATCH_EXTENSION_KEY,
60};
61use store_api::metrics::{REGION_QUERY_CPU_TIME, REGION_QUERY_SCANNED_BYTES};
62use store_api::storage::RegionId;
63use table::table_name::TableName;
64use tokio::time;
65use tokio::time::Instant;
66use tracing::{Instrument, Span};
67
68use crate::dist_plan::analyzer::AliasMapping;
69use crate::dist_plan::analyzer::utils::patch_batch_timezone;
70use crate::dist_plan::dyn_filter_bridge::{
71    CapturedDynFilter, capture_remote_dyn_filters_for_pushdown,
72    query_context_with_refreshed_initial_dyn_filter_regs,
73    register_dyn_filter_subscribers_for_region, register_remote_dyn_filters,
74};
75use crate::dist_plan::{
76    FilterId, RemoteDynFilterProducerId, RemoteDynFilterRegistryLease, Subscriber,
77};
78use crate::metrics::{MERGE_SCAN_ERRORS_TOTAL, MERGE_SCAN_POLL_ELAPSED, MERGE_SCAN_REGIONS};
79use crate::options::{FlowQueryExtensions, remote_dyn_filter_pushdown_enabled_from_extensions};
80use crate::query_engine::QueryEngineState;
81use crate::region_query::RegionQueryHandlerRef;
82
83fn query_engine_state_from_task_context(context: &TaskContext) -> Option<Arc<QueryEngineState>> {
84    context.session_config().get_extension()
85}
86
87fn remote_dyn_filter_enabled(query_ctx: &QueryContextRef) -> Result<bool> {
88    remote_dyn_filter_pushdown_enabled_from_extensions(&query_ctx.extensions())
89        .map_err(|err| DataFusionError::External(Box::new(err)))
90}
91
92fn remote_schema_mismatch(message: impl Into<String>) -> DataFusionError {
93    DataFusionError::ArrowError(Box::new(ArrowError::SchemaError(message.into())), None)
94}
95
96fn record_merge_scan_schema_error() {
97    MERGE_SCAN_ERRORS_TOTAL.inc();
98
99    #[cfg(test)]
100    TEST_MERGE_SCAN_SCHEMA_ERRORS.with(|count| count.set(count.get() + 1));
101}
102
103#[cfg(test)]
104thread_local! {
105    // Prometheus counters are process-global and tests run concurrently. This
106    // companion counter is incremented at the exact production increment site
107    // and is scoped to the polling test thread, making delta assertions stable.
108    static TEST_MERGE_SCAN_SCHEMA_ERRORS: Cell<u64> = const { Cell::new(0) };
109}
110
111#[cfg(test)]
112fn merge_scan_schema_error_count_for_test() -> u64 {
113    TEST_MERGE_SCAN_SCHEMA_ERRORS.with(Cell::get)
114}
115
116/// Returns true when the field is a JSON column, identified either by its
117/// Arrow extension type (`greptime.json` / `greptime.json2`) or by its
118/// `greptime:type=Json` marker.
119fn is_json_field(field: &Field) -> bool {
120    is_any_json_extension_type(field)
121        || field
122            .metadata()
123            .get(datatypes::schema::TYPE_KEY)
124            .map(String::as_ref)
125            == Some("Json")
126}
127
128/// Returns true when two fields describe the same column semantics: same
129/// name, same data type, and same nullability.
130///
131/// Arrow `Field` metadata is auxiliary information (indexing, encoding,
132/// compression hints, ...) and does not participate in the comparison. This
133/// applies to every field, JSON columns included: JSON identity keys
134/// (`greptime:type`, `ARROW:extension:metadata`) are never compared here.
135/// The wire/decoded physical-type difference of JSON columns is exempted
136/// separately by [`json_fields_compatible`].
137fn fields_semantically_equal(expected_field: &Field, actual_field: &Field) -> bool {
138    expected_field.name() == actual_field.name()
139        && expected_field.data_type() == actual_field.data_type()
140        && expected_field.is_nullable() == actual_field.is_nullable()
141}
142
143/// Returns true when two fields are semantically equivalent JSON columns.
144///
145/// A JSON column is carried on the wire in its binary-encoded form (e.g.
146/// `Binary` + `ARROW:extension:name=greptime.json` / `greptime:type=Json`) and,
147/// after decoding on the remote side, in a concretized structured form (e.g.
148/// `Struct(...)` / `List(...)` carrying the same extension metadata). Both forms
149/// describe the same column, so the raw arrow data type must not be compared
150/// directly. The JSON extension identity (`greptime:type`) and the JSON2
151/// settings (`ARROW:extension:metadata`, e.g. type hints) are the semantic
152/// parts of the field and must match.
153fn json_fields_compatible(expected_field: &Field, actual_field: &Field) -> bool {
154    is_json_field(expected_field)
155        && is_json_field(actual_field)
156        && expected_field.name() == actual_field.name()
157        && expected_field.is_nullable() == actual_field.is_nullable()
158        // Both must carry the same JSON marker.
159        && expected_field.metadata().get(datatypes::schema::TYPE_KEY)
160            == actual_field.metadata().get(datatypes::schema::TYPE_KEY)
161        // JSON2 settings (type hints etc.) must match.
162        && expected_field
163            .metadata()
164            .get(arrow_schema::extension::EXTENSION_TYPE_METADATA_KEY)
165            == actual_field
166                .metadata()
167                .get(arrow_schema::extension::EXTENSION_TYPE_METADATA_KEY)
168}
169
170/// Validates the remote schema before positional column handling.
171///
172/// Field metadata (indexing/encoding/compression hints) is non-semantic at
173/// this boundary and is ignored: two fields are equal when their name, data
174/// type, and nullability match (see [`fields_semantically_equal`]). A
175/// timestamp timezone difference is the only intentional exception, accepted
176/// when the fields share the same timestamp unit, distinct timezones, and
177/// equal name and nullability. Top-level Arrow schema metadata is
178/// non-semantic at this boundary as well. JSON columns are compared
179/// semantically (see [`json_fields_compatible`]) because their wire and
180/// decoded representations use different physical arrow types.
181fn validate_remote_schema(
182    expected: &ArrowSchema,
183    actual: &ArrowSchema,
184    source: &str,
185) -> Result<()> {
186    if expected.fields().len() != actual.fields().len() {
187        return Err(remote_schema_mismatch(format!(
188            "MergeScan {source} schema field count mismatch: expected {}, actual {}",
189            expected.fields().len(),
190            actual.fields().len()
191        )));
192    }
193
194    for (index, (expected_field, actual_field)) in expected
195        .fields()
196        .iter()
197        .zip(actual.fields().iter())
198        .enumerate()
199    {
200        // Field metadata does not participate in the semantic comparison.
201        if fields_semantically_equal(expected_field, actual_field) {
202            continue;
203        }
204
205        // JSON columns are equivalent in their binary wire form and their
206        // decoded structured form; compare them semantically instead of
207        // comparing the raw arrow data type.
208        if json_fields_compatible(expected_field, actual_field) {
209            continue;
210        }
211
212        // Intentionally mirrors Arrow Field equality properties, except timezone.
213        let timezone_only_difference = matches!(
214            (expected_field.data_type(), actual_field.data_type()),
215            (
216                ArrowDataType::Timestamp(expected_unit, expected_timezone),
217                ArrowDataType::Timestamp(actual_unit, actual_timezone),
218            ) if expected_unit == actual_unit
219                && expected_timezone != actual_timezone
220                && expected_field.name() == actual_field.name()
221                && expected_field.is_nullable() == actual_field.is_nullable()
222        );
223        if !timezone_only_difference {
224            return Err(remote_schema_mismatch(format!(
225                "MergeScan {source} schema field mismatch at position {index}: expected {:?}, actual {:?}",
226                expected_field, actual_field
227            )));
228        }
229    }
230
231    Ok(())
232}
233
234fn acquire_remote_dyn_filter_registry_lease(
235    context: &TaskContext,
236    query_ctx: &QueryContextRef,
237    captured_dyn_filters: &[CapturedDynFilter],
238) -> Option<RemoteDynFilterRegistryLease> {
239    if captured_dyn_filters.is_empty() {
240        return None;
241    }
242
243    let query_id = query_ctx.remote_query_id_value()?;
244    let query_engine_state = query_engine_state_from_task_context(context)?;
245    Some(
246        query_engine_state
247            .dyn_filter_registry_manager()
248            .acquire_lease(query_id),
249    )
250}
251
252fn register_remote_dyn_filters_for_region(
253    remote_dyn_filter_registry_lease: Option<&RemoteDynFilterRegistryLease>,
254    captured_dyn_filters: &[CapturedDynFilter],
255) {
256    if let Some(remote_dyn_filter_registry_lease) = remote_dyn_filter_registry_lease {
257        register_remote_dyn_filters(
258            remote_dyn_filter_registry_lease.registry(),
259            captured_dyn_filters,
260        );
261    }
262}
263
264struct SubscriberRollbackGuard<'a> {
265    registry: &'a crate::dist_plan::QueryDynFilterRegistry,
266    added: Vec<(FilterId, Subscriber)>,
267}
268
269impl<'a> SubscriberRollbackGuard<'a> {
270    fn new(
271        registry: &'a crate::dist_plan::QueryDynFilterRegistry,
272        added: Vec<(FilterId, Subscriber)>,
273    ) -> Self {
274        Self { registry, added }
275    }
276
277    fn disarm(&mut self) {
278        self.added.clear();
279    }
280}
281
282impl Drop for SubscriberRollbackGuard<'_> {
283    fn drop(&mut self) {
284        for (filter_id, subscriber) in &self.added {
285            self.registry.remove_subscriber(filter_id, subscriber);
286        }
287    }
288}
289
290#[derive(Debug, Hash, PartialOrd, PartialEq, Eq, Clone)]
291pub struct MergeScanLogicalPlan {
292    /// In logical plan phase it only contains one input
293    input: LogicalPlan,
294    /// If this plan is a placeholder
295    is_placeholder: bool,
296    partition_cols: AliasMapping,
297    /// Assigned after dist-plan rewriting so rewriters only deal with plan shape.
298    remote_dyn_filter_producer_id: Option<RemoteDynFilterProducerId>,
299}
300
301impl UserDefinedLogicalNodeCore for MergeScanLogicalPlan {
302    fn name(&self) -> &str {
303        Self::name()
304    }
305
306    // Prevent further optimization.
307    // The input can be retrieved by `self.input()`
308    fn inputs(&self) -> Vec<&LogicalPlan> {
309        vec![]
310    }
311
312    fn schema(&self) -> &datafusion_common::DFSchemaRef {
313        self.input.schema()
314    }
315
316    // Prevent further optimization
317    fn expressions(&self) -> Vec<datafusion_expr::Expr> {
318        vec![]
319    }
320
321    fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
322        write!(
323            f,
324            "MergeScan [is_placeholder={}, remote_input=[\n{}\n]]",
325            self.is_placeholder, self.input
326        )
327    }
328
329    fn with_exprs_and_inputs(
330        &self,
331        _exprs: Vec<datafusion::prelude::Expr>,
332        _inputs: Vec<LogicalPlan>,
333    ) -> Result<Self> {
334        Ok(self.clone())
335    }
336}
337
338impl MergeScanLogicalPlan {
339    pub fn new(input: LogicalPlan, is_placeholder: bool, partition_cols: AliasMapping) -> Self {
340        Self {
341            input,
342            is_placeholder,
343            partition_cols,
344            remote_dyn_filter_producer_id: None,
345        }
346    }
347
348    pub(crate) fn with_remote_dyn_filter_producer_id(
349        mut self,
350        remote_dyn_filter_producer_id: RemoteDynFilterProducerId,
351    ) -> Self {
352        self.remote_dyn_filter_producer_id = Some(remote_dyn_filter_producer_id);
353        self
354    }
355
356    pub fn name() -> &'static str {
357        "MergeScan"
358    }
359
360    /// Create a [LogicalPlan::Extension] node from this merge scan plan
361    pub fn into_logical_plan(self) -> LogicalPlan {
362        LogicalPlan::Extension(Extension {
363            node: Arc::new(self),
364        })
365    }
366
367    pub fn is_placeholder(&self) -> bool {
368        self.is_placeholder
369    }
370
371    pub fn input(&self) -> &LogicalPlan {
372        &self.input
373    }
374
375    pub fn partition_cols(&self) -> &AliasMapping {
376        &self.partition_cols
377    }
378
379    pub fn remote_dyn_filter_producer_id(&self) -> Option<RemoteDynFilterProducerId> {
380        self.remote_dyn_filter_producer_id
381    }
382}
383
384#[derive(Clone)]
385pub struct MergeScanExec {
386    table: TableName,
387    regions: Vec<RegionId>,
388    plan: LogicalPlan,
389    arrow_schema: ArrowSchemaRef,
390    region_query_handler: RegionQueryHandlerRef,
391    metric: ExecutionPlanMetricsSet,
392    properties: Arc<PlanProperties>,
393    /// Metrics from sub stages
394    sub_stage_metrics: Arc<Mutex<HashMap<RegionId, RecordBatchMetrics>>>,
395    /// Metrics for each partition
396    partition_metrics: Arc<Mutex<HashMap<usize, PartitionMetrics>>>,
397    query_ctx: QueryContextRef,
398    /// Optional because RDF must fail open: missing ids skip RDF but keep normal query execution.
399    remote_dyn_filter_producer_id: Option<RemoteDynFilterProducerId>,
400    captured_remote_dyn_filters: Arc<Mutex<Vec<CapturedDynFilter>>>,
401    target_partition: usize,
402    partition_cols: AliasMapping,
403    enable_per_region_metrics: bool,
404}
405
406impl std::fmt::Debug for MergeScanExec {
407    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408        f.debug_struct("MergeScanExec")
409            .field("table", &self.table)
410            .field("regions", &self.regions)
411            .field("plan", &self.plan)
412            .finish()
413    }
414}
415
416impl MergeScanExec {
417    #[allow(clippy::too_many_arguments)]
418    pub fn new(
419        session_state: &SessionState,
420        table: TableName,
421        regions: Vec<RegionId>,
422        plan: LogicalPlan,
423        arrow_schema: &ArrowSchema,
424        region_query_handler: RegionQueryHandlerRef,
425        query_ctx: QueryContextRef,
426        target_partition: usize,
427        partition_cols: AliasMapping,
428        remote_dyn_filter_producer_id: Option<RemoteDynFilterProducerId>,
429        enable_per_region_metrics: bool,
430    ) -> Result<Self> {
431        let arrow_schema = maybe_amend_json2_field(arrow_schema);
432
433        // States the output ordering of the plan.
434        //
435        // When the input plan is a sort, we can use the sort ordering as the output ordering
436        // if the target partition is greater than the number of regions, which means we won't
437        // break the ordering on merging (of MergeScan).
438        //
439        // Otherwise, we need to use the default ordering.
440        let eq_properties = if let LogicalPlan::Sort(sort) = &plan
441            && target_partition >= regions.len()
442        {
443            let lex_ordering = sort
444                .expr
445                .iter()
446                .map(|sort_expr| {
447                    let physical_expr = session_state
448                        .create_physical_expr(sort_expr.expr.clone(), plan.schema())?;
449                    Ok(PhysicalSortExpr::new(
450                        physical_expr,
451                        SortOptions {
452                            descending: !sort_expr.asc,
453                            nulls_first: sort_expr.nulls_first,
454                        },
455                    ))
456                })
457                .collect::<Result<Vec<_>>>()?;
458            EquivalenceProperties::new_with_orderings(arrow_schema.clone(), vec![lex_ordering])
459        } else {
460            EquivalenceProperties::new(arrow_schema.clone())
461        };
462
463        let partition_exprs = partition_cols
464            .iter()
465            .filter_map(|col| {
466                if let Some(first_alias) = col.1.first() {
467                    session_state
468                        .create_physical_expr(
469                            Expr::Column(ColumnExpr::new_unqualified(
470                                first_alias.name().to_string(),
471                            )),
472                            plan.schema(),
473                        )
474                        .ok()
475                } else {
476                    None
477                }
478            })
479            .collect();
480        let partitioning = Partitioning::Hash(partition_exprs, target_partition);
481
482        let properties = Arc::new(PlanProperties::new(
483            eq_properties,
484            partitioning,
485            EmissionType::Incremental,
486            Boundedness::Bounded,
487        ));
488        Ok(Self {
489            table,
490            regions,
491            plan,
492            arrow_schema,
493            region_query_handler,
494            metric: ExecutionPlanMetricsSet::new(),
495            sub_stage_metrics: Arc::default(),
496            partition_metrics: Arc::default(),
497            properties,
498            query_ctx,
499            remote_dyn_filter_producer_id,
500            captured_remote_dyn_filters: Arc::default(),
501            target_partition,
502            partition_cols,
503            enable_per_region_metrics,
504        })
505    }
506
507    pub fn to_stream(
508        &self,
509        context: Arc<TaskContext>,
510        partition: usize,
511    ) -> Result<SendableRecordBatchStream> {
512        // prepare states to move
513        let regions = self.regions.clone();
514        let region_query_handler = self.region_query_handler.clone();
515        let metric = MergeScanMetric::new(&self.metric);
516        let arrow_schema = self.arrow_schema.clone();
517        let query_ctx = self.query_ctx.clone();
518        let sub_stage_metrics_moved = self.sub_stage_metrics.clone();
519        let partition_metrics_moved = self.partition_metrics.clone();
520        let plan = self.plan.clone();
521        let target_partition = self.target_partition;
522        let remote_dyn_filter_enabled = remote_dyn_filter_enabled(&self.query_ctx)?;
523        let captured_remote_dyn_filters = if remote_dyn_filter_enabled {
524            self.captured_remote_dyn_filters()
525        } else {
526            Vec::new()
527        };
528        let dbname = context.task_id().unwrap_or_default();
529        let tracing_context = TracingContext::from_json(context.session_id().as_str());
530        let current_channel = self.query_ctx.channel();
531        let read_preference = self.query_ctx.read_preference();
532        let explain_verbose = self.query_ctx.explain_verbose();
533        let live_analyze_metrics = explain_verbose && self.query_ctx.live_analyze_metrics_enabled();
534        let remote_dyn_filter_registry_lease = acquire_remote_dyn_filter_registry_lease(
535            context.as_ref(),
536            &query_ctx,
537            &captured_remote_dyn_filters,
538        );
539
540        let stream = Box::pin(stream!({
541            let remote_dyn_filter_registry_lease = remote_dyn_filter_registry_lease;
542            // only report metrics once for each MergeScan
543            if partition == 0 {
544                MERGE_SCAN_REGIONS.observe(regions.len() as f64);
545            }
546
547            let _finish_timer = metric.finish_time().timer();
548            let mut ready_timer = metric.ready_time().timer();
549            let mut first_consume_timer = Some(metric.first_consume_time().timer());
550
551            // Per-partition timings, scoped to this partition's stream for `EXPLAIN VERBOSE`.
552            let partition_start = Instant::now();
553            let mut partition_ready_time: Option<Duration> = None;
554            let mut partition_first_consume_time: Option<Duration> = None;
555
556            for region_id in regions
557                .iter()
558                .skip(partition)
559                .step_by(target_partition)
560                .copied()
561            {
562                let region_span = tracing_context.attach(tracing::info_span!(
563                    parent: &Span::current(),
564                    "merge_scan_region",
565                    region_id = %region_id,
566                    partition = partition
567                ));
568                let region_start = Instant::now();
569                register_remote_dyn_filters_for_region(
570                    remote_dyn_filter_registry_lease.as_ref(),
571                    &captured_remote_dyn_filters,
572                );
573                let select_target_start = Instant::now();
574                let target = region_query_handler
575                    .select_target(read_preference, region_id)
576                    .instrument(region_span.clone())
577                    .await
578                    .map_err(|e| {
579                        MERGE_SCAN_ERRORS_TOTAL.inc();
580                        DataFusionError::External(Box::new(e))
581                    })?;
582                let select_target_cost = select_target_start.elapsed();
583                let mut subscriber_rollback =
584                    remote_dyn_filter_registry_lease.as_ref().map(|lease| {
585                        SubscriberRollbackGuard::new(
586                            lease.registry(),
587                            register_dyn_filter_subscribers_for_region(
588                                lease.registry(),
589                                region_id,
590                                target.clone(),
591                                &captured_remote_dyn_filters,
592                            ),
593                        )
594                    });
595                let mut region_query_ctx = query_context_with_refreshed_initial_dyn_filter_regs(
596                    &query_ctx,
597                    region_id,
598                    &captured_remote_dyn_filters,
599                );
600                if live_analyze_metrics {
601                    let remote_query_id = region_query_ctx.remote_query_id().map(str::to_string);
602                    if let Some(remote_query_id) = remote_query_id {
603                        region_query_ctx.set_extension(
604                            SUPPORT_FLIGHT_METRICS_BEFORE_BATCH_EXTENSION_KEY,
605                            remote_query_id,
606                        );
607                    }
608                }
609                let request = QueryRequest {
610                    header: Some(RegionRequestHeader {
611                        tracing_context: tracing_context.to_w3c(),
612                        dbname: dbname.clone(),
613                        query_context: Some((&region_query_ctx).into()),
614                    }),
615                    region_id,
616                    plan: plan.clone(),
617                };
618                if explain_verbose {
619                    common_telemetry::info!(
620                        "Merge scan one region, partition: {}, region_id: {}",
621                        partition,
622                        region_id
623                    );
624                }
625
626                let do_get_start = Instant::now();
627                let do_get_result = region_query_handler
628                    .do_get(&target, request)
629                    .instrument(region_span.clone())
630                    .await;
631                if do_get_result.is_err() {
632                    drop(subscriber_rollback.take());
633                }
634                let mut stream = do_get_result.map_err(|e| {
635                    MERGE_SCAN_ERRORS_TOTAL.inc();
636                    DataFusionError::External(Box::new(e))
637                })?;
638
639                if let Some(subscriber_rollback) = subscriber_rollback.as_mut() {
640                    subscriber_rollback.disarm();
641                }
642                let mut advertised_schema = stream.schema().arrow_schema().clone();
643                validate_remote_schema(
644                    arrow_schema.as_ref(),
645                    advertised_schema.as_ref(),
646                    "advertised remote stream",
647                )
648                .inspect_err(|_| record_merge_scan_schema_error())?;
649                let do_get_cost = select_target_cost + do_get_start.elapsed();
650
651                if let Some(remote_dyn_filter_registry_lease) =
652                    remote_dyn_filter_registry_lease.as_ref()
653                {
654                    remote_dyn_filter_registry_lease
655                        .ensure_fanout_task(region_query_handler.clone());
656                }
657
658                ready_timer.stop();
659                if partition_ready_time.is_none() {
660                    partition_ready_time = Some(partition_start.elapsed());
661                }
662
663                let mut poll_duration = Duration::ZERO;
664                let mut poll_timer = Instant::now();
665                loop {
666                    let batch = if live_analyze_metrics {
667                        match time::timeout(
668                            FLIGHT_METRICS_HEARTBEAT_INTERVAL,
669                            stream.next().instrument(region_span.clone()),
670                        )
671                        .await
672                        {
673                            Ok(batch) => batch,
674                            Err(_) => {
675                                if let Some(metrics) = stream.metrics() {
676                                    let mut sub_stage_metrics =
677                                        sub_stage_metrics_moved.lock().unwrap();
678                                    sub_stage_metrics.insert(region_id, metrics);
679                                }
680                                continue;
681                            }
682                        }
683                    } else {
684                        stream.next().instrument(region_span.clone()).await
685                    };
686                    let Some(batch) = batch else {
687                        break;
688                    };
689                    let poll_elapsed = poll_timer.elapsed();
690                    poll_duration += poll_elapsed;
691
692                    let batch = batch.map_err(|e| DataFusionError::External(Box::new(e)))?;
693                    let df_batch = batch.into_df_record_batch();
694                    if !Arc::ptr_eq(&advertised_schema, df_batch.schema_ref()) {
695                        validate_remote_schema(
696                            arrow_schema.as_ref(),
697                            df_batch.schema_ref().as_ref(),
698                            "remote record batch",
699                        )
700                        .inspect_err(|_| record_merge_scan_schema_error())?;
701                        advertised_schema = df_batch.schema_ref().clone();
702                    }
703                    let batch =
704                        patch_batch_timezone(arrow_schema.clone(), df_batch.columns().to_vec())?;
705                    metric.record_output_batch_rows(batch.num_rows());
706                    if let Some(mut first_consume_timer) = first_consume_timer.take() {
707                        first_consume_timer.stop();
708                        partition_first_consume_time = Some(partition_start.elapsed());
709                    }
710
711                    if let Some(metrics) = stream.metrics() {
712                        let mut sub_stage_metrics = sub_stage_metrics_moved.lock().unwrap();
713                        sub_stage_metrics.insert(region_id, metrics);
714                    }
715
716                    yield Ok(batch);
717                    // reset poll timer
718                    poll_timer = Instant::now();
719                }
720                // Also stop on an exhausted stream that yielded no batch. The `take()`
721                // guard ensures it only records once, on the first such region.
722                if let Some(mut first_consume_timer) = first_consume_timer.take() {
723                    first_consume_timer.stop();
724                    partition_first_consume_time = Some(partition_start.elapsed());
725                }
726                let total_cost = region_start.elapsed();
727
728                // Record region metrics and push to global partition_metrics
729                let region_metrics = RegionMetrics {
730                    region_id,
731                    poll_duration,
732                    do_get_cost,
733                    total_cost,
734                };
735
736                // Push RegionMetrics to global partition_metrics immediately after scanning this region
737                {
738                    let mut partition_metrics_guard = partition_metrics_moved.lock().unwrap();
739                    let partition_metrics = partition_metrics_guard
740                        .entry(partition)
741                        .or_insert_with(|| PartitionMetrics::new(partition, explain_verbose));
742                    partition_metrics.add_region_metrics(region_metrics);
743                }
744
745                if explain_verbose {
746                    common_telemetry::info!(
747                        "Merge scan finish one region, partition: {}, region_id: {}, poll_duration: {:?}, first_consume: {}, do_get_cost: {:?}",
748                        partition,
749                        region_id,
750                        poll_duration,
751                        metric.first_consume_time(),
752                        do_get_cost
753                    );
754                }
755
756                // process metrics after all data is drained.
757                if let Some(metrics) = stream.metrics() {
758                    let load = region_scan_load(&metrics);
759                    let (c, s) = parse_catalog_and_schema_from_db_string(&dbname);
760                    let value = read_meter!(c, s, load, current_channel as u8);
761                    metric.record_greptime_exec_cost(value as usize);
762
763                    // record metrics from sub sgates
764                    let mut sub_stage_metrics = sub_stage_metrics_moved.lock().unwrap();
765                    sub_stage_metrics.insert(region_id, metrics);
766                }
767
768                MERGE_SCAN_POLL_ELAPSED.observe(poll_duration.as_secs_f64());
769            }
770
771            // Stop the global timers for partitions with no region, otherwise they keep
772            // running until drop and inflate the shared metrics. No-op otherwise.
773            ready_timer.stop();
774            if let Some(mut first_consume_timer) = first_consume_timer.take() {
775                first_consume_timer.stop();
776            }
777
778            // Finish partition metrics and log results
779            let partition_finish_time = partition_start.elapsed();
780            {
781                let mut partition_metrics_guard = partition_metrics_moved.lock().unwrap();
782                if let Some(partition_metrics) = partition_metrics_guard.get_mut(&partition) {
783                    partition_metrics.set_timings(
784                        partition_ready_time.unwrap_or_default(),
785                        partition_first_consume_time.unwrap_or_default(),
786                        partition_finish_time,
787                    );
788                    partition_metrics.finish();
789                }
790            }
791        }));
792
793        Ok(Box::pin(RecordBatchStreamAdapter::new(
794            self.arrow_schema.clone(),
795            stream,
796        )))
797    }
798
799    pub fn try_with_new_distribution(&self, distribution: Distribution) -> Option<Self> {
800        let Distribution::HashPartitioned(hash_exprs) = distribution else {
801            // not applicable
802            return None;
803        };
804
805        if let Partitioning::Hash(curr_dist, _) = &self.properties.partitioning
806            && curr_dist == &hash_exprs
807        {
808            // No need to change the distribution
809            return None;
810        }
811
812        let hash_expr_col_names: HashSet<_> = hash_exprs
813            .iter()
814            .filter_map(|expr| {
815                expr.as_any()
816                    .downcast_ref::<Column>()
817                    .map(|col_expr| col_expr.name())
818            })
819            .collect();
820
821        let covers_all_partition_cols = self.partition_cols.values().all(|aliases| {
822            aliases
823                .iter()
824                .any(|col| hash_expr_col_names.contains(col.name()))
825        });
826        if !covers_all_partition_cols {
827            return None;
828        }
829
830        let all_partition_col_aliases: HashSet<_> = self
831            .partition_cols
832            .values()
833            .flat_map(|aliases| aliases.iter().map(|c| c.name()))
834            .collect();
835        let overlaps: Vec<_> = hash_exprs
836            .iter()
837            .filter(|expr| {
838                expr.as_any()
839                    .downcast_ref::<Column>()
840                    .is_some_and(|col_expr| all_partition_col_aliases.contains(col_expr.name()))
841            })
842            .cloned()
843            .collect();
844
845        if overlaps.is_empty() {
846            return None;
847        }
848
849        Some(Self {
850            table: self.table.clone(),
851            regions: self.regions.clone(),
852            plan: self.plan.clone(),
853            arrow_schema: self.arrow_schema.clone(),
854            region_query_handler: self.region_query_handler.clone(),
855            metric: self.metric.clone(),
856            properties: Arc::new(PlanProperties::new(
857                self.properties.eq_properties.clone(),
858                Partitioning::Hash(overlaps, self.target_partition),
859                self.properties.emission_type,
860                self.properties.boundedness,
861            )),
862            sub_stage_metrics: self.sub_stage_metrics.clone(),
863            partition_metrics: self.partition_metrics.clone(),
864            query_ctx: self.query_ctx.clone(),
865            remote_dyn_filter_producer_id: self.remote_dyn_filter_producer_id,
866            captured_remote_dyn_filters: self.captured_remote_dyn_filters.clone(),
867            target_partition: self.target_partition,
868            partition_cols: self.partition_cols.clone(),
869            enable_per_region_metrics: self.enable_per_region_metrics,
870        })
871    }
872
873    fn captured_remote_dyn_filters(&self) -> Vec<CapturedDynFilter> {
874        self.captured_remote_dyn_filters.lock().unwrap().clone()
875    }
876
877    pub fn sub_stage_metrics(&self) -> Vec<RecordBatchMetrics> {
878        let sub_stage_metrics = self.sub_stage_metrics.lock().unwrap();
879        let mut metrics: Vec<_> = sub_stage_metrics.iter().collect();
880        metrics.sort_unstable_by_key(|(region_id, _)| **region_id);
881        metrics
882            .into_iter()
883            .map(|(_, metrics)| metrics.clone())
884            .collect()
885    }
886
887    pub fn regions(&self) -> &[RegionId] {
888        &self.regions
889    }
890
891    pub fn is_flow_sink_scan(&self) -> bool {
892        let Some(sink_table_id) =
893            FlowQueryExtensions::parse_flow_extensions(&self.query_ctx.extensions())
894                .ok()
895                .flatten()
896                .and_then(|extensions| extensions.sink_table_id)
897        else {
898            return false;
899        };
900
901        !self.regions.is_empty()
902            && self
903                .regions
904                .iter()
905                .all(|region_id| region_id.table_id() == sink_table_id)
906    }
907
908    pub fn partition_count(&self) -> usize {
909        self.target_partition
910    }
911
912    pub fn region_count(&self) -> usize {
913        self.regions.len()
914    }
915
916    fn partition_metrics(&self) -> Vec<PartitionMetrics> {
917        self.partition_metrics
918            .lock()
919            .unwrap()
920            .values()
921            .cloned()
922            .collect()
923    }
924}
925
926// If the schema has JSON2 field, AND the field is of empty Struct datatype, amend it with Binary
927// datatype.
928// This is a very hacky way to make it possible to query the whole JSON2 column. Because when
929// querying a whole JSON2 column, like in the SQL `select * from ...`, we can't concretize the JSON2
930// datatype from the query. Hence, the JSON2 datatype remains what in the column schema, i.e., empty
931// Struct. An empty Struct is not alignable like any other concretized JSON2 datatypes, so to make
932// the query work, we amend(rewrite) it to Binary datatype.
933// Why the Binary datatype? Because underlying the scan and projection stage, the JSON2 data are
934// variant shape, will be all converted to bytes.
935// Anyway, this is not clean nor elegant. TODO(LFC) Maybe make it into some plan analyzer rule?
936fn maybe_amend_json2_field(schema: &ArrowSchema) -> ArrowSchemaRef {
937    let schema = schema.clone();
938    let mut new_fields = Vec::with_capacity(schema.fields().len());
939    for field in schema.fields().iter() {
940        let new_field = if is_json2_extension_type(field)
941            && matches!(field.data_type(), DataType::Struct(fields) if fields.is_empty())
942        {
943            let is_legacy_json2 = is_legacy_json2_extension_type(field);
944            let mut new_field = field.as_ref().clone();
945            new_field.set_data_type(DataType::Binary);
946            if is_legacy_json2 {
947                // Pre-type-hint JSON2 is identified partly by its Struct data type. Promote the
948                // ephemeral field before rewriting it to Binary so later checks retain its JSON2
949                // identity.
950                new_field = new_field.with_extension_type(Json2ExtensionType::default());
951            }
952            Arc::new(new_field)
953        } else {
954            field.clone()
955        };
956        new_fields.push(new_field);
957    }
958    Arc::new(ArrowSchema::new_with_metadata(
959        new_fields,
960        schema.metadata().clone(),
961    ))
962}
963
964#[cfg(test)]
965impl MergeScanExec {
966    fn remote_dyn_filter_producer_id(&self) -> Option<RemoteDynFilterProducerId> {
967        self.remote_dyn_filter_producer_id
968    }
969}
970
971/// Metrics for a region of a partition.
972#[derive(Debug, Clone)]
973struct RegionMetrics {
974    region_id: RegionId,
975    poll_duration: Duration,
976    do_get_cost: Duration,
977    /// Total cost to scan the region.
978    total_cost: Duration,
979}
980
981/// Metrics for a partition of a MergeScanExec.
982#[derive(Debug, Clone)]
983struct PartitionMetrics {
984    partition: usize,
985    region_metrics: Vec<RegionMetrics>,
986    total_poll_duration: Duration,
987    total_do_get_cost: Duration,
988    total_regions: usize,
989    /// Time until this partition's scan is ready to emit data.
990    ready_time: Duration,
991    /// Time until this partition's first stream poll resolves (a batch or exhausted).
992    first_consume_time: Duration,
993    /// Time until this partition's scan finishes execution.
994    finish_time: Duration,
995    explain_verbose: bool,
996    finished: bool,
997}
998
999impl PartitionMetrics {
1000    fn new(partition: usize, explain_verbose: bool) -> Self {
1001        Self {
1002            partition,
1003            region_metrics: Vec::new(),
1004            total_poll_duration: Duration::ZERO,
1005            total_do_get_cost: Duration::ZERO,
1006            total_regions: 0,
1007            ready_time: Duration::ZERO,
1008            first_consume_time: Duration::ZERO,
1009            finish_time: Duration::ZERO,
1010            explain_verbose,
1011            finished: false,
1012        }
1013    }
1014
1015    fn add_region_metrics(&mut self, region_metrics: RegionMetrics) {
1016        self.total_poll_duration += region_metrics.poll_duration;
1017        self.total_do_get_cost += region_metrics.do_get_cost;
1018        self.total_regions += 1;
1019        self.region_metrics.push(region_metrics);
1020    }
1021
1022    /// Set the per-partition timings captured during streaming.
1023    fn set_timings(
1024        &mut self,
1025        ready_time: Duration,
1026        first_consume_time: Duration,
1027        finish_time: Duration,
1028    ) {
1029        self.ready_time = ready_time;
1030        self.first_consume_time = first_consume_time;
1031        self.finish_time = finish_time;
1032    }
1033
1034    /// Finish the partition metrics and log the results.
1035    fn finish(&mut self) {
1036        if self.finished {
1037            return;
1038        }
1039        self.finished = true;
1040        self.log_metrics();
1041    }
1042
1043    /// Log partition metrics based on explain_verbose level.
1044    fn log_metrics(&self) {
1045        if self.explain_verbose {
1046            common_telemetry::info!(
1047                "MergeScan partition {} finished: {} regions, total_poll_duration: {:?}, total_do_get_cost: {:?}, ready_time: {:?}, first_consume_time: {:?}, finish_time: {:?}",
1048                self.partition,
1049                self.total_regions,
1050                self.total_poll_duration,
1051                self.total_do_get_cost,
1052                self.ready_time,
1053                self.first_consume_time,
1054                self.finish_time
1055            );
1056        } else {
1057            common_telemetry::debug!(
1058                "MergeScan partition {} finished: {} regions, total_poll_duration: {:?}, total_do_get_cost: {:?}, ready_time: {:?}, first_consume_time: {:?}, finish_time: {:?}",
1059                self.partition,
1060                self.total_regions,
1061                self.total_poll_duration,
1062                self.total_do_get_cost,
1063                self.ready_time,
1064                self.first_consume_time,
1065                self.finish_time
1066            );
1067        }
1068    }
1069}
1070
1071impl Drop for PartitionMetrics {
1072    fn drop(&mut self) {
1073        if !self.finished {
1074            self.log_metrics();
1075        }
1076    }
1077}
1078
1079impl ExecutionPlan for MergeScanExec {
1080    fn as_any(&self) -> &dyn Any {
1081        self
1082    }
1083
1084    fn schema(&self) -> ArrowSchemaRef {
1085        self.arrow_schema.clone()
1086    }
1087
1088    fn properties(&self) -> &Arc<PlanProperties> {
1089        &self.properties
1090    }
1091
1092    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1093        vec![]
1094    }
1095
1096    // DataFusion will swap children unconditionally.
1097    // But since this node is leaf node, it's safe to just return self.
1098    fn with_new_children(
1099        self: Arc<Self>,
1100        _children: Vec<Arc<dyn ExecutionPlan>>,
1101    ) -> Result<Arc<dyn ExecutionPlan>> {
1102        Ok(self.clone())
1103    }
1104
1105    fn handle_child_pushdown_result(
1106        &self,
1107        _phase: FilterPushdownPhase,
1108        child_pushdown_result: ChildPushdownResult,
1109        _config: &datafusion::config::ConfigOptions,
1110    ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
1111        let parent_filters = child_pushdown_result
1112            .parent_filters
1113            .into_iter()
1114            .map(|filter| filter.filter)
1115            .collect::<Vec<_>>();
1116
1117        if !remote_dyn_filter_enabled(&self.query_ctx)? {
1118            // Reject remote pushdown instead of pretending success: this keeps
1119            // DataFusion/local dynamic filter semantics intact while disabling
1120            // only FE -> DN remote dynamic filter propagation.
1121            self.captured_remote_dyn_filters.lock().unwrap().clear();
1122            let new_self = Arc::new(self.clone());
1123
1124            return Ok(FilterPushdownPropagation {
1125                filters: parent_filters.into_iter().map(|_| PushedDown::No).collect(),
1126                updated_node: Some(new_self),
1127            });
1128        }
1129
1130        let Some(remote_dyn_filter_producer_id) = self.remote_dyn_filter_producer_id else {
1131            // Missing RDF identity disables only RDF, not normal execution.
1132            common_telemetry::warn!(
1133                "MergeScan remote dynamic filter producer id is not assigned; skipping remote dynamic filter pushdown"
1134            );
1135            self.captured_remote_dyn_filters.lock().unwrap().clear();
1136            let new_self = Arc::new(self.clone());
1137
1138            return Ok(FilterPushdownPropagation {
1139                filters: parent_filters.into_iter().map(|_| PushedDown::No).collect(),
1140                updated_node: Some(new_self),
1141            });
1142        };
1143        let remote_dyn_filter_pushdown =
1144            capture_remote_dyn_filters_for_pushdown(remote_dyn_filter_producer_id, parent_filters);
1145        *self.captured_remote_dyn_filters.lock().unwrap() =
1146            remote_dyn_filter_pushdown.captured_dyn_filters;
1147        let new_self = Arc::new(self.clone());
1148
1149        Ok(FilterPushdownPropagation {
1150            filters: remote_dyn_filter_pushdown
1151                .pushed_down
1152                .into_iter()
1153                .map(|pushdown_ready| {
1154                    if pushdown_ready {
1155                        PushedDown::Yes
1156                    } else {
1157                        PushedDown::No
1158                    }
1159                })
1160                .collect(),
1161            updated_node: Some(new_self),
1162        })
1163    }
1164
1165    fn execute(
1166        &self,
1167        partition: usize,
1168        context: Arc<TaskContext>,
1169    ) -> Result<SendableRecordBatchStream> {
1170        self.to_stream(context, partition)
1171    }
1172
1173    fn metrics(&self) -> Option<MetricsSet> {
1174        Some(self.metric.clone_inner())
1175    }
1176
1177    fn name(&self) -> &str {
1178        "MergeScanExec"
1179    }
1180}
1181
1182impl DisplayAs for MergeScanExec {
1183    fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1184        write!(f, "MergeScanExec: peers=[")?;
1185        for region_id in self.regions.iter() {
1186            write!(f, "{}, ", region_id)?;
1187        }
1188        write!(f, "]")?;
1189
1190        if matches!(t, DisplayFormatType::Verbose) {
1191            let partition_metrics = self.partition_metrics();
1192            if !partition_metrics.is_empty() {
1193                write!(f, ", metrics={{")?;
1194                for (i, pm) in partition_metrics.iter().enumerate() {
1195                    if i > 0 {
1196                        write!(f, ", ")?;
1197                    }
1198                    write!(
1199                        f,
1200                        "\"partition_{}\":{{\"regions\":{},\"total_poll_duration\":\"{:?}\",\"total_do_get_cost\":\"{:?}\",\"ready_time\":\"{:?}\",\"first_consume_time\":\"{:?}\",\"finish_time\":\"{:?}\",\"region_metrics\":[",
1201                        pm.partition,
1202                        pm.total_regions,
1203                        pm.total_poll_duration,
1204                        pm.total_do_get_cost,
1205                        pm.ready_time,
1206                        pm.first_consume_time,
1207                        pm.finish_time
1208                    )?;
1209                    for (j, rm) in pm.region_metrics.iter().enumerate() {
1210                        if j > 0 {
1211                            write!(f, ",")?;
1212                        }
1213                        write!(
1214                            f,
1215                            "{{\"region_id\":\"{}\",\"poll_duration\":\"{:?}\",\"do_get_cost\":\"{:?}\",\"total_cost\":\"{:?}\"}}",
1216                            rm.region_id, rm.poll_duration, rm.do_get_cost, rm.total_cost
1217                        )?;
1218                    }
1219                    write!(f, "]}}")?;
1220                }
1221                write!(f, "}}")?;
1222            }
1223        }
1224
1225        Ok(())
1226    }
1227}
1228
1229fn region_scan_load(metrics: &RecordBatchMetrics) -> ReadItem {
1230    ReadItem {
1231        cpu_time: metrics.elapsed_compute as u64,
1232        table_scan: region_scan_output_bytes(metrics) as u64,
1233    }
1234}
1235
1236fn report_region_query_load(region_id: RegionId, load: &ReadItem) {
1237    let region_id = region_id.to_string();
1238    REGION_QUERY_CPU_TIME
1239        .with_label_values(&[&region_id])
1240        .inc_by(load.cpu_time);
1241    REGION_QUERY_SCANNED_BYTES
1242        .with_label_values(&[&region_id])
1243        .inc_by(load.table_scan);
1244}
1245
1246fn query_load_region_id(default_region_id: RegionId, metrics: &RecordBatchMetrics) -> RegionId {
1247    metrics
1248        .query_load_region_id
1249        .map(RegionId::from_u64)
1250        .unwrap_or(default_region_id)
1251}
1252
1253impl Drop for MergeScanExec {
1254    fn drop(&mut self) {
1255        // Per-region Prometheus metrics can have high cardinality, so they are
1256        // controlled by `enable_per_region_metrics`. Region-owned counters for
1257        // heartbeat reporting are updated on datanodes when query metrics resolve.
1258        if !self.enable_per_region_metrics {
1259            return;
1260        }
1261
1262        let metrics = self.sub_stage_metrics.lock().unwrap();
1263        for (region_id, metrics) in metrics.iter() {
1264            let load = region_scan_load(metrics);
1265            report_region_query_load(query_load_region_id(*region_id, metrics), &load);
1266        }
1267    }
1268}
1269
1270#[derive(Debug, Clone)]
1271struct MergeScanMetric {
1272    /// Nanosecond elapsed till the scan operator is ready to emit data
1273    ready_time: Time,
1274    /// Nanosecond elapsed till the first record batch emitted from the scan operator gets consumed
1275    first_consume_time: Time,
1276    /// Nanosecond elapsed till the scan operator finished execution
1277    finish_time: Time,
1278    /// Count of rows fetched from remote
1279    output_rows: Count,
1280
1281    /// Gauge for greptime plan execution cost metrics for output
1282    greptime_exec_cost: Gauge,
1283}
1284
1285impl MergeScanMetric {
1286    pub fn new(metric: &ExecutionPlanMetricsSet) -> Self {
1287        Self {
1288            ready_time: MetricBuilder::new(metric).subset_time("ready_time", 1),
1289            first_consume_time: MetricBuilder::new(metric).subset_time("first_consume_time", 1),
1290            finish_time: MetricBuilder::new(metric).subset_time("finish_time", 1),
1291            output_rows: MetricBuilder::new(metric).output_rows(1),
1292            greptime_exec_cost: MetricBuilder::new(metric).gauge(GREPTIME_EXEC_READ_COST, 1),
1293        }
1294    }
1295
1296    pub fn ready_time(&self) -> &Time {
1297        &self.ready_time
1298    }
1299
1300    pub fn first_consume_time(&self) -> &Time {
1301        &self.first_consume_time
1302    }
1303
1304    pub fn finish_time(&self) -> &Time {
1305        &self.finish_time
1306    }
1307
1308    pub fn record_output_batch_rows(&self, num_rows: usize) {
1309        self.output_rows.add(num_rows);
1310    }
1311
1312    pub fn record_greptime_exec_cost(&self, metrics: usize) {
1313        self.greptime_exec_cost.add(metrics);
1314    }
1315}
1316
1317#[cfg(test)]
1318mod tests {
1319    use std::collections::{BTreeSet, HashMap as StdHashMap};
1320    use std::pin::Pin;
1321    use std::sync::atomic::{AtomicUsize, Ordering};
1322    use std::task::{Context, Poll};
1323
1324    use arrow::array::{Int64Array, TimestampMillisecondArray};
1325    use arrow_schema::extension::{
1326        EXTENSION_TYPE_METADATA_KEY, EXTENSION_TYPE_NAME_KEY, ExtensionType,
1327    };
1328    use arrow_schema::{DataType as TestArrowDataType, Field, Fields, TimeUnit};
1329    use async_trait::async_trait;
1330    use common_base::Plugins;
1331    use common_meta::peer::Peer;
1332    use common_query::request::{
1333        INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY, InitialDynFilterRegs,
1334    };
1335    use common_recordbatch::adapter::{PlanMetrics, RecordBatchMetrics};
1336    use common_recordbatch::{
1337        DfRecordBatch, EmptyRecordBatchStream, RecordBatch, RecordBatchStream,
1338    };
1339    use datafusion::config::ConfigOptions;
1340    use datafusion::execution::SessionStateBuilder;
1341    use datafusion::physical_plan::filter_pushdown::ChildFilterPushdownResult;
1342    use datafusion_common::TableReference;
1343    use datafusion_expr::{LogicalPlanBuilder, col, lit};
1344    use datafusion_physical_expr::Distribution;
1345    use datafusion_physical_expr::expressions::{
1346        Column, DynamicFilterPhysicalExpr, lit as physical_lit,
1347    };
1348    use datatypes::extension::json::JsonExtensionType;
1349    use datatypes::prelude::{ConcreteDataType, VectorRef};
1350    use datatypes::schema::{ColumnSchema, Schema};
1351    use datatypes::vectors::{Int64Vector, StringVector, TimestampMillisecondVector};
1352    use futures_util::{Stream, TryStreamExt};
1353    use session::ReadPreference;
1354    use session::context::QueryContext;
1355    use session::query_id::QueryId;
1356    use table::table::scan::REGION_SCAN_EXEC_NAME;
1357    use table::table_name::TableName;
1358    use tokio::sync::{Notify, oneshot};
1359    use uuid::Uuid;
1360
1361    use super::*;
1362    use crate::dist_plan::DynFilterRegistryManager;
1363    use crate::options::QueryOptions;
1364    use crate::query_engine::{QueryEngineContext, QueryEngineState};
1365    use crate::region_query::RegionQueryHandler;
1366
1367    fn test_target(id: u64) -> crate::region_query::RegionQueryTarget {
1368        crate::region_query::RegionQueryTarget::new(Peer {
1369            id,
1370            addr: format!("127.0.0.1:{id}"),
1371        })
1372    }
1373
1374    fn test_query_id(value: u128) -> QueryId {
1375        QueryId::from(Uuid::from_u128(value))
1376    }
1377
1378    #[test]
1379    fn test_amend_legacy_json2_field_preserves_json2_identity() {
1380        let field = Field::new("j", DataType::Struct(Fields::empty()), true).with_metadata(
1381            StdHashMap::from([
1382                (
1383                    EXTENSION_TYPE_NAME_KEY.to_string(),
1384                    JsonExtensionType::NAME.to_string(),
1385                ),
1386                (
1387                    EXTENSION_TYPE_METADATA_KEY.to_string(),
1388                    serde_json::json!({
1389                        "json_structure_settings": { "Structured": null }
1390                    })
1391                    .to_string(),
1392                ),
1393            ]),
1394        );
1395
1396        let schema = maybe_amend_json2_field(&ArrowSchema::new(vec![field]));
1397        let field = schema.field(0);
1398        assert_eq!(&DataType::Binary, field.data_type());
1399        assert_eq!(Some(Json2ExtensionType::NAME), field.extension_type_name());
1400        assert!(is_json2_extension_type(field));
1401    }
1402
1403    fn merge_scan_exec_with_sorted_input(
1404        region_count: u64,
1405        target_partition: usize,
1406    ) -> MergeScanExec {
1407        let session_state = SessionStateBuilder::new().build();
1408        let plan = LogicalPlanBuilder::empty(true)
1409            .project(vec![lit(1i64).alias("ts")])
1410            .unwrap()
1411            .sort(vec![col("ts").sort(false, true)])
1412            .unwrap()
1413            .build()
1414            .unwrap();
1415        let schema = plan.schema().as_arrow().clone();
1416        let regions = (0..region_count)
1417            .map(|region_number| RegionId::new(1024, region_number as u32))
1418            .collect();
1419
1420        MergeScanExec::new(
1421            &session_state,
1422            // The table name is not relevant to these ordering metadata tests;
1423            // `MergeScanExec::new` requires one to model the production plan.
1424            TableName::new("catalog", "schema", "table"),
1425            regions,
1426            plan,
1427            &schema,
1428            Arc::new(TestRegionQueryHandler::default()),
1429            QueryContext::arc(),
1430            target_partition,
1431            AliasMapping::new(),
1432            None,
1433            false,
1434        )
1435        .unwrap()
1436    }
1437
1438    fn task_context_with_engine_state(
1439        state: Arc<QueryEngineState>,
1440        query_ctx: QueryContextRef,
1441    ) -> Arc<TaskContext> {
1442        let mut session_state = state.session_state();
1443        session_state.config_mut().set_extension(state);
1444        QueryEngineContext::new(session_state, query_ctx).build_task_ctx()
1445    }
1446
1447    fn remote_dyn_filter_test_exec(
1448        handler: crate::region_query::RegionQueryHandlerRef,
1449        query_ctx: QueryContextRef,
1450    ) -> MergeScanExec {
1451        let session_state = SessionStateBuilder::new().build();
1452        let plan = LogicalPlanBuilder::empty(true)
1453            .project(vec![lit(1i32).alias("col1")])
1454            .unwrap()
1455            .build()
1456            .unwrap();
1457        let schema = plan.schema().as_arrow().clone();
1458
1459        MergeScanExec::new(
1460            &session_state,
1461            TableName::new("catalog", "schema", "table"),
1462            vec![RegionId::new(1024, 1)],
1463            plan,
1464            &schema,
1465            handler,
1466            query_ctx,
1467            1,
1468            AliasMapping::new(),
1469            Some(RemoteDynFilterProducerId::new(42)),
1470            false,
1471        )
1472        .unwrap()
1473    }
1474
1475    fn install_remote_dyn_filter(exec: &MergeScanExec) -> Arc<DynamicFilterPhysicalExpr> {
1476        let dyn_filter = Arc::new(DynamicFilterPhysicalExpr::new(
1477            vec![Arc::new(Column::new("host", 0)) as Arc<_>],
1478            physical_lit(true) as _,
1479        ));
1480        exec.handle_child_pushdown_result(
1481            FilterPushdownPhase::Post,
1482            ChildPushdownResult {
1483                parent_filters: vec![ChildFilterPushdownResult {
1484                    filter: dyn_filter.clone() as Arc<dyn datafusion_physical_expr::PhysicalExpr>,
1485                    child_results: vec![PushedDown::Yes],
1486                }],
1487                self_filters: Vec::new(),
1488            },
1489            &ConfigOptions::new(),
1490        )
1491        .unwrap();
1492        dyn_filter
1493    }
1494
1495    fn query_engine_state(
1496        handler: crate::region_query::RegionQueryHandlerRef,
1497    ) -> Arc<QueryEngineState> {
1498        Arc::new(QueryEngineState::new(
1499            catalog::memory::new_memory_catalog_manager().unwrap(),
1500            None,
1501            Some(handler),
1502            None,
1503            None,
1504            None,
1505            false,
1506            Plugins::default(),
1507            QueryOptions::default(),
1508        ))
1509    }
1510
1511    fn empty_record_batch_stream(
1512        request: &common_query::request::QueryRequest,
1513    ) -> common_recordbatch::SendableRecordBatchStream {
1514        let arrow_schema = request.plan.schema().as_arrow().clone();
1515        Box::pin(EmptyRecordBatchStream::new(Arc::new(
1516            datatypes::schema::Schema::try_from(Arc::new(arrow_schema)).unwrap(),
1517        )))
1518    }
1519
1520    fn pending_record_batch_stream(
1521        request: &common_query::request::QueryRequest,
1522    ) -> common_recordbatch::SendableRecordBatchStream {
1523        let stream = futures_util::stream::pending::<
1524            datafusion_common::Result<datafusion::arrow::record_batch::RecordBatch>,
1525        >();
1526        let arrow_schema = request.plan.schema().as_arrow().clone();
1527        let stream = RecordBatchStreamAdapter::new(Arc::new(arrow_schema), stream);
1528        Box::pin(
1529            common_recordbatch::adapter::RecordBatchStreamAdapter::try_new(Box::pin(stream))
1530                .unwrap(),
1531        )
1532    }
1533
1534    #[test]
1535    fn merge_scan_does_not_advertise_ordering_when_partition_may_merge_regions() {
1536        let exec = merge_scan_exec_with_sorted_input(3, 2);
1537
1538        assert!(
1539            exec.properties().output_ordering().is_none(),
1540            "target_partition < region_count means one output partition may concatenate multiple sorted region streams"
1541        );
1542    }
1543
1544    #[test]
1545    fn merge_scan_advertises_ordering_when_each_partition_reads_at_most_one_region() {
1546        let exec = merge_scan_exec_with_sorted_input(3, 3);
1547
1548        assert!(exec.properties().output_ordering().is_some());
1549    }
1550
1551    #[test]
1552    fn merge_scan_advertises_ordering_when_partitions_exceed_regions() {
1553        let exec = merge_scan_exec_with_sorted_input(3, 4);
1554
1555        assert!(exec.properties().output_ordering().is_some());
1556    }
1557
1558    #[test]
1559    fn sub_stage_metrics_are_sorted_by_region_id() {
1560        let exec = merge_scan_exec_with_sorted_input(0, 1);
1561        let higher_region = RegionId::new(1024, 2);
1562        let lower_region = RegionId::new(1024, 1);
1563        let higher_metrics = RecordBatchMetrics {
1564            plan_metrics: vec![PlanMetrics {
1565                plan: "higher region".to_string(),
1566                plan_name: "higher region".to_string(),
1567                level: 0,
1568                metrics: Vec::new(),
1569            }],
1570            ..Default::default()
1571        };
1572        let lower_metrics = RecordBatchMetrics {
1573            plan_metrics: vec![PlanMetrics {
1574                plan: "lower region".to_string(),
1575                plan_name: "lower region".to_string(),
1576                level: 0,
1577                metrics: Vec::new(),
1578            }],
1579            ..Default::default()
1580        };
1581
1582        let mut sub_stage_metrics = exec.sub_stage_metrics.lock().unwrap();
1583        sub_stage_metrics.insert(higher_region, higher_metrics);
1584        sub_stage_metrics.insert(lower_region, lower_metrics);
1585        drop(sub_stage_metrics);
1586
1587        let metrics = exec.sub_stage_metrics();
1588        let plans: Vec<_> = metrics
1589            .iter()
1590            .map(|metrics| metrics.plan_metrics[0].plan.as_str())
1591            .collect();
1592
1593        assert_eq!(plans, ["lower region", "higher region"]);
1594    }
1595
1596    #[test]
1597    fn remote_dyn_filter_producer_registration_defers_subscriber_registration() {
1598        let registry_manager = Arc::new(DynFilterRegistryManager::default());
1599        let query_ctx = QueryContext::arc();
1600        let query_id = query_ctx
1601            .remote_query_id_value()
1602            .expect("query context must have remote query id");
1603        let lease = registry_manager.acquire_lease(query_id);
1604        let region_id = RegionId::new(1024, 7);
1605        let dyn_filter = Arc::new(DynamicFilterPhysicalExpr::new(
1606            vec![Arc::new(Column::new("host", 0)) as Arc<_>],
1607            physical_lit(true) as _,
1608        )) as Arc<dyn datafusion_physical_expr::PhysicalExpr>;
1609        let captured = capture_remote_dyn_filters_for_pushdown(
1610            RemoteDynFilterProducerId::new(42),
1611            vec![dyn_filter],
1612        );
1613        assert_eq!(captured.captured_dyn_filters.len(), 1);
1614
1615        register_remote_dyn_filters_for_region(Some(&lease), &captured.captured_dyn_filters);
1616        let region_query_ctx = query_context_with_refreshed_initial_dyn_filter_regs(
1617            &query_ctx,
1618            region_id,
1619            &captured.captured_dyn_filters,
1620        );
1621
1622        let entries = lease.registry().entries();
1623        assert_eq!(entries.len(), 1);
1624        assert!(entries[0].subscribers().is_empty());
1625        assert!(
1626            !entries[0].fanout_started_for_test(),
1627            "fanout must start only after do_get succeeds"
1628        );
1629        assert!(
1630            region_query_ctx
1631                .extension(INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY)
1632                .is_some(),
1633            "initial RDF registrations must be present in the do_get query context"
1634        );
1635    }
1636
1637    #[tokio::test]
1638    async fn failed_do_get_rolls_back_new_subscriber_without_starting_fanout() {
1639        let handler = Arc::new(FailingRegionQueryHandler::default());
1640        let query_ctx = QueryContext::arc();
1641        let state = Arc::new(QueryEngineState::new(
1642            catalog::memory::new_memory_catalog_manager().unwrap(),
1643            None,
1644            Some(handler.clone()),
1645            None,
1646            None,
1647            None,
1648            false,
1649            Plugins::default(),
1650            QueryOptions::default(),
1651        ));
1652        let lease = state
1653            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
1654            .unwrap();
1655        handler.set_registry(lease.registry_arc_for_test());
1656        let task_ctx = task_context_with_engine_state(state, query_ctx.clone());
1657        let plan = LogicalPlanBuilder::empty(true)
1658            .project(vec![lit(1i32).alias("col1")])
1659            .unwrap()
1660            .build()
1661            .unwrap();
1662        let schema = plan.schema().as_arrow().clone();
1663        let exec = MergeScanExec::new(
1664            &SessionStateBuilder::new().build(),
1665            TableName::new("catalog", "schema", "table"),
1666            vec![RegionId::new(1024, 1)],
1667            plan,
1668            &schema,
1669            handler.clone(),
1670            query_ctx,
1671            1,
1672            AliasMapping::new(),
1673            Some(RemoteDynFilterProducerId::new(42)),
1674            false,
1675        )
1676        .unwrap();
1677        let dyn_filter = Arc::new(DynamicFilterPhysicalExpr::new(
1678            vec![Arc::new(Column::new("host", 0)) as Arc<_>],
1679            physical_lit(true) as _,
1680        )) as Arc<dyn datafusion_physical_expr::PhysicalExpr>;
1681        exec.handle_child_pushdown_result(
1682            FilterPushdownPhase::Post,
1683            ChildPushdownResult {
1684                parent_filters: vec![ChildFilterPushdownResult {
1685                    filter: dyn_filter,
1686                    child_results: vec![PushedDown::Yes],
1687                }],
1688                self_filters: Vec::new(),
1689            },
1690            &ConfigOptions::new(),
1691        )
1692        .unwrap();
1693
1694        let mut stream = exec.to_stream(task_ctx, 0).unwrap();
1695        assert!(stream.next().await.unwrap().is_err());
1696        assert_eq!(handler.do_get_calls.load(Ordering::SeqCst), 1);
1697        assert!(handler.saw_subscriber.load(Ordering::SeqCst));
1698
1699        let entries = lease.registry().entries();
1700        assert_eq!(entries.len(), 1);
1701        assert!(entries[0].subscribers().is_empty());
1702        assert!(!entries[0].fanout_started_for_test());
1703    }
1704
1705    #[tokio::test]
1706    async fn aborting_pending_do_get_poll_rolls_back_subscriber_without_starting_fanout() {
1707        let handler = Arc::new(PendingDoGetHandler::default());
1708        let query_ctx = QueryContext::arc();
1709        let state = query_engine_state(handler.clone());
1710        let lease = state
1711            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
1712            .unwrap();
1713        let exec = remote_dyn_filter_test_exec(handler.clone(), query_ctx.clone());
1714        install_remote_dyn_filter(&exec);
1715        let stream = exec
1716            .to_stream(task_context_with_engine_state(state, query_ctx), 0)
1717            .unwrap();
1718
1719        let poll = tokio::spawn(async move {
1720            let mut stream = stream;
1721            stream.next().await
1722        });
1723        handler.do_get_entered.notified().await;
1724        let entries = lease.registry().entries();
1725        assert_eq!(entries.len(), 1);
1726        assert_eq!(entries[0].subscribers().len(), 1);
1727        assert!(!entries[0].fanout_started_for_test());
1728
1729        poll.abort();
1730        assert!(poll.await.unwrap_err().is_cancelled());
1731
1732        assert!(entries[0].subscribers().is_empty());
1733        assert!(!entries[0].fanout_started_for_test());
1734    }
1735
1736    #[tokio::test]
1737    async fn failed_do_get_preserves_preexisting_duplicate_subscriber() {
1738        let handler = Arc::new(FailingRegionQueryHandler::default());
1739        let query_ctx = QueryContext::arc();
1740        let state = query_engine_state(handler.clone());
1741        let lease = state
1742            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
1743            .unwrap();
1744        let exec = remote_dyn_filter_test_exec(handler.clone(), query_ctx.clone());
1745        install_remote_dyn_filter(&exec);
1746
1747        register_remote_dyn_filters_for_region(Some(&lease), &exec.captured_remote_dyn_filters());
1748        let entry = lease.registry().entries().pop().unwrap();
1749        let subscriber = Subscriber::new(RegionId::new(1024, 1), test_target(1));
1750        assert!(matches!(
1751            lease
1752                .registry()
1753                .register_subscriber(entry.filter_id(), subscriber.clone()),
1754            crate::dist_plan::SubscriberRegistration::Added
1755        ));
1756
1757        let mut stream = exec
1758            .to_stream(task_context_with_engine_state(state, query_ctx), 0)
1759            .unwrap();
1760        assert!(stream.next().await.unwrap().is_err());
1761
1762        assert_eq!(entry.subscribers(), vec![subscriber]);
1763        assert!(!entry.fanout_started_for_test());
1764    }
1765
1766    #[tokio::test]
1767    async fn failed_target_selection_registers_no_subscriber_or_fanout() {
1768        let handler = Arc::new(SelectTargetErrorHandler::default());
1769        let query_ctx = QueryContext::arc();
1770        let state = query_engine_state(handler.clone());
1771        let lease = state
1772            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
1773            .unwrap();
1774        let exec = remote_dyn_filter_test_exec(handler.clone(), query_ctx.clone());
1775        install_remote_dyn_filter(&exec);
1776
1777        let mut stream = exec
1778            .to_stream(task_context_with_engine_state(state, query_ctx), 0)
1779            .unwrap();
1780        assert!(stream.next().await.unwrap().is_err());
1781
1782        assert_eq!(handler.select_target_calls.load(Ordering::SeqCst), 1);
1783        assert_eq!(handler.do_get_calls.load(Ordering::SeqCst), 0);
1784        let entries = lease.registry().entries();
1785        assert_eq!(entries.len(), 1);
1786        assert!(entries[0].subscribers().is_empty());
1787        assert!(!entries[0].fanout_started_for_test());
1788    }
1789
1790    #[tokio::test]
1791    async fn frozen_target_is_reused_for_do_get_update_and_unregister() {
1792        let (first_update_entered_tx, first_update_entered_rx) = oneshot::channel();
1793        let (release_first_update_tx, release_first_update_rx) = oneshot::channel();
1794        let (second_update_entered_tx, second_update_entered_rx) = oneshot::channel();
1795        let (release_second_update_tx, release_second_update_rx) = oneshot::channel();
1796        let (unregister_tx, unregister_rx) = oneshot::channel();
1797        let handler = Arc::new(RoutingRegionQueryHandler::new(
1798            first_update_entered_tx,
1799            release_first_update_rx,
1800            second_update_entered_tx,
1801            release_second_update_rx,
1802            unregister_tx,
1803        ));
1804        let query_ctx = QueryContext::arc();
1805        let state = query_engine_state(handler.clone());
1806        let lease = state
1807            .acquire_remote_dyn_filter_registry_lease(&query_ctx)
1808            .unwrap();
1809        let exec = remote_dyn_filter_test_exec(handler.clone(), query_ctx.clone());
1810        let dyn_filter = install_remote_dyn_filter(&exec);
1811
1812        let stream = exec
1813            .to_stream(task_context_with_engine_state(state, query_ctx), 0)
1814            .unwrap();
1815        let poll = tokio::spawn(async move {
1816            let mut stream = stream;
1817            stream.next().await
1818        });
1819        handler.do_get_entered.notified().await;
1820        assert_eq!(handler.do_get_targets(), vec![1]);
1821
1822        first_update_entered_rx.await.unwrap();
1823        dyn_filter.update(physical_lit(false) as _).unwrap();
1824        release_first_update_tx.send(()).unwrap();
1825        second_update_entered_rx.await.unwrap();
1826        assert_eq!(handler.update_targets(), vec![1, 1]);
1827
1828        poll.abort();
1829        assert!(poll.await.unwrap_err().is_cancelled());
1830        drop(exec);
1831        drop(dyn_filter);
1832        release_second_update_tx.send(()).unwrap();
1833        unregister_rx.await.unwrap();
1834        assert_eq!(handler.unregister_targets(), vec![1]);
1835        drop(lease);
1836    }
1837
1838    #[tokio::test]
1839    async fn immediate_eof_do_get_receives_refreshed_remote_dyn_filter_snapshot() {
1840        let handler = Arc::new(ImmediateEofRegionQueryHandler::default());
1841        let query_ctx = QueryContext::arc();
1842        let state = query_engine_state(handler.clone());
1843        let exec = remote_dyn_filter_test_exec(handler.clone(), query_ctx.clone());
1844        let dyn_filter = install_remote_dyn_filter(&exec);
1845        dyn_filter.update(physical_lit(false) as _).unwrap();
1846
1847        let mut stream = exec
1848            .to_stream(task_context_with_engine_state(state, query_ctx), 0)
1849            .unwrap();
1850        assert!(stream.next().await.is_none());
1851
1852        let registrations = handler.registrations();
1853        assert_eq!(registrations.regs.len(), 1);
1854        let snapshot = registrations.regs[0].initial_snapshot.as_ref().unwrap();
1855        assert!(snapshot.generation > 0);
1856        assert!(!snapshot.is_complete);
1857        assert_eq!(
1858            snapshot
1859                .payload
1860                .decode_datafusion_expr(
1861                    &TaskContext::default(),
1862                    &ArrowSchema::new(vec![arrow_schema::Field::new(
1863                        "host",
1864                        arrow_schema::DataType::Boolean,
1865                        false,
1866                    )]),
1867                    common_query::request::REMOTE_DYN_FILTER_PAYLOAD_MAX_BYTES,
1868                )
1869                .unwrap()
1870                .to_string(),
1871            "false"
1872        );
1873    }
1874
1875    #[test]
1876    fn remote_dyn_filter_registry_cleanup_waits_for_last_query_scoped_stream_drop() {
1877        let registry_manager = Arc::new(DynFilterRegistryManager::default());
1878        let query_id = test_query_id(1);
1879
1880        let first = registry_manager.acquire_lease(query_id);
1881        let second = registry_manager.acquire_lease(query_id);
1882
1883        drop(first);
1884        assert_eq!(registry_manager.registry_count(), 1);
1885
1886        drop(second);
1887        assert_eq!(registry_manager.registry_count(), 0);
1888    }
1889
1890    #[test]
1891    fn remote_dyn_filter_registry_cleanup_shares_query_scope_across_independent_leases() {
1892        let registry_manager = Arc::new(DynFilterRegistryManager::default());
1893        let query_id = test_query_id(1);
1894
1895        let first_exec_like_lease = registry_manager.acquire_lease(query_id);
1896        let second_exec_like_lease = registry_manager.acquire_lease(query_id);
1897
1898        drop(first_exec_like_lease);
1899        assert_eq!(registry_manager.registry_count(), 1);
1900
1901        drop(second_exec_like_lease);
1902        assert_eq!(registry_manager.registry_count(), 0);
1903    }
1904
1905    #[derive(Clone)]
1906    struct TestRegionResponse {
1907        advertised_schema: Arc<Schema>,
1908        batches: Vec<RecordBatch>,
1909    }
1910
1911    #[derive(Default)]
1912    struct TestRegionQueryHandler {
1913        responses: HashMap<RegionId, TestRegionResponse>,
1914    }
1915
1916    impl TestRegionQueryHandler {
1917        fn new(responses: impl IntoIterator<Item = (RegionId, RecordBatch)>) -> Self {
1918            let responses = responses
1919                .into_iter()
1920                .map(|(region_id, batch)| {
1921                    (
1922                        region_id,
1923                        TestRegionResponse {
1924                            advertised_schema: batch.schema.clone(),
1925                            batches: vec![batch],
1926                        },
1927                    )
1928                })
1929                .collect();
1930            Self { responses }
1931        }
1932
1933        fn with_responses(
1934            responses: impl IntoIterator<Item = (RegionId, Arc<Schema>, Vec<RecordBatch>)>,
1935        ) -> Self {
1936            let responses = responses
1937                .into_iter()
1938                .map(|(region_id, advertised_schema, batches)| {
1939                    (
1940                        region_id,
1941                        TestRegionResponse {
1942                            advertised_schema,
1943                            batches,
1944                        },
1945                    )
1946                })
1947                .collect();
1948            Self { responses }
1949        }
1950    }
1951
1952    struct TestRecordBatchStream {
1953        schema: Arc<Schema>,
1954        batches: Vec<RecordBatch>,
1955        index: usize,
1956    }
1957
1958    impl Stream for TestRecordBatchStream {
1959        type Item = common_recordbatch::error::Result<RecordBatch>;
1960
1961        fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1962            if let Some(batch) = self.batches.get(self.index).cloned() {
1963                self.index += 1;
1964                Poll::Ready(Some(Ok(batch)))
1965            } else {
1966                Poll::Ready(None)
1967            }
1968        }
1969    }
1970
1971    impl RecordBatchStream for TestRecordBatchStream {
1972        fn schema(&self) -> Arc<Schema> {
1973            self.schema.clone()
1974        }
1975
1976        fn output_ordering(&self) -> Option<&[common_recordbatch::OrderOption]> {
1977            None
1978        }
1979
1980        fn metrics(&self) -> Option<RecordBatchMetrics> {
1981            None
1982        }
1983    }
1984
1985    #[derive(Default)]
1986    struct FailingRegionQueryHandler {
1987        do_get_calls: AtomicUsize,
1988        saw_subscriber: std::sync::atomic::AtomicBool,
1989        registry: std::sync::Mutex<Option<Arc<crate::dist_plan::QueryDynFilterRegistry>>>,
1990    }
1991
1992    impl FailingRegionQueryHandler {
1993        fn set_registry(&self, registry: Arc<crate::dist_plan::QueryDynFilterRegistry>) {
1994            *self.registry.lock().unwrap() = Some(registry);
1995        }
1996    }
1997
1998    #[async_trait]
1999    impl RegionQueryHandler for FailingRegionQueryHandler {
2000        async fn select_target(
2001            &self,
2002            _read_preference: ReadPreference,
2003            _region_id: RegionId,
2004        ) -> crate::error::Result<crate::region_query::RegionQueryTarget> {
2005            Ok(test_target(1))
2006        }
2007
2008        async fn do_get(
2009            &self,
2010            _target: &crate::region_query::RegionQueryTarget,
2011            _request: common_query::request::QueryRequest,
2012        ) -> crate::error::Result<common_recordbatch::SendableRecordBatchStream> {
2013            self.do_get_calls.fetch_add(1, Ordering::SeqCst);
2014            self.saw_subscriber.store(
2015                self.registry
2016                    .lock()
2017                    .unwrap()
2018                    .as_ref()
2019                    .is_some_and(|registry| {
2020                        registry
2021                            .entries()
2022                            .iter()
2023                            .any(|entry| !entry.subscribers().is_empty())
2024                    }),
2025                Ordering::SeqCst,
2026            );
2027            crate::error::UnimplementedSnafu {
2028                operation: "test do_get failure",
2029            }
2030            .fail()
2031        }
2032
2033        async fn handle_remote_dyn_filter_update(
2034            &self,
2035            _target: &crate::region_query::RegionQueryTarget,
2036            _query_id: String,
2037            _update: api::v1::region::RemoteDynFilterUpdate,
2038        ) -> crate::error::Result<()> {
2039            unimplemented!("test only")
2040        }
2041
2042        async fn handle_remote_dyn_filter_unregister(
2043            &self,
2044            _target: &crate::region_query::RegionQueryTarget,
2045            _query_id: String,
2046            _unregister: api::v1::region::RemoteDynFilterUnregister,
2047        ) -> crate::error::Result<()> {
2048            unimplemented!("test only")
2049        }
2050    }
2051
2052    #[derive(Default)]
2053    struct PendingDoGetHandler {
2054        do_get_entered: Notify,
2055        never_complete: Notify,
2056    }
2057
2058    #[async_trait]
2059    impl RegionQueryHandler for PendingDoGetHandler {
2060        async fn select_target(
2061            &self,
2062            _read_preference: ReadPreference,
2063            _region_id: RegionId,
2064        ) -> crate::error::Result<crate::region_query::RegionQueryTarget> {
2065            Ok(test_target(1))
2066        }
2067
2068        async fn do_get(
2069            &self,
2070            _target: &crate::region_query::RegionQueryTarget,
2071            _request: common_query::request::QueryRequest,
2072        ) -> crate::error::Result<common_recordbatch::SendableRecordBatchStream> {
2073            self.do_get_entered.notify_one();
2074            self.never_complete.notified().await;
2075            unreachable!("the test aborts the pending do_get future")
2076        }
2077
2078        async fn handle_remote_dyn_filter_update(
2079            &self,
2080            _target: &crate::region_query::RegionQueryTarget,
2081            _query_id: String,
2082            _update: api::v1::region::RemoteDynFilterUpdate,
2083        ) -> crate::error::Result<()> {
2084            unreachable!("fanout must not start while do_get is pending")
2085        }
2086
2087        async fn handle_remote_dyn_filter_unregister(
2088            &self,
2089            _target: &crate::region_query::RegionQueryTarget,
2090            _query_id: String,
2091            _unregister: api::v1::region::RemoteDynFilterUnregister,
2092        ) -> crate::error::Result<()> {
2093            unreachable!("fanout must not start while do_get is pending")
2094        }
2095    }
2096
2097    #[derive(Default)]
2098    struct SelectTargetErrorHandler {
2099        select_target_calls: AtomicUsize,
2100        do_get_calls: AtomicUsize,
2101    }
2102
2103    #[async_trait]
2104    impl RegionQueryHandler for SelectTargetErrorHandler {
2105        async fn select_target(
2106            &self,
2107            _read_preference: ReadPreference,
2108            _region_id: RegionId,
2109        ) -> crate::error::Result<crate::region_query::RegionQueryTarget> {
2110            self.select_target_calls.fetch_add(1, Ordering::SeqCst);
2111            crate::error::UnimplementedSnafu {
2112                operation: "test target selection failure",
2113            }
2114            .fail()
2115        }
2116
2117        async fn do_get(
2118            &self,
2119            _target: &crate::region_query::RegionQueryTarget,
2120            _request: common_query::request::QueryRequest,
2121        ) -> crate::error::Result<common_recordbatch::SendableRecordBatchStream> {
2122            self.do_get_calls.fetch_add(1, Ordering::SeqCst);
2123            unreachable!("do_get must not run after select_target fails")
2124        }
2125
2126        async fn handle_remote_dyn_filter_update(
2127            &self,
2128            _target: &crate::region_query::RegionQueryTarget,
2129            _query_id: String,
2130            _update: api::v1::region::RemoteDynFilterUpdate,
2131        ) -> crate::error::Result<()> {
2132            unreachable!("fanout must not start after select_target fails")
2133        }
2134
2135        async fn handle_remote_dyn_filter_unregister(
2136            &self,
2137            _target: &crate::region_query::RegionQueryTarget,
2138            _query_id: String,
2139            _unregister: api::v1::region::RemoteDynFilterUnregister,
2140        ) -> crate::error::Result<()> {
2141            unreachable!("fanout must not start after select_target fails")
2142        }
2143    }
2144
2145    struct RoutingRegionQueryHandler {
2146        route: Mutex<crate::region_query::RegionQueryTarget>,
2147        do_get_entered: Notify,
2148        do_get_targets: Mutex<Vec<u64>>,
2149        update_targets: Mutex<Vec<u64>>,
2150        unregister_targets: Mutex<Vec<u64>>,
2151        update_calls: AtomicUsize,
2152        first_update_entered_tx: Mutex<Option<oneshot::Sender<()>>>,
2153        release_first_update_rx: Mutex<Option<oneshot::Receiver<()>>>,
2154        second_update_entered_tx: Mutex<Option<oneshot::Sender<()>>>,
2155        release_second_update_rx: Mutex<Option<oneshot::Receiver<()>>>,
2156        unregister_tx: Mutex<Option<oneshot::Sender<()>>>,
2157    }
2158
2159    impl RoutingRegionQueryHandler {
2160        fn new(
2161            first_update_entered_tx: oneshot::Sender<()>,
2162            release_first_update_rx: oneshot::Receiver<()>,
2163            second_update_entered_tx: oneshot::Sender<()>,
2164            release_second_update_rx: oneshot::Receiver<()>,
2165            unregister_tx: oneshot::Sender<()>,
2166        ) -> Self {
2167            Self {
2168                route: Mutex::new(test_target(1)),
2169                do_get_entered: Notify::new(),
2170                do_get_targets: Mutex::new(Vec::new()),
2171                update_targets: Mutex::new(Vec::new()),
2172                unregister_targets: Mutex::new(Vec::new()),
2173                update_calls: AtomicUsize::new(0),
2174                first_update_entered_tx: Mutex::new(Some(first_update_entered_tx)),
2175                release_first_update_rx: Mutex::new(Some(release_first_update_rx)),
2176                second_update_entered_tx: Mutex::new(Some(second_update_entered_tx)),
2177                release_second_update_rx: Mutex::new(Some(release_second_update_rx)),
2178                unregister_tx: Mutex::new(Some(unregister_tx)),
2179            }
2180        }
2181
2182        fn do_get_targets(&self) -> Vec<u64> {
2183            self.do_get_targets.lock().unwrap().clone()
2184        }
2185
2186        fn update_targets(&self) -> Vec<u64> {
2187            self.update_targets.lock().unwrap().clone()
2188        }
2189
2190        fn unregister_targets(&self) -> Vec<u64> {
2191            self.unregister_targets.lock().unwrap().clone()
2192        }
2193    }
2194
2195    #[async_trait]
2196    impl RegionQueryHandler for RoutingRegionQueryHandler {
2197        async fn select_target(
2198            &self,
2199            _read_preference: ReadPreference,
2200            _region_id: RegionId,
2201        ) -> crate::error::Result<crate::region_query::RegionQueryTarget> {
2202            let mut route = self.route.lock().unwrap();
2203            let target = route.clone();
2204            *route = test_target(2);
2205            Ok(target)
2206        }
2207
2208        async fn do_get(
2209            &self,
2210            target: &crate::region_query::RegionQueryTarget,
2211            request: common_query::request::QueryRequest,
2212        ) -> crate::error::Result<common_recordbatch::SendableRecordBatchStream> {
2213            self.do_get_targets.lock().unwrap().push(target.peer().id);
2214            self.do_get_entered.notify_one();
2215            Ok(pending_record_batch_stream(&request))
2216        }
2217
2218        async fn handle_remote_dyn_filter_update(
2219            &self,
2220            target: &crate::region_query::RegionQueryTarget,
2221            _query_id: String,
2222            _update: api::v1::region::RemoteDynFilterUpdate,
2223        ) -> crate::error::Result<()> {
2224            self.update_targets.lock().unwrap().push(target.peer().id);
2225            match self.update_calls.fetch_add(1, Ordering::SeqCst) {
2226                0 => {
2227                    if let Some(tx) = self.first_update_entered_tx.lock().unwrap().take() {
2228                        let _ = tx.send(());
2229                    }
2230                    let release = { self.release_first_update_rx.lock().unwrap().take() };
2231                    if let Some(release) = release {
2232                        let _ = release.await;
2233                    }
2234                }
2235                1 => {
2236                    if let Some(tx) = self.second_update_entered_tx.lock().unwrap().take() {
2237                        let _ = tx.send(());
2238                    }
2239                    let release = { self.release_second_update_rx.lock().unwrap().take() };
2240                    if let Some(release) = release {
2241                        let _ = release.await;
2242                    }
2243                }
2244                _ => {}
2245            }
2246            Ok(())
2247        }
2248
2249        async fn handle_remote_dyn_filter_unregister(
2250            &self,
2251            target: &crate::region_query::RegionQueryTarget,
2252            _query_id: String,
2253            _unregister: api::v1::region::RemoteDynFilterUnregister,
2254        ) -> crate::error::Result<()> {
2255            self.unregister_targets
2256                .lock()
2257                .unwrap()
2258                .push(target.peer().id);
2259            if let Some(tx) = self.unregister_tx.lock().unwrap().take() {
2260                let _ = tx.send(());
2261            }
2262            Ok(())
2263        }
2264    }
2265
2266    #[derive(Default)]
2267    struct ImmediateEofRegionQueryHandler {
2268        registrations: Mutex<Option<InitialDynFilterRegs>>,
2269    }
2270
2271    impl ImmediateEofRegionQueryHandler {
2272        fn registrations(&self) -> InitialDynFilterRegs {
2273            self.registrations.lock().unwrap().clone().unwrap()
2274        }
2275    }
2276
2277    #[async_trait]
2278    impl RegionQueryHandler for ImmediateEofRegionQueryHandler {
2279        async fn select_target(
2280            &self,
2281            _read_preference: ReadPreference,
2282            _region_id: RegionId,
2283        ) -> crate::error::Result<crate::region_query::RegionQueryTarget> {
2284            Ok(test_target(1))
2285        }
2286
2287        async fn do_get(
2288            &self,
2289            _target: &crate::region_query::RegionQueryTarget,
2290            request: common_query::request::QueryRequest,
2291        ) -> crate::error::Result<common_recordbatch::SendableRecordBatchStream> {
2292            let registrations = request
2293                .header
2294                .clone()
2295                .and_then(|header| header.query_context)
2296                .and_then(|query_context| {
2297                    query_context
2298                        .extensions
2299                        .get(INITIAL_REMOTE_DYN_FILTER_REGISTRATIONS_EXTENSION_KEY)
2300                        .cloned()
2301                })
2302                .map(|serialized| InitialDynFilterRegs::from_extension_value(&serialized).unwrap())
2303                .unwrap();
2304            *self.registrations.lock().unwrap() = Some(registrations);
2305            Ok(empty_record_batch_stream(&request))
2306        }
2307
2308        async fn handle_remote_dyn_filter_update(
2309            &self,
2310            _target: &crate::region_query::RegionQueryTarget,
2311            _query_id: String,
2312            _update: api::v1::region::RemoteDynFilterUpdate,
2313        ) -> crate::error::Result<()> {
2314            Ok(())
2315        }
2316
2317        async fn handle_remote_dyn_filter_unregister(
2318            &self,
2319            _target: &crate::region_query::RegionQueryTarget,
2320            _query_id: String,
2321            _unregister: api::v1::region::RemoteDynFilterUnregister,
2322        ) -> crate::error::Result<()> {
2323            Ok(())
2324        }
2325    }
2326
2327    #[async_trait]
2328    impl RegionQueryHandler for TestRegionQueryHandler {
2329        async fn select_target(
2330            &self,
2331            _read_preference: ReadPreference,
2332            _region_id: RegionId,
2333        ) -> crate::error::Result<crate::region_query::RegionQueryTarget> {
2334            Ok(test_target(1))
2335        }
2336
2337        async fn do_get(
2338            &self,
2339            _target: &crate::region_query::RegionQueryTarget,
2340            request: common_query::request::QueryRequest,
2341        ) -> crate::error::Result<common_recordbatch::SendableRecordBatchStream> {
2342            let response = self
2343                .responses
2344                .get(&request.region_id)
2345                .expect("test handler needs a response for every requested region");
2346            Ok(Box::pin(TestRecordBatchStream {
2347                schema: response.advertised_schema.clone(),
2348                batches: response.batches.clone(),
2349                index: 0,
2350            }))
2351        }
2352
2353        async fn handle_remote_dyn_filter_update(
2354            &self,
2355            _target: &crate::region_query::RegionQueryTarget,
2356            _query_id: String,
2357            _update: api::v1::region::RemoteDynFilterUpdate,
2358        ) -> crate::error::Result<()> {
2359            unimplemented!("test only")
2360        }
2361
2362        async fn handle_remote_dyn_filter_unregister(
2363            &self,
2364            _target: &crate::region_query::RegionQueryTarget,
2365            _query_id: String,
2366            _unregister: api::v1::region::RemoteDynFilterUnregister,
2367        ) -> crate::error::Result<()> {
2368            unimplemented!("test only")
2369        }
2370    }
2371
2372    fn int64_schema(columns: &[&str]) -> Arc<Schema> {
2373        Arc::new(Schema::new(
2374            columns
2375                .iter()
2376                .map(|name| ColumnSchema::new(*name, ConcreteDataType::int64_datatype(), false))
2377                .collect(),
2378        ))
2379    }
2380
2381    fn record_batch(schema: Arc<Schema>, columns: Vec<VectorRef>) -> RecordBatch {
2382        RecordBatch::new(schema, columns).expect("test record batch must match its schema")
2383    }
2384
2385    fn expected_int64_schema() -> ArrowSchema {
2386        int64_schema(&["a", "b"]).arrow_schema().as_ref().clone()
2387    }
2388
2389    fn merge_scan_exec(
2390        responses: Vec<(RegionId, RecordBatch)>,
2391        expected_schema: ArrowSchema,
2392        target_partition: usize,
2393    ) -> MergeScanExec {
2394        let regions = responses.iter().map(|(region_id, _)| *region_id).collect();
2395        merge_scan_exec_with_handler(
2396            regions,
2397            expected_schema,
2398            Arc::new(TestRegionQueryHandler::new(responses)),
2399            target_partition,
2400        )
2401    }
2402
2403    fn merge_scan_exec_with_handler(
2404        regions: Vec<RegionId>,
2405        expected_schema: ArrowSchema,
2406        handler: Arc<TestRegionQueryHandler>,
2407        target_partition: usize,
2408    ) -> MergeScanExec {
2409        let plan = LogicalPlanBuilder::empty(true).build().unwrap();
2410        MergeScanExec::new(
2411            &SessionStateBuilder::new().build(),
2412            TableName::new("catalog", "schema", "table"),
2413            regions,
2414            plan,
2415            &expected_schema,
2416            handler,
2417            QueryContext::arc(),
2418            target_partition,
2419            AliasMapping::new(),
2420            None,
2421            false,
2422        )
2423        .unwrap()
2424    }
2425
2426    async fn collect_merge_scan(
2427        exec: MergeScanExec,
2428    ) -> datafusion_common::Result<Vec<DfRecordBatch>> {
2429        exec.execute(0, Arc::new(TaskContext::default()))?
2430            .try_collect()
2431            .await
2432    }
2433
2434    fn assert_int64_batch(batch: &DfRecordBatch, values: (i64, i64)) {
2435        assert_eq!(batch.schema().as_ref(), &expected_int64_schema());
2436        let a = batch
2437            .column(0)
2438            .as_any()
2439            .downcast_ref::<Int64Array>()
2440            .unwrap();
2441        let b = batch
2442            .column(1)
2443            .as_any()
2444            .downcast_ref::<Int64Array>()
2445            .unwrap();
2446        assert_eq!((a.value(0), b.value(0)), values);
2447    }
2448
2449    #[tokio::test]
2450    async fn qbs_merge_scan_remote_schema_identity_canonical_single_region() {
2451        let batch = record_batch(
2452            int64_schema(&["a", "b"]),
2453            vec![
2454                Arc::new(Int64Vector::from_slice([11])) as _,
2455                Arc::new(Int64Vector::from_slice([12])) as _,
2456            ],
2457        );
2458        let batches = collect_merge_scan(merge_scan_exec(
2459            vec![(RegionId::new(1024, 1), batch)],
2460            expected_int64_schema(),
2461            1,
2462        ))
2463        .await
2464        .unwrap();
2465        assert_eq!(batches.len(), 1);
2466        assert_int64_batch(&batches[0], (11, 12));
2467    }
2468
2469    #[tokio::test]
2470    async fn qbs_merge_scan_remote_schema_identity_canonical_two_regions() {
2471        let batch = || {
2472            record_batch(
2473                int64_schema(&["a", "b"]),
2474                vec![
2475                    Arc::new(Int64Vector::from_slice([11])) as _,
2476                    Arc::new(Int64Vector::from_slice([12])) as _,
2477                ],
2478            )
2479        };
2480        let batches = collect_merge_scan(merge_scan_exec(
2481            vec![
2482                (RegionId::new(1024, 1), batch()),
2483                (RegionId::new(1024, 2), batch()),
2484            ],
2485            expected_int64_schema(),
2486            1,
2487        ))
2488        .await
2489        .unwrap();
2490        assert_eq!(batches.len(), 2);
2491        for batch in &batches {
2492            assert_int64_batch(batch, (11, 12));
2493        }
2494    }
2495
2496    #[tokio::test]
2497    async fn qbs_merge_scan_remote_schema_identity_swapped_columns_never_relabels_positionally() {
2498        let batch = record_batch(
2499            int64_schema(&["b", "a"]),
2500            vec![
2501                Arc::new(Int64Vector::from_slice([2002])) as _,
2502                Arc::new(Int64Vector::from_slice([1002])) as _,
2503            ],
2504        );
2505        assert!(
2506            collect_merge_scan(merge_scan_exec(
2507                vec![(RegionId::new(1024, 1), batch)],
2508                expected_int64_schema(),
2509                1,
2510            ))
2511            .await
2512            .is_err()
2513        );
2514    }
2515
2516    #[tokio::test]
2517    async fn qbs_merge_scan_remote_schema_identity_allows_timestamp_timezone_only_patch() {
2518        let remote_arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
2519            "ts",
2520            TestArrowDataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
2521            false,
2522        )]));
2523        let remote_schema = Arc::new(Schema::try_from(remote_arrow_schema).unwrap());
2524        let timestamp_array: Arc<dyn arrow::array::Array> =
2525            Arc::new(TimestampMillisecondArray::from(vec![1002]).with_timezone("UTC"));
2526        let timestamp = TimestampMillisecondVector::try_from_arrow_array(timestamp_array).unwrap();
2527        let batch = record_batch(remote_schema, vec![Arc::new(timestamp) as _]);
2528        let expected_schema = ArrowSchema::new(vec![Field::new(
2529            "ts",
2530            TestArrowDataType::Timestamp(TimeUnit::Millisecond, Some("Asia/Shanghai".into())),
2531            false,
2532        )]);
2533        let batches = collect_merge_scan(merge_scan_exec(
2534            vec![(RegionId::new(1024, 1), batch)],
2535            expected_schema.clone(),
2536            1,
2537        ))
2538        .await
2539        .unwrap();
2540        assert_eq!(batches[0].schema().as_ref(), &expected_schema);
2541    }
2542
2543    #[tokio::test]
2544    async fn qbs_merge_scan_remote_schema_identity_rejects_incompatible_type() {
2545        let batch = record_batch(
2546            Arc::new(Schema::new(vec![
2547                ColumnSchema::new("a", ConcreteDataType::string_datatype(), false),
2548                ColumnSchema::new("b", ConcreteDataType::int64_datatype(), false),
2549            ])),
2550            vec![
2551                Arc::new(StringVector::from_slice(&["not-an-int"])) as _,
2552                Arc::new(Int64Vector::from_slice([12])) as _,
2553            ],
2554        );
2555        assert!(
2556            collect_merge_scan(merge_scan_exec(
2557                vec![(RegionId::new(1024, 1), batch)],
2558                expected_int64_schema(),
2559                1,
2560            ))
2561            .await
2562            .is_err()
2563        );
2564    }
2565
2566    #[tokio::test]
2567    async fn qbs_merge_scan_remote_schema_identity_rejects_too_few_columns() {
2568        let batch = record_batch(
2569            int64_schema(&["a"]),
2570            vec![Arc::new(Int64Vector::from_slice([11])) as _],
2571        );
2572        assert!(
2573            collect_merge_scan(merge_scan_exec(
2574                vec![(RegionId::new(1024, 1), batch)],
2575                expected_int64_schema(),
2576                1,
2577            ))
2578            .await
2579            .is_err()
2580        );
2581    }
2582
2583    #[tokio::test]
2584    async fn qbs_merge_scan_remote_schema_identity_rejects_too_many_columns() {
2585        let batch = record_batch(
2586            int64_schema(&["a", "b", "extra"]),
2587            vec![
2588                Arc::new(Int64Vector::from_slice([11])) as _,
2589                Arc::new(Int64Vector::from_slice([12])) as _,
2590                Arc::new(Int64Vector::from_slice([13])) as _,
2591            ],
2592        );
2593        assert!(
2594            collect_merge_scan(merge_scan_exec(
2595                vec![(RegionId::new(1024, 1), batch)],
2596                expected_int64_schema(),
2597                1,
2598            ))
2599            .await
2600            .is_err()
2601        );
2602    }
2603
2604    #[test]
2605    fn merge_scan_remote_schema_identity_allows_top_level_metadata_mismatch() {
2606        let fields = expected_int64_schema().fields().clone();
2607        let expected = ArrowSchema::new_with_metadata(
2608            fields.clone(),
2609            StdHashMap::from([("greptime:version".to_string(), "1".to_string())]),
2610        );
2611        let actual = ArrowSchema::new_with_metadata(
2612            fields,
2613            StdHashMap::from([("greptime:version".to_string(), "0".to_string())]),
2614        );
2615        assert!(validate_remote_schema(&expected, &actual, "test").is_ok());
2616    }
2617
2618    /// Builds the metadata of a JSON column field. `wire_form` mirrors the
2619    /// binary-encoded representation (adds `ARROW:extension:name`), while the
2620    /// decoded structured form only carries the semantic keys.
2621    fn json_field_metadata(wire_form: bool, json_settings: &str) -> StdHashMap<String, String> {
2622        let mut metadata = StdHashMap::from([
2623            (datatypes::schema::TYPE_KEY.to_string(), "Json".to_string()),
2624            (
2625                arrow_schema::extension::EXTENSION_TYPE_METADATA_KEY.to_string(),
2626                json_settings.to_string(),
2627            ),
2628        ]);
2629        if wire_form {
2630            metadata.insert(
2631                arrow_schema::extension::EXTENSION_TYPE_NAME_KEY.to_string(),
2632                "greptime.json".to_string(),
2633            );
2634        }
2635        metadata
2636    }
2637
2638    #[test]
2639    fn merge_scan_remote_schema_identity_accepts_json_wire_binary_vs_decoded_struct() {
2640        // The merge-scan (expected) side carries a JSON2 column in its binary
2641        // wire form (Binary + `ARROW:extension:name`); the remote decoded
2642        // stream carries the concretized structured form (Struct + the same
2643        // semantic extension metadata, without the arrow extension name). This
2644        // mirrors the failing `json2_limit` case: the two representations must
2645        // validate as equal.
2646        let json_settings = r#"{"json_settings":{"type_hints":[]}}"#;
2647        let expected = ArrowSchema::new(vec![
2648            Field::new("j", TestArrowDataType::Binary, true)
2649                .with_metadata(json_field_metadata(true, json_settings)),
2650        ]);
2651        let actual =
2652            ArrowSchema::new(vec![
2653                Field::new(
2654                    "j",
2655                    TestArrowDataType::Struct(arrow_schema::Fields::from(vec![Arc::new(
2656                        Field::new("a", TestArrowDataType::Utf8View, true),
2657                    )])),
2658                    true,
2659                )
2660                .with_metadata(json_field_metadata(false, json_settings)),
2661            ]);
2662        assert!(validate_remote_schema(&expected, &actual, "test").is_ok());
2663    }
2664
2665    #[test]
2666    fn merge_scan_remote_schema_identity_accepts_json_decoded_struct_vs_wire_binary() {
2667        // The reverse direction: expected side is the decoded structured form
2668        // while the actual remote stream advertises the binary wire form.
2669        let json_settings = r#"{"json_settings":{"type_hints":[]}}"#;
2670        let expected =
2671            ArrowSchema::new(vec![
2672                Field::new(
2673                    "j",
2674                    TestArrowDataType::Struct(arrow_schema::Fields::from(vec![Arc::new(
2675                        Field::new("a", TestArrowDataType::Utf8View, true),
2676                    )])),
2677                    true,
2678                )
2679                .with_metadata(json_field_metadata(false, json_settings)),
2680            ]);
2681        let actual = ArrowSchema::new(vec![
2682            Field::new("j", TestArrowDataType::Binary, true)
2683                .with_metadata(json_field_metadata(true, json_settings)),
2684        ]);
2685        assert!(validate_remote_schema(&expected, &actual, "test").is_ok());
2686    }
2687
2688    #[test]
2689    fn merge_scan_remote_schema_identity_rejects_json_fields_with_different_json_settings() {
2690        // JSON2 settings (type hints) are semantic: two JSON columns with
2691        // different settings describe different logical structures and must be
2692        // rejected.
2693        let expected = ArrowSchema::new(vec![
2694            Field::new("j", TestArrowDataType::Binary, true).with_metadata(json_field_metadata(
2695                true,
2696                r#"{"json_settings":{"type_hints":[["a",{"JsonType":"Int64"}]]}}"#,
2697            )),
2698        ]);
2699        let actual =
2700            ArrowSchema::new(vec![
2701                Field::new(
2702                    "j",
2703                    TestArrowDataType::Struct(arrow_schema::Fields::from(vec![Arc::new(
2704                        Field::new("a", TestArrowDataType::Utf8View, true),
2705                    )])),
2706                    true,
2707                )
2708                .with_metadata(json_field_metadata(
2709                    false,
2710                    r#"{"json_settings":{"type_hints":[["a",{"JsonType":"String"}]]}}"#,
2711                )),
2712            ]);
2713        assert!(validate_remote_schema(&expected, &actual, "test").is_err());
2714    }
2715
2716    #[test]
2717    fn merge_scan_remote_schema_identity_ignores_json_extension_metadata_when_physical_type_matches()
2718     {
2719        // Field metadata (including the JSON extension identity) is not part
2720        // of the semantic field identity: a JSON column in its binary wire
2721        // form and a plain Binary column share the same name, physical type,
2722        // and nullability, so they validate as equal.
2723        let expected = ArrowSchema::new(vec![
2724            Field::new("j", TestArrowDataType::Binary, true).with_metadata(json_field_metadata(
2725                true,
2726                r#"{"json_settings":{"type_hints":[]}}"#,
2727            )),
2728        ]);
2729        let actual = ArrowSchema::new(vec![Field::new("j", TestArrowDataType::Binary, true)]);
2730        assert!(validate_remote_schema(&expected, &actual, "test").is_ok());
2731
2732        // The JSON compatibility exemption is scoped to JSON columns only: a
2733        // pair of non-JSON fields with different physical types is still
2734        // rejected even though one side is Binary.
2735        let plain_binary = ArrowSchema::new(vec![Field::new("v", TestArrowDataType::Binary, true)]);
2736        let plain_struct = ArrowSchema::new(vec![Field::new(
2737            "v",
2738            TestArrowDataType::Struct(arrow_schema::Fields::from(vec![Arc::new(Field::new(
2739                "a",
2740                TestArrowDataType::Utf8View,
2741                true,
2742            ))])),
2743            true,
2744        )]);
2745        assert!(validate_remote_schema(&plain_binary, &plain_struct, "test").is_err());
2746    }
2747
2748    #[test]
2749    fn merge_scan_remote_schema_identity_ignores_field_metadata_but_rejects_nullability_mismatch() {
2750        let expected = expected_int64_schema();
2751        let metadata_mismatch = ArrowSchema::new_with_metadata(
2752            vec![
2753                expected
2754                    .field(0)
2755                    .as_ref()
2756                    .clone()
2757                    .with_metadata(StdHashMap::from([(
2758                        "remote".to_string(),
2759                        "different".to_string(),
2760                    )])),
2761                expected.field(1).as_ref().clone(),
2762            ],
2763            expected.metadata().clone(),
2764        );
2765        let nullability_mismatch = ArrowSchema::new_with_metadata(
2766            vec![
2767                expected.field(0).as_ref().clone().with_nullable(true),
2768                expected.field(1).as_ref().clone(),
2769            ],
2770            expected.metadata().clone(),
2771        );
2772        // Field metadata is auxiliary and does not participate in the
2773        // semantic field comparison.
2774        assert!(validate_remote_schema(&expected, &metadata_mismatch, "test").is_ok());
2775        assert!(validate_remote_schema(&expected, &nullability_mismatch, "test").is_err());
2776    }
2777
2778    #[test]
2779    fn merge_scan_remote_schema_identity_ignores_skipping_index_field_metadata() {
2780        // Regression: a remote stream may advertise auxiliary field metadata
2781        // (e.g. `greptime:skipping_index`) that the expected schema does not
2782        // carry. Field metadata is not part of the semantic field identity
2783        // (name + data type + nullability) and must not fail validation.
2784        let skipping_index_metadata = StdHashMap::from([(
2785            "greptime:skipping_index".to_string(),
2786            r#"{"granularity":1,"false-positive-rate-in-10000":100,"index-type":"BloomFilter"}"#
2787                .to_string(),
2788        )]);
2789        let expected =
2790            ArrowSchema::new(vec![Field::new("value", TestArrowDataType::Float64, true)]);
2791        let actual_with_metadata = ArrowSchema::new(vec![
2792            Field::new("value", TestArrowDataType::Float64, true)
2793                .with_metadata(skipping_index_metadata),
2794        ]);
2795        assert!(validate_remote_schema(&expected, &actual_with_metadata, "test").is_ok());
2796
2797        // A real data type mismatch is still rejected.
2798        let wrong_type =
2799            ArrowSchema::new(vec![Field::new("value", TestArrowDataType::Int64, true)]);
2800        assert!(validate_remote_schema(&expected, &wrong_type, "test").is_err());
2801
2802        // A name mismatch is still rejected.
2803        let wrong_name =
2804            ArrowSchema::new(vec![Field::new("other", TestArrowDataType::Float64, true)]);
2805        assert!(validate_remote_schema(&expected, &wrong_name, "test").is_err());
2806    }
2807
2808    #[test]
2809    fn merge_scan_remote_schema_identity_rejects_timestamp_timezone_plus_field_mismatches() {
2810        let expected = ArrowSchema::new(vec![Field::new(
2811            "ts",
2812            TestArrowDataType::Timestamp(TimeUnit::Millisecond, Some("Asia/Shanghai".into())),
2813            false,
2814        )]);
2815        let different_unit = ArrowSchema::new(vec![Field::new(
2816            "ts",
2817            TestArrowDataType::Timestamp(TimeUnit::Second, Some("UTC".into())),
2818            false,
2819        )]);
2820        let different_name = ArrowSchema::new(vec![Field::new(
2821            "other",
2822            TestArrowDataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
2823            false,
2824        )]);
2825        let nullability = ArrowSchema::new(vec![Field::new(
2826            "ts",
2827            TestArrowDataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
2828            true,
2829        )]);
2830        let metadata = ArrowSchema::new(vec![
2831            Field::new(
2832                "ts",
2833                TestArrowDataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
2834                false,
2835            )
2836            .with_metadata(StdHashMap::from([(
2837                "remote".to_string(),
2838                "different".to_string(),
2839            )])),
2840        ]);
2841        for actual in [&different_unit, &different_name, &nullability] {
2842            assert!(validate_remote_schema(&expected, actual, "test").is_err());
2843        }
2844        // Field metadata is non-semantic and is ignored even for timezone
2845        // pairs: a metadata difference on an otherwise timezone-only
2846        // difference is accepted.
2847        assert!(validate_remote_schema(&expected, &metadata, "test").is_ok());
2848    }
2849
2850    #[test]
2851    fn merge_scan_remote_schema_identity_returns_arrow_schema_error() {
2852        let expected = expected_int64_schema();
2853        let actual = ArrowSchema::new(vec![Field::new("other", TestArrowDataType::Int64, false)]);
2854        match validate_remote_schema(&expected, &actual, "test").unwrap_err() {
2855            DataFusionError::ArrowError(error, None) => match error.as_ref() {
2856                ArrowError::SchemaError(message) => {
2857                    assert!(message.contains("field count mismatch"))
2858                }
2859                error => panic!("expected ArrowError::SchemaError, got {error:?}"),
2860            },
2861            error => panic!("expected DataFusionError::ArrowError(_, None), got {error:?}"),
2862        }
2863    }
2864
2865    #[tokio::test]
2866    async fn merge_scan_remote_schema_identity_rejects_incompatible_empty_advertised_schema() {
2867        let region_id = RegionId::new(1024, 1);
2868        let exec = merge_scan_exec_with_handler(
2869            vec![region_id],
2870            expected_int64_schema(),
2871            Arc::new(TestRegionQueryHandler::with_responses(vec![(
2872                region_id,
2873                int64_schema(&["a"]),
2874                vec![],
2875            )])),
2876            1,
2877        );
2878        let errors_before = merge_scan_schema_error_count_for_test();
2879        assert!(collect_merge_scan(exec).await.is_err());
2880        assert_eq!(merge_scan_schema_error_count_for_test(), errors_before + 1);
2881    }
2882
2883    #[tokio::test]
2884    async fn merge_scan_remote_schema_identity_allows_top_level_metadata_version_mismatch() {
2885        let fields = expected_int64_schema().fields().clone();
2886        let expected = ArrowSchema::new_with_metadata(
2887            fields.clone(),
2888            StdHashMap::from([("greptime:version".to_string(), "1".to_string())]),
2889        );
2890        let remote_schema = Arc::new(
2891            Schema::try_from(Arc::new(ArrowSchema::new_with_metadata(
2892                fields,
2893                StdHashMap::from([("greptime:version".to_string(), "0".to_string())]),
2894            )))
2895            .unwrap(),
2896        );
2897        let batch = record_batch(
2898            remote_schema.clone(),
2899            vec![
2900                Arc::new(Int64Vector::from_slice([11])) as _,
2901                Arc::new(Int64Vector::from_slice([12])) as _,
2902            ],
2903        );
2904        let batches = collect_merge_scan(merge_scan_exec_with_handler(
2905            vec![RegionId::new(1024, 1)],
2906            expected.clone(),
2907            Arc::new(TestRegionQueryHandler::with_responses(vec![(
2908                RegionId::new(1024, 1),
2909                remote_schema,
2910                vec![batch],
2911            )])),
2912            1,
2913        ))
2914        .await
2915        .unwrap();
2916        assert_eq!(batches[0].schema().as_ref(), &expected);
2917    }
2918
2919    #[tokio::test]
2920    async fn merge_scan_remote_schema_identity_rejects_advertised_schema_inner_batch_mismatch() {
2921        let region_id = RegionId::new(1024, 1);
2922        let advertised_schema = int64_schema(&["a", "b"]);
2923        let inner_batch = record_batch(
2924            int64_schema(&["b", "a"]),
2925            vec![
2926                Arc::new(Int64Vector::from_slice([12])) as _,
2927                Arc::new(Int64Vector::from_slice([11])) as _,
2928            ],
2929        )
2930        .into_df_record_batch();
2931        let inner_schema = inner_batch.schema_ref().clone();
2932        let batch = RecordBatch::from_df_record_batch(advertised_schema.clone(), inner_batch);
2933        assert!(Arc::ptr_eq(
2934            advertised_schema.arrow_schema(),
2935            batch.schema.arrow_schema()
2936        ));
2937        assert!(!Arc::ptr_eq(
2938            advertised_schema.arrow_schema(),
2939            &inner_schema
2940        ));
2941        let exec = merge_scan_exec_with_handler(
2942            vec![region_id],
2943            expected_int64_schema(),
2944            Arc::new(TestRegionQueryHandler::with_responses(vec![(
2945                region_id,
2946                advertised_schema,
2947                vec![batch],
2948            )])),
2949            1,
2950        );
2951        let errors_before = merge_scan_schema_error_count_for_test();
2952        assert!(collect_merge_scan(exec).await.is_err());
2953        assert_eq!(merge_scan_schema_error_count_for_test(), errors_before + 1);
2954    }
2955
2956    #[tokio::test]
2957    async fn merge_scan_remote_schema_identity_rejects_unchecked_inner_extra_column() {
2958        let region_id = RegionId::new(1024, 1);
2959        let advertised_schema = int64_schema(&["a", "b"]);
2960        let inner_batch = record_batch(
2961            int64_schema(&["a", "b", "extra"]),
2962            vec![
2963                Arc::new(Int64Vector::from_slice([11])) as _,
2964                Arc::new(Int64Vector::from_slice([12])) as _,
2965                Arc::new(Int64Vector::from_slice([13])) as _,
2966            ],
2967        )
2968        .into_df_record_batch();
2969        assert!(!Arc::ptr_eq(
2970            advertised_schema.arrow_schema(),
2971            inner_batch.schema_ref()
2972        ));
2973        let batch = RecordBatch::from_df_record_batch(advertised_schema.clone(), inner_batch);
2974        let exec = merge_scan_exec_with_handler(
2975            vec![region_id],
2976            expected_int64_schema(),
2977            Arc::new(TestRegionQueryHandler::with_responses(vec![(
2978                region_id,
2979                advertised_schema,
2980                vec![batch],
2981            )])),
2982            1,
2983        );
2984        assert!(collect_merge_scan(exec).await.is_err());
2985    }
2986
2987    #[tokio::test]
2988    async fn merge_scan_remote_schema_identity_validates_structurally_equal_distinct_batch_schema()
2989    {
2990        let region_id = RegionId::new(1024, 1);
2991        let advertised_schema = int64_schema(&["a", "b"]);
2992        let inner_schema = Arc::new(
2993            Schema::try_from(Arc::new(advertised_schema.arrow_schema().as_ref().clone())).unwrap(),
2994        );
2995        let inner_batch = record_batch(
2996            inner_schema,
2997            vec![
2998                Arc::new(Int64Vector::from_slice([11])) as _,
2999                Arc::new(Int64Vector::from_slice([12])) as _,
3000            ],
3001        )
3002        .into_df_record_batch();
3003        assert_eq!(advertised_schema.arrow_schema(), inner_batch.schema_ref());
3004        assert!(!Arc::ptr_eq(
3005            advertised_schema.arrow_schema(),
3006            inner_batch.schema_ref()
3007        ));
3008        let batch = RecordBatch::from_df_record_batch(advertised_schema.clone(), inner_batch);
3009        let batches = collect_merge_scan(merge_scan_exec_with_handler(
3010            vec![region_id],
3011            expected_int64_schema(),
3012            Arc::new(TestRegionQueryHandler::with_responses(vec![(
3013                region_id,
3014                advertised_schema,
3015                vec![batch],
3016            )])),
3017            1,
3018        ))
3019        .await
3020        .unwrap();
3021        assert_eq!(batches.len(), 1);
3022        assert_int64_batch(&batches[0], (11, 12));
3023    }
3024
3025    #[test]
3026    fn try_with_new_distribution_preserves_remote_dyn_filter_producer_id() {
3027        let remote_dyn_filter_producer_id = RemoteDynFilterProducerId::new(42);
3028
3029        // Build a plan whose schema contains "col1"
3030        let plan = LogicalPlanBuilder::empty(true)
3031            .project(vec![lit(1i32).alias("col1")])
3032            .unwrap()
3033            .build()
3034            .unwrap();
3035
3036        let schema = plan.schema().as_arrow().clone();
3037        let table = TableName::new("catalog", "schema", "table");
3038        let regions = vec![RegionId::new(1024, 1)];
3039        let query_ctx = QueryContext::arc();
3040
3041        // Non-empty partition_cols so try_with_new_distribution can detect an overlap
3042        let mut partition_cols = AliasMapping::new();
3043        partition_cols.insert(
3044            "col1".to_string(),
3045            BTreeSet::from([ColumnExpr::new(Some(TableReference::bare("table")), "col1")]),
3046        );
3047
3048        let session_state = SessionStateBuilder::new().build();
3049
3050        let handler = Arc::new(TestRegionQueryHandler::default());
3051        let target_partition = 2;
3052
3053        let exec = MergeScanExec::new(
3054            &session_state,
3055            table,
3056            regions,
3057            plan,
3058            &schema,
3059            handler,
3060            query_ctx,
3061            target_partition,
3062            partition_cols,
3063            Some(remote_dyn_filter_producer_id),
3064            false,
3065        )
3066        .unwrap();
3067
3068        assert_eq!(
3069            exec.remote_dyn_filter_producer_id(),
3070            Some(remote_dyn_filter_producer_id)
3071        );
3072
3073        // A distribution that differs from the current partitioning but shares a
3074        // column name present in partition_cols, so try_with_new_distribution
3075        // produces a clone instead of returning None.
3076        let new_dist = Distribution::HashPartitioned(vec![
3077            Arc::new(Column::new("col1", 0)),
3078            Arc::new(Column::new("col2", 1)),
3079        ]);
3080
3081        let cloned = exec
3082            .try_with_new_distribution(new_dist)
3083            .expect("expected a cloned exec with overlapping partition col");
3084
3085        assert_eq!(
3086            cloned.remote_dyn_filter_producer_id(),
3087            Some(remote_dyn_filter_producer_id),
3088            "try_with_new_distribution must preserve remote dynamic filter producer id"
3089        );
3090    }
3091
3092    #[test]
3093    fn remote_dyn_filter_preflight_removes_parent_filter_after_dn_runtime_is_ready() {
3094        let remote_dyn_filter_producer_id = RemoteDynFilterProducerId::new(42);
3095        let plan = LogicalPlanBuilder::empty(true)
3096            .project(vec![lit(1i32).alias("col1")])
3097            .unwrap()
3098            .build()
3099            .unwrap();
3100
3101        let schema = plan.schema().as_arrow().clone();
3102        let table = TableName::new("catalog", "schema", "table");
3103        let regions = vec![RegionId::new(1024, 1)];
3104        let query_ctx = QueryContext::arc();
3105        let session_state = SessionStateBuilder::new().build();
3106        let handler = Arc::new(TestRegionQueryHandler::default());
3107        let exec = MergeScanExec::new(
3108            &session_state,
3109            table,
3110            regions,
3111            plan,
3112            &schema,
3113            handler,
3114            query_ctx,
3115            1,
3116            AliasMapping::new(),
3117            Some(remote_dyn_filter_producer_id),
3118            false,
3119        )
3120        .unwrap();
3121        let dyn_filter = Arc::new(DynamicFilterPhysicalExpr::new(
3122            vec![Arc::new(Column::new("host", 0)) as Arc<_>],
3123            physical_lit(true) as _,
3124        )) as Arc<dyn datafusion_physical_expr::PhysicalExpr>;
3125
3126        let propagation = exec
3127            .handle_child_pushdown_result(
3128                FilterPushdownPhase::Post,
3129                ChildPushdownResult {
3130                    parent_filters: vec![ChildFilterPushdownResult {
3131                        filter: dyn_filter,
3132                        child_results: vec![PushedDown::Yes],
3133                    }],
3134                    self_filters: Vec::new(),
3135                },
3136                &ConfigOptions::new(),
3137            )
3138            .unwrap();
3139
3140        assert_eq!(exec.captured_remote_dyn_filters().len(), 1);
3141        assert!(matches!(propagation.filters.as_slice(), [PushedDown::Yes]));
3142    }
3143
3144    #[test]
3145    fn scan_output_bytes_uses_plan_name() {
3146        let metrics = RecordBatchMetrics {
3147            plan_metrics: vec![PlanMetrics {
3148                plan: "SeqScan: region=1".to_string(),
3149                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
3150                level: 0,
3151                metrics: vec![("output_bytes".to_string(), 42)],
3152            }],
3153            ..Default::default()
3154        };
3155
3156        assert_eq!(region_scan_output_bytes(&metrics), 42);
3157    }
3158
3159    #[test]
3160    fn scan_output_bytes_defaults_to_zero_without_region_scan() {
3161        let metrics = RecordBatchMetrics {
3162            plan_metrics: vec![PlanMetrics {
3163                plan: "ProjectionExec".to_string(),
3164                plan_name: "ProjectionExec".to_string(),
3165                level: 0,
3166                metrics: vec![("output_bytes".to_string(), 42)],
3167            }],
3168            ..Default::default()
3169        };
3170
3171        assert_eq!(region_scan_output_bytes(&metrics), 0);
3172    }
3173
3174    #[test]
3175    fn scan_output_bytes_sums_multiple_region_scans() {
3176        let metrics = RecordBatchMetrics {
3177            plan_metrics: vec![
3178                PlanMetrics {
3179                    plan: "RegionScanExec: region=1".to_string(),
3180                    plan_name: REGION_SCAN_EXEC_NAME.to_string(),
3181                    level: 0,
3182                    metrics: vec![("output_bytes".to_string(), 42)],
3183                },
3184                PlanMetrics {
3185                    plan: "RegionScanExec: region=2".to_string(),
3186                    plan_name: REGION_SCAN_EXEC_NAME.to_string(),
3187                    level: 0,
3188                    metrics: vec![("output_bytes".to_string(), 18)],
3189                },
3190            ],
3191            ..Default::default()
3192        };
3193
3194        assert_eq!(region_scan_output_bytes(&metrics), 60);
3195    }
3196
3197    #[test]
3198    fn merge_scan_reports_region_query_load_on_drop() {
3199        use store_api::metrics::{REGION_QUERY_CPU_TIME, REGION_QUERY_SCANNED_BYTES};
3200
3201        let region_id = RegionId::new(1024, 10002);
3202        let region_id_label = region_id.to_string();
3203        let labels = [&region_id_label];
3204        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&labels);
3205        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&labels);
3206
3207        let plan = LogicalPlanBuilder::empty(true)
3208            .project(vec![lit(1i32).alias("col1")])
3209            .unwrap()
3210            .build()
3211            .unwrap();
3212        let schema = plan.schema().as_arrow().clone();
3213        let exec = MergeScanExec::new(
3214            &SessionStateBuilder::new().build(),
3215            TableName::new("catalog", "schema", "table"),
3216            vec![region_id],
3217            plan,
3218            &schema,
3219            Arc::new(TestRegionQueryHandler::default()),
3220            QueryContext::arc(),
3221            1,
3222            AliasMapping::new(),
3223            None,
3224            true,
3225        )
3226        .unwrap();
3227
3228        let metrics = RecordBatchMetrics {
3229            elapsed_compute: 42,
3230            plan_metrics: vec![PlanMetrics {
3231                plan: "RegionScanExec: region=1".to_string(),
3232                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
3233                level: 0,
3234                metrics: vec![("output_bytes".to_string(), 24)],
3235            }],
3236            ..Default::default()
3237        };
3238        exec.sub_stage_metrics
3239            .lock()
3240            .unwrap()
3241            .insert(region_id, metrics);
3242
3243        assert_eq!(REGION_QUERY_CPU_TIME.with_label_values(&labels).get(), 0);
3244        assert_eq!(
3245            REGION_QUERY_SCANNED_BYTES.with_label_values(&labels).get(),
3246            0
3247        );
3248
3249        drop(exec);
3250
3251        assert_eq!(REGION_QUERY_CPU_TIME.with_label_values(&labels).get(), 42);
3252        assert_eq!(
3253            REGION_QUERY_SCANNED_BYTES.with_label_values(&labels).get(),
3254            24
3255        );
3256
3257        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&labels);
3258        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&labels);
3259    }
3260
3261    #[test]
3262    fn merge_scan_reports_query_load_with_metrics_region_id() {
3263        use store_api::metrics::{REGION_QUERY_CPU_TIME, REGION_QUERY_SCANNED_BYTES};
3264
3265        let logical_region_id = RegionId::new(1024, 10002);
3266        let physical_region_id = RegionId::new(1024, 1);
3267        let logical_region_id_label = logical_region_id.to_string();
3268        let physical_region_id_label = physical_region_id.to_string();
3269        let logical_labels = [&logical_region_id_label];
3270        let physical_labels = [&physical_region_id_label];
3271        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&logical_labels);
3272        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&logical_labels);
3273        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&physical_labels);
3274        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&physical_labels);
3275
3276        let plan = LogicalPlanBuilder::empty(true)
3277            .project(vec![lit(1i32).alias("col1")])
3278            .unwrap()
3279            .build()
3280            .unwrap();
3281        let schema = plan.schema().as_arrow().clone();
3282        let exec = MergeScanExec::new(
3283            &SessionStateBuilder::new().build(),
3284            TableName::new("catalog", "schema", "table"),
3285            vec![logical_region_id],
3286            plan,
3287            &schema,
3288            Arc::new(TestRegionQueryHandler::default()),
3289            QueryContext::arc(),
3290            1,
3291            AliasMapping::new(),
3292            None,
3293            true,
3294        )
3295        .unwrap();
3296
3297        let metrics = RecordBatchMetrics {
3298            elapsed_compute: 42,
3299            query_load_region_id: Some(physical_region_id.as_u64()),
3300            plan_metrics: vec![PlanMetrics {
3301                plan: "RegionScanExec: region=1".to_string(),
3302                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
3303                level: 0,
3304                metrics: vec![("output_bytes".to_string(), 24)],
3305            }],
3306            ..Default::default()
3307        };
3308        exec.sub_stage_metrics
3309            .lock()
3310            .unwrap()
3311            .insert(logical_region_id, metrics);
3312
3313        drop(exec);
3314
3315        assert_eq!(
3316            REGION_QUERY_CPU_TIME
3317                .with_label_values(&logical_labels)
3318                .get(),
3319            0
3320        );
3321        assert_eq!(
3322            REGION_QUERY_SCANNED_BYTES
3323                .with_label_values(&logical_labels)
3324                .get(),
3325            0
3326        );
3327        assert_eq!(
3328            REGION_QUERY_CPU_TIME
3329                .with_label_values(&physical_labels)
3330                .get(),
3331            42
3332        );
3333        assert_eq!(
3334            REGION_QUERY_SCANNED_BYTES
3335                .with_label_values(&physical_labels)
3336                .get(),
3337            24
3338        );
3339
3340        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&logical_labels);
3341        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&logical_labels);
3342        let _ = REGION_QUERY_CPU_TIME.remove_label_values(&physical_labels);
3343        let _ = REGION_QUERY_SCANNED_BYTES.remove_label_values(&physical_labels);
3344    }
3345}