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