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