Skip to main content

promql/extension_plan/
histogram_fold.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::borrow::Cow;
16use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18use std::task::Poll;
19use std::time::Instant;
20
21use common_telemetry::warn;
22use datafusion::arrow::array::{Array, ArrayRef, AsArray};
23use datafusion::arrow::compute::{SortOptions, concat_batches};
24use datafusion::arrow::datatypes::{DataType, Float64Type, SchemaRef};
25use datafusion::arrow::record_batch::RecordBatch;
26use datafusion::common::stats::Precision;
27use datafusion::common::tree_node::TreeNodeRecursion;
28use datafusion::common::{DFSchema, DFSchemaRef, Statistics};
29use datafusion::error::{DataFusionError, Result as DataFusionResult};
30use datafusion::execution::TaskContext;
31use datafusion::logical_expr::{LogicalPlan, UserDefinedLogicalNodeCore};
32use datafusion::physical_expr::{
33    EquivalenceProperties, LexRequirement, OrderingRequirements, PhysicalSortRequirement,
34};
35use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
36use datafusion::physical_plan::expressions::{Column as PhyColumn, TryCastExpr as PhyTryCast};
37use datafusion::physical_plan::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
38use datafusion::physical_plan::{
39    DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, ExecutionPlanProperties,
40    InputDistributionRequirements, Partitioning, PhysicalExpr, PlanProperties, RecordBatchStream,
41    SendableRecordBatchStream, StatisticsArgs,
42};
43use datafusion::prelude::{Column, Expr};
44use datafusion_expr::{EmptyRelation, ident};
45use datatypes::arrow_array::string_array_value_at_index;
46use datatypes::prelude::{ConcreteDataType, DataType as GtDataType};
47use datatypes::value::{OrderedF64, Value, ValueRef};
48use datatypes::vectors::{Helper, MutableVector, VectorRef};
49use futures::{Stream, StreamExt, ready};
50use greptime_proto::substrait_extension as pb;
51use prost::Message;
52use snafu::ResultExt;
53
54use crate::error::{DeserializeSnafu, Result};
55use crate::extension_plan::{resolve_column_name, serialize_column_index};
56
57/// `HistogramFold` will fold the conventional (non-native) histogram ([1]) for later
58/// computing.
59///
60/// Specifically, it will transform the `le` and `field` column into a complex
61/// type, and samples on other tag columns:
62/// - `le` will become a [ListArray] of [f64]. With each bucket bound parsed
63/// - `field` will become a [ListArray] of [f64]
64/// - other columns will be sampled every `bucket_num` element, but their types won't change.
65///
66/// Due to the folding or sampling, the output rows number will become `input_rows` / `bucket_num`.
67///
68/// # Requirement
69/// - Input should be sorted on `<tag list>, ts, le ASC`.
70/// - The value set of `le` should be same. I.e., buckets of every series should be same.
71///
72/// [1]: https://prometheus.io/docs/concepts/metric_types/#histogram
73#[derive(Debug, PartialEq, Hash, Eq)]
74pub struct HistogramFold {
75    /// Name of the `le` column. It's a special column in prometheus
76    /// for implementing conventional histogram. It's a string column
77    /// with "literal" float value, like "+Inf", "0.001" etc.
78    le_column: String,
79    ts_column: String,
80    input: LogicalPlan,
81    field_column: String,
82    /// Native histogram companion column for a mixed classic/native input.
83    histogram_column: Option<String>,
84    operation: HistogramFoldOperation,
85    output_schema: DFSchemaRef,
86    unfix: Option<UnfixIndices>,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd)]
90pub enum HistogramFoldOperation {
91    Quantile(OrderedF64),
92    Fraction {
93        lower: OrderedF64,
94        upper: OrderedF64,
95    },
96}
97
98impl HistogramFoldOperation {
99    pub const fn function_name(self) -> &'static str {
100        match self {
101            Self::Quantile(_) => "histogram_quantile",
102            Self::Fraction { .. } => "histogram_fraction",
103        }
104    }
105}
106
107impl std::fmt::Display for HistogramFoldOperation {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::Quantile(quantile) => write!(f, "quantile={quantile}"),
111            Self::Fraction { lower, upper } => {
112                write!(f, "fraction=[{lower}, {upper}]")
113            }
114        }
115    }
116}
117
118#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
119struct UnfixIndices {
120    pub le_column_idx: u64,
121    pub ts_column_idx: u64,
122    pub field_column_idx: u64,
123}
124
125impl UserDefinedLogicalNodeCore for HistogramFold {
126    fn name(&self) -> &str {
127        Self::name()
128    }
129
130    fn inputs(&self) -> Vec<&LogicalPlan> {
131        vec![&self.input]
132    }
133
134    fn schema(&self) -> &DFSchemaRef {
135        &self.output_schema
136    }
137
138    fn expressions(&self) -> Vec<Expr> {
139        if self.unfix.is_some() {
140            return vec![];
141        }
142
143        let mut exprs = vec![
144            ident(&self.le_column),
145            ident(&self.ts_column),
146            ident(&self.field_column),
147        ];
148        exprs.extend(self.input.schema().fields().iter().filter_map(|f| {
149            let name = f.name();
150            if name != &self.le_column && name != &self.ts_column && name != &self.field_column {
151                Some(ident(name))
152            } else {
153                None
154            }
155        }));
156        exprs
157    }
158
159    fn necessary_children_exprs(&self, output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
160        if self.unfix.is_some() {
161            return None;
162        }
163
164        let input_schema = self.input.schema();
165        let le_column_index = input_schema.index_of_column_by_name(None, &self.le_column)?;
166
167        if output_columns.is_empty() {
168            let indices = (0..input_schema.fields().len()).collect::<Vec<_>>();
169            return Some(vec![indices]);
170        }
171
172        if let Some(histogram_column) = &self.histogram_column {
173            let mut necessary_indices = output_columns.to_vec();
174            for column in [
175                &self.le_column,
176                &self.ts_column,
177                &self.field_column,
178                histogram_column,
179            ] {
180                necessary_indices.push(input_schema.index_of_column_by_name(None, column)?);
181            }
182            necessary_indices.sort_unstable();
183            necessary_indices.dedup();
184            return Some(vec![necessary_indices]);
185        }
186
187        let mut necessary_indices = output_columns
188            .iter()
189            .map(|&output_column| {
190                if output_column < le_column_index {
191                    output_column
192                } else {
193                    output_column + 1
194                }
195            })
196            .collect::<Vec<_>>();
197        necessary_indices.push(le_column_index);
198        necessary_indices.sort_unstable();
199        necessary_indices.dedup();
200        Some(vec![necessary_indices])
201    }
202
203    fn fmt_for_explain(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        write!(
205            f,
206            "HistogramFold: le={}, field={}",
207            self.le_column, self.field_column
208        )?;
209        if let Some(histogram) = &self.histogram_column {
210            write!(f, ", histogram={histogram}")?;
211        }
212        write!(f, ", {}", self.operation)
213    }
214
215    fn with_exprs_and_inputs(
216        &self,
217        _exprs: Vec<Expr>,
218        inputs: Vec<LogicalPlan>,
219    ) -> DataFusionResult<Self> {
220        if inputs.is_empty() {
221            return Err(DataFusionError::Internal(
222                "HistogramFold must have at least one input".to_string(),
223            ));
224        }
225
226        let input: LogicalPlan = inputs.into_iter().next().unwrap();
227        let input_schema = input.schema();
228
229        if let Some(unfix) = &self.unfix {
230            let le_column =
231                resolve_column_name(unfix.le_column_idx, input_schema, "HistogramFold", "le")?;
232            let ts_column =
233                resolve_column_name(unfix.ts_column_idx, input_schema, "HistogramFold", "ts")?;
234            let field_column = resolve_column_name(
235                unfix.field_column_idx,
236                input_schema,
237                "HistogramFold",
238                "field",
239            )?;
240
241            let output_schema = Self::convert_schema(input_schema, &le_column)?;
242
243            Ok(Self {
244                le_column,
245                ts_column,
246                input,
247                field_column,
248                histogram_column: None,
249                operation: self.operation,
250                output_schema,
251                unfix: None,
252            })
253        } else {
254            Ok(Self {
255                le_column: self.le_column.clone(),
256                ts_column: self.ts_column.clone(),
257                input,
258                field_column: self.field_column.clone(),
259                histogram_column: self.histogram_column.clone(),
260                operation: self.operation,
261                output_schema: self.output_schema.clone(),
262                unfix: None,
263            })
264        }
265    }
266}
267
268impl HistogramFold {
269    pub fn new(
270        le_column: String,
271        field_column: String,
272        ts_column: String,
273        quantile: f64,
274        input: LogicalPlan,
275    ) -> DataFusionResult<Self> {
276        Self::new_with_operation(
277            le_column,
278            field_column,
279            ts_column,
280            HistogramFoldOperation::Quantile(quantile.into()),
281            None,
282            input,
283        )
284    }
285
286    pub fn new_with_operation(
287        le_column: String,
288        field_column: String,
289        ts_column: String,
290        operation: HistogramFoldOperation,
291        histogram_column: Option<String>,
292        input: LogicalPlan,
293    ) -> DataFusionResult<Self> {
294        let input_schema = input.schema();
295        Self::check_schema(input_schema, &le_column, &field_column, &ts_column)?;
296        if let Some(histogram_column) = &histogram_column {
297            Self::check_column(input_schema, histogram_column)?;
298        }
299        let output_schema = if histogram_column.is_some() {
300            input_schema.clone()
301        } else {
302            Self::convert_schema(input_schema, &le_column)?
303        };
304        Ok(Self {
305            le_column,
306            ts_column,
307            input,
308            field_column,
309            histogram_column,
310            operation,
311            output_schema,
312            unfix: None,
313        })
314    }
315
316    pub const fn name() -> &'static str {
317        "HistogramFold"
318    }
319
320    fn check_schema(
321        input_schema: &DFSchemaRef,
322        le_column: &str,
323        field_column: &str,
324        ts_column: &str,
325    ) -> DataFusionResult<()> {
326        Self::check_column(input_schema, le_column)?;
327        Self::check_column(input_schema, ts_column)?;
328        Self::check_column(input_schema, field_column)
329    }
330
331    fn check_column(input_schema: &DFSchemaRef, column: &str) -> DataFusionResult<()> {
332        if !input_schema.has_column_with_unqualified_name(column) {
333            return Err(DataFusionError::SchemaError(
334                Box::new(datafusion::common::SchemaError::FieldNotFound {
335                    field: Box::new(Column::new(None::<String>, column)),
336                    valid_fields: input_schema.columns(),
337                }),
338                Box::new(None),
339            ));
340        }
341        Ok(())
342    }
343
344    pub fn to_execution_plan(&self, exec_input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
345        let input_schema = self.input.schema();
346        // safety: those fields are checked in `check_schema()`
347        let le_column_index = input_schema
348            .index_of_column_by_name(None, &self.le_column)
349            .unwrap();
350        let field_column_index = input_schema
351            .index_of_column_by_name(None, &self.field_column)
352            .unwrap();
353        let histogram_column_index = self
354            .histogram_column
355            .as_ref()
356            .map(|column| input_schema.index_of_column_by_name(None, column).unwrap());
357        let ts_column_index = input_schema
358            .index_of_column_by_name(None, &self.ts_column)
359            .unwrap();
360
361        let tag_columns = exec_input
362            .schema()
363            .fields()
364            .iter()
365            .enumerate()
366            .filter_map(|(idx, field)| {
367                if idx == le_column_index
368                    || idx == field_column_index
369                    || Some(idx) == histogram_column_index
370                    || idx == ts_column_index
371                {
372                    None
373                } else {
374                    Some(Arc::new(PhyColumn::new(field.name(), idx)) as _)
375                }
376            })
377            .collect::<Vec<_>>();
378
379        let mut partition_exprs = tag_columns.clone();
380        partition_exprs.push(Arc::new(PhyColumn::new(
381            self.input.schema().field(ts_column_index).name(),
382            ts_column_index,
383        )) as _);
384
385        let output_schema: SchemaRef = self.output_schema.inner().clone();
386        let properties = Arc::new(PlanProperties::new(
387            EquivalenceProperties::new(output_schema.clone()),
388            Partitioning::UnknownPartitioning(exec_input.output_partitioning().partition_count()),
389            EmissionType::Incremental,
390            Boundedness::Bounded,
391        ));
392        Arc::new(HistogramFoldExec {
393            le_column_index,
394            field_column_index,
395            histogram_column_index,
396            ts_column_index,
397            input: exec_input,
398            tag_columns,
399            partition_exprs,
400            operation: self.operation,
401            output_schema,
402            metric: ExecutionPlanMetricsSet::new(),
403            properties,
404        })
405    }
406
407    /// Transform the schema
408    ///
409    /// - `le` will be removed
410    ///
411    /// Column qualifiers are preserved so downstream plan nodes can keep
412    /// referencing the columns by their original qualified names.
413    fn convert_schema(
414        input_schema: &DFSchemaRef,
415        le_column: &str,
416    ) -> DataFusionResult<DFSchemaRef> {
417        // safety: those fields are checked in `check_schema()`
418        let mut new_fields = Vec::with_capacity(input_schema.fields().len() - 1);
419        for (qualifier, field) in input_schema.iter() {
420            if field.name() != le_column {
421                new_fields.push((qualifier.cloned(), field.clone()));
422            }
423        }
424        Ok(Arc::new(DFSchema::new_with_metadata(
425            new_fields,
426            HashMap::new(),
427        )?))
428    }
429
430    pub fn serialize(&self) -> DataFusionResult<Vec<u8>> {
431        if self.histogram_column.is_some() {
432            return Err(DataFusionError::NotImplemented(
433                "mixed HistogramFold is frontend-only".to_string(),
434            ));
435        }
436        let HistogramFoldOperation::Quantile(quantile) = self.operation else {
437            return Err(DataFusionError::NotImplemented(
438                "HistogramFold fraction is frontend-only".to_string(),
439            ));
440        };
441        let le_column_idx = serialize_column_index(self.input.schema(), &self.le_column);
442        let ts_column_idx = serialize_column_index(self.input.schema(), &self.ts_column);
443        let field_column_idx = serialize_column_index(self.input.schema(), &self.field_column);
444
445        Ok(pb::HistogramFold {
446            le_column_idx,
447            ts_column_idx,
448            field_column_idx,
449            quantile: quantile.into(),
450        }
451        .encode_to_vec())
452    }
453
454    pub fn deserialize(bytes: &[u8]) -> Result<Self> {
455        let pb_histogram_fold = pb::HistogramFold::decode(bytes).context(DeserializeSnafu)?;
456        let placeholder_plan = LogicalPlan::EmptyRelation(EmptyRelation {
457            produce_one_row: false,
458            schema: Arc::new(DFSchema::empty()),
459        });
460
461        let unfix = UnfixIndices {
462            le_column_idx: pb_histogram_fold.le_column_idx,
463            ts_column_idx: pb_histogram_fold.ts_column_idx,
464            field_column_idx: pb_histogram_fold.field_column_idx,
465        };
466
467        Ok(Self {
468            le_column: String::new(),
469            ts_column: String::new(),
470            input: placeholder_plan,
471            field_column: String::new(),
472            histogram_column: None,
473            operation: HistogramFoldOperation::Quantile(pb_histogram_fold.quantile.into()),
474            output_schema: Arc::new(DFSchema::empty()),
475            unfix: Some(unfix),
476        })
477    }
478}
479
480impl PartialOrd for HistogramFold {
481    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
482        // Compare fields in order excluding output_schema
483        match self.le_column.partial_cmp(&other.le_column) {
484            Some(core::cmp::Ordering::Equal) => {}
485            ord => return ord,
486        }
487        match self.ts_column.partial_cmp(&other.ts_column) {
488            Some(core::cmp::Ordering::Equal) => {}
489            ord => return ord,
490        }
491        match self.input.partial_cmp(&other.input) {
492            Some(core::cmp::Ordering::Equal) => {}
493            ord => return ord,
494        }
495        match self.field_column.partial_cmp(&other.field_column) {
496            Some(core::cmp::Ordering::Equal) => {}
497            ord => return ord,
498        }
499        match self.histogram_column.partial_cmp(&other.histogram_column) {
500            Some(core::cmp::Ordering::Equal) => {}
501            ord => return ord,
502        }
503        self.operation.partial_cmp(&other.operation)
504    }
505}
506
507#[derive(Debug)]
508pub struct HistogramFoldExec {
509    /// Index for `le` column in the schema of input.
510    le_column_index: usize,
511    input: Arc<dyn ExecutionPlan>,
512    output_schema: SchemaRef,
513    /// Index for field column in the schema of input.
514    field_column_index: usize,
515    /// Index for the native histogram companion column on mixed inputs.
516    histogram_column_index: Option<usize>,
517    ts_column_index: usize,
518    /// Tag columns are all columns except `le`, `field` and `ts` columns.
519    tag_columns: Vec<Arc<dyn PhysicalExpr>>,
520    partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
521    operation: HistogramFoldOperation,
522    metric: ExecutionPlanMetricsSet,
523    properties: Arc<PlanProperties>,
524}
525
526impl ExecutionPlan for HistogramFoldExec {
527    fn apply_expressions(
528        &self,
529        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> datafusion_common::Result<TreeNodeRecursion>,
530    ) -> DataFusionResult<TreeNodeRecursion> {
531        datafusion::physical_plan::apply_expression_roots(
532            self.tag_columns.iter().chain(self.partition_exprs.iter()),
533            f,
534        )
535    }
536
537    fn properties(&self) -> &Arc<PlanProperties> {
538        &self.properties
539    }
540
541    fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
542        let mut cols = self
543            .tag_columns
544            .iter()
545            .map(|expr| PhysicalSortRequirement {
546                expr: expr.clone(),
547                options: None,
548            })
549            .collect::<Vec<PhysicalSortRequirement>>();
550        // add ts
551        cols.push(PhysicalSortRequirement {
552            expr: Arc::new(PhyColumn::new(
553                self.input.schema().field(self.ts_column_index).name(),
554                self.ts_column_index,
555            )),
556            options: None,
557        });
558        if self.histogram_column_index.is_none() {
559            // add le ASC
560            cols.push(PhysicalSortRequirement {
561                expr: Arc::new(PhyTryCast::new(
562                    Arc::new(PhyColumn::new(
563                        self.input.schema().field(self.le_column_index).name(),
564                        self.le_column_index,
565                    )),
566                    DataType::Float64,
567                )),
568                options: Some(SortOptions {
569                    descending: false,  // numeric bounds ascending
570                    nulls_first: false, // unparsable bounds last
571                }),
572            });
573        }
574
575        // Safety: `cols` is not empty
576        let requirement = LexRequirement::new(cols).unwrap();
577
578        vec![Some(OrderingRequirements::Hard(vec![requirement]))]
579    }
580
581    fn input_distribution_requirements(&self) -> InputDistributionRequirements {
582        InputDistributionRequirements::new(vec![Distribution::KeyPartitioned(
583            self.partition_exprs.clone(),
584        )])
585    }
586
587    fn maintains_input_order(&self) -> Vec<bool> {
588        vec![self.histogram_column_index.is_none(); self.children().len()]
589    }
590
591    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
592        vec![&self.input]
593    }
594
595    // cannot change schema with this method
596    fn with_new_children(
597        self: Arc<Self>,
598        children: Vec<Arc<dyn ExecutionPlan>>,
599    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
600        assert!(!children.is_empty());
601        let new_input = children[0].clone();
602        let properties = Arc::new(PlanProperties::new(
603            EquivalenceProperties::new(self.output_schema.clone()),
604            Partitioning::UnknownPartitioning(new_input.output_partitioning().partition_count()),
605            EmissionType::Incremental,
606            Boundedness::Bounded,
607        ));
608        Ok(Arc::new(Self {
609            input: new_input,
610            metric: self.metric.clone(),
611            le_column_index: self.le_column_index,
612            ts_column_index: self.ts_column_index,
613            tag_columns: self.tag_columns.clone(),
614            partition_exprs: self.partition_exprs.clone(),
615            operation: self.operation,
616            output_schema: self.output_schema.clone(),
617            field_column_index: self.field_column_index,
618            histogram_column_index: self.histogram_column_index,
619            properties,
620        }))
621    }
622
623    fn execute(
624        &self,
625        partition: usize,
626        context: Arc<TaskContext>,
627    ) -> DataFusionResult<SendableRecordBatchStream> {
628        let baseline_metric = BaselineMetrics::new(&self.metric, partition);
629
630        let batch_size = context.session_config().batch_size();
631        let input = self.input.execute(partition, context)?;
632        let output_schema = self.output_schema.clone();
633
634        let mut normal_indices = (0..input.schema().fields().len()).collect::<HashSet<_>>();
635        normal_indices.remove(&self.field_column_index);
636        normal_indices.remove(&self.le_column_index);
637        if let Some(histogram_column_index) = self.histogram_column_index {
638            normal_indices.remove(&histogram_column_index);
639        }
640        let mode = if self.histogram_column_index.is_some() {
641            FoldMode::Safe
642        } else {
643            FoldMode::Optimistic
644        };
645        Ok(Box::pin(HistogramFoldStream {
646            le_column_index: self.le_column_index,
647            field_column_index: self.field_column_index,
648            histogram_column_index: self.histogram_column_index,
649            operation: self.operation,
650            normal_indices: normal_indices.into_iter().collect(),
651            bucket_size: None,
652            input_buffer: vec![],
653            input,
654            output_schema,
655            input_schema: self.input.schema(),
656            mode,
657            safe_group: None,
658            metric: baseline_metric,
659            batch_size,
660            input_buffered_rows: 0,
661            output_buffer: HistogramFoldStream::empty_output_buffer(&self.input.schema())?,
662            output_buffered_rows: 0,
663        }))
664    }
665
666    fn metrics(&self) -> Option<MetricsSet> {
667        Some(self.metric.clone_inner())
668    }
669
670    fn statistics_from_inputs(
671        &self,
672        _input_stats: &[Arc<Statistics>],
673        _args: &StatisticsArgs,
674    ) -> DataFusionResult<Arc<Statistics>> {
675        Ok(Arc::new(Statistics {
676            num_rows: Precision::Absent,
677            total_byte_size: Precision::Absent,
678            column_statistics: Statistics::unknown_column(&self.schema()),
679        }))
680    }
681
682    fn name(&self) -> &str {
683        "HistogramFoldExec"
684    }
685}
686
687impl DisplayAs for HistogramFoldExec {
688    fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
689        match t {
690            DisplayFormatType::Default
691            | DisplayFormatType::Verbose
692            | DisplayFormatType::TreeRender => {
693                write!(
694                    f,
695                    "HistogramFoldExec: le=@{}, field=@{}",
696                    self.le_column_index, self.field_column_index
697                )?;
698                if let Some(histogram) = self.histogram_column_index {
699                    write!(f, ", histogram=@{histogram}")?;
700                }
701                write!(f, ", {}", self.operation)
702            }
703        }
704    }
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
708enum FoldMode {
709    Optimistic,
710    Safe,
711}
712
713pub struct HistogramFoldStream {
714    // internal states
715    le_column_index: usize,
716    field_column_index: usize,
717    histogram_column_index: Option<usize>,
718    operation: HistogramFoldOperation,
719    /// Columns need not folding. This indices is based on input schema
720    normal_indices: Vec<usize>,
721    bucket_size: Option<usize>,
722    /// Expected output batch size
723    batch_size: usize,
724    output_schema: SchemaRef,
725    input_schema: SchemaRef,
726    mode: FoldMode,
727    safe_group: Option<SafeGroup>,
728
729    // buffers
730    input_buffer: Vec<RecordBatch>,
731    input_buffered_rows: usize,
732    output_buffer: Vec<Box<dyn MutableVector>>,
733    output_buffered_rows: usize,
734
735    // runtime things
736    input: SendableRecordBatchStream,
737    metric: BaselineMetrics,
738}
739
740#[derive(Debug, Default)]
741struct SafeGroup {
742    tag_values: Vec<Value>,
743    buckets: Vec<f64>,
744    counters: Vec<f64>,
745    native_samples: Vec<(Value, Value)>,
746}
747
748impl RecordBatchStream for HistogramFoldStream {
749    fn schema(&self) -> SchemaRef {
750        self.output_schema.clone()
751    }
752}
753
754impl Stream for HistogramFoldStream {
755    type Item = DataFusionResult<RecordBatch>;
756
757    fn poll_next(
758        mut self: std::pin::Pin<&mut Self>,
759        cx: &mut std::task::Context<'_>,
760    ) -> Poll<Option<Self::Item>> {
761        let poll = loop {
762            match ready!(self.input.poll_next_unpin(cx)) {
763                Some(batch) => {
764                    let batch = batch?;
765                    let timer = Instant::now();
766                    let Some(result) = self.fold_input(batch)? else {
767                        self.metric.elapsed_compute().add_elapsed(timer);
768                        continue;
769                    };
770                    self.metric.elapsed_compute().add_elapsed(timer);
771                    break Poll::Ready(Some(result));
772                }
773                None => {
774                    self.flush_remaining()?;
775                    break Poll::Ready(self.take_output_buf()?.map(Ok));
776                }
777            }
778        };
779        self.metric.record_poll(poll)
780    }
781}
782
783impl HistogramFoldStream {
784    /// The inner most `Result` is for `poll_next()`
785    pub fn fold_input(
786        &mut self,
787        input: RecordBatch,
788    ) -> DataFusionResult<Option<DataFusionResult<RecordBatch>>> {
789        match self.mode {
790            FoldMode::Safe => {
791                self.push_input_buf(input);
792                self.process_safe_mode_buffer()?;
793            }
794            FoldMode::Optimistic => {
795                self.push_input_buf(input);
796                let Some(bucket_num) = self.calculate_bucket_num_from_buffer()? else {
797                    return Ok(None);
798                };
799                self.bucket_size = Some(bucket_num);
800
801                if self.input_buffered_rows < bucket_num {
802                    // not enough rows to fold
803                    return Ok(None);
804                }
805
806                self.fold_buf(bucket_num)?;
807            }
808        }
809
810        self.maybe_take_output()
811    }
812
813    /// Generate input-aligned output builders. Classic-only output drops `le` later.
814    pub fn empty_output_buffer(
815        schema: &SchemaRef,
816    ) -> DataFusionResult<Vec<Box<dyn MutableVector>>> {
817        let mut builders = Vec::with_capacity(schema.fields().len());
818        for field in schema.fields() {
819            let concrete_datatype = ConcreteDataType::try_from(field.data_type()).unwrap();
820            let mutable_vector = concrete_datatype.create_mutable_vector(0);
821            builders.push(mutable_vector);
822        }
823
824        Ok(builders)
825    }
826
827    /// Determines bucket count using buffered batches, concatenating them to
828    /// detect the first complete bucket that may span batch boundaries.
829    fn calculate_bucket_num_from_buffer(&mut self) -> DataFusionResult<Option<usize>> {
830        if let Some(size) = self.bucket_size {
831            return Ok(Some(size));
832        }
833
834        if self.input_buffer.is_empty() {
835            return Ok(None);
836        }
837
838        let batch_refs: Vec<&RecordBatch> = self.input_buffer.iter().collect();
839        let batch = concat_batches(&self.input_schema, batch_refs)?;
840        self.find_first_complete_bucket(&batch)
841    }
842
843    /// Returns the first group's candidate bucket count once a group boundary is
844    /// observed. Optimistic validation checks its bounds before folding.
845    fn find_first_complete_bucket(&self, batch: &RecordBatch) -> DataFusionResult<Option<usize>> {
846        if batch.num_rows() == 0 {
847            return Ok(None);
848        }
849
850        let vectors = Helper::try_into_vectors(batch.columns())
851            .map_err(|e| DataFusionError::Execution(e.to_string()))?;
852        let mut tag_values_buf = Vec::with_capacity(self.normal_indices.len());
853        self.collect_tag_values(&vectors, 0, &mut tag_values_buf);
854
855        for row in 1..batch.num_rows() {
856            if !self.is_same_group(&vectors, row, &tag_values_buf) {
857                // A new series/timestamp proves that the previous group is
858                // complete, including any numerically equal +Inf boundaries.
859                return Ok(Some(row));
860            }
861        }
862
863        Ok(None)
864    }
865
866    /// Fold record batches from input buffer and put to output buffer
867    fn fold_buf(&mut self, bucket_num: usize) -> DataFusionResult<()> {
868        let batch = concat_batches(&self.input_schema, self.input_buffer.drain(..).as_ref())?;
869        let mut remaining_rows = self.input_buffered_rows;
870        let mut cursor = 0;
871
872        // TODO(LFC): Try to get rid of the Arrow array to vector conversion here.
873        let vectors = Helper::try_into_vectors(batch.columns())
874            .map_err(|e| DataFusionError::Execution(e.to_string()))?;
875        let le_array = batch.column(self.le_column_index);
876        let field_array = batch.column(self.field_column_index);
877        let field_array = field_array.as_primitive::<Float64Type>();
878        let mut tag_values_buf = Vec::with_capacity(self.normal_indices.len());
879
880        // Keep one row of lookahead: +Inf alone does not prove completion.
881        // A group ending at the batch boundary stays buffered until more input
882        // arrives, or flush_remaining completes it at EOF.
883        while remaining_rows > bucket_num && self.mode == FoldMode::Optimistic {
884            self.collect_tag_values(&vectors, cursor, &mut tag_values_buf);
885            if !self.validate_optimistic_group(
886                &vectors,
887                le_array,
888                cursor,
889                bucket_num,
890                &tag_values_buf,
891            ) {
892                let remaining_input_batch = batch.slice(cursor, remaining_rows);
893                self.switch_to_safe_mode(remaining_input_batch)?;
894                return Ok(());
895            }
896
897            // "sample" normal columns
898            for (idx, value) in self.normal_indices.iter().zip(tag_values_buf.iter()) {
899                self.output_buffer[*idx].push_value_ref(value);
900            }
901            // "fold" `le` and field columns
902            let mut bucket = Vec::with_capacity(bucket_num);
903            let mut counters = Vec::with_capacity(bucket_num);
904            for bias in 0..bucket_num {
905                let position = cursor + bias;
906                let le = string_array_value_at_index(le_array, position)
907                    .and_then(|value| value.parse::<f64>().ok())
908                    .unwrap_or(f64::NAN);
909                bucket.push(le);
910
911                let counter = if field_array.is_valid(position) {
912                    field_array.value(position)
913                } else {
914                    f64::NAN
915                };
916                counters.push(counter);
917            }
918            // ignore invalid data
919            let result = Self::evaluate_row(self.operation, &bucket, &counters).unwrap_or(f64::NAN);
920            self.output_buffer[self.field_column_index].push_value_ref(&ValueRef::from(result));
921            cursor += bucket_num;
922            remaining_rows -= bucket_num;
923            self.output_buffered_rows += 1;
924        }
925
926        let remaining_input_batch = batch.slice(cursor, remaining_rows);
927        self.input_buffered_rows = remaining_input_batch.num_rows();
928        if self.input_buffered_rows > 0 {
929            self.input_buffer.push(remaining_input_batch);
930        }
931
932        Ok(())
933    }
934
935    fn push_input_buf(&mut self, batch: RecordBatch) {
936        self.input_buffered_rows += batch.num_rows();
937        self.input_buffer.push(batch);
938    }
939
940    fn maybe_take_output(&mut self) -> DataFusionResult<Option<DataFusionResult<RecordBatch>>> {
941        if self.output_buffered_rows >= self.batch_size {
942            return Ok(self.take_output_buf()?.map(Ok));
943        }
944        Ok(None)
945    }
946
947    fn switch_to_safe_mode(&mut self, remaining_batch: RecordBatch) -> DataFusionResult<()> {
948        self.mode = FoldMode::Safe;
949        self.bucket_size = None;
950        self.input_buffer.clear();
951        self.input_buffered_rows = remaining_batch.num_rows();
952
953        if self.input_buffered_rows > 0 {
954            self.input_buffer.push(remaining_batch);
955            self.process_safe_mode_buffer()?;
956        }
957
958        Ok(())
959    }
960
961    fn collect_tag_values<'a>(
962        &self,
963        vectors: &'a [VectorRef],
964        row: usize,
965        tag_values: &mut Vec<ValueRef<'a>>,
966    ) {
967        tag_values.clear();
968        for idx in self.normal_indices.iter() {
969            tag_values.push(vectors[*idx].get_ref(row));
970        }
971    }
972
973    fn validate_optimistic_group(
974        &self,
975        vectors: &[VectorRef],
976        le_array: &ArrayRef,
977        cursor: usize,
978        bucket_num: usize,
979        tag_values: &[ValueRef<'_>],
980    ) -> bool {
981        let inf_index = cursor + bucket_num - 1;
982        if self.is_same_group(vectors, inf_index + 1, tag_values) {
983            return false;
984        }
985        if !Self::is_positive_infinity(le_array, inf_index) {
986            return false;
987        }
988        if (cursor..=inf_index).any(|row| {
989            string_array_value_at_index(le_array, row)
990                .and_then(|value| value.parse::<f64>().ok())
991                .is_none()
992        }) {
993            return false;
994        }
995
996        for offset in 1..bucket_num {
997            let row = cursor + offset;
998            for (idx, expected) in self.normal_indices.iter().zip(tag_values.iter()) {
999                if vectors[*idx].get_ref(row) != *expected {
1000                    return false;
1001                }
1002            }
1003        }
1004        true
1005    }
1006
1007    /// Checks whether a row belongs to the current group (same series).
1008    fn is_same_group(
1009        &self,
1010        vectors: &[VectorRef],
1011        row: usize,
1012        tag_values: &[ValueRef<'_>],
1013    ) -> bool {
1014        self.normal_indices
1015            .iter()
1016            .zip(tag_values.iter())
1017            .all(|(idx, expected)| vectors[*idx].get_ref(row) == *expected)
1018    }
1019
1020    fn push_output_row(&mut self, tag_values: &[ValueRef<'_>], result: f64) {
1021        debug_assert_eq!(self.normal_indices.len(), tag_values.len());
1022        for (idx, value) in self.normal_indices.iter().zip(tag_values.iter()) {
1023            self.output_buffer[*idx].push_value_ref(value);
1024        }
1025        self.output_buffer[self.field_column_index].push_value_ref(&ValueRef::from(result));
1026        self.output_buffered_rows += 1;
1027    }
1028
1029    fn push_mixed_output_row(
1030        &mut self,
1031        tag_values: &[Value],
1032        le: &Value,
1033        result: Option<f64>,
1034        histogram: &Value,
1035    ) {
1036        let histogram_column_index = self.histogram_column_index.unwrap();
1037        for (idx, value) in self.normal_indices.iter().zip(tag_values) {
1038            self.output_buffer[*idx].push_value_ref(&value.as_value_ref());
1039        }
1040        self.output_buffer[self.le_column_index].push_value_ref(&le.as_value_ref());
1041        self.output_buffer[self.field_column_index]
1042            .push_value_ref(&result.map_or(ValueRef::Null, ValueRef::from));
1043        self.output_buffer[histogram_column_index].push_value_ref(&histogram.as_value_ref());
1044        self.output_buffered_rows += 1;
1045    }
1046
1047    fn finalize_safe_group(&mut self) -> DataFusionResult<()> {
1048        let Some(group) = self.safe_group.take() else {
1049            return Ok(());
1050        };
1051        if group.tag_values.is_empty() {
1052            return Ok(());
1053        }
1054
1055        if self.histogram_column_index.is_some() {
1056            let classic_result = if group.buckets.is_empty() {
1057                None
1058            } else {
1059                let mut buckets = group
1060                    .buckets
1061                    .into_iter()
1062                    .zip(group.counters)
1063                    .collect::<Vec<_>>();
1064                buckets.sort_by(|lhs, rhs| lhs.0.total_cmp(&rhs.0));
1065                let (bounds, counters): (Vec<_>, Vec<_>) = buckets.into_iter().unzip();
1066                let has_inf = bounds
1067                    .last()
1068                    .is_some_and(|value| value.is_infinite() && value.is_sign_positive());
1069                Some(if has_inf {
1070                    Self::evaluate_row(self.operation, &bounds, &counters).unwrap_or(f64::NAN)
1071                } else {
1072                    f64::NAN
1073                })
1074            };
1075            let has_null_native = group.native_samples.iter().any(|(le, _)| le.is_null());
1076            for (le, histogram) in group.native_samples.iter().filter(|(le, _)| !le.is_null()) {
1077                self.push_mixed_output_row(&group.tag_values, le, None, histogram);
1078            }
1079            for (le, histogram) in group.native_samples.iter().filter(|(le, _)| le.is_null()) {
1080                self.push_mixed_output_row(&group.tag_values, le, classic_result, histogram);
1081            }
1082            if !has_null_native && let Some(result) = classic_result {
1083                self.push_mixed_output_row(
1084                    &group.tag_values,
1085                    &Value::Null,
1086                    Some(result),
1087                    &Value::Null,
1088                );
1089            }
1090            return Ok(());
1091        }
1092        if group.buckets.is_empty() {
1093            return Ok(());
1094        }
1095
1096        let has_inf = group
1097            .buckets
1098            .last()
1099            .is_some_and(|value| value.is_infinite() && value.is_sign_positive());
1100        let result = if has_inf {
1101            Self::evaluate_row(self.operation, &group.buckets, &group.counters).unwrap_or(f64::NAN)
1102        } else {
1103            f64::NAN
1104        };
1105        let tag_value_refs = group
1106            .tag_values
1107            .iter()
1108            .map(Value::as_value_ref)
1109            .collect::<Vec<_>>();
1110        self.push_output_row(&tag_value_refs, result);
1111        Ok(())
1112    }
1113
1114    fn process_safe_mode_buffer(&mut self) -> DataFusionResult<()> {
1115        if self.input_buffer.is_empty() {
1116            self.input_buffered_rows = 0;
1117            return Ok(());
1118        }
1119
1120        let batch = concat_batches(&self.input_schema, self.input_buffer.drain(..).as_ref())?;
1121        self.input_buffered_rows = 0;
1122        let vectors = Helper::try_into_vectors(batch.columns())
1123            .map_err(|e| DataFusionError::Execution(e.to_string()))?;
1124        let le_array = batch.column(self.le_column_index);
1125        let field_array = batch
1126            .column(self.field_column_index)
1127            .as_primitive::<Float64Type>();
1128        let mut tag_values_buf = Vec::with_capacity(self.normal_indices.len());
1129
1130        for row in 0..batch.num_rows() {
1131            self.collect_tag_values(&vectors, row, &mut tag_values_buf);
1132            let should_start_new_group = self
1133                .safe_group
1134                .as_ref()
1135                .is_none_or(|group| !Self::tag_values_equal(&group.tag_values, &tag_values_buf));
1136            if should_start_new_group {
1137                self.finalize_safe_group()?;
1138                self.safe_group = Some(SafeGroup {
1139                    tag_values: tag_values_buf.iter().cloned().map(Value::from).collect(),
1140                    buckets: Vec::new(),
1141                    counters: Vec::new(),
1142                    native_samples: Vec::new(),
1143                });
1144            }
1145
1146            let Some(group) = self.safe_group.as_mut() else {
1147                continue;
1148            };
1149
1150            let mixed = self.histogram_column_index.is_some();
1151            let bucket = string_array_value_at_index(le_array, row)
1152                .and_then(|value| value.parse::<f64>().ok());
1153            if let Some(bucket) = bucket
1154                && (!mixed || field_array.is_valid(row))
1155            {
1156                let counter = if field_array.is_valid(row) {
1157                    field_array.value(row)
1158                } else {
1159                    f64::NAN
1160                };
1161                group.buckets.push(bucket);
1162                group.counters.push(counter);
1163            }
1164            if let Some(histogram_column_index) = self.histogram_column_index {
1165                let histogram = vectors[histogram_column_index].get(row);
1166                if !histogram.is_null() {
1167                    group
1168                        .native_samples
1169                        .push((vectors[self.le_column_index].get(row), histogram));
1170                }
1171            }
1172        }
1173
1174        Ok(())
1175    }
1176
1177    fn tag_values_equal(group_values: &[Value], current: &[ValueRef<'_>]) -> bool {
1178        group_values.len() == current.len()
1179            && group_values
1180                .iter()
1181                .zip(current.iter())
1182                .all(|(group, now)| group.as_value_ref() == *now)
1183    }
1184
1185    /// Compute result from output buffer
1186    fn take_output_buf(&mut self) -> DataFusionResult<Option<RecordBatch>> {
1187        if self.output_buffered_rows == 0 {
1188            if self.input_buffered_rows != 0 {
1189                warn!(
1190                    "input buffer is not empty, {} rows remaining",
1191                    self.input_buffered_rows
1192                );
1193            }
1194            return Ok(None);
1195        }
1196
1197        let mut output_buf = Self::empty_output_buffer(&self.input_schema)?;
1198        std::mem::swap(&mut self.output_buffer, &mut output_buf);
1199        let mut columns = Vec::with_capacity(output_buf.len());
1200        for builder in output_buf.iter_mut() {
1201            columns.push(builder.to_vector().to_arrow_array());
1202        }
1203        if self.histogram_column_index.is_none() {
1204            columns.remove(self.le_column_index);
1205        }
1206
1207        self.output_buffered_rows = 0;
1208        RecordBatch::try_new(self.output_schema.clone(), columns)
1209            .map(Some)
1210            .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))
1211    }
1212
1213    fn flush_remaining(&mut self) -> DataFusionResult<()> {
1214        if self.mode == FoldMode::Optimistic && self.input_buffered_rows > 0 {
1215            let buffered_batches: Vec<_> = std::mem::take(&mut self.input_buffer);
1216            if !buffered_batches.is_empty() {
1217                let batch = concat_batches(&self.input_schema, buffered_batches.as_slice())?;
1218                self.switch_to_safe_mode(batch)?;
1219            } else {
1220                self.input_buffered_rows = 0;
1221            }
1222        }
1223
1224        if self.mode == FoldMode::Safe {
1225            self.process_safe_mode_buffer()?;
1226            self.finalize_safe_group()?;
1227        }
1228
1229        Ok(())
1230    }
1231
1232    fn is_positive_infinity(le_array: &ArrayRef, index: usize) -> bool {
1233        matches!(
1234            string_array_value_at_index(le_array, index).and_then(|value| value.parse::<f64>().ok()),
1235            Some(value) if value.is_infinite() && value.is_sign_positive()
1236        )
1237    }
1238
1239    /// Evaluate the field column and return the result
1240    fn evaluate_row(
1241        operation: HistogramFoldOperation,
1242        bucket: &[f64],
1243        counter: &[f64],
1244    ) -> DataFusionResult<f64> {
1245        // check bucket
1246        if bucket.is_empty()
1247            || matches!(operation, HistogramFoldOperation::Quantile(_)) && bucket.len() == 1
1248        {
1249            return Ok(f64::NAN);
1250        }
1251        if bucket.last() != Some(&f64::INFINITY) {
1252            return Err(DataFusionError::Execution(
1253                "last bucket should be +Inf".to_string(),
1254            ));
1255        }
1256        if bucket.len() != counter.len() {
1257            return Err(DataFusionError::Execution(
1258                "bucket and counter should have the same length".to_string(),
1259            ));
1260        }
1261        if let HistogramFoldOperation::Quantile(quantile) = operation {
1262            let quantile = f64::from(quantile);
1263            if quantile < 0.0 {
1264                return Ok(f64::NEG_INFINITY);
1265            } else if quantile > 1.0 {
1266                return Ok(f64::INFINITY);
1267            } else if quantile.is_nan() {
1268                return Ok(f64::NAN);
1269            }
1270        }
1271
1272        // Detect equal numeric boundaries during validation so unique buckets
1273        // need neither another scan nor an allocation.
1274        let has_duplicates = !bucket.windows(2).all(|w| w[0] < w[1]);
1275        if has_duplicates && !bucket.windows(2).all(|w| w[0] <= w[1]) {
1276            return Ok(f64::NAN);
1277        }
1278        // Counts must be coalesced before repairing monotonicity.
1279        let coalesced = has_duplicates.then(|| {
1280            let mut bounds = Vec::with_capacity(bucket.len());
1281            let mut counts = Vec::with_capacity(counter.len());
1282            for (&bound, &count) in bucket.iter().zip(counter) {
1283                if bounds.last() == Some(&bound) {
1284                    *counts.last_mut().unwrap() += count;
1285                } else {
1286                    bounds.push(bound);
1287                    counts.push(count);
1288                }
1289            }
1290            (bounds, counts)
1291        });
1292        let (bucket, counter) = match &coalesced {
1293            Some((bounds, counts)) => (bounds.as_slice(), counts.as_slice()),
1294            None => (bucket, counter),
1295        };
1296        if matches!(operation, HistogramFoldOperation::Quantile(_)) && bucket.len() < 2 {
1297            return Ok(f64::NAN);
1298        }
1299        let counter = match operation {
1300            HistogramFoldOperation::Quantile(_) => {
1301                let needs_fix = counter.iter().any(|v| !v.is_finite())
1302                    || !counter.windows(2).all(|w| w[0] <= w[1]);
1303                if !needs_fix {
1304                    Cow::Borrowed(counter)
1305                } else {
1306                    let mut fixed = Vec::with_capacity(counter.len());
1307                    let mut prev = 0.0;
1308                    for (idx, &v) in counter.iter().enumerate() {
1309                        let mut val = if v.is_finite() { v } else { prev };
1310                        if idx > 0 && val < prev {
1311                            val = prev;
1312                        }
1313                        fixed.push(val);
1314                        prev = val;
1315                    }
1316                    Cow::Owned(fixed)
1317                }
1318            }
1319            HistogramFoldOperation::Fraction { .. } => Cow::Borrowed(counter),
1320        };
1321
1322        Ok(match operation {
1323            HistogramFoldOperation::Quantile(quantile) => {
1324                Self::evaluate_quantile(quantile.into(), bucket, &counter)
1325            }
1326            HistogramFoldOperation::Fraction { lower, upper } => {
1327                Self::evaluate_fraction(lower.into(), upper.into(), bucket, &counter)
1328            }
1329        })
1330    }
1331
1332    fn evaluate_quantile(quantile: f64, bucket: &[f64], counter: &[f64]) -> f64 {
1333        let total = *counter.last().unwrap();
1334        let expected_pos = total * quantile;
1335        let mut fit_bucket_pos = 0;
1336        while fit_bucket_pos < bucket.len() && counter[fit_bucket_pos] < expected_pos {
1337            fit_bucket_pos += 1;
1338        }
1339        if fit_bucket_pos >= bucket.len() - 1 {
1340            bucket[bucket.len() - 2]
1341        } else {
1342            let upper_bound = bucket[fit_bucket_pos];
1343            let upper_count = counter[fit_bucket_pos];
1344            let mut lower_bound = bucket[0].min(0.0);
1345            let mut lower_count = 0.0;
1346            if fit_bucket_pos > 0 {
1347                lower_bound = bucket[fit_bucket_pos - 1];
1348                lower_count = counter[fit_bucket_pos - 1];
1349            }
1350            if (upper_count - lower_count).abs() < 1e-10 {
1351                return f64::NAN;
1352            }
1353            lower_bound
1354                + (upper_bound - lower_bound) / (upper_count - lower_count)
1355                    * (expected_pos - lower_count)
1356        }
1357    }
1358
1359    fn evaluate_fraction(lower: f64, upper: f64, bucket: &[f64], counter: &[f64]) -> f64 {
1360        let total = *counter.last().unwrap();
1361        if total == 0.0 || lower.is_nan() || upper.is_nan() {
1362            return f64::NAN;
1363        }
1364        if lower >= upper {
1365            return 0.0;
1366        }
1367
1368        let mut rank = 0.0;
1369        let mut lower_rank = 0.0;
1370        let mut upper_rank = 0.0;
1371        let mut lower_set = false;
1372        let mut upper_set = false;
1373        let mut lower_bound = if bucket[0] > 0.0 {
1374            0.0
1375        } else {
1376            f64::NEG_INFINITY
1377        };
1378
1379        for (idx, (&upper_bound, &upper_count)) in bucket.iter().zip(counter).enumerate() {
1380            if idx > 0 {
1381                lower_bound = bucket[idx - 1];
1382            }
1383            let interpolate = |value: f64| {
1384                if lower_bound == f64::NEG_INFINITY {
1385                    upper_count
1386                } else {
1387                    rank + (upper_count - rank) * (value - lower_bound)
1388                        / (upper_bound - lower_bound)
1389                }
1390            };
1391
1392            if !lower_set && lower_bound >= lower {
1393                lower_rank = rank;
1394                lower_set = true;
1395            }
1396            if !upper_set && lower_bound >= upper {
1397                upper_rank = rank;
1398                upper_set = true;
1399            }
1400            if lower_set && upper_set {
1401                break;
1402            }
1403            if !lower_set && lower_bound < lower && upper_bound > lower {
1404                lower_rank = interpolate(lower);
1405                lower_set = true;
1406            }
1407            if !upper_set && lower_bound < upper && upper_bound > upper {
1408                upper_rank = interpolate(upper);
1409                upper_set = true;
1410            }
1411            if lower_set && upper_set {
1412                break;
1413            }
1414            rank = upper_count;
1415        }
1416
1417        if !lower_set || lower_rank > total {
1418            lower_rank = total;
1419        }
1420        if !upper_set || upper_rank > total {
1421            upper_rank = total;
1422        }
1423        (upper_rank - lower_rank) / total
1424    }
1425}
1426
1427#[cfg(test)]
1428mod test {
1429    use std::sync::Arc;
1430
1431    use datafusion::arrow::array::{
1432        DictionaryArray, Float64Array, StringDictionaryBuilder, TimestampMillisecondArray,
1433    };
1434    use datafusion::arrow::datatypes::{Field, Schema, SchemaRef, TimeUnit, UInt32Type};
1435    use datafusion::common::ToDFSchema;
1436    use datafusion::datasource::memory::MemorySourceConfig;
1437    use datafusion::datasource::source::DataSourceExec;
1438    use datafusion::logical_expr::EmptyRelation;
1439    use datafusion::physical_plan::{ChildrenPropertiesMode, ReplaceChildrenOptions};
1440    use datafusion::prelude::SessionContext;
1441    use datatypes::arrow_array::StringArray;
1442    use futures::FutureExt;
1443
1444    use super::*;
1445
1446    fn project_batch(batch: &RecordBatch, indices: &[usize]) -> RecordBatch {
1447        let fields = indices
1448            .iter()
1449            .map(|&idx| batch.schema().field(idx).clone())
1450            .collect::<Vec<_>>();
1451        let columns = indices
1452            .iter()
1453            .map(|&idx| batch.column(idx).clone())
1454            .collect::<Vec<_>>();
1455        let schema = Arc::new(Schema::new(fields));
1456        RecordBatch::try_new(schema, columns).unwrap()
1457    }
1458
1459    /// The batches and schema shared by [`prepare_test_data`] and
1460    /// [`prepare_test_data_with_two_partitions`].
1461    fn prepare_test_data_batches() -> (Vec<RecordBatch>, SchemaRef) {
1462        let schema = Arc::new(Schema::new(vec![
1463            Field::new("host", DataType::Utf8, true),
1464            Field::new("le", DataType::Utf8, true),
1465            Field::new("val", DataType::Float64, true),
1466        ]));
1467
1468        // 12 items
1469        let host_column_1 = Arc::new(StringArray::from(vec![
1470            "host_1", "host_1", "host_1", "host_1", "host_1", "host_2", "host_2", "host_2",
1471            "host_2", "host_2", "host_3", "host_3",
1472        ])) as _;
1473        let le_column_1 = Arc::new(StringArray::from(vec![
1474            "0.001", "0.1", "10", "1000", "+Inf", "0.001", "0.1", "10", "1000", "+inf", "0.001",
1475            "0.1",
1476        ])) as _;
1477        let val_column_1 = Arc::new(Float64Array::from(vec![
1478            0_0.0, 1.0, 1.0, 5.0, 5.0, 0_0.0, 20.0, 60.0, 70.0, 100.0, 0_1.0, 1.0,
1479        ])) as _;
1480
1481        // 2 items
1482        let host_column_2 = Arc::new(StringArray::from(vec!["host_3", "host_3"])) as _;
1483        let le_column_2 = Arc::new(StringArray::from(vec!["10", "1000"])) as _;
1484        let val_column_2 = Arc::new(Float64Array::from(vec![1.0, 1.0])) as _;
1485
1486        // 11 items
1487        let host_column_3 = Arc::new(StringArray::from(vec![
1488            "host_3", "host_4", "host_4", "host_4", "host_4", "host_4", "host_5", "host_5",
1489            "host_5", "host_5", "host_5",
1490        ])) as _;
1491        let le_column_3 = Arc::new(StringArray::from(vec![
1492            "+INF", "0.001", "0.1", "10", "1000", "+iNf", "0.001", "0.1", "10", "1000", "+Inf",
1493        ])) as _;
1494        let val_column_3 = Arc::new(Float64Array::from(vec![
1495            1.0, 0_0.0, 0.0, 0.0, 0.0, 0.0, 0_0.0, 1.0, 2.0, 3.0, 4.0,
1496        ])) as _;
1497
1498        let data_1 = RecordBatch::try_new(
1499            schema.clone(),
1500            vec![host_column_1, le_column_1, val_column_1],
1501        )
1502        .unwrap();
1503        let data_2 = RecordBatch::try_new(
1504            schema.clone(),
1505            vec![host_column_2, le_column_2, val_column_2],
1506        )
1507        .unwrap();
1508        let data_3 = RecordBatch::try_new(
1509            schema.clone(),
1510            vec![host_column_3, le_column_3, val_column_3],
1511        )
1512        .unwrap();
1513
1514        (vec![data_1, data_2, data_3], schema)
1515    }
1516
1517    fn prepare_test_data() -> DataSourceExec {
1518        let (batches, schema) = prepare_test_data_batches();
1519        DataSourceExec::new(Arc::new(
1520            MemorySourceConfig::try_new(&[batches], schema, None).unwrap(),
1521        ))
1522    }
1523
1524    /// The batches from [`prepare_test_data_batches`], laid out over two
1525    /// partitions, so that partitioning metadata is checked against a real
1526    /// multi-partition input.
1527    fn prepare_test_data_with_two_partitions() -> DataSourceExec {
1528        let (mut batches, schema) = prepare_test_data_batches();
1529        let first_partition = vec![batches.remove(0)];
1530        DataSourceExec::new(Arc::new(
1531            MemorySourceConfig::try_new(&[first_partition, batches], schema, None).unwrap(),
1532        ))
1533    }
1534
1535    fn build_fold_exec_from_batches(
1536        batches: Vec<RecordBatch>,
1537        schema: SchemaRef,
1538        quantile: f64,
1539        ts_column_index: usize,
1540    ) -> Arc<HistogramFoldExec> {
1541        build_fold_exec_from_batches_with_operation(
1542            batches,
1543            schema,
1544            HistogramFoldOperation::Quantile(quantile.into()),
1545            ts_column_index,
1546        )
1547    }
1548
1549    fn build_fold_exec_from_batches_with_operation(
1550        batches: Vec<RecordBatch>,
1551        schema: SchemaRef,
1552        operation: HistogramFoldOperation,
1553        ts_column_index: usize,
1554    ) -> Arc<HistogramFoldExec> {
1555        let input: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1556            MemorySourceConfig::try_new(&[batches], schema.clone(), None).unwrap(),
1557        )));
1558        let output_schema: SchemaRef = Arc::new(
1559            HistogramFold::convert_schema(&Arc::new(input.schema().to_dfschema().unwrap()), "le")
1560                .unwrap()
1561                .as_arrow()
1562                .clone(),
1563        );
1564
1565        let (tag_columns, partition_exprs, properties) =
1566            build_test_plan_properties(&input, output_schema.clone(), ts_column_index);
1567
1568        Arc::new(HistogramFoldExec {
1569            le_column_index: 1,
1570            field_column_index: 2,
1571            histogram_column_index: None,
1572            operation,
1573            ts_column_index,
1574            input,
1575            output_schema,
1576            tag_columns,
1577            partition_exprs,
1578            metric: ExecutionPlanMetricsSet::new(),
1579            properties,
1580        })
1581    }
1582
1583    type PlanPropsResult = (
1584        Vec<Arc<dyn PhysicalExpr>>,
1585        Vec<Arc<dyn PhysicalExpr>>,
1586        Arc<PlanProperties>,
1587    );
1588
1589    fn build_test_plan_properties(
1590        input: &Arc<dyn ExecutionPlan>,
1591        output_schema: SchemaRef,
1592        ts_column_index: usize,
1593    ) -> PlanPropsResult {
1594        let tag_columns = input
1595            .schema()
1596            .fields()
1597            .iter()
1598            .enumerate()
1599            .filter_map(|(idx, field)| {
1600                if idx == 1 || idx == 2 || idx == ts_column_index {
1601                    None
1602                } else {
1603                    Some(Arc::new(PhyColumn::new(field.name(), idx)) as _)
1604                }
1605            })
1606            .collect::<Vec<_>>();
1607
1608        let partition_exprs = if tag_columns.is_empty() {
1609            vec![Arc::new(PhyColumn::new(
1610                input.schema().field(ts_column_index).name(),
1611                ts_column_index,
1612            )) as _]
1613        } else {
1614            tag_columns.clone()
1615        };
1616
1617        let properties = PlanProperties::new(
1618            EquivalenceProperties::new(output_schema.clone()),
1619            Partitioning::UnknownPartitioning(
1620                input.properties().output_partitioning().partition_count(),
1621            ),
1622            EmissionType::Incremental,
1623            Boundedness::Bounded,
1624        );
1625
1626        (tag_columns, partition_exprs, Arc::new(properties))
1627    }
1628
1629    #[tokio::test]
1630    async fn fold_overall() {
1631        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(prepare_test_data());
1632        let output_schema: SchemaRef = Arc::new(
1633            HistogramFold::convert_schema(
1634                &Arc::new(memory_exec.schema().to_dfschema().unwrap()),
1635                "le",
1636            )
1637            .unwrap()
1638            .as_arrow()
1639            .clone(),
1640        );
1641        let (tag_columns, partition_exprs, properties) =
1642            build_test_plan_properties(&memory_exec, output_schema.clone(), 0);
1643        let fold_exec = Arc::new(HistogramFoldExec {
1644            le_column_index: 1,
1645            field_column_index: 2,
1646            histogram_column_index: None,
1647            operation: HistogramFoldOperation::Quantile(0.4.into()),
1648            ts_column_index: 0,
1649            input: memory_exec,
1650            output_schema,
1651            tag_columns,
1652            partition_exprs,
1653            metric: ExecutionPlanMetricsSet::new(),
1654            properties,
1655        });
1656
1657        let session_context = SessionContext::default();
1658        let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
1659            .await
1660            .unwrap();
1661        let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1662            .unwrap()
1663            .to_string();
1664
1665        let expected = String::from(
1666            "+--------+-------------------+
1667| host   | val               |
1668+--------+-------------------+
1669| host_1 | 257.5             |
1670| host_2 | 5.05              |
1671| host_3 | 0.0004            |
1672| host_4 | NaN               |
1673| host_5 | 6.040000000000001 |
1674+--------+-------------------+",
1675        );
1676        assert_eq!(result_literal, expected);
1677    }
1678
1679    #[tokio::test]
1680    async fn fold_dictionary_encoded_labels() {
1681        let dictionary_type =
1682            DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8));
1683        let schema = Arc::new(Schema::new(vec![
1684            Field::new("host", dictionary_type.clone(), true),
1685            Field::new("le", dictionary_type, true),
1686            Field::new("val", DataType::Float64, true),
1687        ]));
1688
1689        let mut host = StringDictionaryBuilder::<UInt32Type>::new();
1690        let mut le = StringDictionaryBuilder::<UInt32Type>::new();
1691        for value in ["host_1", "host_1", "host_1"] {
1692            host.append_value(value);
1693        }
1694        for value in ["0.1", "1", "+Inf"] {
1695            le.append_value(value);
1696        }
1697        let batch = RecordBatch::try_new(
1698            schema.clone(),
1699            vec![
1700                Arc::new(host.finish()),
1701                Arc::new(le.finish()),
1702                Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0])),
1703            ],
1704        )
1705        .unwrap();
1706
1707        let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
1708        let result =
1709            datafusion::physical_plan::collect(fold_exec, SessionContext::default().task_ctx())
1710                .await
1711                .unwrap();
1712
1713        assert_eq!(result.len(), 1);
1714        assert_eq!(result[0].num_rows(), 1);
1715        let host = result[0]
1716            .column(0)
1717            .as_any()
1718            .downcast_ref::<DictionaryArray<UInt32Type>>()
1719            .unwrap();
1720        assert_eq!(host.values().len(), 1);
1721        assert_eq!(
1722            string_array_value_at_index(result[0].column(0), 0),
1723            Some("host_1")
1724        );
1725        let value = result[0].column(1).as_primitive::<Float64Type>().value(0);
1726        assert!((value - 0.55).abs() < 1e-12);
1727    }
1728
1729    #[tokio::test]
1730    async fn pruning_should_keep_le_column_for_exec() {
1731        let schema = Arc::new(Schema::new(vec![
1732            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
1733            Field::new("le", DataType::Utf8, true),
1734            Field::new("val", DataType::Float64, true),
1735        ]));
1736        let df_schema = schema.clone().to_dfschema_ref().unwrap();
1737        let input = LogicalPlan::EmptyRelation(EmptyRelation {
1738            produce_one_row: false,
1739            schema: df_schema,
1740        });
1741        let plan = HistogramFold::new(
1742            "le".to_string(),
1743            "val".to_string(),
1744            "ts".to_string(),
1745            0.5,
1746            input,
1747        )
1748        .unwrap();
1749
1750        let output_columns = [0usize, 1usize];
1751        let required = plan.necessary_children_exprs(&output_columns).unwrap();
1752        let required = &required[0];
1753        assert_eq!(required.as_slice(), &[0, 1, 2]);
1754
1755        let input_batch = RecordBatch::try_new(
1756            schema,
1757            vec![
1758                Arc::new(TimestampMillisecondArray::from(vec![0, 0])),
1759                Arc::new(StringArray::from(vec!["0.1", "+Inf"])),
1760                Arc::new(Float64Array::from(vec![1.0, 2.0])),
1761            ],
1762        )
1763        .unwrap();
1764        let projected = project_batch(&input_batch, required);
1765        let projected_schema = projected.schema();
1766        let memory_exec = Arc::new(DataSourceExec::new(Arc::new(
1767            MemorySourceConfig::try_new(&[vec![projected]], projected_schema, None).unwrap(),
1768        )));
1769
1770        let fold_exec = plan.to_execution_plan(memory_exec);
1771        let session_context = SessionContext::default();
1772        let output_batches =
1773            datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
1774                .await
1775                .unwrap();
1776        assert_eq!(output_batches.len(), 1);
1777
1778        let output_batch = &output_batches[0];
1779        assert_eq!(output_batch.num_rows(), 1);
1780
1781        let ts = output_batch
1782            .column(0)
1783            .as_any()
1784            .downcast_ref::<TimestampMillisecondArray>()
1785            .unwrap();
1786        assert_eq!(ts.values(), &[0i64]);
1787
1788        let values = output_batch
1789            .column(1)
1790            .as_any()
1791            .downcast_ref::<Float64Array>()
1792            .unwrap();
1793        assert!((values.value(0) - 0.1).abs() < 1e-12);
1794
1795        // Simulate the pre-fix pruning behavior: omit the `le` column from the child input.
1796        let le_index = 1usize;
1797        let broken_required = output_columns
1798            .iter()
1799            .map(|&output_column| {
1800                if output_column < le_index {
1801                    output_column
1802                } else {
1803                    output_column + 1
1804                }
1805            })
1806            .collect::<Vec<_>>();
1807
1808        let broken = project_batch(&input_batch, &broken_required);
1809        let broken_schema = broken.schema();
1810        let broken_exec = Arc::new(DataSourceExec::new(Arc::new(
1811            MemorySourceConfig::try_new(&[vec![broken]], broken_schema, None).unwrap(),
1812        )));
1813        let broken_fold_exec = plan.to_execution_plan(broken_exec);
1814        let session_context = SessionContext::default();
1815        let broken_result = std::panic::AssertUnwindSafe(async {
1816            datafusion::physical_plan::collect(broken_fold_exec, session_context.task_ctx()).await
1817        })
1818        .catch_unwind()
1819        .await;
1820        assert!(broken_result.is_err());
1821    }
1822
1823    #[test]
1824    fn fold_does_not_claim_hash_partitioning() {
1825        let input: Arc<dyn ExecutionPlan> = Arc::new(prepare_test_data_with_two_partitions());
1826        let logical_input = LogicalPlan::EmptyRelation(EmptyRelation {
1827            produce_one_row: false,
1828            schema: Arc::new(input.schema().to_dfschema().unwrap()),
1829        });
1830        let logical_fold = HistogramFold::new(
1831            "le".to_string(),
1832            "val".to_string(),
1833            "host".to_string(),
1834            0.5,
1835            logical_input,
1836        )
1837        .unwrap();
1838        let constructed = logical_fold.to_execution_plan(input.clone());
1839        assert!(matches!(
1840            constructed.output_partitioning(),
1841            Partitioning::UnknownPartitioning(2)
1842        ));
1843
1844        let rebuilt = constructed
1845            .replace_children(
1846                vec![input],
1847                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1848            )
1849            .unwrap();
1850        assert!(matches!(
1851            rebuilt.output_partitioning(),
1852            Partitioning::UnknownPartitioning(2)
1853        ));
1854    }
1855
1856    #[test]
1857    fn confirm_schema() {
1858        let input_schema = Schema::new(vec![
1859            Field::new("host", DataType::Utf8, true),
1860            Field::new("le", DataType::Utf8, true),
1861            Field::new("val", DataType::Float64, true),
1862        ])
1863        .to_dfschema_ref()
1864        .unwrap();
1865        let expected_output_schema = Schema::new(vec![
1866            Field::new("host", DataType::Utf8, true),
1867            Field::new("val", DataType::Float64, true),
1868        ])
1869        .to_dfschema_ref()
1870        .unwrap();
1871
1872        let actual = HistogramFold::convert_schema(&input_schema, "le").unwrap();
1873        assert_eq!(actual, expected_output_schema)
1874    }
1875
1876    #[tokio::test]
1877    async fn fallback_to_safe_mode_on_missing_inf() {
1878        let schema = Arc::new(Schema::new(vec![
1879            Field::new("host", DataType::Utf8, true),
1880            Field::new("le", DataType::Utf8, true),
1881            Field::new("val", DataType::Float64, true),
1882        ]));
1883        let host_column = Arc::new(StringArray::from(vec!["a", "a", "b", "b", "c", "c"])) as _;
1884        let le_column = Arc::new(StringArray::from(vec![
1885            "0.1", "+Inf", "0.1", "1.0", "0.1", "+Inf",
1886        ])) as _;
1887        let val_column = Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 3.0, 1.0, 5.0])) as _;
1888        let batch =
1889            RecordBatch::try_new(schema.clone(), vec![host_column, le_column, val_column]).unwrap();
1890        let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
1891        let session_context = SessionContext::default();
1892        let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
1893            .await
1894            .unwrap();
1895        let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1896            .unwrap()
1897            .to_string();
1898
1899        let expected = String::from(
1900            "+------+-----+
1901| host | val |
1902+------+-----+
1903| a    | 0.1 |
1904| b    | NaN |
1905| c    | 0.1 |
1906+------+-----+",
1907        );
1908        assert_eq!(result_literal, expected);
1909    }
1910
1911    #[tokio::test]
1912    async fn emit_nan_when_no_inf_present() {
1913        let schema = Arc::new(Schema::new(vec![
1914            Field::new("host", DataType::Utf8, true),
1915            Field::new("le", DataType::Utf8, true),
1916            Field::new("val", DataType::Float64, true),
1917        ]));
1918        let host_column = Arc::new(StringArray::from(vec!["c", "c"])) as _;
1919        let le_column = Arc::new(StringArray::from(vec!["0.1", "1.0"])) as _;
1920        let val_column = Arc::new(Float64Array::from(vec![1.0, 2.0])) as _;
1921        let batch =
1922            RecordBatch::try_new(schema.clone(), vec![host_column, le_column, val_column]).unwrap();
1923        let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.9, 0);
1924        let session_context = SessionContext::default();
1925        let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
1926            .await
1927            .unwrap();
1928        let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1929            .unwrap()
1930            .to_string();
1931
1932        let expected = String::from(
1933            "+------+-----+
1934| host | val |
1935+------+-----+
1936| c    | NaN |
1937+------+-----+",
1938        );
1939        assert_eq!(result_literal, expected);
1940    }
1941
1942    #[tokio::test]
1943    async fn ignore_unparsable_bucket_bounds() {
1944        let schema = Arc::new(Schema::new(vec![
1945            Field::new("host", DataType::Utf8, true),
1946            Field::new("le", DataType::Utf8, true),
1947            Field::new("val", DataType::Float64, true),
1948        ]));
1949        let batch = RecordBatch::try_new(
1950            schema.clone(),
1951            vec![
1952                Arc::new(StringArray::from(vec!["a", "a", "a", "b"])),
1953                Arc::new(StringArray::from(vec![
1954                    Some("bad"),
1955                    Some("1"),
1956                    Some("+Inf"),
1957                    None,
1958                ])),
1959                Arc::new(Float64Array::from(vec![1.0, 2.0, 4.0, 1.0])),
1960            ],
1961        )
1962        .unwrap();
1963        let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
1964
1965        let result =
1966            datafusion::physical_plan::collect(fold_exec, SessionContext::default().task_ctx())
1967                .await
1968                .unwrap();
1969
1970        assert_eq!(result.len(), 1);
1971        assert_eq!(result[0].num_rows(), 1);
1972        assert_eq!(
1973            string_array_value_at_index(result[0].column(0), 0),
1974            Some("a")
1975        );
1976        assert_eq!(
1977            result[0].column(1).as_primitive::<Float64Type>().value(0),
1978            1.0
1979        );
1980    }
1981
1982    #[tokio::test]
1983    async fn trailing_invalid_bounds_fall_back_before_eof() {
1984        let schema = Arc::new(Schema::new(vec![
1985            Field::new("host", DataType::Utf8, false),
1986            Field::new("le", DataType::Utf8, true),
1987            Field::new("val", DataType::Float64, false),
1988        ]));
1989        for trailing_bound in [Some("bad"), None] {
1990            let batch = RecordBatch::try_new(
1991                schema.clone(),
1992                vec![
1993                    Arc::new(StringArray::from(vec![
1994                        "a", "a", "a", "b", "b", "b", "c", "c", "c",
1995                    ])),
1996                    Arc::new(StringArray::from(
1997                        [Some("1"), Some("+Inf"), trailing_bound].repeat(3),
1998                    )),
1999                    Arc::new(Float64Array::from([2.0, 4.0, 99.0].repeat(3))),
2000                ],
2001            )
2002            .unwrap();
2003            let fold = build_fold_exec_from_batches(vec![batch.clone()], schema.clone(), 0.5, 0);
2004            let mut stream = HistogramFoldStream {
2005                le_column_index: 1,
2006                field_column_index: 2,
2007                histogram_column_index: None,
2008                operation: fold.operation,
2009                normal_indices: vec![0],
2010                bucket_size: None,
2011                batch_size: 1,
2012                output_schema: fold.output_schema.clone(),
2013                input_schema: schema.clone(),
2014                mode: FoldMode::Optimistic,
2015                safe_group: None,
2016                input_buffer: vec![],
2017                input_buffered_rows: 0,
2018                output_buffer: HistogramFoldStream::empty_output_buffer(&schema).unwrap(),
2019                output_buffered_rows: 0,
2020                input: fold
2021                    .input
2022                    .execute(0, SessionContext::default().task_ctx())
2023                    .unwrap(),
2024                metric: BaselineMetrics::new(&fold.metric, 0),
2025            };
2026
2027            // Neither +Inf nor a batch boundary alone completes the group.
2028            assert!(stream.fold_input(batch.slice(0, 2)).unwrap().is_none());
2029            assert!(stream.fold_input(batch.slice(2, 1)).unwrap().is_none());
2030            assert_eq!(stream.mode, FoldMode::Optimistic);
2031
2032            for (offset, host) in [(3, "a"), (6, "b")] {
2033                let output = stream
2034                    .fold_input(batch.slice(offset, 3))
2035                    .unwrap()
2036                    .expect("a completed group should produce output before EOF")
2037                    .unwrap();
2038                assert_eq!(stream.mode, FoldMode::Safe);
2039                assert_eq!(stream.input_buffered_rows, 0);
2040                assert!(stream.input_buffer.is_empty());
2041                assert_eq!(output.num_rows(), 1);
2042                assert_eq!(string_array_value_at_index(output.column(0), 0), Some(host));
2043                assert_eq!(output.column(1).as_primitive::<Float64Type>().value(0), 1.0);
2044            }
2045
2046            stream.flush_remaining().unwrap();
2047            let output = stream.take_output_buf().unwrap().unwrap();
2048            assert_eq!(output.num_rows(), 1);
2049            assert_eq!(string_array_value_at_index(output.column(0), 0), Some("c"));
2050            assert_eq!(output.column(1).as_primitive::<Float64Type>().value(0), 1.0);
2051            assert!(stream.take_output_buf().unwrap().is_none());
2052        }
2053    }
2054
2055    #[tokio::test]
2056    async fn safe_mode_handles_misaligned_groups() {
2057        let schema = Arc::new(Schema::new(vec![
2058            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
2059            Field::new("le", DataType::Utf8, true),
2060            Field::new("val", DataType::Float64, true),
2061        ]));
2062
2063        let ts_column = Arc::new(TimestampMillisecondArray::from(vec![
2064            2900000, 2900000, 2900000, 3000000, 3000000, 3000000, 3000000, 3005000, 3005000,
2065            3010000, 3010000, 3010000, 3010000, 3010000,
2066        ])) as _;
2067        let le_column = Arc::new(StringArray::from(vec![
2068            "0.1", "1", "5", "0.1", "1", "5", "+Inf", "0.1", "+Inf", "0.1", "1", "3", "5", "+Inf",
2069        ])) as _;
2070        let val_column = Arc::new(Float64Array::from(vec![
2071            0.0, 0.0, 0.0, 50.0, 70.0, 110.0, 120.0, 10.0, 30.0, 10.0, 20.0, 30.0, 40.0, 50.0,
2072        ])) as _;
2073        let batch =
2074            RecordBatch::try_new(schema.clone(), vec![ts_column, le_column, val_column]).unwrap();
2075        let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
2076        let session_context = SessionContext::default();
2077        let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
2078            .await
2079            .unwrap();
2080
2081        let mut values = Vec::new();
2082        for batch in result {
2083            let array = batch.column(1).as_primitive::<Float64Type>();
2084            values.extend(array.iter().map(|v| v.unwrap()));
2085        }
2086
2087        assert_eq!(values.len(), 4);
2088        assert!(values[0].is_nan());
2089        assert!((values[1] - 0.55).abs() < 1e-10);
2090        assert!((values[2] - 0.1).abs() < 1e-10);
2091        assert!((values[3] - 2.0).abs() < 1e-10);
2092    }
2093
2094    #[tokio::test]
2095    async fn missing_buckets_at_first_timestamp() {
2096        let schema = Arc::new(Schema::new(vec![
2097            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
2098            Field::new("le", DataType::Utf8, true),
2099            Field::new("val", DataType::Float64, true),
2100        ]));
2101
2102        let ts_column = Arc::new(TimestampMillisecondArray::from(vec![
2103            2_900_000, 3_000_000, 3_000_000, 3_000_000, 3_000_000, 3_005_000, 3_005_000, 3_010_000,
2104            3_010_000, 3_010_000, 3_010_000, 3_010_000,
2105        ])) as _;
2106        let le_column = Arc::new(StringArray::from(vec![
2107            "0.1", "0.1", "1", "5", "+Inf", "0.1", "+Inf", "0.1", "1", "3", "5", "+Inf",
2108        ])) as _;
2109        let val_column = Arc::new(Float64Array::from(vec![
2110            0.0, 50.0, 70.0, 110.0, 120.0, 10.0, 30.0, 10.0, 20.0, 30.0, 40.0, 50.0,
2111        ])) as _;
2112
2113        let batch =
2114            RecordBatch::try_new(schema.clone(), vec![ts_column, le_column, val_column]).unwrap();
2115        let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
2116        let session_context = SessionContext::default();
2117        let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
2118            .await
2119            .unwrap();
2120
2121        let mut values = Vec::new();
2122        for batch in result {
2123            let array = batch.column(1).as_primitive::<Float64Type>();
2124            values.extend(array.iter().map(|v| v.unwrap()));
2125        }
2126
2127        assert_eq!(values.len(), 4);
2128        assert!(values[0].is_nan());
2129        assert!((values[1] - 0.55).abs() < 1e-10);
2130        assert!((values[2] - 0.1).abs() < 1e-10);
2131        assert!((values[3] - 2.0).abs() < 1e-10);
2132    }
2133
2134    #[tokio::test]
2135    async fn missing_inf_in_first_group() {
2136        let schema = Arc::new(Schema::new(vec![
2137            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
2138            Field::new("le", DataType::Utf8, true),
2139            Field::new("val", DataType::Float64, true),
2140        ]));
2141
2142        let ts_column = Arc::new(TimestampMillisecondArray::from(vec![
2143            1000, 1000, 1000, 2000, 2000, 2000, 2000,
2144        ])) as _;
2145        let le_column = Arc::new(StringArray::from(vec![
2146            "0.1", "1", "5", "0.1", "1", "5", "+Inf",
2147        ])) as _;
2148        let val_column = Arc::new(Float64Array::from(vec![
2149            0.0, 0.0, 0.0, 10.0, 20.0, 30.0, 30.0,
2150        ])) as _;
2151        let batch =
2152            RecordBatch::try_new(schema.clone(), vec![ts_column, le_column, val_column]).unwrap();
2153        let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
2154        let session_context = SessionContext::default();
2155        let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
2156            .await
2157            .unwrap();
2158
2159        let mut values = Vec::new();
2160        for batch in result {
2161            let array = batch.column(1).as_primitive::<Float64Type>();
2162            values.extend(array.iter().map(|v| v.unwrap()));
2163        }
2164
2165        assert_eq!(values.len(), 2);
2166        assert!(values[0].is_nan());
2167        assert!((values[1] - 0.55).abs() < 1e-10, "{values:?}");
2168    }
2169
2170    #[test]
2171    fn evaluate_quantile_duplicate_bounds() {
2172        let cases = [
2173            // Issue #9443: coalesce before repairing the count at 1000.
2174            (
2175                vec![10.0, 10.0, 100.0, 100.0, 1000.0, f64::INFINITY],
2176                vec![40.0, 40.0, 50.0, 50.0, 50.0, 100.0],
2177                0.95,
2178                77.5,
2179            ),
2180            // Repairing the split counts first would incorrectly inflate them.
2181            (
2182                vec![10.0, 10.0, 100.0, 100.0, f64::INFINITY],
2183                vec![30.0, 10.0, 40.0, 20.0, 60.0],
2184                0.5,
2185                7.5,
2186            ),
2187            (
2188                vec![10.0, f64::INFINITY, f64::INFINITY],
2189                vec![40.0, 20.0, 30.0],
2190                0.5,
2191                6.25,
2192            ),
2193            (
2194                vec![f64::INFINITY, f64::INFINITY],
2195                vec![20.0, 30.0],
2196                0.5,
2197                f64::NAN,
2198            ),
2199            (
2200                vec![-0.0, 0.0, 10.0, f64::INFINITY],
2201                vec![10.0, 10.0, 40.0, 40.0],
2202                0.75,
2203                5.0,
2204            ),
2205        ];
2206        for (bounds, counts, quantile, expected) in cases {
2207            let actual = HistogramFoldStream::evaluate_row(
2208                HistogramFoldOperation::Quantile(quantile.into()),
2209                &bounds,
2210                &counts,
2211            )
2212            .unwrap();
2213            if expected.is_nan() {
2214                assert!(actual.is_nan());
2215            } else {
2216                assert!((actual - expected).abs() < 1e-10, "{actual} != {expected}");
2217            }
2218        }
2219    }
2220
2221    #[tokio::test]
2222    async fn duplicate_infinity_bounds_across_batches() {
2223        let schema = Arc::new(Schema::new(vec![
2224            Field::new(
2225                "ts",
2226                DataType::Timestamp(TimeUnit::Millisecond, None),
2227                false,
2228            ),
2229            Field::new("le", DataType::Utf8, false),
2230            Field::new("val", DataType::Float64, false),
2231        ]));
2232        let duplicate = [("10", 20.0), ("10.0", 20.0), ("+Inf", 25.0), ("+inf", 25.0)];
2233        let regular = [("10", 20.0), ("10.0", 20.0), ("+Inf", 50.0)];
2234        // Cover discovery, an already established optimistic bucket count,
2235        // fallback, and the final group at EOF.
2236        for groups in [
2237            vec![duplicate.as_slice()],
2238            vec![duplicate.as_slice(), regular.as_slice()],
2239            vec![regular.as_slice(), duplicate.as_slice(), regular.as_slice()],
2240        ] {
2241            let mut timestamps = Vec::new();
2242            let mut bounds = Vec::new();
2243            let mut counts = Vec::new();
2244            for (timestamp, group) in groups.iter().enumerate() {
2245                for &(bound, count) in *group {
2246                    timestamps.push(timestamp as i64);
2247                    bounds.push(bound);
2248                    counts.push(count);
2249                }
2250            }
2251            let batch = RecordBatch::try_new(
2252                schema.clone(),
2253                vec![
2254                    Arc::new(TimestampMillisecondArray::from(timestamps)),
2255                    Arc::new(StringArray::from(bounds)),
2256                    Arc::new(Float64Array::from(counts)),
2257                ],
2258            )
2259            .unwrap();
2260            for batch_size in 1..=batch.num_rows() {
2261                let batches = (0..batch.num_rows())
2262                    .step_by(batch_size)
2263                    .map(|offset| batch.slice(offset, batch_size.min(batch.num_rows() - offset)))
2264                    .collect();
2265                let fold = build_fold_exec_from_batches(batches, schema.clone(), 0.5, 0);
2266                let result =
2267                    datafusion::physical_plan::collect(fold, SessionContext::default().task_ctx())
2268                        .await
2269                        .unwrap();
2270                let values: Vec<_> = result
2271                    .iter()
2272                    .flat_map(|batch| {
2273                        batch
2274                            .column(1)
2275                            .as_primitive::<Float64Type>()
2276                            .values()
2277                            .iter()
2278                            .copied()
2279                    })
2280                    .collect();
2281                assert_eq!(values, vec![6.25; groups.len()], "batch size {batch_size}");
2282            }
2283        }
2284    }
2285
2286    #[tokio::test]
2287    async fn duplicate_bounds_across_batches_and_safe_fallback() {
2288        let schema = Arc::new(Schema::new(vec![
2289            Field::new(
2290                "ts",
2291                DataType::Timestamp(TimeUnit::Millisecond, None),
2292                false,
2293            ),
2294            Field::new("le", DataType::Utf8, false),
2295            Field::new("val", DataType::Float64, false),
2296        ]));
2297        // The first group establishes six buckets for optimistic folding. The
2298        // shorter second group forces safe mode; both have duplicate bounds.
2299        let batch = RecordBatch::try_new(
2300            schema.clone(),
2301            vec![
2302                Arc::new(TimestampMillisecondArray::from(vec![
2303                    1000, 1000, 1000, 1000, 1000, 1000, 2000, 2000, 2000, 2000, 2000,
2304                ])),
2305                Arc::new(StringArray::from(vec![
2306                    "10", "10.0", "100", "100.0", "1000", "+Inf", "10", "10.0", "100", "100.0",
2307                    "+Inf",
2308                ])),
2309                Arc::new(Float64Array::from(vec![
2310                    40.0, 40.0, 50.0, 50.0, 50.0, 100.0, 40.0, 40.0, 50.0, 50.0, 100.0,
2311                ])),
2312            ],
2313        )
2314        .unwrap();
2315        // Split each pair of equal boundaries across an input batch boundary.
2316        let batches = (0..batch.num_rows()).map(|i| batch.slice(i, 1)).collect();
2317        let fold = build_fold_exec_from_batches(batches, schema, 0.95, 0);
2318        let result = datafusion::physical_plan::collect(fold, SessionContext::default().task_ctx())
2319            .await
2320            .unwrap();
2321        let values: Vec<_> = result
2322            .iter()
2323            .flat_map(|batch| {
2324                batch
2325                    .column(1)
2326                    .as_primitive::<Float64Type>()
2327                    .values()
2328                    .iter()
2329                    .copied()
2330            })
2331            .collect();
2332        assert_eq!(values, vec![77.5, 77.5]);
2333    }
2334
2335    #[test]
2336    fn evaluate_row_normal_case() {
2337        let bucket = [0.0, 1.0, 2.0, 3.0, 4.0, f64::INFINITY];
2338
2339        #[derive(Debug)]
2340        struct Case {
2341            quantile: f64,
2342            counters: Vec<f64>,
2343            expected: f64,
2344        }
2345
2346        let cases = [
2347            Case {
2348                quantile: 0.9,
2349                counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2350                expected: 4.0,
2351            },
2352            Case {
2353                quantile: 0.89,
2354                counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2355                expected: 4.0,
2356            },
2357            Case {
2358                quantile: 0.78,
2359                counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2360                expected: 3.9,
2361            },
2362            Case {
2363                quantile: 0.5,
2364                counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2365                expected: 2.5,
2366            },
2367            Case {
2368                quantile: 0.5,
2369                counters: vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
2370                expected: f64::NAN,
2371            },
2372            Case {
2373                quantile: 1.0,
2374                counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2375                expected: 4.0,
2376            },
2377            Case {
2378                quantile: 0.0,
2379                counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2380                expected: f64::NAN,
2381            },
2382            Case {
2383                quantile: 1.1,
2384                counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2385                expected: f64::INFINITY,
2386            },
2387            Case {
2388                quantile: -1.0,
2389                counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2390                expected: f64::NEG_INFINITY,
2391            },
2392        ];
2393
2394        for case in cases {
2395            let actual = HistogramFoldStream::evaluate_row(
2396                HistogramFoldOperation::Quantile(case.quantile.into()),
2397                &bucket,
2398                &case.counters,
2399            )
2400            .unwrap();
2401            assert_eq!(
2402                format!("{actual}"),
2403                format!("{}", case.expected),
2404                "{:?}",
2405                case
2406            );
2407        }
2408    }
2409
2410    #[test]
2411    fn evaluate_out_of_order_input() {
2412        let bucket = [0.0, 1.0, 2.0, 3.0, 4.0, f64::INFINITY];
2413        let counters = [5.0, 4.0, 3.0, 2.0, 1.0, 0.0];
2414        let result = HistogramFoldStream::evaluate_row(
2415            HistogramFoldOperation::Quantile(0.5.into()),
2416            &bucket,
2417            &counters,
2418        )
2419        .unwrap();
2420        assert_eq!(0.0, result);
2421    }
2422
2423    #[test]
2424    fn evaluate_wrong_bucket() {
2425        let bucket = [0.0, 1.0, 2.0, 3.0, 4.0, f64::INFINITY, 5.0];
2426        let counters = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
2427        let result = HistogramFoldStream::evaluate_row(
2428            HistogramFoldOperation::Quantile(0.5.into()),
2429            &bucket,
2430            &counters,
2431        );
2432        assert!(result.is_err());
2433    }
2434
2435    #[test]
2436    fn evaluate_small_fraction() {
2437        let bucket = [0.0, 2.0, 4.0, 6.0, f64::INFINITY];
2438        let counters = [0.0, 1.0 / 300.0, 2.0 / 300.0, 0.01, 0.01];
2439        let result = HistogramFoldStream::evaluate_row(
2440            HistogramFoldOperation::Quantile(0.5.into()),
2441            &bucket,
2442            &counters,
2443        )
2444        .unwrap();
2445        assert_eq!(3.0, result);
2446    }
2447
2448    #[test]
2449    fn evaluate_non_monotonic_counter() {
2450        let bucket = [0.0, 1.0, 2.0, 3.0, f64::INFINITY];
2451        let counters = [0.1, 0.2, 0.4, 0.17, 0.5];
2452        let result = HistogramFoldStream::evaluate_row(
2453            HistogramFoldOperation::Quantile(0.5.into()),
2454            &bucket,
2455            &counters,
2456        )
2457        .unwrap();
2458        assert!((result - 1.25).abs() < 1e-10, "{result}");
2459    }
2460
2461    #[test]
2462    fn evaluate_nan_counter() {
2463        let bucket = [0.0, 1.0, 2.0, 3.0, f64::INFINITY];
2464        let counters = [f64::NAN, 1.0, 2.0, 3.0, 3.0];
2465        let result = HistogramFoldStream::evaluate_row(
2466            HistogramFoldOperation::Quantile(0.5.into()),
2467            &bucket,
2468            &counters,
2469        )
2470        .unwrap();
2471        assert!((result - 1.5).abs() < 1e-10, "{result}");
2472    }
2473
2474    #[test]
2475    fn evaluate_classic_histogram_fraction() {
2476        let buckets = [1.0, 2.0, f64::INFINITY];
2477        let counters = [2.0, 4.0, 4.0];
2478        let fraction = |lower, upper| {
2479            HistogramFoldStream::evaluate_row(
2480                HistogramFoldOperation::Fraction {
2481                    lower: OrderedF64::from(lower),
2482                    upper: OrderedF64::from(upper),
2483                },
2484                &buckets,
2485                &counters,
2486            )
2487            .unwrap()
2488        };
2489
2490        assert_eq!(fraction(0.0, 1.0), 0.5);
2491        assert_eq!(fraction(f64::NEG_INFINITY, f64::INFINITY), 1.0);
2492        assert_eq!(fraction(2.0, 1.0), 0.0);
2493
2494        assert_eq!(
2495            HistogramFoldStream::evaluate_row(
2496                HistogramFoldOperation::Fraction {
2497                    lower: 0.0.into(),
2498                    upper: 1.0.into(),
2499                },
2500                &[1.0, 1.0, f64::INFINITY],
2501                &[1.0, 2.0, 4.0],
2502            )
2503            .unwrap(),
2504            0.75
2505        );
2506
2507        assert_eq!(
2508            HistogramFoldStream::evaluate_row(
2509                HistogramFoldOperation::Fraction {
2510                    lower: f64::NEG_INFINITY.into(),
2511                    upper: f64::INFINITY.into(),
2512                },
2513                &[f64::INFINITY],
2514                &[4.0],
2515            )
2516            .unwrap(),
2517            1.0
2518        );
2519    }
2520
2521    #[tokio::test]
2522    async fn fraction_handles_single_inf_bucket_after_safe_fallback() {
2523        let schema = Arc::new(Schema::new(vec![
2524            Field::new("host", DataType::Utf8, false),
2525            Field::new("le", DataType::Utf8, false),
2526            Field::new("val", DataType::Float64, false),
2527        ]));
2528        let batch = RecordBatch::try_new(
2529            schema.clone(),
2530            vec![
2531                Arc::new(StringArray::from(vec!["a", "a", "b"])),
2532                Arc::new(StringArray::from(vec!["1", "+Inf", "+Inf"])),
2533                Arc::new(Float64Array::from(vec![2.0, 4.0, 4.0])),
2534            ],
2535        )
2536        .unwrap();
2537        let fold = build_fold_exec_from_batches_with_operation(
2538            vec![batch],
2539            schema,
2540            HistogramFoldOperation::Fraction {
2541                lower: f64::NEG_INFINITY.into(),
2542                upper: f64::INFINITY.into(),
2543            },
2544            0,
2545        );
2546
2547        let batches =
2548            datafusion::physical_plan::collect(fold, SessionContext::default().task_ctx())
2549                .await
2550                .unwrap();
2551        let values = batches[0].column(1).as_primitive::<Float64Type>();
2552        assert_eq!(values.values(), &[1.0, 1.0]);
2553    }
2554
2555    fn build_empty_relation(schema: &Arc<Schema>) -> LogicalPlan {
2556        LogicalPlan::EmptyRelation(EmptyRelation {
2557            produce_one_row: false,
2558            schema: schema.clone().to_dfschema_ref().unwrap(),
2559        })
2560    }
2561
2562    #[tokio::test]
2563    async fn encode_decode_histogram_fold() {
2564        let schema = Arc::new(Schema::new(vec![
2565            Field::new("ts", DataType::Int64, false),
2566            Field::new("le", DataType::Utf8, false),
2567            Field::new("val", DataType::Float64, false),
2568        ]));
2569        let input_plan = build_empty_relation(&schema);
2570        let plan_node = HistogramFold::new(
2571            "le".to_string(),
2572            "val".to_string(),
2573            "ts".to_string(),
2574            0.8,
2575            input_plan.clone(),
2576        )
2577        .unwrap();
2578        let fraction_node = HistogramFold::new_with_operation(
2579            "le".to_string(),
2580            "val".to_string(),
2581            "ts".to_string(),
2582            HistogramFoldOperation::Fraction {
2583                lower: 0.0.into(),
2584                upper: 1.0.into(),
2585            },
2586            None,
2587            input_plan.clone(),
2588        )
2589        .unwrap();
2590        assert!(fraction_node.serialize().is_err());
2591
2592        let bytes = plan_node.serialize().unwrap();
2593
2594        let histogram_fold = HistogramFold::deserialize(&bytes).unwrap();
2595        // need fix
2596        let histogram_fold = histogram_fold
2597            .with_exprs_and_inputs(vec![], vec![input_plan])
2598            .unwrap();
2599
2600        assert_eq!(histogram_fold.le_column, "le");
2601        assert_eq!(histogram_fold.ts_column, "ts");
2602        assert_eq!(histogram_fold.field_column, "val");
2603        assert_eq!(
2604            histogram_fold.operation,
2605            HistogramFoldOperation::Quantile(OrderedF64::from(0.8))
2606        );
2607        assert_eq!(histogram_fold.output_schema.fields().len(), 2);
2608        assert_eq!(histogram_fold.output_schema.field(0).name(), "ts");
2609        assert_eq!(histogram_fold.output_schema.field(1).name(), "val");
2610    }
2611}