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