Skip to main content

common_recordbatch/
adapter.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::{self, Display};
16use std::future::Future;
17use std::marker::PhantomData;
18use std::pin::Pin;
19use std::str::FromStr;
20use std::sync::Arc;
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::task::{Context, Poll};
23
24use common_base::readable_size::ReadableSize;
25use common_telemetry::tracing::{Span, info_span};
26use common_time::util::format_nanoseconds_human_readable;
27use datafusion::arrow::compute::cast;
28use datafusion::arrow::datatypes::SchemaRef as DfSchemaRef;
29use datafusion::error::Result as DfResult;
30use datafusion::execution::context::ExecutionProps;
31use datafusion::logical_expr::Expr;
32use datafusion::logical_expr::utils::conjunction;
33use datafusion::physical_expr::create_physical_expr;
34use datafusion::physical_plan::metrics::{BaselineMetrics, MetricValue};
35use datafusion::physical_plan::{
36    DisplayFormatType, ExecutionPlan, ExecutionPlanVisitor, PhysicalExpr,
37    RecordBatchStream as DfRecordBatchStream, accept,
38};
39use datafusion_common::arrow::error::ArrowError;
40use datafusion_common::{DataFusionError, ToDFSchema};
41use datatypes::arrow::array::Array;
42use datatypes::arrow::datatypes::DataType as ArrowDataType;
43use datatypes::schema::{ColumnExtType, Schema, SchemaRef};
44use futures::ready;
45use jsonb;
46use pin_project::pin_project;
47use snafu::ResultExt;
48
49use crate::error::{self, Result};
50use crate::filter::batch_filter;
51use crate::{
52    DfRecordBatch, DfSendableRecordBatchStream, OrderOption, RecordBatch, RecordBatchStream,
53    SendableRecordBatchStream, Stream,
54};
55
56const REGION_SCAN_EXEC_NAME: &str = "RegionScanExec";
57
58type FutureStream =
59    Pin<Box<dyn std::future::Future<Output = Result<SendableRecordBatchStream>> + Send>>;
60
61/// Casts the `RecordBatch`es of `stream` against the `output_schema`.
62#[pin_project]
63pub struct RecordBatchStreamTypeAdapter<T, E> {
64    #[pin]
65    stream: T,
66    projected_schema: DfSchemaRef,
67    projection: Vec<usize>,
68    predicate: Option<Arc<dyn PhysicalExpr>>,
69    phantom: PhantomData<E>,
70}
71
72impl<T, E> RecordBatchStreamTypeAdapter<T, E>
73where
74    T: Stream<Item = std::result::Result<DfRecordBatch, E>>,
75    E: std::error::Error + Send + Sync + 'static,
76{
77    pub fn new(projected_schema: DfSchemaRef, stream: T, projection: Option<Vec<usize>>) -> Self {
78        let projection = if let Some(projection) = projection {
79            projection
80        } else {
81            (0..projected_schema.fields().len()).collect()
82        };
83
84        Self {
85            stream,
86            projected_schema,
87            projection,
88            predicate: None,
89            phantom: Default::default(),
90        }
91    }
92
93    pub fn with_filter(mut self, filters: Vec<Expr>) -> Result<Self> {
94        let filters = if let Some(expr) = conjunction(filters) {
95            let df_schema = self
96                .projected_schema
97                .clone()
98                .to_dfschema_ref()
99                .context(error::PhysicalExprSnafu)?;
100
101            let filters = create_physical_expr(&expr, &df_schema, &ExecutionProps::new())
102                .context(error::PhysicalExprSnafu)?;
103            Some(filters)
104        } else {
105            None
106        };
107        self.predicate = filters;
108        Ok(self)
109    }
110}
111
112impl<T, E> DfRecordBatchStream for RecordBatchStreamTypeAdapter<T, E>
113where
114    T: Stream<Item = std::result::Result<DfRecordBatch, E>>,
115    E: std::error::Error + Send + Sync + 'static,
116{
117    fn schema(&self) -> DfSchemaRef {
118        self.projected_schema.clone()
119    }
120}
121
122impl<T, E> Stream for RecordBatchStreamTypeAdapter<T, E>
123where
124    T: Stream<Item = std::result::Result<DfRecordBatch, E>>,
125    E: std::error::Error + Send + Sync + 'static,
126{
127    type Item = DfResult<DfRecordBatch>;
128
129    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
130        let this = self.project();
131
132        let batch = futures::ready!(this.stream.poll_next(cx))
133            .map(|r| r.map_err(|e| DataFusionError::External(Box::new(e))));
134
135        let projected_schema = this.projected_schema.clone();
136        let projection = this.projection.clone();
137        let predicate = this.predicate.clone();
138
139        let batch = batch.map(|b| {
140            b.and_then(|b| {
141                let projected_column = b.project(&projection)?;
142                if projected_column.schema().fields.len() != projected_schema.fields.len() {
143                   return Err(DataFusionError::ArrowError(Box::new(ArrowError::SchemaError(format!(
144                        "Trying to cast a RecordBatch into an incompatible schema. RecordBatch: {}, Target: {}",
145                        projected_column.schema(),
146                        projected_schema,
147                    ))), None));
148                }
149
150                let mut columns = Vec::with_capacity(projected_schema.fields.len());
151                for (idx,field) in projected_schema.fields.iter().enumerate() {
152                    let column = projected_column.column(idx);
153                    let extype = field.metadata().get("greptime:type").and_then(|s| ColumnExtType::from_str(s).ok());
154                    let output = custom_cast(&column, field.data_type(), extype)?;
155                    columns.push(output)
156                }
157                let record_batch = DfRecordBatch::try_new(projected_schema, columns)?;
158                let record_batch = if let Some(predicate) = predicate {
159                    batch_filter(&record_batch, &predicate)?
160                } else {
161                    record_batch
162                };
163                Ok(record_batch)
164            })
165        });
166
167        Poll::Ready(batch)
168    }
169
170    #[inline]
171    fn size_hint(&self) -> (usize, Option<usize>) {
172        self.stream.size_hint()
173    }
174}
175
176/// Greptime SendableRecordBatchStream -> DataFusion RecordBatchStream.
177/// The reverse one is [RecordBatchStreamAdapter].
178pub struct DfRecordBatchStreamAdapter {
179    stream: SendableRecordBatchStream,
180}
181
182impl DfRecordBatchStreamAdapter {
183    pub fn new(stream: SendableRecordBatchStream) -> Self {
184        Self { stream }
185    }
186}
187
188impl DfRecordBatchStream for DfRecordBatchStreamAdapter {
189    fn schema(&self) -> DfSchemaRef {
190        self.stream.schema().arrow_schema().clone()
191    }
192}
193
194impl Stream for DfRecordBatchStreamAdapter {
195    type Item = DfResult<DfRecordBatch>;
196
197    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
198        match Pin::new(&mut self.stream).poll_next(cx) {
199            Poll::Pending => Poll::Pending,
200            Poll::Ready(Some(recordbatch)) => match recordbatch {
201                Ok(recordbatch) => Poll::Ready(Some(Ok(recordbatch.into_df_record_batch()))),
202                Err(e) => Poll::Ready(Some(Err(DataFusionError::External(Box::new(e))))),
203            },
204            Poll::Ready(None) => Poll::Ready(None),
205        }
206    }
207
208    #[inline]
209    fn size_hint(&self) -> (usize, Option<usize>) {
210        self.stream.size_hint()
211    }
212}
213
214/// DataFusion [SendableRecordBatchStream](DfSendableRecordBatchStream) -> Greptime [RecordBatchStream].
215/// The reverse one is [DfRecordBatchStreamAdapter].
216/// It can collect metrics from DataFusion execution plan.
217pub struct RecordBatchStreamAdapter {
218    schema: SchemaRef,
219    stream: DfSendableRecordBatchStream,
220    metrics: Option<BaselineMetrics>,
221    /// Aggregated plan-level metrics. Resolved after an [ExecutionPlan] is finished.
222    metrics_2: Metrics,
223    query_load_region_id: Option<u64>,
224    query_stat_counters: Option<RegionQueryStatCounters>,
225    /// Display plan and metrics in verbose mode.
226    explain_verbose: bool,
227    span: Span,
228}
229
230/// Query statistic counters owned by a region.
231#[derive(Debug, Clone)]
232pub struct RegionQueryStatCounters {
233    /// The total query CPU time in nanoseconds.
234    pub query_cpu_time: Arc<AtomicU64>,
235    /// The total scanned bytes.
236    pub query_scanned_bytes: Arc<AtomicU64>,
237}
238
239/// Json encoded metrics. Contains metric from a whole plan tree.
240enum Metrics {
241    Unavailable,
242    Unresolved(Arc<dyn ExecutionPlan>),
243    PartialResolved(Arc<dyn ExecutionPlan>, RecordBatchMetrics),
244    Resolved(RecordBatchMetrics),
245}
246
247impl RecordBatchStreamAdapter {
248    pub fn try_new(stream: DfSendableRecordBatchStream) -> Result<Self> {
249        let schema =
250            Arc::new(Schema::try_from(stream.schema()).context(error::SchemaConversionSnafu)?);
251        Ok(Self {
252            schema,
253            stream,
254            metrics: None,
255            metrics_2: Metrics::Unavailable,
256            query_load_region_id: None,
257            query_stat_counters: None,
258            explain_verbose: false,
259            span: Span::current(),
260        })
261    }
262
263    pub fn try_new_with_span(stream: DfSendableRecordBatchStream, span: Span) -> Result<Self> {
264        let schema =
265            Arc::new(Schema::try_from(stream.schema()).context(error::SchemaConversionSnafu)?);
266        let subspan = info_span!(parent: &span, "RecordBatchStreamAdapter");
267        Ok(Self {
268            schema,
269            stream,
270            metrics: None,
271            metrics_2: Metrics::Unavailable,
272            query_load_region_id: None,
273            query_stat_counters: None,
274            explain_verbose: false,
275            span: subspan,
276        })
277    }
278
279    pub fn set_metrics2(&mut self, plan: Arc<dyn ExecutionPlan>) {
280        self.metrics_2 = Metrics::Unresolved(plan)
281    }
282
283    fn record_query_stats_on_drop(&self) {
284        let Some(counters) = &self.query_stat_counters else {
285            return;
286        };
287
288        match &self.metrics_2 {
289            Metrics::Unresolved(df_plan) => {
290                let metrics = collect_lightweight_query_load_metrics(
291                    df_plan.as_ref(),
292                    self.query_load_region_id,
293                );
294                record_query_stats(counters, &metrics);
295            }
296            Metrics::PartialResolved(_, metrics) | Metrics::Resolved(metrics) => {
297                record_query_stats(counters, metrics);
298            }
299            Metrics::Unavailable => {}
300        }
301    }
302
303    pub fn set_query_load_region_id(&mut self, region_id: Option<u64>) {
304        self.query_load_region_id = region_id;
305    }
306
307    pub fn set_query_stat_counters(&mut self, counters: Option<RegionQueryStatCounters>) {
308        self.query_stat_counters = counters;
309    }
310
311    /// Set the verbose mode for displaying plan and metrics.
312    pub fn set_explain_verbose(&mut self, verbose: bool) {
313        self.explain_verbose = verbose;
314    }
315
316    fn collect_plan_metrics(&self, df_plan: &Arc<dyn ExecutionPlan>) -> RecordBatchMetrics {
317        collect_full_metrics(
318            df_plan.as_ref(),
319            self.explain_verbose,
320            self.query_load_region_id,
321        )
322    }
323
324    fn collect_partial_metrics(
325        df_plan: &dyn ExecutionPlan,
326        explain_verbose: bool,
327        query_load_region_id: Option<u64>,
328    ) -> RecordBatchMetrics {
329        if explain_verbose {
330            collect_full_metrics(df_plan, explain_verbose, query_load_region_id)
331        } else {
332            collect_lightweight_query_load_metrics(df_plan, query_load_region_id)
333        }
334    }
335
336    fn update_plan_metrics(&mut self, final_metrics: bool) {
337        if final_metrics {
338            let df_plan = match &self.metrics_2 {
339                Metrics::Unresolved(df_plan) | Metrics::PartialResolved(df_plan, _) => {
340                    df_plan.clone()
341                }
342                Metrics::Unavailable | Metrics::Resolved(_) => return,
343            };
344            let metrics = self.collect_plan_metrics(&df_plan);
345            self.metrics_2 = Metrics::Resolved(metrics);
346        } else {
347            let explain_verbose = self.explain_verbose;
348            let query_load_region_id = self.query_load_region_id;
349            match &mut self.metrics_2 {
350                Metrics::Unresolved(df_plan) => {
351                    let df_plan = df_plan.clone();
352                    let metrics = Self::collect_partial_metrics(
353                        df_plan.as_ref(),
354                        explain_verbose,
355                        query_load_region_id,
356                    );
357                    self.metrics_2 = Metrics::PartialResolved(df_plan, metrics);
358                }
359                Metrics::PartialResolved(df_plan, metrics) => {
360                    *metrics = Self::collect_partial_metrics(
361                        df_plan.as_ref(),
362                        explain_verbose,
363                        query_load_region_id,
364                    );
365                }
366                Metrics::Unavailable | Metrics::Resolved(_) => {}
367            }
368        }
369    }
370}
371
372/// Extracts total `output_bytes` from region scan plan nodes.
373pub fn region_scan_output_bytes(metrics: &RecordBatchMetrics) -> usize {
374    metrics
375        .plan_metrics
376        .iter()
377        .filter(|pm| pm.plan_name == REGION_SCAN_EXEC_NAME)
378        .flat_map(|pm| &pm.metrics)
379        .filter_map(|(name, value)| (name == "output_bytes").then_some(*value))
380        .sum()
381}
382
383fn record_query_stats(counters: &RegionQueryStatCounters, metrics: &RecordBatchMetrics) {
384    counters
385        .query_cpu_time
386        .fetch_add(metrics.elapsed_compute as u64, Ordering::Relaxed);
387    counters
388        .query_scanned_bytes
389        .fetch_add(region_scan_output_bytes(metrics) as u64, Ordering::Relaxed);
390}
391
392/// Collects the complete plan metrics used by terminal metrics and verbose analyze output.
393fn collect_full_metrics(
394    df_plan: &dyn ExecutionPlan,
395    explain_verbose: bool,
396    query_load_region_id: Option<u64>,
397) -> RecordBatchMetrics {
398    let mut metric_collector = MetricCollector::new(explain_verbose);
399    accept(df_plan, &mut metric_collector).unwrap();
400    metric_collector.record_batch_metrics.query_load_region_id = query_load_region_id;
401    metric_collector.record_batch_metrics
402}
403
404/// Collects the minimal metrics needed for query-load reporting before EOF.
405///
406/// This intentionally avoids [`MetricCollector`]'s full per-plan aggregation,
407/// sorting, and plan formatting work on the normal query hot path. The result
408/// is still enough for early-stop/cancellation paths to report per-region CPU
409/// time, scanned bytes, and physical-region attribution.
410fn collect_lightweight_query_load_metrics(
411    df_plan: &dyn ExecutionPlan,
412    query_load_region_id: Option<u64>,
413) -> RecordBatchMetrics {
414    let mut metrics = RecordBatchMetrics {
415        query_load_region_id,
416        ..Default::default()
417    };
418    collect_lightweight_query_load_metrics_inner(df_plan, 0, &mut metrics);
419    metrics
420}
421
422/// Recursively walks the physical plan and reads raw metric values without
423/// formatting plan nodes.
424fn collect_lightweight_query_load_metrics_inner(
425    df_plan: &dyn ExecutionPlan,
426    level: usize,
427    record_batch_metrics: &mut RecordBatchMetrics,
428) {
429    let is_region_scan = df_plan.name() == REGION_SCAN_EXEC_NAME;
430    let mut region_scan_output_bytes = None;
431
432    if let Some(metrics) = df_plan.metrics() {
433        for metric in metrics.iter() {
434            let value = metric.value();
435            match value {
436                MetricValue::ElapsedCompute(elapsed_compute) => {
437                    record_batch_metrics.elapsed_compute += elapsed_compute.value();
438                }
439                MetricValue::CurrentMemoryUsage(memory_usage) => {
440                    record_batch_metrics.memory_usage += memory_usage.value();
441                }
442                _ => {}
443            }
444
445            if is_region_scan && value.name() == "output_bytes" {
446                *region_scan_output_bytes.get_or_insert(0) += value.as_usize();
447            }
448        }
449    }
450
451    if let Some(output_bytes) = region_scan_output_bytes {
452        record_batch_metrics.plan_metrics.push(PlanMetrics {
453            plan: df_plan.name().to_string(),
454            plan_name: df_plan.name().to_string(),
455            level,
456            metrics: vec![("output_bytes".to_string(), output_bytes)],
457        });
458    }
459
460    for child in df_plan.children() {
461        collect_lightweight_query_load_metrics_inner(
462            child.as_ref(),
463            level + 1,
464            record_batch_metrics,
465        );
466    }
467}
468
469impl RecordBatchStream for RecordBatchStreamAdapter {
470    fn name(&self) -> &str {
471        "RecordBatchStreamAdapter"
472    }
473
474    fn schema(&self) -> SchemaRef {
475        self.schema.clone()
476    }
477
478    fn metrics(&self) -> Option<RecordBatchMetrics> {
479        match &self.metrics_2 {
480            Metrics::Unresolved(df_plan) => {
481                if self.explain_verbose {
482                    Some(self.collect_plan_metrics(df_plan))
483                } else {
484                    None
485                }
486            }
487            Metrics::PartialResolved(df_plan, metrics) => Some(if self.explain_verbose {
488                self.collect_plan_metrics(df_plan)
489            } else {
490                metrics.clone()
491            }),
492            Metrics::Resolved(metrics) => Some(metrics.clone()),
493            Metrics::Unavailable => None,
494        }
495    }
496
497    fn output_ordering(&self) -> Option<&[OrderOption]> {
498        None
499    }
500}
501
502impl Stream for RecordBatchStreamAdapter {
503    type Item = Result<RecordBatch>;
504
505    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
506        let timer = self
507            .metrics
508            .as_ref()
509            .map(|m| m.elapsed_compute().clone())
510            .unwrap_or_default();
511        let _guard = timer.timer();
512        let poll_span = info_span!(parent: &self.span, "poll_next");
513        let _entered = poll_span.enter();
514        match Pin::new(&mut self.stream).poll_next(cx) {
515            Poll::Pending => Poll::Pending,
516            Poll::Ready(Some(df_record_batch)) => {
517                let df_record_batch = df_record_batch?;
518                self.update_plan_metrics(false);
519                Poll::Ready(Some(Ok(RecordBatch::from_df_record_batch(
520                    self.schema(),
521                    df_record_batch,
522                ))))
523            }
524            Poll::Ready(None) => {
525                self.update_plan_metrics(true);
526                Poll::Ready(None)
527            }
528        }
529    }
530
531    #[inline]
532    fn size_hint(&self) -> (usize, Option<usize>) {
533        self.stream.size_hint()
534    }
535}
536
537impl Drop for RecordBatchStreamAdapter {
538    fn drop(&mut self) {
539        self.record_query_stats_on_drop();
540    }
541}
542
543/// An [ExecutionPlanVisitor] to collect metrics from a [ExecutionPlan].
544pub struct MetricCollector {
545    current_level: usize,
546    pub record_batch_metrics: RecordBatchMetrics,
547    verbose: bool,
548}
549
550impl MetricCollector {
551    pub fn new(verbose: bool) -> Self {
552        Self {
553            current_level: 0,
554            record_batch_metrics: RecordBatchMetrics::default(),
555            verbose,
556        }
557    }
558}
559
560impl ExecutionPlanVisitor for MetricCollector {
561    type Error = !;
562
563    fn pre_visit(&mut self, plan: &dyn ExecutionPlan) -> std::result::Result<bool, Self::Error> {
564        // skip if no metric available
565        let Some(metric) = plan.metrics() else {
566            self.record_batch_metrics.plan_metrics.push(PlanMetrics {
567                plan: plan.name().to_string(),
568                plan_name: plan.name().to_string(),
569                level: self.current_level,
570                metrics: vec![],
571            });
572            self.current_level += 1;
573            return Ok(true);
574        };
575
576        // scrape plan metrics
577        let metric = metric
578            .aggregate_by_name()
579            .sorted_for_display()
580            .timestamps_removed();
581        let mut plan_metric = PlanMetrics {
582            plan: one_line(plan, self.verbose).to_string(),
583            plan_name: plan.name().to_string(),
584            level: self.current_level,
585            metrics: Vec::with_capacity(metric.iter().size_hint().0),
586        };
587        for m in metric.iter() {
588            plan_metric
589                .metrics
590                .push((m.value().name().to_string(), m.value().as_usize()));
591
592            // aggregate high-level metrics
593            match m.value() {
594                MetricValue::ElapsedCompute(ec) => {
595                    self.record_batch_metrics.elapsed_compute += ec.value()
596                }
597                MetricValue::CurrentMemoryUsage(m) => {
598                    self.record_batch_metrics.memory_usage += m.value()
599                }
600                _ => {}
601            }
602        }
603        self.record_batch_metrics.plan_metrics.push(plan_metric);
604
605        self.current_level += 1;
606        Ok(true)
607    }
608
609    fn post_visit(&mut self, _plan: &dyn ExecutionPlan) -> std::result::Result<bool, Self::Error> {
610        self.current_level -= 1;
611        Ok(true)
612    }
613}
614
615/// Returns a single-line summary of the root of the plan.
616/// If the `verbose` flag is set, it will display detailed information about the plan.
617fn one_line(plan: &dyn ExecutionPlan, verbose: bool) -> impl fmt::Display + '_ {
618    struct Wrapper<'a> {
619        plan: &'a dyn ExecutionPlan,
620        format_type: DisplayFormatType,
621    }
622
623    impl fmt::Display for Wrapper<'_> {
624        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625            self.plan.fmt_as(self.format_type, f)?;
626            writeln!(f)
627        }
628    }
629
630    let format_type = if verbose {
631        DisplayFormatType::Verbose
632    } else {
633        DisplayFormatType::Default
634    };
635    Wrapper { plan, format_type }
636}
637
638/// [`RecordBatchMetrics`] carrys metrics value
639/// from datanode to frontend through gRPC
640#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
641pub struct RecordBatchMetrics {
642    // High-level aggregated metrics
643    /// CPU consumption in nanoseconds
644    pub elapsed_compute: usize,
645    /// Memory used by the plan in bytes
646    pub memory_usage: usize,
647    // Detailed per-plan metrics
648    /// An ordered list of plan metrics, from top to bottom in post-order.
649    pub plan_metrics: Vec<PlanMetrics>,
650    /// Region id that should receive query-load metrics for this scan.
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub query_load_region_id: Option<u64>,
653    /// Per-region watermark for incremental-read checkpoint advancement.
654    ///
655    /// The watermark is the latest sequence (`seq`) this query round safely read
656    /// for each participating region. Flow uses it to decide where the next
657    /// incremental round can resume.
658    ///
659    /// - `Some(seq)`: the query proved it safely read up to `seq`; downstream
660    ///   may advance the checkpoint to this value.
661    /// - `None`: the region participated but the query could not prove a safe
662    ///   read upper-bound, so the checkpoint must not advance for this region.
663    ///
664    /// Omitted when empty for backward compatibility.
665    #[serde(default, skip_serializing_if = "Vec::is_empty")]
666    pub region_watermarks: Vec<RegionWatermarkEntry>,
667}
668
669#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
670pub struct RegionWatermarkEntry {
671    pub region_id: u64,
672    #[serde(default, skip_serializing_if = "Option::is_none")]
673    pub watermark: Option<u64>,
674}
675
676/// Determines if a metric name represents a time measurement that should be formatted.
677fn is_time_metric(metric_name: &str) -> bool {
678    metric_name.contains("elapsed") || metric_name.contains("time") || metric_name.contains("cost")
679}
680
681/// Determines if a metric name represents a bytes measurement that should be formatted.
682fn is_bytes_metric(metric_name: &str) -> bool {
683    metric_name.contains("bytes") || metric_name.contains("mem")
684}
685
686fn format_bytes_human_readable(bytes: usize) -> String {
687    format!("{}", ReadableSize(bytes as u64))
688}
689
690/// Only display `plan_metrics` with indent `  ` (2 spaces).
691impl Display for RecordBatchMetrics {
692    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
693        for metric in &self.plan_metrics {
694            write!(
695                f,
696                "{:indent$}{} metrics=[",
697                " ",
698                metric.plan.trim_end(),
699                indent = metric.level * 2,
700            )?;
701            for (label, value) in &metric.metrics {
702                if is_time_metric(label) {
703                    write!(
704                        f,
705                        "{}: {}, ",
706                        label,
707                        format_nanoseconds_human_readable(*value),
708                    )?;
709                } else if is_bytes_metric(label) {
710                    write!(f, "{}: {}, ", label, format_bytes_human_readable(*value),)?;
711                } else {
712                    write!(f, "{}: {}, ", label, value)?;
713                }
714            }
715            writeln!(f, "]")?;
716        }
717
718        Ok(())
719    }
720}
721
722#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
723pub struct PlanMetrics {
724    /// The plan name
725    pub plan: String,
726    /// The stable execution plan name.
727    #[serde(default)]
728    pub plan_name: String,
729    /// The level of the plan, starts from 0
730    pub level: usize,
731    /// An ordered key-value list of metrics.
732    /// Key is metric label and value is metric value.
733    pub metrics: Vec<(String, usize)>,
734}
735
736enum AsyncRecordBatchStreamAdapterState {
737    Uninit(FutureStream),
738    Ready(SendableRecordBatchStream),
739    Failed,
740}
741
742pub struct AsyncRecordBatchStreamAdapter {
743    schema: SchemaRef,
744    state: AsyncRecordBatchStreamAdapterState,
745}
746
747impl AsyncRecordBatchStreamAdapter {
748    pub fn new(schema: SchemaRef, stream: FutureStream) -> Self {
749        Self {
750            schema,
751            state: AsyncRecordBatchStreamAdapterState::Uninit(stream),
752        }
753    }
754}
755
756impl RecordBatchStream for AsyncRecordBatchStreamAdapter {
757    fn schema(&self) -> SchemaRef {
758        self.schema.clone()
759    }
760
761    fn output_ordering(&self) -> Option<&[OrderOption]> {
762        None
763    }
764
765    fn metrics(&self) -> Option<RecordBatchMetrics> {
766        None
767    }
768}
769
770impl Stream for AsyncRecordBatchStreamAdapter {
771    type Item = Result<RecordBatch>;
772
773    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
774        loop {
775            match &mut self.state {
776                AsyncRecordBatchStreamAdapterState::Uninit(stream_future) => {
777                    match ready!(Pin::new(stream_future).poll(cx)) {
778                        Ok(stream) => {
779                            self.state = AsyncRecordBatchStreamAdapterState::Ready(stream);
780                            continue;
781                        }
782                        Err(e) => {
783                            self.state = AsyncRecordBatchStreamAdapterState::Failed;
784                            return Poll::Ready(Some(Err(e)));
785                        }
786                    };
787                }
788                AsyncRecordBatchStreamAdapterState::Ready(stream) => {
789                    return Poll::Ready(ready!(Pin::new(stream).poll_next(cx)));
790                }
791                AsyncRecordBatchStreamAdapterState::Failed => return Poll::Ready(None),
792            }
793        }
794    }
795
796    // This is not supported for lazy stream.
797    #[inline]
798    fn size_hint(&self) -> (usize, Option<usize>) {
799        (0, None)
800    }
801}
802
803/// Custom cast function that handles Map -> Binary (JSON) conversion
804fn custom_cast(
805    array: &dyn Array,
806    target_type: &ArrowDataType,
807    extype: Option<ColumnExtType>,
808) -> std::result::Result<Arc<dyn Array>, ArrowError> {
809    if let ArrowDataType::Map(_, _) = array.data_type()
810        && let ArrowDataType::Binary = target_type
811    {
812        return convert_map_to_json_binary(array, extype);
813    }
814
815    cast(array, target_type)
816}
817
818/// Convert a Map array to a Binary array containing JSON data
819fn convert_map_to_json_binary(
820    array: &dyn Array,
821    extype: Option<ColumnExtType>,
822) -> std::result::Result<Arc<dyn Array>, ArrowError> {
823    use datatypes::arrow::array::{BinaryArray, MapArray};
824    use serde_json::Value;
825
826    let map_array = array
827        .as_any()
828        .downcast_ref::<MapArray>()
829        .ok_or_else(|| ArrowError::CastError("Failed to downcast to MapArray".to_string()))?;
830
831    let mut json_values = Vec::with_capacity(map_array.len());
832
833    for i in 0..map_array.len() {
834        if map_array.is_null(i) {
835            json_values.push(None);
836        } else {
837            // Extract the map entry at index i
838            let map_entry = map_array.value(i);
839            let key_value_array = map_entry
840                .as_any()
841                .downcast_ref::<datatypes::arrow::array::StructArray>()
842                .ok_or_else(|| {
843                    ArrowError::CastError("Failed to downcast to StructArray".to_string())
844                })?;
845
846            // Convert to JSON object
847            let mut json_obj = serde_json::Map::with_capacity(key_value_array.len());
848
849            for j in 0..key_value_array.len() {
850                if key_value_array.is_null(j) {
851                    continue;
852                }
853                let key_field = key_value_array.column(0);
854                let value_field = key_value_array.column(1);
855
856                if key_field.is_null(j) {
857                    continue;
858                }
859
860                let key = key_field
861                    .as_any()
862                    .downcast_ref::<datatypes::arrow::array::StringArray>()
863                    .ok_or_else(|| {
864                        ArrowError::CastError("Failed to downcast key to StringArray".to_string())
865                    })?
866                    .value(j);
867
868                let value = if value_field.is_null(j) {
869                    Value::Null
870                } else {
871                    let value_str = value_field
872                        .as_any()
873                        .downcast_ref::<datatypes::arrow::array::StringArray>()
874                        .ok_or_else(|| {
875                            ArrowError::CastError(
876                                "Failed to downcast value to StringArray".to_string(),
877                            )
878                        })?
879                        .value(j);
880                    Value::String(value_str.to_string())
881                };
882
883                json_obj.insert(key.to_string(), value);
884            }
885
886            let json_value = Value::Object(json_obj);
887            let json_bytes = match extype {
888                Some(ColumnExtType::Json) => {
889                    let json_string = match serde_json::to_string(&json_value) {
890                        Ok(s) => s,
891                        Err(e) => {
892                            return Err(ArrowError::CastError(format!(
893                                "Failed to serialize JSON: {}",
894                                e
895                            )));
896                        }
897                    };
898                    match jsonb::parse_value(json_string.as_bytes()) {
899                        Ok(jsonb_value) => jsonb_value.to_vec(),
900                        Err(e) => {
901                            return Err(ArrowError::CastError(format!(
902                                "Failed to serialize JSONB: {}",
903                                e
904                            )));
905                        }
906                    }
907                }
908                _ => match serde_json::to_vec(&json_value) {
909                    Ok(b) => b,
910                    Err(e) => {
911                        return Err(ArrowError::CastError(format!(
912                            "Failed to serialize JSON: {}",
913                            e
914                        )));
915                    }
916                },
917            };
918            json_values.push(Some(json_bytes));
919        }
920    }
921
922    let binary_array = BinaryArray::from_iter(json_values);
923    Ok(Arc::new(binary_array))
924}
925
926#[cfg(test)]
927mod test {
928    use std::any::Any;
929    use std::time::Duration;
930
931    use common_error::ext::BoxedError;
932    use common_error::mock::MockError;
933    use common_error::status_code::StatusCode;
934    use datafusion::execution::TaskContext;
935    use datafusion::physical_expr::{EquivalenceProperties, Partitioning};
936    use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
937    use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder, MetricsSet};
938    use datafusion::physical_plan::{DisplayAs, PlanProperties};
939    use datatypes::arrow::array::{ArrayRef, MapArray, StringArray, StructArray};
940    use datatypes::arrow::buffer::OffsetBuffer;
941    use datatypes::arrow::datatypes::Field;
942    use datatypes::prelude::ConcreteDataType;
943    use datatypes::schema::ColumnSchema;
944    use datatypes::vectors::Int32Vector;
945    use futures::StreamExt;
946    use serde_json::json;
947    use snafu::IntoError;
948
949    use super::*;
950    use crate::RecordBatches;
951    use crate::error::Error;
952
953    #[derive(Debug)]
954    struct TestMetricsExec {
955        properties: Arc<PlanProperties>,
956        metrics: ExecutionPlanMetricsSet,
957    }
958
959    impl TestMetricsExec {
960        fn new(schema: DfSchemaRef) -> Self {
961            Self::with_output_bytes(schema, &[24])
962        }
963
964        fn with_output_bytes(schema: DfSchemaRef, output_bytes_by_partition: &[usize]) -> Self {
965            let metrics = ExecutionPlanMetricsSet::new();
966            let elapsed_compute = MetricBuilder::new(&metrics).elapsed_compute(0);
967            elapsed_compute.add_duration(Duration::from_nanos(42));
968            for (partition, output_bytes) in output_bytes_by_partition.iter().copied().enumerate() {
969                let metric = MetricBuilder::new(&metrics).output_bytes(partition);
970                metric.add(output_bytes);
971            }
972
973            Self {
974                properties: Arc::new(PlanProperties::new(
975                    EquivalenceProperties::new(schema),
976                    Partitioning::UnknownPartitioning(output_bytes_by_partition.len().max(1)),
977                    EmissionType::Incremental,
978                    Boundedness::Bounded,
979                )),
980                metrics,
981            }
982        }
983    }
984
985    impl DisplayAs for TestMetricsExec {
986        fn fmt_as(&self, _t: DisplayFormatType, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
987            panic!("non-verbose lightweight partial metrics must not format the plan")
988        }
989    }
990
991    impl ExecutionPlan for TestMetricsExec {
992        fn name(&self) -> &str {
993            REGION_SCAN_EXEC_NAME
994        }
995
996        fn as_any(&self) -> &dyn Any {
997            self
998        }
999
1000        fn properties(&self) -> &Arc<PlanProperties> {
1001            &self.properties
1002        }
1003
1004        fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1005            vec![]
1006        }
1007
1008        fn with_new_children(
1009            self: Arc<Self>,
1010            _children: Vec<Arc<dyn ExecutionPlan>>,
1011        ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
1012            Ok(self)
1013        }
1014
1015        fn execute(
1016            &self,
1017            _partition: usize,
1018            _context: Arc<TaskContext>,
1019        ) -> datafusion_common::Result<DfSendableRecordBatchStream> {
1020            unreachable!("the test passes a separate stream to RecordBatchStreamAdapter")
1021        }
1022
1023        fn metrics(&self) -> Option<MetricsSet> {
1024            Some(self.metrics.clone_inner())
1025        }
1026    }
1027
1028    #[test]
1029    fn test_lightweight_query_load_metrics_sums_region_scan_output_bytes() {
1030        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1031            "a",
1032            ConcreteDataType::int32_datatype(),
1033            false,
1034        )]));
1035        let plan = TestMetricsExec::with_output_bytes(schema.arrow_schema().clone(), &[24, 18]);
1036
1037        let metrics = collect_lightweight_query_load_metrics(&plan, Some(42));
1038
1039        assert_eq!(metrics.query_load_region_id, Some(42));
1040        assert_eq!(region_scan_output_bytes(&metrics), 42);
1041        assert_eq!(metrics.plan_metrics.len(), 1);
1042        assert_eq!(
1043            metrics.plan_metrics[0].metrics,
1044            vec![("output_bytes".to_string(), 42)]
1045        );
1046    }
1047
1048    #[tokio::test]
1049    async fn test_record_batch_stream_adapter_collects_lightweight_partial_metrics() {
1050        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1051            "a",
1052            ConcreteDataType::int32_datatype(),
1053            false,
1054        )]));
1055        let batch1 = RecordBatch::new(
1056            schema.clone(),
1057            vec![Arc::new(Int32Vector::from_slice([1])) as _],
1058        )
1059        .unwrap()
1060        .into_df_record_batch();
1061        let batch2 = RecordBatch::new(
1062            schema.clone(),
1063            vec![Arc::new(Int32Vector::from_slice([2])) as _],
1064        )
1065        .unwrap()
1066        .into_df_record_batch();
1067        let df_stream = Box::pin(
1068            datafusion::physical_plan::stream::RecordBatchStreamAdapter::new(
1069                schema.arrow_schema().clone(),
1070                futures::stream::iter(vec![Ok(batch1), Ok(batch2)]),
1071            ),
1072        );
1073        let plan = Arc::new(TestMetricsExec::new(schema.arrow_schema().clone()));
1074
1075        let mut adapter = RecordBatchStreamAdapter::try_new(df_stream).unwrap();
1076        adapter.set_metrics2(plan);
1077        adapter.set_query_load_region_id(Some(42));
1078
1079        assert!(adapter.metrics().is_none());
1080        assert!(adapter.next().await.unwrap().is_ok());
1081        let metrics = adapter
1082            .metrics()
1083            .expect("non-verbose queries need partial query-load metrics before EOF");
1084        assert_eq!(metrics.elapsed_compute, 42);
1085        assert_eq!(metrics.query_load_region_id, Some(42));
1086        assert_eq!(region_scan_output_bytes(&metrics), 24);
1087        assert_eq!(metrics.plan_metrics.len(), 1);
1088        assert_eq!(metrics.plan_metrics[0].plan, REGION_SCAN_EXEC_NAME);
1089        assert_eq!(metrics.plan_metrics[0].plan_name, REGION_SCAN_EXEC_NAME);
1090        assert_eq!(
1091            metrics.plan_metrics[0].metrics,
1092            vec![("output_bytes".to_string(), 24)]
1093        );
1094    }
1095
1096    #[test]
1097    fn test_record_batch_stream_adapter_reuses_partial_query_stats_on_drop() {
1098        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1099            "a",
1100            ConcreteDataType::int32_datatype(),
1101            false,
1102        )]));
1103        let df_stream = Box::pin(
1104            datafusion::physical_plan::stream::RecordBatchStreamAdapter::new(
1105                schema.arrow_schema().clone(),
1106                futures::stream::empty::<datafusion::error::Result<DfRecordBatch>>(),
1107            ),
1108        );
1109        let counters = RegionQueryStatCounters {
1110            query_cpu_time: Arc::new(AtomicU64::new(10)),
1111            query_scanned_bytes: Arc::new(AtomicU64::new(20)),
1112        };
1113        let stale_metrics = RecordBatchMetrics {
1114            elapsed_compute: 1,
1115            plan_metrics: vec![PlanMetrics {
1116                plan: REGION_SCAN_EXEC_NAME.to_string(),
1117                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
1118                level: 0,
1119                metrics: vec![("output_bytes".to_string(), 2)],
1120            }],
1121            ..Default::default()
1122        };
1123        let adapter = RecordBatchStreamAdapter {
1124            schema: schema.clone(),
1125            stream: df_stream,
1126            metrics: None,
1127            metrics_2: Metrics::PartialResolved(
1128                Arc::new(TestMetricsExec::new(schema.arrow_schema().clone())),
1129                stale_metrics,
1130            ),
1131            query_load_region_id: None,
1132            query_stat_counters: Some(counters.clone()),
1133            explain_verbose: false,
1134            span: Span::current(),
1135        };
1136
1137        drop(adapter);
1138
1139        assert_eq!(counters.query_cpu_time.load(Ordering::Relaxed), 11);
1140        assert_eq!(counters.query_scanned_bytes.load(Ordering::Relaxed), 22);
1141    }
1142
1143    #[tokio::test]
1144    async fn test_async_recordbatch_stream_adaptor() {
1145        struct MaybeErrorRecordBatchStream {
1146            items: Vec<Result<RecordBatch>>,
1147        }
1148
1149        impl RecordBatchStream for MaybeErrorRecordBatchStream {
1150            fn schema(&self) -> SchemaRef {
1151                unimplemented!()
1152            }
1153
1154            fn output_ordering(&self) -> Option<&[OrderOption]> {
1155                None
1156            }
1157
1158            fn metrics(&self) -> Option<RecordBatchMetrics> {
1159                None
1160            }
1161        }
1162
1163        impl Stream for MaybeErrorRecordBatchStream {
1164            type Item = Result<RecordBatch>;
1165
1166            fn poll_next(
1167                mut self: Pin<&mut Self>,
1168                _: &mut Context<'_>,
1169            ) -> Poll<Option<Self::Item>> {
1170                if let Some(batch) = self.items.pop() {
1171                    Poll::Ready(Some(Ok(batch?)))
1172                } else {
1173                    Poll::Ready(None)
1174                }
1175            }
1176        }
1177
1178        fn new_future_stream(
1179            maybe_recordbatches: Result<Vec<Result<RecordBatch>>>,
1180        ) -> FutureStream {
1181            Box::pin(async move {
1182                maybe_recordbatches
1183                    .map(|items| Box::pin(MaybeErrorRecordBatchStream { items }) as _)
1184            })
1185        }
1186
1187        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1188            "a",
1189            ConcreteDataType::int32_datatype(),
1190            false,
1191        )]));
1192        let batch1 = RecordBatch::new(
1193            schema.clone(),
1194            vec![Arc::new(Int32Vector::from_slice([1])) as _],
1195        )
1196        .unwrap();
1197        let batch2 = RecordBatch::new(
1198            schema.clone(),
1199            vec![Arc::new(Int32Vector::from_slice([2])) as _],
1200        )
1201        .unwrap();
1202
1203        let success_stream = new_future_stream(Ok(vec![Ok(batch1.clone()), Ok(batch2.clone())]));
1204        let adapter = AsyncRecordBatchStreamAdapter::new(schema.clone(), success_stream);
1205        let collected = RecordBatches::try_collect(Box::pin(adapter)).await.unwrap();
1206        assert_eq!(
1207            collected,
1208            RecordBatches::try_new(schema.clone(), vec![batch2.clone(), batch1.clone()]).unwrap()
1209        );
1210
1211        let poll_err_stream = new_future_stream(Ok(vec![
1212            Ok(batch1.clone()),
1213            Err(error::ExternalSnafu
1214                .into_error(BoxedError::new(MockError::new(StatusCode::Unknown)))),
1215        ]));
1216        let adapter = AsyncRecordBatchStreamAdapter::new(schema.clone(), poll_err_stream);
1217        let err = RecordBatches::try_collect(Box::pin(adapter))
1218            .await
1219            .unwrap_err();
1220        assert!(
1221            matches!(err, Error::External { .. }),
1222            "unexpected err {err}"
1223        );
1224
1225        let failed_to_init_stream =
1226            new_future_stream(Err(error::ExternalSnafu
1227                .into_error(BoxedError::new(MockError::new(StatusCode::Internal)))));
1228        let adapter = AsyncRecordBatchStreamAdapter::new(schema.clone(), failed_to_init_stream);
1229        let err = RecordBatches::try_collect(Box::pin(adapter))
1230            .await
1231            .unwrap_err();
1232        assert!(
1233            matches!(err, Error::External { .. }),
1234            "unexpected err {err}"
1235        );
1236    }
1237
1238    #[test]
1239    fn test_convert_map_to_json_binary() {
1240        let keys = StringArray::from(vec![Some("a"), Some("b"), Some("c"), Some("x")]);
1241        let values = StringArray::from(vec![Some("1"), None, Some("3"), Some("42")]);
1242        let key_field = Arc::new(Field::new("key", ArrowDataType::Utf8, false));
1243        let value_field = Arc::new(Field::new("value", ArrowDataType::Utf8, true));
1244        let struct_type = ArrowDataType::Struct(vec![key_field, value_field].into());
1245
1246        let entries_field = Arc::new(Field::new("entries", struct_type, false));
1247
1248        let struct_array = StructArray::from(vec![
1249            (
1250                Arc::new(Field::new("key", ArrowDataType::Utf8, false)),
1251                Arc::new(keys) as ArrayRef,
1252            ),
1253            (
1254                Arc::new(Field::new("value", ArrowDataType::Utf8, true)),
1255                Arc::new(values) as ArrayRef,
1256            ),
1257        ]);
1258
1259        let offsets = OffsetBuffer::from_lengths([3, 0, 1]);
1260        let nulls = datatypes::arrow::buffer::NullBuffer::from(vec![true, false, true]);
1261
1262        let map_array = MapArray::new(
1263            entries_field,
1264            offsets,
1265            struct_array,
1266            Some(nulls), // nulls
1267            false,
1268        );
1269
1270        let result = convert_map_to_json_binary(&map_array, None).unwrap();
1271        let binary_array = result
1272            .as_any()
1273            .downcast_ref::<datatypes::arrow::array::BinaryArray>()
1274            .unwrap();
1275
1276        let expected_jsons = [
1277            Some(r#"{"a":"1","b":null,"c":"3"}"#),
1278            None,
1279            Some(r#"{"x":"42"}"#),
1280        ];
1281
1282        for (i, _) in expected_jsons.iter().enumerate() {
1283            if let Some(expected) = &expected_jsons[i] {
1284                assert!(!binary_array.is_null(i));
1285                let actual_bytes = binary_array.value(i);
1286                let actual_str = std::str::from_utf8(actual_bytes).unwrap();
1287                assert_eq!(actual_str, *expected);
1288            } else {
1289                assert!(binary_array.is_null(i));
1290            }
1291        }
1292
1293        let result_json =
1294            convert_map_to_json_binary(&map_array, Some(ColumnExtType::Json)).unwrap();
1295        let binary_array_json = result_json
1296            .as_any()
1297            .downcast_ref::<datatypes::arrow::array::BinaryArray>()
1298            .unwrap();
1299
1300        for (i, _) in expected_jsons.iter().enumerate() {
1301            if expected_jsons[i].is_some() {
1302                assert!(!binary_array_json.is_null(i));
1303                let actual_bytes = binary_array_json.value(i);
1304                assert_ne!(actual_bytes, expected_jsons[i].unwrap().as_bytes());
1305            } else {
1306                assert!(binary_array_json.is_null(i));
1307            }
1308        }
1309    }
1310
1311    #[test]
1312    fn test_record_query_stats_updates_region_counters() {
1313        let counters = RegionQueryStatCounters {
1314            query_cpu_time: Arc::new(AtomicU64::new(10)),
1315            query_scanned_bytes: Arc::new(AtomicU64::new(20)),
1316        };
1317        let metrics = RecordBatchMetrics {
1318            elapsed_compute: 2_000_000,
1319            plan_metrics: vec![PlanMetrics {
1320                plan: "RegionScanExec: region=1".to_string(),
1321                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
1322                level: 0,
1323                metrics: vec![("output_bytes".to_string(), 42)],
1324            }],
1325            ..Default::default()
1326        };
1327
1328        record_query_stats(&counters, &metrics);
1329
1330        assert_eq!(counters.query_cpu_time.load(Ordering::Relaxed), 2_000_010);
1331        assert_eq!(counters.query_scanned_bytes.load(Ordering::Relaxed), 62);
1332    }
1333
1334    #[test]
1335    fn test_record_batch_stream_adapter_records_query_stats_on_drop() {
1336        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1337            "a",
1338            ConcreteDataType::int32_datatype(),
1339            false,
1340        )]));
1341        let df_stream = Box::pin(
1342            datafusion::physical_plan::stream::RecordBatchStreamAdapter::new(
1343                schema.arrow_schema().clone(),
1344                futures::stream::empty::<datafusion::error::Result<DfRecordBatch>>(),
1345            ),
1346        );
1347        let counters = RegionQueryStatCounters {
1348            query_cpu_time: Arc::new(AtomicU64::new(10)),
1349            query_scanned_bytes: Arc::new(AtomicU64::new(20)),
1350        };
1351        let metrics = RecordBatchMetrics {
1352            elapsed_compute: 2_000_000,
1353            plan_metrics: vec![PlanMetrics {
1354                plan: "RegionScanExec: region=1".to_string(),
1355                plan_name: REGION_SCAN_EXEC_NAME.to_string(),
1356                level: 0,
1357                metrics: vec![("output_bytes".to_string(), 42)],
1358            }],
1359            ..Default::default()
1360        };
1361        let adapter = RecordBatchStreamAdapter {
1362            schema,
1363            stream: df_stream,
1364            metrics: None,
1365            metrics_2: Metrics::Resolved(metrics),
1366            query_load_region_id: None,
1367            query_stat_counters: Some(counters.clone()),
1368            explain_verbose: false,
1369            span: Span::current(),
1370        };
1371
1372        drop(adapter);
1373
1374        assert_eq!(counters.query_cpu_time.load(Ordering::Relaxed), 2_000_010);
1375        assert_eq!(counters.query_scanned_bytes.load(Ordering::Relaxed), 62);
1376    }
1377
1378    #[test]
1379    fn test_recordbatch_metrics_deserializes_without_region_watermarks() {
1380        let metrics: RecordBatchMetrics = serde_json::from_value(json!({
1381            "elapsed_compute": 12,
1382            "memory_usage": 34,
1383            "plan_metrics": []
1384        }))
1385        .unwrap();
1386
1387        assert!(metrics.region_watermarks.is_empty());
1388        assert_eq!(metrics.elapsed_compute, 12);
1389        assert_eq!(metrics.memory_usage, 34);
1390    }
1391
1392    #[test]
1393    fn test_plan_metrics_deserializes_without_plan_name() {
1394        let metrics: RecordBatchMetrics = serde_json::from_value(json!({
1395            "elapsed_compute": 12,
1396            "memory_usage": 34,
1397            "plan_metrics": [{
1398                "plan": "SeqScan: region=1",
1399                "level": 0,
1400                "metrics": []
1401            }]
1402        }))
1403        .unwrap();
1404
1405        assert_eq!(metrics.plan_metrics[0].plan_name, "");
1406    }
1407
1408    #[test]
1409    fn test_recordbatch_metrics_region_watermarks_serde_roundtrip() {
1410        let metrics = RecordBatchMetrics {
1411            region_watermarks: vec![
1412                RegionWatermarkEntry {
1413                    region_id: 1,
1414                    watermark: Some(100),
1415                },
1416                RegionWatermarkEntry {
1417                    region_id: 2,
1418                    watermark: None,
1419                },
1420            ],
1421            ..Default::default()
1422        };
1423
1424        let value = serde_json::to_value(&metrics).unwrap();
1425        assert_eq!(
1426            value.get("region_watermarks").unwrap(),
1427            &json!([
1428                { "region_id": 1, "watermark": 100 },
1429                { "region_id": 2 }
1430            ])
1431        );
1432
1433        let decoded: RecordBatchMetrics = serde_json::from_value(value).unwrap();
1434        assert_eq!(decoded.region_watermarks, metrics.region_watermarks);
1435    }
1436
1437    #[test]
1438    fn test_recordbatch_metrics_skips_empty_region_watermarks_on_serialize() {
1439        let value = serde_json::to_value(RecordBatchMetrics::default()).unwrap();
1440        assert!(value.get("region_watermarks").is_none());
1441    }
1442}