Skip to main content

promql/functions/
native_histogram.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Native histogram PromQL helpers.
16
17use std::any::Any;
18use std::hash::{Hash, Hasher};
19use std::mem::size_of;
20use std::sync::Arc;
21
22use common_query::native_histogram::*;
23use common_query::prometheus::format_prometheus_float;
24use common_query::promql_annotations::PromqlAnnotationCollector;
25use datafusion::arrow::array::{
26    Array, ArrayRef, BooleanArray, Float64Array, Float64Builder, Int64Array, StringBuilder,
27    StructArray, TimestampMillisecondArray, UInt64Array,
28};
29use datafusion::arrow::compute::filter;
30use datafusion::arrow::datatypes::{DataType, Field, TimeUnit};
31use datafusion::common::{DataFusionError, Result as DfResult};
32use datafusion::logical_expr::{Accumulator as DfAccumulator, AggregateUDF, ScalarUDF, Volatility};
33use datafusion::physical_plan::ColumnarValue;
34use datafusion_common::ScalarValue;
35use datafusion_expr::function::AccumulatorArgs;
36use datafusion_expr::{ScalarFunctionArgs, ScalarUDFImpl, Signature, create_udaf, create_udf};
37
38use crate::functions::{
39    AvgOverTime, Deriv, DoubleExponentialSmoothing, IDelta, Increase, LastOverTime, MaxOverTime,
40    MinOverTime, PredictLinear, QuantileOverTime, Rate, StddevOverTime, StdvarOverTime,
41    SumOverTime, extract_array, extract_range_dict,
42};
43use crate::range_array::{RangeArray, unpack};
44
45fn extract_histogram_array(value: &ColumnarValue, func_name: &str) -> DfResult<ArrayRef> {
46    let array = extract_array(value)?;
47    if array.data_type() != &native_histogram_arrow_type() {
48        return Err(DataFusionError::Execution(format!(
49            "{func_name}: expected native histogram struct, found {}",
50            array.data_type()
51        )));
52    }
53    Ok(array)
54}
55
56fn read_scalar_f64_arg(
57    value: &ColumnarValue,
58    row: usize,
59    len: usize,
60    func_name: &str,
61) -> DfResult<f64> {
62    match value {
63        ColumnarValue::Scalar(ScalarValue::Float64(value)) => Ok(value.unwrap_or(f64::NAN)),
64        ColumnarValue::Array(array) => {
65            let array = array
66                .as_any()
67                .downcast_ref::<Float64Array>()
68                .ok_or_else(|| {
69                    DataFusionError::Execution(format!(
70                        "{func_name}: expected Float64 argument, found {}",
71                        array.data_type()
72                    ))
73                })?;
74            if array.len() != len {
75                return Err(DataFusionError::Execution(format!(
76                    "{func_name}: Float64 argument length mismatch: {} vs {len}",
77                    array.len()
78                )));
79            }
80            Ok(if array.is_null(row) {
81                f64::NAN
82            } else {
83                array.value(row)
84            })
85        }
86        other => Err(DataFusionError::Execution(format!(
87            "{func_name}: expected Float64 argument, found {}",
88            other.data_type()
89        ))),
90    }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94enum AnnotationReturn {
95    FloatNull,
96    BooleanTrue,
97    BooleanFalse,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101enum AnnotationLevel {
102    Info,
103    Warning,
104}
105
106impl AnnotationReturn {
107    fn data_type(self) -> DataType {
108        match self {
109            Self::FloatNull => DataType::Float64,
110            Self::BooleanTrue | Self::BooleanFalse => DataType::Boolean,
111        }
112    }
113
114    fn scalar_value(self) -> ScalarValue {
115        match self {
116            Self::FloatNull => ScalarValue::Float64(None),
117            Self::BooleanTrue => ScalarValue::Boolean(Some(true)),
118            Self::BooleanFalse => ScalarValue::Boolean(Some(false)),
119        }
120    }
121}
122
123#[derive(Debug, Clone)]
124struct NativeHistogramAnnotationUdf {
125    name: &'static str,
126    signature: Signature,
127    return_kind: AnnotationReturn,
128    level: AnnotationLevel,
129    message: String,
130    collector: Option<PromqlAnnotationCollector>,
131}
132
133impl NativeHistogramAnnotationUdf {
134    fn new(
135        name: &'static str,
136        return_kind: AnnotationReturn,
137        level: AnnotationLevel,
138        message: String,
139        collector: Option<PromqlAnnotationCollector>,
140    ) -> Self {
141        Self {
142            name,
143            signature: Signature::variadic_any(Volatility::Volatile),
144            return_kind,
145            level,
146            message,
147            collector,
148        }
149    }
150}
151
152impl PartialEq for NativeHistogramAnnotationUdf {
153    fn eq(&self, other: &Self) -> bool {
154        self.name == other.name
155            && self.return_kind == other.return_kind
156            && self.level == other.level
157            && self.message == other.message
158    }
159}
160
161impl Eq for NativeHistogramAnnotationUdf {}
162
163impl Hash for NativeHistogramAnnotationUdf {
164    fn hash<H: Hasher>(&self, state: &mut H) {
165        self.name.hash(state);
166        self.return_kind.hash(state);
167        self.level.hash(state);
168        self.message.hash(state);
169    }
170}
171
172impl ScalarUDFImpl for NativeHistogramAnnotationUdf {
173    fn as_any(&self) -> &dyn Any {
174        self
175    }
176
177    fn name(&self) -> &str {
178        self.name
179    }
180
181    fn signature(&self) -> &Signature {
182        &self.signature
183    }
184
185    fn return_type(&self, _arg_types: &[DataType]) -> DfResult<DataType> {
186        Ok(self.return_kind.data_type())
187    }
188
189    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> DfResult<ColumnarValue> {
190        let has_dropped_sample = !args.args.is_empty()
191            && (0..args.number_rows).any(|row| {
192                args.args.iter().all(|arg| match arg {
193                    ColumnarValue::Array(array) => array.is_valid(row),
194                    ColumnarValue::Scalar(value) => !value.is_null(),
195                })
196            });
197        if has_dropped_sample
198            && let Some(collector) = args
199                .config_options
200                .extensions
201                .get::<PromqlAnnotationCollector>()
202                .cloned()
203                .or_else(|| self.collector.clone())
204        {
205            match self.level {
206                AnnotationLevel::Info => collector.record_info(self.message.clone()),
207                AnnotationLevel::Warning => collector.record_warning(self.message.clone()),
208            }
209        }
210        Ok(ColumnarValue::Scalar(self.return_kind.scalar_value()))
211    }
212}
213
214pub struct NativeHistogramDrop;
215
216impl NativeHistogramDrop {
217    const fn float_null_name() -> &'static str {
218        "prom_native_histogram_drop_float"
219    }
220
221    const fn bool_false_name() -> &'static str {
222        "prom_native_histogram_drop_bool"
223    }
224
225    const fn bool_true_name() -> &'static str {
226        "prom_native_histogram_keep_bool"
227    }
228
229    pub fn float_null_udf(
230        message: String,
231        collector: Option<PromqlAnnotationCollector>,
232    ) -> ScalarUDF {
233        ScalarUDF::new_from_impl(NativeHistogramAnnotationUdf::new(
234            Self::float_null_name(),
235            AnnotationReturn::FloatNull,
236            AnnotationLevel::Info,
237            message,
238            collector,
239        ))
240    }
241
242    pub fn bool_false_udf(
243        message: String,
244        collector: Option<PromqlAnnotationCollector>,
245    ) -> ScalarUDF {
246        ScalarUDF::new_from_impl(NativeHistogramAnnotationUdf::new(
247            Self::bool_false_name(),
248            AnnotationReturn::BooleanFalse,
249            AnnotationLevel::Info,
250            message,
251            collector,
252        ))
253    }
254
255    pub fn bool_true_udf(
256        message: String,
257        collector: Option<PromqlAnnotationCollector>,
258    ) -> ScalarUDF {
259        ScalarUDF::new_from_impl(NativeHistogramAnnotationUdf::new(
260            Self::bool_true_name(),
261            AnnotationReturn::BooleanTrue,
262            AnnotationLevel::Info,
263            message,
264            collector,
265        ))
266    }
267
268    pub fn warning_bool_false_udf(
269        message: String,
270        collector: Option<PromqlAnnotationCollector>,
271    ) -> ScalarUDF {
272        ScalarUDF::new_from_impl(NativeHistogramAnnotationUdf::new(
273            Self::bool_false_name(),
274            AnnotationReturn::BooleanFalse,
275            AnnotationLevel::Warning,
276            message,
277            collector,
278        ))
279    }
280}
281
282fn record_info(collector: &Option<PromqlAnnotationCollector>, message: impl Into<String>) {
283    if let Some(collector) = collector {
284        collector.record_info(message);
285    }
286}
287
288fn record_warning(collector: &Option<PromqlAnnotationCollector>, message: impl Into<String>) {
289    if let Some(collector) = collector {
290        collector.record_warning(message);
291    }
292}
293
294fn record_custom_reconciliation(
295    collector: &Option<PromqlAnnotationCollector>,
296    name: &'static str,
297    lhs: &NativeHistogram,
298    rhs: &NativeHistogram,
299) {
300    if lhs.needs_custom_reconciliation(rhs) {
301        record_info(
302            collector,
303            format!("{name}: reconciled native histograms with different custom buckets"),
304        );
305    }
306}
307
308fn record_counter_reset_contradiction(
309    collector: &Option<PromqlAnnotationCollector>,
310    name: &'static str,
311    lhs: &NativeHistogram,
312    rhs: &NativeHistogram,
313) {
314    if lhs.counter_reset_hints_contradict(rhs) {
315        record_counter_reset_contradiction_warning(collector, name);
316    }
317}
318
319fn record_counter_reset_contradiction_warning(
320    collector: &Option<PromqlAnnotationCollector>,
321    name: &'static str,
322) {
323    record_warning(
324        collector,
325        format!("{name}: native histogram counter reset hints contradict"),
326    );
327}
328
329fn scalar_histogram_udf<F>(
330    name: &'static str,
331    extra_input_types: Vec<DataType>,
332    calc: F,
333) -> ScalarUDF
334where
335    F: Fn(&NativeHistogram, &[ColumnarValue], usize, usize, &'static str) -> DfResult<f64>
336        + Send
337        + Sync
338        + 'static,
339{
340    let mut input_types = vec![native_histogram_arrow_type()];
341    input_types.extend(extra_input_types);
342    create_udf(
343        name,
344        input_types,
345        DataType::Float64,
346        Volatility::Volatile,
347        Arc::new(move |input: &[ColumnarValue]| {
348            if input.is_empty() {
349                return Err(DataFusionError::Plan(format!(
350                    "{name} requires a native histogram argument"
351                )));
352            }
353            let histograms = extract_histogram_array(&input[0], name)?;
354            let histograms = histograms
355                .as_any()
356                .downcast_ref::<StructArray>()
357                .expect("validated native histogram struct");
358            let mut result = Float64Builder::with_capacity(histograms.len());
359            for row in 0..histograms.len() {
360                match read_histogram(histograms, row)? {
361                    Some(histogram) => {
362                        result.append_value(calc(&histogram, input, row, histograms.len(), name)?)
363                    }
364                    None => result.append_null(),
365                }
366            }
367            Ok(ColumnarValue::Array(Arc::new(result.finish())))
368        }) as _,
369    )
370}
371
372fn histogram_pair_udf(
373    name: &'static str,
374    op: fn(&NativeHistogram, &NativeHistogram) -> Option<NativeHistogram>,
375) -> ScalarUDF {
376    histogram_pair_udf_with_collector(name, op, None)
377}
378
379fn histogram_pair_udf_with_collector(
380    name: &'static str,
381    op: fn(&NativeHistogram, &NativeHistogram) -> Option<NativeHistogram>,
382    collector: Option<PromqlAnnotationCollector>,
383) -> ScalarUDF {
384    create_udf(
385        name,
386        vec![native_histogram_arrow_type(), native_histogram_arrow_type()],
387        native_histogram_arrow_type(),
388        Volatility::Volatile,
389        Arc::new(move |input: &[ColumnarValue]| {
390            let lhs = extract_histogram_array(&input[0], name)?;
391            let rhs = extract_histogram_array(&input[1], name)?;
392            if lhs.len() != rhs.len() {
393                return Err(DataFusionError::Execution(format!(
394                    "{name}: native histogram argument length mismatch: {} vs {}",
395                    lhs.len(),
396                    rhs.len()
397                )));
398            }
399
400            let lhs = lhs
401                .as_any()
402                .downcast_ref::<StructArray>()
403                .expect("validated native histogram struct");
404            let rhs = rhs
405                .as_any()
406                .downcast_ref::<StructArray>()
407                .expect("validated native histogram struct");
408            let mut result = Vec::with_capacity(lhs.len());
409            for row in 0..lhs.len() {
410                result.push(
411                    match (read_histogram(lhs, row)?, read_histogram(rhs, row)?) {
412                        (Some(lhs), Some(rhs)) => {
413                            record_custom_reconciliation(&collector, name, &lhs, &rhs);
414                            record_counter_reset_contradiction(&collector, name, &lhs, &rhs);
415                            let result = op(&lhs, &rhs);
416                            if result.is_none() {
417                                record_warning(
418                                    &collector,
419                                    format!(
420                                    "{name}: dropped native histogram sample with incompatible schemas"
421                                    ),
422                                );
423                            }
424                            result
425                        }
426                        _ => None,
427                    },
428                );
429            }
430            Ok(ColumnarValue::Array(build_histogram_array(&result)))
431        }) as _,
432    )
433}
434
435fn histogram_transform_udf(
436    name: &'static str,
437    op: fn(NativeHistogram) -> NativeHistogram,
438) -> ScalarUDF {
439    create_udf(
440        name,
441        vec![native_histogram_arrow_type()],
442        native_histogram_arrow_type(),
443        Volatility::Volatile,
444        Arc::new(move |input: &[ColumnarValue]| {
445            let histograms = extract_histogram_array(&input[0], name)?;
446            let histograms = histograms
447                .as_any()
448                .downcast_ref::<StructArray>()
449                .expect("validated native histogram struct");
450            let mut result = Vec::with_capacity(histograms.len());
451            for row in 0..histograms.len() {
452                result.push(read_histogram(histograms, row)?.map(op));
453            }
454            Ok(ColumnarValue::Array(build_histogram_array(&result)))
455        }) as _,
456    )
457}
458
459fn histogram_string_udf(name: &'static str) -> ScalarUDF {
460    create_udf(
461        name,
462        vec![native_histogram_arrow_type()],
463        DataType::Utf8,
464        Volatility::Volatile,
465        Arc::new(move |input: &[ColumnarValue]| {
466            let histograms = extract_histogram_array(&input[0], name)?;
467            let histograms = histograms
468                .as_any()
469                .downcast_ref::<StructArray>()
470                .expect("validated native histogram struct");
471            let mut result = StringBuilder::with_capacity(histograms.len(), histograms.len() * 32);
472            for row in 0..histograms.len() {
473                match read_histogram(histograms, row)? {
474                    Some(histogram) => result.append_value(histogram.promql_string()),
475                    None => result.append_null(),
476                }
477            }
478            Ok(ColumnarValue::Array(Arc::new(result.finish())))
479        }) as _,
480    )
481}
482
483fn histogram_scalar_udf(
484    name: &'static str,
485    input_types: Vec<DataType>,
486    histogram_index: usize,
487    scalar_index: usize,
488    op: fn(NativeHistogram, f64) -> Option<NativeHistogram>,
489) -> ScalarUDF {
490    create_udf(
491        name,
492        input_types,
493        native_histogram_arrow_type(),
494        Volatility::Volatile,
495        Arc::new(move |input: &[ColumnarValue]| {
496            let histograms = extract_histogram_array(&input[histogram_index], name)?;
497            let histograms = histograms
498                .as_any()
499                .downcast_ref::<StructArray>()
500                .expect("validated native histogram struct");
501            let mut result = Vec::with_capacity(histograms.len());
502            for row in 0..histograms.len() {
503                result.push(match read_histogram(histograms, row)? {
504                    Some(histogram) => {
505                        let scalar =
506                            read_scalar_f64_arg(&input[scalar_index], row, histograms.len(), name)?;
507                        op(histogram, scalar)
508                    }
509                    None => None,
510                });
511            }
512            Ok(ColumnarValue::Array(build_histogram_array(&result)))
513        }) as _,
514    )
515}
516
517fn histogram_compare_udf(
518    name: &'static str,
519    op: fn(&NativeHistogram, &NativeHistogram) -> bool,
520) -> ScalarUDF {
521    create_udf(
522        name,
523        vec![native_histogram_arrow_type(), native_histogram_arrow_type()],
524        DataType::Boolean,
525        Volatility::Volatile,
526        Arc::new(move |input: &[ColumnarValue]| {
527            let lhs = extract_histogram_array(&input[0], name)?;
528            let rhs = extract_histogram_array(&input[1], name)?;
529            if lhs.len() != rhs.len() {
530                return Err(DataFusionError::Execution(format!(
531                    "{name}: native histogram argument length mismatch: {} vs {}",
532                    lhs.len(),
533                    rhs.len()
534                )));
535            }
536
537            let lhs = lhs
538                .as_any()
539                .downcast_ref::<StructArray>()
540                .expect("validated native histogram struct");
541            let rhs = rhs
542                .as_any()
543                .downcast_ref::<StructArray>()
544                .expect("validated native histogram struct");
545            let mut result = Vec::with_capacity(lhs.len());
546            for row in 0..lhs.len() {
547                result.push(
548                    match (read_histogram(lhs, row)?, read_histogram(rhs, row)?) {
549                        (Some(lhs), Some(rhs)) => Some(op(&lhs, &rhs)),
550                        _ => None,
551                    },
552                );
553            }
554            Ok(ColumnarValue::Array(Arc::new(BooleanArray::from(result))))
555        }) as _,
556    )
557}
558
559pub struct NativeHistogramAdd;
560
561impl NativeHistogramAdd {
562    pub const fn name() -> &'static str {
563        "prom_native_histogram_add"
564    }
565
566    pub fn scalar_udf() -> ScalarUDF {
567        histogram_pair_udf(Self::name(), NativeHistogram::add)
568    }
569
570    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
571        histogram_pair_udf_with_collector(Self::name(), NativeHistogram::add, collector)
572    }
573}
574
575pub struct NativeHistogramSub;
576
577impl NativeHistogramSub {
578    pub const fn name() -> &'static str {
579        "prom_native_histogram_sub"
580    }
581
582    pub fn scalar_udf() -> ScalarUDF {
583        histogram_pair_udf(Self::name(), NativeHistogram::sub)
584    }
585
586    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
587        histogram_pair_udf_with_collector(Self::name(), NativeHistogram::sub, collector)
588    }
589}
590
591pub struct NativeHistogramMulScalar;
592
593impl NativeHistogramMulScalar {
594    pub const fn name() -> &'static str {
595        "prom_native_histogram_mul_scalar"
596    }
597
598    pub fn scalar_udf() -> ScalarUDF {
599        histogram_scalar_udf(
600            Self::name(),
601            vec![native_histogram_arrow_type(), DataType::Float64],
602            0,
603            1,
604            |histogram, scalar| Some(histogram.scale(scalar)),
605        )
606    }
607}
608
609pub struct NativeHistogramScalarMul;
610
611impl NativeHistogramScalarMul {
612    pub const fn name() -> &'static str {
613        "prom_native_histogram_scalar_mul"
614    }
615
616    pub fn scalar_udf() -> ScalarUDF {
617        histogram_scalar_udf(
618            Self::name(),
619            vec![DataType::Float64, native_histogram_arrow_type()],
620            1,
621            0,
622            |histogram, scalar| Some(histogram.scale(scalar)),
623        )
624    }
625}
626
627pub struct NativeHistogramDivScalar;
628
629impl NativeHistogramDivScalar {
630    pub const fn name() -> &'static str {
631        "prom_native_histogram_div_scalar"
632    }
633
634    pub fn scalar_udf() -> ScalarUDF {
635        histogram_scalar_udf(
636            Self::name(),
637            vec![native_histogram_arrow_type(), DataType::Float64],
638            0,
639            1,
640            |histogram, scalar| Some(histogram.divide_by(scalar)),
641        )
642    }
643}
644
645pub struct NativeHistogramNeg;
646
647impl NativeHistogramNeg {
648    pub const fn name() -> &'static str {
649        "prom_native_histogram_neg"
650    }
651
652    pub fn scalar_udf() -> ScalarUDF {
653        histogram_transform_udf(Self::name(), NativeHistogram::negated)
654    }
655}
656
657pub struct NativeHistogramEq;
658
659impl NativeHistogramEq {
660    pub const fn name() -> &'static str {
661        "prom_native_histogram_eq"
662    }
663
664    pub fn scalar_udf() -> ScalarUDF {
665        histogram_compare_udf(Self::name(), NativeHistogram::promql_eq)
666    }
667}
668
669pub struct NativeHistogramNotEq;
670
671impl NativeHistogramNotEq {
672    pub const fn name() -> &'static str {
673        "prom_native_histogram_not_eq"
674    }
675
676    pub fn scalar_udf() -> ScalarUDF {
677        histogram_compare_udf(Self::name(), |lhs, rhs| !lhs.promql_eq(rhs))
678    }
679}
680
681pub struct NativeHistogramCount;
682
683impl NativeHistogramCount {
684    pub const fn name() -> &'static str {
685        "prom_native_histogram_count"
686    }
687
688    pub fn scalar_udf() -> ScalarUDF {
689        scalar_histogram_udf(Self::name(), vec![], |histogram, _, _, _, _| {
690            Ok(histogram.count)
691        })
692    }
693}
694
695pub struct NativeHistogramSum;
696
697impl NativeHistogramSum {
698    pub const fn name() -> &'static str {
699        "prom_native_histogram_sum"
700    }
701
702    pub fn scalar_udf() -> ScalarUDF {
703        scalar_histogram_udf(Self::name(), vec![], |histogram, _, _, _, _| {
704            Ok(histogram.sum)
705        })
706    }
707}
708
709pub struct NativeHistogramAvg;
710
711impl NativeHistogramAvg {
712    pub const fn name() -> &'static str {
713        "prom_native_histogram_avg"
714    }
715
716    pub fn scalar_udf() -> ScalarUDF {
717        scalar_histogram_udf(Self::name(), vec![], |histogram, _, _, _, _| {
718            Ok(histogram.sum / histogram.count)
719        })
720    }
721}
722
723pub struct NativeHistogramStddev;
724
725impl NativeHistogramStddev {
726    pub const fn name() -> &'static str {
727        "prom_native_histogram_stddev"
728    }
729
730    pub fn scalar_udf() -> ScalarUDF {
731        scalar_histogram_udf(Self::name(), vec![], |histogram, _, _, _, _| {
732            Ok(histogram.estimated_stddev())
733        })
734    }
735}
736
737pub struct NativeHistogramStdvar;
738
739impl NativeHistogramStdvar {
740    pub const fn name() -> &'static str {
741        "prom_native_histogram_stdvar"
742    }
743
744    pub fn scalar_udf() -> ScalarUDF {
745        scalar_histogram_udf(Self::name(), vec![], |histogram, _, _, _, _| {
746            Ok(histogram.estimated_stdvar())
747        })
748    }
749}
750
751/// Formats float samples as PromQL label values.
752pub struct PromqlFloatToString;
753
754impl PromqlFloatToString {
755    pub const fn name() -> &'static str {
756        "prom_float_to_string"
757    }
758
759    pub fn scalar_udf() -> ScalarUDF {
760        create_udf(
761            Self::name(),
762            vec![DataType::Float64],
763            DataType::Utf8,
764            Volatility::Volatile,
765            Arc::new(|input: &[ColumnarValue]| {
766                let values = extract_array(&input[0])?;
767                let values = values
768                    .as_any()
769                    .downcast_ref::<Float64Array>()
770                    .expect("validated Float64 input");
771                let mut result = StringBuilder::new();
772                for value in values.iter() {
773                    match value {
774                        Some(value) => result.append_value(format_prometheus_float(value)),
775                        None => result.append_null(),
776                    }
777                }
778                Ok(ColumnarValue::Array(Arc::new(result.finish())))
779            }),
780        )
781    }
782}
783
784pub struct NativeHistogramToString;
785
786impl NativeHistogramToString {
787    pub const fn name() -> &'static str {
788        "prom_native_histogram_to_string"
789    }
790
791    pub fn scalar_udf() -> ScalarUDF {
792        histogram_string_udf(Self::name())
793    }
794}
795
796pub struct NativeHistogramQuantile;
797
798impl NativeHistogramQuantile {
799    pub const fn name() -> &'static str {
800        "prom_native_histogram_quantile"
801    }
802
803    pub fn scalar_udf() -> ScalarUDF {
804        Self::scalar_udf_with_collector(None)
805    }
806
807    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
808        scalar_histogram_udf(
809            Self::name(),
810            vec![DataType::Float64],
811            move |histogram, input, row, len, name| {
812                let q = read_scalar_f64_arg(&input[1], row, len, name)?;
813                let (value, info) = histogram.quantile_with_info(q);
814                if let Some(info) = info {
815                    let message = match info {
816                        NativeHistogramQuantileInfo::NaNSkew => {
817                            "input to histogram_quantile has NaN observations, result is skewed higher"
818                        }
819                        NativeHistogramQuantileInfo::NaNResult => {
820                            "input to histogram_quantile has NaN observations, result is NaN"
821                        }
822                    };
823                    record_info(&collector, message);
824                }
825                Ok(value)
826            },
827        )
828    }
829}
830
831pub struct NativeHistogramFraction;
832
833impl NativeHistogramFraction {
834    pub const fn name() -> &'static str {
835        "prom_native_histogram_fraction"
836    }
837
838    pub fn scalar_udf() -> ScalarUDF {
839        Self::scalar_udf_with_collector(None)
840    }
841
842    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
843        scalar_histogram_udf(
844            Self::name(),
845            vec![DataType::Float64, DataType::Float64],
846            move |histogram, input, row, len, name| {
847                let lower = read_scalar_f64_arg(&input[1], row, len, name)?;
848                let upper = read_scalar_f64_arg(&input[2], row, len, name)?;
849                let (value, excluded_nans) = histogram.fraction_with_info(lower, upper);
850                if excluded_nans {
851                    record_info(
852                        &collector,
853                        "input to histogram_fraction has NaN observations, which are excluded from all fractions",
854                    );
855                }
856                Ok(value)
857            },
858        )
859    }
860}
861
862#[derive(Debug, Clone, Copy)]
863enum NativeHistogramAggregateKind {
864    Sum,
865    Avg,
866}
867
868impl NativeHistogramAggregateKind {
869    const fn name(self) -> &'static str {
870        match self {
871            Self::Sum => NativeHistogramAggSum::name(),
872            Self::Avg => NativeHistogramAggAvg::name(),
873        }
874    }
875
876    const fn needs_count(self) -> bool {
877        matches!(self, Self::Avg)
878    }
879}
880
881pub struct NativeHistogramAggSum;
882
883impl NativeHistogramAggSum {
884    pub const fn name() -> &'static str {
885        "prom_native_histogram_agg_sum"
886    }
887
888    pub fn aggregate_udf() -> AggregateUDF {
889        native_histogram_aggregate_udf(NativeHistogramAggregateKind::Sum, None)
890    }
891
892    pub fn aggregate_udf_with_collector(
893        collector: Option<PromqlAnnotationCollector>,
894    ) -> AggregateUDF {
895        native_histogram_aggregate_udf(NativeHistogramAggregateKind::Sum, collector)
896    }
897}
898
899pub struct NativeHistogramAggAvg;
900
901impl NativeHistogramAggAvg {
902    pub const fn name() -> &'static str {
903        "prom_native_histogram_agg_avg"
904    }
905
906    pub fn aggregate_udf() -> AggregateUDF {
907        native_histogram_aggregate_udf(NativeHistogramAggregateKind::Avg, None)
908    }
909
910    pub fn aggregate_udf_with_collector(
911        collector: Option<PromqlAnnotationCollector>,
912    ) -> AggregateUDF {
913        native_histogram_aggregate_udf(NativeHistogramAggregateKind::Avg, collector)
914    }
915}
916
917#[derive(Debug)]
918struct NativeHistogramAggregateAccumulator {
919    kind: NativeHistogramAggregateKind,
920    value: Option<NativeHistogram>,
921    count: u64,
922    dropped_incompatible: bool,
923    counter_reset_seen: bool,
924    not_counter_reset_seen: bool,
925    collector: Option<PromqlAnnotationCollector>,
926}
927
928impl NativeHistogramAggregateAccumulator {
929    fn new(
930        kind: NativeHistogramAggregateKind,
931        collector: Option<PromqlAnnotationCollector>,
932    ) -> Self {
933        Self {
934            kind,
935            value: None,
936            count: 0,
937            dropped_incompatible: false,
938            counter_reset_seen: false,
939            not_counter_reset_seen: false,
940            collector,
941        }
942    }
943
944    fn from_args(
945        kind: NativeHistogramAggregateKind,
946        collector: Option<PromqlAnnotationCollector>,
947        _args: AccumulatorArgs,
948    ) -> DfResult<Box<dyn DfAccumulator>> {
949        Ok(Box::new(Self::new(kind, collector)))
950    }
951
952    fn observe_reset_hints(&mut self, counter_reset_seen: bool, not_counter_reset_seen: bool) {
953        self.counter_reset_seen |= counter_reset_seen;
954        self.not_counter_reset_seen |= not_counter_reset_seen;
955        if self.counter_reset_seen && self.not_counter_reset_seen {
956            record_counter_reset_contradiction_warning(&self.collector, self.kind.name());
957        }
958    }
959
960    fn push_histogram(&mut self, histogram: NativeHistogram, count: u64) -> DfResult<()> {
961        if self.kind.needs_count() && count == 0 {
962            return Ok(());
963        }
964
965        self.observe_reset_hints(
966            histogram.reset_hint == COUNTER_RESET_HINT,
967            histogram.reset_hint == NOT_COUNTER_RESET_HINT,
968        );
969        if self.dropped_incompatible {
970            return Ok(());
971        }
972        let combined_count = if self.kind.needs_count() {
973            self.count.checked_add(count).ok_or_else(|| {
974                DataFusionError::Execution(format!(
975                    "{}: native histogram sample count overflow",
976                    self.kind.name()
977                ))
978            })?
979        } else {
980            self.count
981        };
982        let value = match self.value.take() {
983            Some(value) => {
984                record_custom_reconciliation(&self.collector, self.kind.name(), &value, &histogram);
985                let combined = match self.kind {
986                    NativeHistogramAggregateKind::Sum => value.add(&histogram),
987                    NativeHistogramAggregateKind::Avg => {
988                        weighted_histogram_mean(value, self.count, histogram, count, combined_count)
989                    }
990                };
991                match combined {
992                    Some(value) => Some(value),
993                    None => {
994                        self.record_incompatible();
995                        None
996                    }
997                }
998            }
999            None => Some(histogram),
1000        };
1001        if !self.dropped_incompatible {
1002            self.value = value;
1003            self.count = combined_count;
1004        }
1005        Ok(())
1006    }
1007
1008    fn mark_incompatible(&mut self) {
1009        self.value = None;
1010        self.count = 0;
1011        self.dropped_incompatible = true;
1012    }
1013
1014    fn record_incompatible(&mut self) {
1015        self.mark_incompatible();
1016        record_warning(
1017            &self.collector,
1018            format!(
1019                "{}: dropped native histogram aggregate with incompatible schemas",
1020                self.kind.name()
1021            ),
1022        );
1023    }
1024}
1025
1026fn weighted_histogram_mean(
1027    left: NativeHistogram,
1028    left_count: u64,
1029    right: NativeHistogram,
1030    right_count: u64,
1031    total_count: u64,
1032) -> Option<NativeHistogram> {
1033    let total_count = total_count as f64;
1034    left.scale(left_count as f64 / total_count)
1035        .add(&right.scale(right_count as f64 / total_count))
1036}
1037
1038fn range_fold_histograms(
1039    samples: Vec<NativeHistogram>,
1040    kind: NativeHistogramAggregateKind,
1041    name: &'static str,
1042    collector: &Option<PromqlAnnotationCollector>,
1043) -> Option<NativeHistogram> {
1044    if samples
1045        .iter()
1046        .any(|histogram| histogram.reset_hint == COUNTER_RESET_HINT)
1047        && samples
1048            .iter()
1049            .any(|histogram| histogram.reset_hint == NOT_COUNTER_RESET_HINT)
1050    {
1051        record_counter_reset_contradiction_warning(collector, name);
1052    }
1053
1054    let mut value = None;
1055    let mut count = 0u64;
1056    for histogram in samples {
1057        value = match value {
1058            Some(value) => {
1059                record_custom_reconciliation(collector, name, &value, &histogram);
1060                let next_count = count.checked_add(1)?;
1061                let combined = match kind {
1062                    NativeHistogramAggregateKind::Sum => value.add(&histogram),
1063                    NativeHistogramAggregateKind::Avg => {
1064                        weighted_histogram_mean(value, count, histogram, 1, next_count)
1065                    }
1066                };
1067                match combined {
1068                    Some(value) => Some(value),
1069                    None => {
1070                        record_warning(
1071                            collector,
1072                            format!(
1073                                "{name}: dropped native histogram range with incompatible schemas"
1074                            ),
1075                        );
1076                        return None;
1077                    }
1078                }
1079            }
1080            None => Some(histogram),
1081        };
1082        count = count.checked_add(1)?;
1083    }
1084    value
1085}
1086
1087#[derive(Debug, Clone, Copy)]
1088enum NativeHistogramRangeHistogramKind {
1089    Sum,
1090    Avg,
1091    Last,
1092}
1093
1094#[derive(Debug, Clone, Copy)]
1095enum NativeHistogramRangeFloatKind {
1096    Absent,
1097    Count,
1098    Present,
1099    Changes,
1100    Resets,
1101}
1102
1103fn collect_window_histograms(
1104    histograms: &StructArray,
1105    offset: usize,
1106    length: usize,
1107) -> DfResult<Option<Vec<NativeHistogram>>> {
1108    let mut samples = Vec::with_capacity(length);
1109    for row in offset..offset + length {
1110        let Some(histogram) = read_histogram(histograms, row)? else {
1111            return Ok(None);
1112        };
1113        samples.push(histogram);
1114    }
1115    Ok(Some(samples))
1116}
1117
1118fn native_histogram_range_histogram(
1119    input: &[ColumnarValue],
1120    kind: NativeHistogramRangeHistogramKind,
1121    func_name: &'static str,
1122    collector: Option<PromqlAnnotationCollector>,
1123) -> DfResult<ColumnarValue> {
1124    if input.len() != 2 {
1125        return Err(DataFusionError::Plan(format!(
1126            "{func_name} function should have 2 inputs"
1127        )));
1128    }
1129
1130    let ts_range = extract_range_dict(
1131        &input[0],
1132        func_name,
1133        "timestamp range vector",
1134        &DataType::Timestamp(TimeUnit::Millisecond, None),
1135    )?;
1136    let value_range = extract_range_dict(
1137        &input[1],
1138        func_name,
1139        "value range vector",
1140        &native_histogram_arrow_type(),
1141    )?;
1142    if ts_range.keys().values() != value_range.keys().values() {
1143        return Err(DataFusionError::Execution(format!(
1144            "{func_name}: timestamp and value ranges should have the same window layout"
1145        )));
1146    }
1147
1148    let histograms = value_range
1149        .values()
1150        .as_any()
1151        .downcast_ref::<StructArray>()
1152        .expect("validated native histogram range");
1153    let mut result = Vec::with_capacity(value_range.keys().len());
1154    for key in value_range.keys().values() {
1155        let (offset, length) = unpack(*key);
1156        let offset = offset as usize;
1157        let length = length as usize;
1158        if length == 0 {
1159            result.push(None);
1160            continue;
1161        }
1162        if matches!(kind, NativeHistogramRangeHistogramKind::Last) {
1163            let histogram = if (offset..offset + length).any(|row| histograms.is_null(row)) {
1164                None
1165            } else {
1166                read_histogram(histograms, offset + length - 1)?
1167            };
1168            result.push(histogram);
1169            continue;
1170        }
1171        let Some(samples) = collect_window_histograms(histograms, offset, length)? else {
1172            result.push(None);
1173            continue;
1174        };
1175        let histogram = match kind {
1176            NativeHistogramRangeHistogramKind::Sum => range_fold_histograms(
1177                samples,
1178                NativeHistogramAggregateKind::Sum,
1179                func_name,
1180                &collector,
1181            ),
1182            NativeHistogramRangeHistogramKind::Avg => range_fold_histograms(
1183                samples,
1184                NativeHistogramAggregateKind::Avg,
1185                func_name,
1186                &collector,
1187            ),
1188            NativeHistogramRangeHistogramKind::Last => samples.last().cloned(),
1189        };
1190        result.push(histogram);
1191    }
1192
1193    Ok(ColumnarValue::Array(build_histogram_array(&result)))
1194}
1195
1196fn native_histogram_range_float(
1197    input: &[ColumnarValue],
1198    kind: NativeHistogramRangeFloatKind,
1199    func_name: &'static str,
1200) -> DfResult<ColumnarValue> {
1201    if input.len() != 2 {
1202        return Err(DataFusionError::Plan(format!(
1203            "{func_name} function should have 2 inputs"
1204        )));
1205    }
1206
1207    let ts_range = extract_range_dict(
1208        &input[0],
1209        func_name,
1210        "timestamp range vector",
1211        &DataType::Timestamp(TimeUnit::Millisecond, None),
1212    )?;
1213    let value_range = extract_range_dict(
1214        &input[1],
1215        func_name,
1216        "value range vector",
1217        &native_histogram_arrow_type(),
1218    )?;
1219    if ts_range.keys().values() != value_range.keys().values() {
1220        return Err(DataFusionError::Execution(format!(
1221            "{func_name}: timestamp and value ranges should have the same window layout"
1222        )));
1223    }
1224
1225    let timestamps = ts_range
1226        .values()
1227        .as_any()
1228        .downcast_ref::<TimestampMillisecondArray>()
1229        .expect("validated timestamp range")
1230        .values();
1231    let histograms = value_range
1232        .values()
1233        .as_any()
1234        .downcast_ref::<StructArray>()
1235        .expect("validated native histogram range");
1236    let mut result = Float64Builder::with_capacity(value_range.keys().len());
1237    for key in value_range.keys().values() {
1238        let (offset, length) = unpack(*key);
1239        let offset = offset as usize;
1240        let length = length as usize;
1241        if length == 0 {
1242            match kind {
1243                NativeHistogramRangeFloatKind::Absent => result.append_value(1.0),
1244                _ => result.append_null(),
1245            }
1246            continue;
1247        }
1248        if matches!(kind, NativeHistogramRangeFloatKind::Absent) {
1249            result.append_null();
1250            continue;
1251        }
1252        if matches!(
1253            kind,
1254            NativeHistogramRangeFloatKind::Count | NativeHistogramRangeFloatKind::Present
1255        ) {
1256            if (offset..offset + length).any(|row| histograms.is_null(row)) {
1257                result.append_null();
1258            } else if matches!(kind, NativeHistogramRangeFloatKind::Count) {
1259                result.append_value(length as f64);
1260            } else {
1261                result.append_value(1.0);
1262            }
1263            continue;
1264        }
1265        let Some(samples) = collect_window_histograms(histograms, offset, length)? else {
1266            result.append_null();
1267            continue;
1268        };
1269        let value = match kind {
1270            NativeHistogramRangeFloatKind::Absent => {
1271                result.append_null();
1272                continue;
1273            }
1274            NativeHistogramRangeFloatKind::Count => length as f64,
1275            NativeHistogramRangeFloatKind::Present => 1.0,
1276            NativeHistogramRangeFloatKind::Changes => samples
1277                .windows(2)
1278                .filter(|pair| !pair[0].promql_eq(&pair[1]))
1279                .count() as f64,
1280            NativeHistogramRangeFloatKind::Resets => samples
1281                .windows(2)
1282                .zip(timestamps[offset..offset + length].windows(2))
1283                .filter(|(pair, ts_pair)| {
1284                    (pair[0].reset_hint == GAUGE_RESET_HINT)
1285                        != (pair[1].reset_hint == GAUGE_RESET_HINT)
1286                        || pair[1].detect_counter_reset(&pair[0], ts_pair[0], ts_pair[1])
1287                })
1288                .count() as f64,
1289        };
1290        result.append_value(value);
1291    }
1292
1293    Ok(ColumnarValue::Array(Arc::new(result.finish())))
1294}
1295
1296fn create_native_range_histogram_udf(
1297    name: &'static str,
1298    kind: NativeHistogramRangeHistogramKind,
1299    collector: Option<PromqlAnnotationCollector>,
1300) -> ScalarUDF {
1301    create_udf(
1302        name,
1303        vec![
1304            RangeArray::convert_data_type(DataType::Timestamp(TimeUnit::Millisecond, None)),
1305            RangeArray::convert_data_type(native_histogram_arrow_type()),
1306        ],
1307        native_histogram_arrow_type(),
1308        Volatility::Volatile,
1309        Arc::new(move |input: &[ColumnarValue]| {
1310            native_histogram_range_histogram(input, kind, name, collector.clone())
1311        }) as _,
1312    )
1313}
1314
1315fn create_native_range_float_udf(
1316    name: &'static str,
1317    kind: NativeHistogramRangeFloatKind,
1318) -> ScalarUDF {
1319    create_udf(
1320        name,
1321        vec![
1322            RangeArray::convert_data_type(DataType::Timestamp(TimeUnit::Millisecond, None)),
1323            RangeArray::convert_data_type(native_histogram_arrow_type()),
1324        ],
1325        DataType::Float64,
1326        Volatility::Volatile,
1327        Arc::new(move |input: &[ColumnarValue]| native_histogram_range_float(input, kind, name))
1328            as _,
1329    )
1330}
1331
1332pub struct NativeHistogramSumOverTime;
1333pub struct NativeHistogramAvgOverTime;
1334pub struct NativeHistogramAbsentOverTime;
1335pub struct NativeHistogramCountOverTime;
1336pub struct NativeHistogramLastOverTime;
1337pub struct NativeHistogramPresentOverTime;
1338pub struct NativeHistogramChanges;
1339pub struct NativeHistogramResets;
1340
1341impl NativeHistogramSumOverTime {
1342    pub const fn name() -> &'static str {
1343        "prom_native_histogram_sum_over_time"
1344    }
1345
1346    pub fn scalar_udf() -> ScalarUDF {
1347        Self::scalar_udf_with_collector(None)
1348    }
1349
1350    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
1351        create_native_range_histogram_udf(
1352            Self::name(),
1353            NativeHistogramRangeHistogramKind::Sum,
1354            collector,
1355        )
1356    }
1357}
1358
1359impl NativeHistogramAvgOverTime {
1360    pub const fn name() -> &'static str {
1361        "prom_native_histogram_avg_over_time"
1362    }
1363
1364    pub fn scalar_udf() -> ScalarUDF {
1365        Self::scalar_udf_with_collector(None)
1366    }
1367
1368    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
1369        create_native_range_histogram_udf(
1370            Self::name(),
1371            NativeHistogramRangeHistogramKind::Avg,
1372            collector,
1373        )
1374    }
1375}
1376
1377impl NativeHistogramAbsentOverTime {
1378    pub const fn name() -> &'static str {
1379        "prom_native_histogram_absent_over_time"
1380    }
1381
1382    pub fn scalar_udf() -> ScalarUDF {
1383        create_native_range_float_udf(Self::name(), NativeHistogramRangeFloatKind::Absent)
1384    }
1385}
1386
1387impl NativeHistogramCountOverTime {
1388    pub const fn name() -> &'static str {
1389        "prom_native_histogram_count_over_time"
1390    }
1391
1392    pub fn scalar_udf() -> ScalarUDF {
1393        create_native_range_float_udf(Self::name(), NativeHistogramRangeFloatKind::Count)
1394    }
1395}
1396
1397impl NativeHistogramLastOverTime {
1398    pub const fn name() -> &'static str {
1399        "prom_native_histogram_last_over_time"
1400    }
1401
1402    pub fn scalar_udf() -> ScalarUDF {
1403        create_native_range_histogram_udf(
1404            Self::name(),
1405            NativeHistogramRangeHistogramKind::Last,
1406            None,
1407        )
1408    }
1409}
1410
1411impl NativeHistogramPresentOverTime {
1412    pub const fn name() -> &'static str {
1413        "prom_native_histogram_present_over_time"
1414    }
1415
1416    pub fn scalar_udf() -> ScalarUDF {
1417        create_native_range_float_udf(Self::name(), NativeHistogramRangeFloatKind::Present)
1418    }
1419}
1420
1421impl NativeHistogramChanges {
1422    pub const fn name() -> &'static str {
1423        "prom_native_histogram_changes"
1424    }
1425
1426    pub fn scalar_udf() -> ScalarUDF {
1427        create_native_range_float_udf(Self::name(), NativeHistogramRangeFloatKind::Changes)
1428    }
1429}
1430
1431impl NativeHistogramResets {
1432    pub const fn name() -> &'static str {
1433        "prom_native_histogram_resets"
1434    }
1435
1436    pub fn scalar_udf() -> ScalarUDF {
1437        create_native_range_float_udf(Self::name(), NativeHistogramRangeFloatKind::Resets)
1438    }
1439}
1440
1441/// Coordinated float/native-histogram range evaluation.
1442///
1443/// The function name is passed as the first scalar argument so these two UDF names are enough for
1444/// distributed plan decoding. The remaining leading arguments are timestamp, float, and histogram
1445/// ranges, followed by the ordinary function arguments.
1446pub struct MixedRange;
1447
1448impl MixedRange {
1449    const fn float_name() -> &'static str {
1450        "prom_mixed_range_float"
1451    }
1452
1453    const fn histogram_name() -> &'static str {
1454        "prom_mixed_range_histogram"
1455    }
1456
1457    pub fn float_udf(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
1458        ScalarUDF::new_from_impl(MixedRangeUdf::new(MixedRangeOutput::Float, collector))
1459    }
1460
1461    pub fn histogram_udf(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
1462        ScalarUDF::new_from_impl(MixedRangeUdf::new(MixedRangeOutput::Histogram, collector))
1463    }
1464}
1465
1466#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1467enum MixedRangeOutput {
1468    Float,
1469    Histogram,
1470}
1471
1472impl MixedRangeOutput {
1473    fn name(self) -> &'static str {
1474        match self {
1475            Self::Float => MixedRange::float_name(),
1476            Self::Histogram => MixedRange::histogram_name(),
1477        }
1478    }
1479
1480    fn data_type(self) -> DataType {
1481        match self {
1482            Self::Float => DataType::Float64,
1483            Self::Histogram => native_histogram_arrow_type(),
1484        }
1485    }
1486}
1487
1488#[derive(Debug, Clone)]
1489struct MixedRangeUdf {
1490    output: MixedRangeOutput,
1491    signature: Signature,
1492    collector: Option<PromqlAnnotationCollector>,
1493}
1494
1495impl MixedRangeUdf {
1496    fn new(output: MixedRangeOutput, collector: Option<PromqlAnnotationCollector>) -> Self {
1497        Self {
1498            output,
1499            signature: Signature::variadic_any(Volatility::Volatile),
1500            collector,
1501        }
1502    }
1503}
1504
1505impl PartialEq for MixedRangeUdf {
1506    fn eq(&self, other: &Self) -> bool {
1507        self.output == other.output
1508    }
1509}
1510
1511impl Eq for MixedRangeUdf {}
1512
1513impl Hash for MixedRangeUdf {
1514    fn hash<H: Hasher>(&self, state: &mut H) {
1515        self.output.hash(state);
1516    }
1517}
1518
1519impl ScalarUDFImpl for MixedRangeUdf {
1520    fn as_any(&self) -> &dyn Any {
1521        self
1522    }
1523
1524    fn name(&self) -> &str {
1525        self.output.name()
1526    }
1527
1528    fn signature(&self) -> &Signature {
1529        &self.signature
1530    }
1531
1532    fn return_type(&self, _arg_types: &[DataType]) -> DfResult<DataType> {
1533        Ok(self.output.data_type())
1534    }
1535
1536    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> DfResult<ColumnarValue> {
1537        let collector = args
1538            .config_options
1539            .extensions
1540            .get::<PromqlAnnotationCollector>()
1541            .cloned()
1542            .or_else(|| self.collector.clone());
1543        mixed_range(&args, self.output, collector)
1544    }
1545}
1546
1547#[derive(Debug, Clone, Copy)]
1548enum MixedRangeFunction {
1549    Rate,
1550    Increase,
1551    // Raw-delta modes sum floats while preserving mixed-range drop/warning semantics.
1552    RawDeltaRate,
1553    RawDeltaIncrease,
1554    Delta,
1555    IDelta,
1556    IRate,
1557    Changes,
1558    Resets,
1559    AvgOverTime,
1560    MinOverTime,
1561    MaxOverTime,
1562    SumOverTime,
1563    CountOverTime,
1564    LastOverTime,
1565    AbsentOverTime,
1566    PresentOverTime,
1567    StddevOverTime,
1568    StdvarOverTime,
1569    QuantileOverTime,
1570    Deriv,
1571    PredictLinear,
1572    DoubleExponentialSmoothing,
1573    HoltWinters,
1574}
1575
1576impl MixedRangeFunction {
1577    fn parse(value: &ColumnarValue) -> DfResult<Self> {
1578        let name = match value {
1579            ColumnarValue::Scalar(ScalarValue::Utf8(Some(name)))
1580            | ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(name)))
1581            | ColumnarValue::Scalar(ScalarValue::Utf8View(Some(name))) => name.as_str(),
1582            other => {
1583                return Err(DataFusionError::Execution(format!(
1584                    "mixed range function name must be a non-null string scalar, found {}",
1585                    other.data_type()
1586                )));
1587            }
1588        };
1589
1590        match name {
1591            "rate" => Ok(Self::Rate),
1592            "increase" => Ok(Self::Increase),
1593            "raw_delta_rate" => Ok(Self::RawDeltaRate),
1594            "raw_delta_increase" => Ok(Self::RawDeltaIncrease),
1595            "delta" => Ok(Self::Delta),
1596            "idelta" => Ok(Self::IDelta),
1597            "irate" => Ok(Self::IRate),
1598            "changes" => Ok(Self::Changes),
1599            "resets" => Ok(Self::Resets),
1600            "avg_over_time" => Ok(Self::AvgOverTime),
1601            "min_over_time" => Ok(Self::MinOverTime),
1602            "max_over_time" => Ok(Self::MaxOverTime),
1603            "sum_over_time" => Ok(Self::SumOverTime),
1604            "count_over_time" => Ok(Self::CountOverTime),
1605            "last_over_time" => Ok(Self::LastOverTime),
1606            "absent_over_time" => Ok(Self::AbsentOverTime),
1607            "present_over_time" => Ok(Self::PresentOverTime),
1608            "stddev_over_time" => Ok(Self::StddevOverTime),
1609            "stdvar_over_time" => Ok(Self::StdvarOverTime),
1610            "quantile_over_time" => Ok(Self::QuantileOverTime),
1611            "deriv" => Ok(Self::Deriv),
1612            "predict_linear" => Ok(Self::PredictLinear),
1613            "double_exponential_smoothing" => Ok(Self::DoubleExponentialSmoothing),
1614            "holt_winters" => Ok(Self::HoltWinters),
1615            _ => Err(DataFusionError::Execution(format!(
1616                "unsupported mixed range function: {name}"
1617            ))),
1618        }
1619    }
1620
1621    fn name(self) -> &'static str {
1622        match self {
1623            Self::Rate => "rate",
1624            Self::Increase => "increase",
1625            Self::RawDeltaRate => "rate",
1626            Self::RawDeltaIncrease => "increase",
1627            Self::Delta => "delta",
1628            Self::IDelta => "idelta",
1629            Self::IRate => "irate",
1630            Self::Changes => "changes",
1631            Self::Resets => "resets",
1632            Self::AvgOverTime => "avg_over_time",
1633            Self::MinOverTime => "min_over_time",
1634            Self::MaxOverTime => "max_over_time",
1635            Self::SumOverTime => "sum_over_time",
1636            Self::CountOverTime => "count_over_time",
1637            Self::LastOverTime => "last_over_time",
1638            Self::AbsentOverTime => "absent_over_time",
1639            Self::PresentOverTime => "present_over_time",
1640            Self::StddevOverTime => "stddev_over_time",
1641            Self::StdvarOverTime => "stdvar_over_time",
1642            Self::QuantileOverTime => "quantile_over_time",
1643            Self::Deriv => "deriv",
1644            Self::PredictLinear => "predict_linear",
1645            Self::DoubleExponentialSmoothing => "double_exponential_smoothing",
1646            Self::HoltWinters => "holt_winters",
1647        }
1648    }
1649
1650    fn policy(self) -> MixedRangePolicy {
1651        match self {
1652            Self::Rate
1653            | Self::Increase
1654            | Self::RawDeltaRate
1655            | Self::RawDeltaIncrease
1656            | Self::Delta
1657            | Self::AvgOverTime
1658            | Self::SumOverTime => MixedRangePolicy::DropMixed,
1659            Self::IDelta | Self::IRate => MixedRangePolicy::LastTwo,
1660            Self::LastOverTime => MixedRangePolicy::Last,
1661            Self::Changes
1662            | Self::Resets
1663            | Self::CountOverTime
1664            | Self::AbsentOverTime
1665            | Self::PresentOverTime => MixedRangePolicy::Combined,
1666            Self::MinOverTime
1667            | Self::MaxOverTime
1668            | Self::StddevOverTime
1669            | Self::StdvarOverTime
1670            | Self::QuantileOverTime
1671            | Self::Deriv
1672            | Self::PredictLinear
1673            | Self::DoubleExponentialSmoothing
1674            | Self::HoltWinters => MixedRangePolicy::FloatOnly,
1675        }
1676    }
1677
1678    fn float_udf(self) -> Option<ScalarUDF> {
1679        match self {
1680            Self::Rate => Some(Rate::scalar_udf()),
1681            Self::Increase => Some(Increase::scalar_udf()),
1682            Self::RawDeltaRate | Self::RawDeltaIncrease => Some(SumOverTime::scalar_udf()),
1683            Self::Delta => Some(crate::functions::Delta::scalar_udf()),
1684            Self::IDelta => Some(IDelta::<false>::scalar_udf()),
1685            Self::IRate => Some(IDelta::<true>::scalar_udf()),
1686            Self::AvgOverTime => Some(AvgOverTime::scalar_udf()),
1687            Self::MinOverTime => Some(MinOverTime::scalar_udf()),
1688            Self::MaxOverTime => Some(MaxOverTime::scalar_udf()),
1689            Self::SumOverTime => Some(SumOverTime::scalar_udf()),
1690            Self::LastOverTime => Some(LastOverTime::scalar_udf()),
1691            Self::StddevOverTime => Some(StddevOverTime::scalar_udf()),
1692            Self::StdvarOverTime => Some(StdvarOverTime::scalar_udf()),
1693            Self::QuantileOverTime => Some(QuantileOverTime::scalar_udf()),
1694            Self::Deriv => Some(Deriv::scalar_udf()),
1695            Self::PredictLinear => Some(PredictLinear::scalar_udf()),
1696            Self::DoubleExponentialSmoothing | Self::HoltWinters => {
1697                Some(DoubleExponentialSmoothing::scalar_udf())
1698            }
1699            Self::Changes
1700            | Self::Resets
1701            | Self::CountOverTime
1702            | Self::AbsentOverTime
1703            | Self::PresentOverTime => None,
1704        }
1705    }
1706
1707    fn histogram_udf(self, collector: Option<PromqlAnnotationCollector>) -> Option<ScalarUDF> {
1708        match self {
1709            Self::Rate => Some(NativeHistogramRate::scalar_udf_with_collector(collector)),
1710            Self::Increase => Some(NativeHistogramIncrease::scalar_udf_with_collector(
1711                collector,
1712            )),
1713            Self::Delta => Some(NativeHistogramDelta::scalar_udf_with_collector(collector)),
1714            Self::IDelta => Some(NativeHistogramIDelta::scalar_udf_with_collector(collector)),
1715            Self::IRate => Some(NativeHistogramIRate::scalar_udf_with_collector(collector)),
1716            Self::AvgOverTime => Some(NativeHistogramAvgOverTime::scalar_udf_with_collector(
1717                collector,
1718            )),
1719            Self::SumOverTime => Some(NativeHistogramSumOverTime::scalar_udf_with_collector(
1720                collector,
1721            )),
1722            Self::LastOverTime => Some(NativeHistogramLastOverTime::scalar_udf()),
1723            Self::RawDeltaRate | Self::RawDeltaIncrease => None,
1724            _ => None,
1725        }
1726    }
1727}
1728
1729#[derive(Debug, Clone, Copy)]
1730enum MixedRangePolicy {
1731    DropMixed,
1732    LastTwo,
1733    Last,
1734    Combined,
1735    FloatOnly,
1736}
1737
1738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1739enum SampleLane {
1740    Float,
1741    Histogram,
1742}
1743
1744fn mixed_range(
1745    args: &ScalarFunctionArgs,
1746    output: MixedRangeOutput,
1747    collector: Option<PromqlAnnotationCollector>,
1748) -> DfResult<ColumnarValue> {
1749    if args.args.len() < 4 {
1750        return Err(DataFusionError::Plan(format!(
1751            "{} function should have at least 4 inputs",
1752            output.name()
1753        )));
1754    }
1755    let function = MixedRangeFunction::parse(&args.args[0])?;
1756    let name = function.name();
1757    let ts_range = extract_range_dict(
1758        &args.args[1],
1759        name,
1760        "timestamp range vector",
1761        &DataType::Timestamp(TimeUnit::Millisecond, None),
1762    )?;
1763    let float_range = extract_range_dict(
1764        &args.args[2],
1765        name,
1766        "float range vector",
1767        &DataType::Float64,
1768    )?;
1769    let histogram_range = extract_range_dict(
1770        &args.args[3],
1771        name,
1772        "native histogram range vector",
1773        &native_histogram_arrow_type(),
1774    )?;
1775    let keys = ts_range.keys().values();
1776    if float_range.keys().values() != keys || histogram_range.keys().values() != keys {
1777        return Err(DataFusionError::Execution(format!(
1778            "{name}: timestamp, float, and native histogram ranges should have the same layout"
1779        )));
1780    }
1781    if args.number_rows != keys.len() {
1782        return Err(DataFusionError::Execution(format!(
1783            "{name}: range inputs have {} windows but the batch has {} rows",
1784            keys.len(),
1785            args.number_rows
1786        )));
1787    }
1788
1789    let timestamps = ts_range
1790        .values()
1791        .as_any()
1792        .downcast_ref::<TimestampMillisecondArray>()
1793        .expect("validated timestamp range");
1794    let floats = float_range
1795        .values()
1796        .as_any()
1797        .downcast_ref::<Float64Array>()
1798        .expect("validated float range");
1799    let histograms = histogram_range
1800        .values()
1801        .as_any()
1802        .downcast_ref::<StructArray>()
1803        .expect("validated native histogram range");
1804    if floats.len() != timestamps.len() || histograms.len() != timestamps.len() {
1805        return Err(DataFusionError::Execution(format!(
1806            "{name}: timestamp, float, and native histogram values should be row-aligned"
1807        )));
1808    }
1809
1810    let bounds = checked_window_bounds(keys, timestamps.len(), name)?;
1811    let float_valid = (0..floats.len())
1812        .map(|row| floats.is_valid(row))
1813        .collect::<Vec<_>>();
1814    let histogram_valid = (0..histograms.len())
1815        .map(|row| histograms.is_valid(row))
1816        .collect::<Vec<_>>();
1817    let float_prefix = validity_prefix(&float_valid, name)?;
1818    let histogram_prefix = validity_prefix(&histogram_valid, name)?;
1819
1820    if matches!(function.policy(), MixedRangePolicy::Combined) {
1821        if output != MixedRangeOutput::Float {
1822            return Err(DataFusionError::Execution(format!(
1823                "{name} does not return native histograms"
1824            )));
1825        }
1826        return combined_range_float(
1827            function,
1828            &bounds,
1829            timestamps,
1830            floats,
1831            histograms,
1832            &float_valid,
1833            &histogram_valid,
1834            &float_prefix,
1835            &histogram_prefix,
1836        );
1837    }
1838
1839    let selections = select_mixed_range_lanes(
1840        function,
1841        &bounds,
1842        timestamps,
1843        &float_valid,
1844        &histogram_valid,
1845        &float_prefix,
1846        &histogram_prefix,
1847        &collector,
1848    );
1849    let (lane, values, valid, prefix) = match output {
1850        MixedRangeOutput::Float => (
1851            SampleLane::Float,
1852            floats as &dyn Array,
1853            float_valid.as_slice(),
1854            float_prefix.as_slice(),
1855        ),
1856        MixedRangeOutput::Histogram => (
1857            SampleLane::Histogram,
1858            histograms as &dyn Array,
1859            histogram_valid.as_slice(),
1860            histogram_prefix.as_slice(),
1861        ),
1862    };
1863    let input = compact_lane_input(
1864        timestamps,
1865        values,
1866        valid,
1867        prefix,
1868        &bounds,
1869        &selections,
1870        lane,
1871        name,
1872    )?;
1873
1874    let udf = match output {
1875        MixedRangeOutput::Float => function.float_udf(),
1876        MixedRangeOutput::Histogram => function.histogram_udf(collector),
1877    }
1878    .ok_or_else(|| {
1879        DataFusionError::Execution(format!(
1880            "{name} does not return {} values",
1881            match output {
1882                MixedRangeOutput::Float => "float",
1883                MixedRangeOutput::Histogram => "native histogram",
1884            }
1885        ))
1886    })?;
1887    let mut input = input;
1888    input.extend_from_slice(&args.args[4..]);
1889    invoke_range_udf(udf, input, args, bounds.len())
1890}
1891
1892/// Decodes packed range keys into validated half-open `(offset, end)` bounds.
1893fn checked_window_bounds(
1894    keys: &[i64],
1895    value_len: usize,
1896    name: &str,
1897) -> DfResult<Vec<(usize, usize)>> {
1898    keys.iter()
1899        .map(|key| {
1900            let (offset, length) = unpack(*key);
1901            let offset = offset as usize;
1902            let end = offset
1903                .checked_add(length as usize)
1904                .filter(|end| *end <= value_len)
1905                .ok_or_else(|| {
1906                    DataFusionError::Execution(format!(
1907                        "{name}: invalid range ({offset}, {length}) for {value_len} values"
1908                    ))
1909                })?;
1910            Ok((offset, end))
1911        })
1912        .collect()
1913}
1914
1915/// Builds prefix counts where `prefix[i]` is the number of valid samples in `valid[..i]`.
1916fn validity_prefix(valid: &[bool], name: &str) -> DfResult<Vec<usize>> {
1917    let capacity = valid.len().checked_add(1).ok_or_else(|| {
1918        DataFusionError::Execution(format!("{name}: sample validity length overflow"))
1919    })?;
1920    let mut prefix = Vec::with_capacity(capacity);
1921    prefix.push(0usize);
1922    for is_valid in valid {
1923        prefix.push(
1924            prefix
1925                .last()
1926                .copied()
1927                .unwrap()
1928                .checked_add(usize::from(*is_valid))
1929                .ok_or_else(|| {
1930                    DataFusionError::Execution(format!("{name}: sample count overflow"))
1931                })?,
1932        );
1933    }
1934    Ok(prefix)
1935}
1936
1937/// Returns the number of valid samples in the half-open window `[offset, end)`.
1938fn valid_count(prefix: &[usize], offset: usize, end: usize) -> usize {
1939    prefix[end]
1940        .checked_sub(prefix[offset])
1941        .expect("validity prefix is monotonic")
1942}
1943
1944/// Selects the sample lane for each window and records policy-required annotations.
1945#[allow(clippy::too_many_arguments)]
1946fn select_mixed_range_lanes(
1947    function: MixedRangeFunction,
1948    bounds: &[(usize, usize)],
1949    timestamps: &TimestampMillisecondArray,
1950    float_valid: &[bool],
1951    histogram_valid: &[bool],
1952    float_prefix: &[usize],
1953    histogram_prefix: &[usize],
1954    collector: &Option<PromqlAnnotationCollector>,
1955) -> Vec<Option<SampleLane>> {
1956    bounds
1957        .iter()
1958        .map(|(offset, end)| {
1959            let float_count = valid_count(float_prefix, *offset, *end);
1960            let histogram_count = valid_count(histogram_prefix, *offset, *end);
1961            match function.policy() {
1962                MixedRangePolicy::DropMixed => match (float_count > 0, histogram_count > 0) {
1963                    (true, true) => {
1964                        record_warning(
1965                            collector,
1966                            format!(
1967                                "{}: encountered a mix of float and native histogram samples",
1968                                function.name()
1969                            ),
1970                        );
1971                        None
1972                    }
1973                    (true, false) => Some(SampleLane::Float),
1974                    (false, true) => Some(SampleLane::Histogram),
1975                    (false, false) => None,
1976                },
1977                MixedRangePolicy::FloatOnly => {
1978                    if float_count > 0 && histogram_count > 0 {
1979                        record_info(
1980                            collector,
1981                            format!(
1982                                "{}: ignored native histogram samples",
1983                                function.name()
1984                            ),
1985                        );
1986                    }
1987                    (float_count > 0).then_some(SampleLane::Float)
1988                }
1989                MixedRangePolicy::Last => (*offset..*end).rev().find_map(|row| {
1990                    if histogram_valid[row] {
1991                        Some(SampleLane::Histogram)
1992                    } else if float_valid[row] {
1993                        Some(SampleLane::Float)
1994                    } else {
1995                        None
1996                    }
1997                }),
1998                MixedRangePolicy::LastTwo => {
1999                    let mut last_two = Vec::with_capacity(2);
2000                    for row in (*offset..*end).rev() {
2001                        // Prometheus keeps a float as the newest sample if both lanes have the
2002                        // same timestamp, while the histogram becomes the preceding sample.
2003                        if float_valid[row] {
2004                            last_two.push((SampleLane::Float, timestamps.value(row)));
2005                        }
2006                        if last_two.len() < 2 && histogram_valid[row] {
2007                            last_two.push((SampleLane::Histogram, timestamps.value(row)));
2008                        }
2009                        if last_two.len() == 2 {
2010                            break;
2011                        }
2012                    }
2013                    if last_two.len() < 2 || last_two[0].1 == last_two[1].1 {
2014                        None
2015                    } else if last_two[0].0 == last_two[1].0 {
2016                        Some(last_two[0].0)
2017                    } else {
2018                        record_warning(
2019                            collector,
2020                            format!(
2021                                "{}: encountered a mix of float and native histogram samples in the last two points",
2022                                function.name()
2023                            ),
2024                        );
2025                        None
2026                    }
2027                }
2028                MixedRangePolicy::Combined => unreachable!(),
2029            }
2030        })
2031        .collect()
2032}
2033
2034/// Filters null placeholders from one sample lane and remaps its selected windows.
2035/// Windows assigned to the other lane become empty.
2036#[allow(clippy::too_many_arguments)]
2037fn compact_lane_input(
2038    timestamps: &TimestampMillisecondArray,
2039    values: &dyn Array,
2040    valid: &[bool],
2041    prefix: &[usize],
2042    bounds: &[(usize, usize)],
2043    selections: &[Option<SampleLane>],
2044    lane: SampleLane,
2045    name: &str,
2046) -> DfResult<Vec<ColumnarValue>> {
2047    let mask = BooleanArray::from(valid.to_vec());
2048    let filtered_timestamps = filter(timestamps, &mask)?;
2049    let filtered_values = filter(values, &mask)?;
2050    let ranges = bounds
2051        .iter()
2052        .zip(selections)
2053        .map(|((offset, end), selected)| {
2054            let compact_offset = prefix[*offset];
2055            let compact_length = if *selected == Some(lane) {
2056                valid_count(prefix, *offset, *end)
2057            } else {
2058                0
2059            };
2060            Ok((
2061                u32::try_from(compact_offset).map_err(|_| {
2062                    DataFusionError::Execution(format!(
2063                        "{name}: compacted range offset exceeds u32"
2064                    ))
2065                })?,
2066                u32::try_from(compact_length).map_err(|_| {
2067                    DataFusionError::Execution(format!(
2068                        "{name}: compacted range length exceeds u32"
2069                    ))
2070                })?,
2071            ))
2072        })
2073        .collect::<DfResult<Vec<_>>>()?;
2074    let timestamp_range = RangeArray::from_ranges(filtered_timestamps, ranges.clone())
2075        .map_err(DataFusionError::from)?;
2076    let value_range =
2077        RangeArray::from_ranges(filtered_values, ranges).map_err(DataFusionError::from)?;
2078    Ok(vec![
2079        ColumnarValue::Array(Arc::new(timestamp_range.into_dict())),
2080        ColumnarValue::Array(Arc::new(value_range.into_dict())),
2081    ])
2082}
2083
2084fn invoke_range_udf(
2085    udf: ScalarUDF,
2086    input: Vec<ColumnarValue>,
2087    outer_args: &ScalarFunctionArgs,
2088    number_rows: usize,
2089) -> DfResult<ColumnarValue> {
2090    let arg_fields = input
2091        .iter()
2092        .enumerate()
2093        .map(|(index, value)| Arc::new(Field::new(format!("arg_{index}"), value.data_type(), true)))
2094        .collect();
2095    udf.invoke_with_args(ScalarFunctionArgs {
2096        args: input,
2097        arg_fields,
2098        number_rows,
2099        return_field: outer_args.return_field.clone(),
2100        config_options: outer_args.config_options.clone(),
2101    })
2102}
2103
2104enum MixedSample {
2105    Float(f64),
2106    Histogram(NativeHistogram),
2107}
2108
2109#[allow(clippy::too_many_arguments)]
2110fn combined_range_float(
2111    function: MixedRangeFunction,
2112    bounds: &[(usize, usize)],
2113    timestamps: &TimestampMillisecondArray,
2114    floats: &Float64Array,
2115    histograms: &StructArray,
2116    float_valid: &[bool],
2117    histogram_valid: &[bool],
2118    float_prefix: &[usize],
2119    histogram_prefix: &[usize],
2120) -> DfResult<ColumnarValue> {
2121    let mut result = Float64Builder::with_capacity(bounds.len());
2122    for (offset, end) in bounds {
2123        let sample_count = valid_count(float_prefix, *offset, *end)
2124            .checked_add(valid_count(histogram_prefix, *offset, *end))
2125            .ok_or_else(|| {
2126                DataFusionError::Execution(format!("{}: sample count overflow", function.name()))
2127            })?;
2128        match function {
2129            MixedRangeFunction::CountOverTime => {
2130                if sample_count == 0 {
2131                    result.append_null();
2132                } else {
2133                    result.append_value(sample_count as f64);
2134                }
2135            }
2136            MixedRangeFunction::AbsentOverTime => {
2137                if sample_count == 0 {
2138                    result.append_value(1.0);
2139                } else {
2140                    result.append_null();
2141                }
2142            }
2143            MixedRangeFunction::PresentOverTime => {
2144                if sample_count == 0 {
2145                    result.append_null();
2146                } else {
2147                    result.append_value(1.0);
2148                }
2149            }
2150            MixedRangeFunction::Changes | MixedRangeFunction::Resets => {
2151                if sample_count == 0 {
2152                    result.append_null();
2153                    continue;
2154                }
2155                let mut count = 0usize;
2156                let mut previous = None;
2157                for row in *offset..*end {
2158                    if float_valid[row] {
2159                        count += mixed_transition(
2160                            function,
2161                            &mut previous,
2162                            timestamps.value(row),
2163                            MixedSample::Float(floats.value(row)),
2164                        );
2165                    }
2166                    if histogram_valid[row] {
2167                        let histogram = read_histogram(histograms, row)?
2168                            .expect("validated native histogram sample");
2169                        count += mixed_transition(
2170                            function,
2171                            &mut previous,
2172                            timestamps.value(row),
2173                            MixedSample::Histogram(histogram),
2174                        );
2175                    }
2176                }
2177                result.append_value(count as f64);
2178            }
2179            _ => {
2180                return Err(DataFusionError::Internal(format!(
2181                    "{} does not support combined range evaluation",
2182                    function.name()
2183                )));
2184            }
2185        }
2186    }
2187    Ok(ColumnarValue::Array(Arc::new(result.finish())))
2188}
2189
2190fn mixed_transition(
2191    function: MixedRangeFunction,
2192    previous: &mut Option<(i64, MixedSample)>,
2193    timestamp: i64,
2194    current: MixedSample,
2195) -> usize {
2196    let changed = previous.as_ref().is_some_and(|(previous_ts, previous)| {
2197        match (function, previous, &current) {
2198            (
2199                MixedRangeFunction::Changes,
2200                MixedSample::Float(previous),
2201                MixedSample::Float(current),
2202            ) => current != previous && !(current.is_nan() && previous.is_nan()),
2203            (
2204                MixedRangeFunction::Changes,
2205                MixedSample::Histogram(previous),
2206                MixedSample::Histogram(current),
2207            ) => !current.promql_eq(previous),
2208            (MixedRangeFunction::Changes, _, _) => true,
2209            (
2210                MixedRangeFunction::Resets,
2211                MixedSample::Float(previous),
2212                MixedSample::Float(current),
2213            ) => current < previous,
2214            (
2215                MixedRangeFunction::Resets,
2216                MixedSample::Histogram(previous),
2217                MixedSample::Histogram(current),
2218            ) => {
2219                (previous.reset_hint == GAUGE_RESET_HINT)
2220                    != (current.reset_hint == GAUGE_RESET_HINT)
2221                    || current.detect_counter_reset(previous, *previous_ts, timestamp)
2222            }
2223            (MixedRangeFunction::Resets, _, _) => true,
2224            _ => unreachable!(),
2225        }
2226    });
2227    *previous = Some((timestamp, current));
2228    usize::from(changed)
2229}
2230
2231fn native_histogram_scalar(histogram: Option<NativeHistogram>) -> ScalarValue {
2232    let array = build_histogram_array(&[histogram]);
2233    let histogram = array
2234        .as_any()
2235        .downcast_ref::<StructArray>()
2236        .expect("native histogram array is a StructArray")
2237        .clone();
2238    ScalarValue::Struct(Arc::new(histogram))
2239}
2240
2241fn native_histogram_aggregate_udf(
2242    kind: NativeHistogramAggregateKind,
2243    collector: Option<PromqlAnnotationCollector>,
2244) -> AggregateUDF {
2245    let state_types = if kind.needs_count() {
2246        vec![
2247            native_histogram_arrow_type(),
2248            DataType::UInt64,
2249            DataType::Boolean,
2250            DataType::Boolean,
2251            DataType::Boolean,
2252        ]
2253    } else {
2254        vec![
2255            native_histogram_arrow_type(),
2256            DataType::Boolean,
2257            DataType::Boolean,
2258            DataType::Boolean,
2259        ]
2260    };
2261
2262    create_udaf(
2263        kind.name(),
2264        vec![native_histogram_arrow_type()],
2265        Arc::new(native_histogram_arrow_type()),
2266        Volatility::Volatile,
2267        Arc::new(move |args| {
2268            NativeHistogramAggregateAccumulator::from_args(kind, collector.clone(), args)
2269        }),
2270        Arc::new(state_types),
2271    )
2272}
2273
2274impl DfAccumulator for NativeHistogramAggregateAccumulator {
2275    fn update_batch(&mut self, values: &[ArrayRef]) -> DfResult<()> {
2276        let histograms = values
2277            .first()
2278            .and_then(|array| array.as_any().downcast_ref::<StructArray>())
2279            .ok_or_else(|| {
2280                DataFusionError::Execution(format!(
2281                    "{}: expected native histogram struct input",
2282                    self.kind.name()
2283                ))
2284            })?;
2285
2286        for row in 0..histograms.len() {
2287            let Some(histogram) = read_histogram(histograms, row)? else {
2288                continue;
2289            };
2290            self.push_histogram(histogram, 1)?;
2291        }
2292
2293        Ok(())
2294    }
2295
2296    fn evaluate(&mut self) -> DfResult<ScalarValue> {
2297        let histogram = match (self.kind, self.dropped_incompatible, self.value.clone()) {
2298            (_, true, _) => None,
2299            (_, false, None) => None,
2300            (NativeHistogramAggregateKind::Sum, false, value) => value,
2301            (NativeHistogramAggregateKind::Avg, false, Some(value)) if self.count > 0 => {
2302                Some(value)
2303            }
2304            (NativeHistogramAggregateKind::Avg, _, _) => None,
2305        };
2306
2307        Ok(native_histogram_scalar(histogram))
2308    }
2309
2310    fn size(&self) -> usize {
2311        size_of::<Self>()
2312            + self.value.as_ref().map_or(0, |histogram| {
2313                histogram.custom_values.capacity() * size_of::<f64>()
2314                    + histogram.positive_spans.capacity() * size_of::<Span>()
2315                    + histogram.negative_spans.capacity() * size_of::<Span>()
2316                    + histogram.positive_buckets.capacity() * size_of::<f64>()
2317                    + histogram.negative_buckets.capacity() * size_of::<f64>()
2318            })
2319    }
2320
2321    fn state(&mut self) -> DfResult<Vec<ScalarValue>> {
2322        let mut state = vec![native_histogram_scalar(self.value.clone())];
2323        if self.kind.needs_count() {
2324            state.push(ScalarValue::UInt64(Some(self.count)));
2325        }
2326        state.push(ScalarValue::Boolean(Some(self.dropped_incompatible)));
2327        state.push(ScalarValue::Boolean(Some(self.counter_reset_seen)));
2328        state.push(ScalarValue::Boolean(Some(self.not_counter_reset_seen)));
2329        Ok(state)
2330    }
2331
2332    fn merge_batch(&mut self, states: &[ArrayRef]) -> DfResult<()> {
2333        if states.is_empty() {
2334            return Ok(());
2335        }
2336
2337        let histograms = states[0]
2338            .as_any()
2339            .downcast_ref::<StructArray>()
2340            .ok_or_else(|| {
2341                DataFusionError::Execution(format!(
2342                    "{}: expected native histogram struct state",
2343                    self.kind.name()
2344                ))
2345            })?;
2346        let counts = if self.kind.needs_count() {
2347            Some(
2348                states
2349                    .get(1)
2350                    .and_then(|array| array.as_any().downcast_ref::<UInt64Array>())
2351                    .ok_or_else(|| {
2352                        DataFusionError::Execution(format!(
2353                            "{}: expected UInt64 count state",
2354                            self.kind.name()
2355                        ))
2356                    })?,
2357            )
2358        } else {
2359            None
2360        };
2361        let dropped_index = if self.kind.needs_count() { 2 } else { 1 };
2362        let dropped = states
2363            .get(dropped_index)
2364            .and_then(|array| array.as_any().downcast_ref::<BooleanArray>())
2365            .ok_or_else(|| {
2366                DataFusionError::Execution(format!(
2367                    "{}: expected Boolean dropped state",
2368                    self.kind.name()
2369                ))
2370            })?;
2371        let counter_reset_seen = states
2372            .get(dropped_index + 1)
2373            .and_then(|array| array.as_any().downcast_ref::<BooleanArray>())
2374            .ok_or_else(|| {
2375                DataFusionError::Execution(format!(
2376                    "{}: expected Boolean counter reset state",
2377                    self.kind.name()
2378                ))
2379            })?;
2380        let not_counter_reset_seen = states
2381            .get(dropped_index + 2)
2382            .and_then(|array| array.as_any().downcast_ref::<BooleanArray>())
2383            .ok_or_else(|| {
2384                DataFusionError::Execution(format!(
2385                    "{}: expected Boolean not-counter-reset state",
2386                    self.kind.name()
2387                ))
2388            })?;
2389
2390        for row in 0..histograms.len() {
2391            self.observe_reset_hints(
2392                counter_reset_seen.value(row),
2393                not_counter_reset_seen.value(row),
2394            );
2395            if dropped.value(row) {
2396                self.mark_incompatible();
2397            }
2398            if self.dropped_incompatible {
2399                continue;
2400            }
2401            let Some(histogram) = read_histogram(histograms, row)? else {
2402                continue;
2403            };
2404            let count = counts.map(|counts| counts.value(row)).unwrap_or(1);
2405            self.push_histogram(histogram, count)?;
2406        }
2407
2408        Ok(())
2409    }
2410}
2411
2412fn histogram_delta(
2413    samples: &[NativeHistogram],
2414    timestamps: &[i64],
2415    is_counter: bool,
2416) -> Option<NativeHistogram> {
2417    if samples.len() < 2 || samples.len() != timestamps.len() {
2418        return None;
2419    }
2420
2421    if !is_counter {
2422        return samples
2423            .last()?
2424            .sub(samples.first()?)
2425            .map(NativeHistogram::into_gauge);
2426    }
2427
2428    let first_reset = samples[1].detect_counter_reset(&samples[0], timestamps[0], timestamps[1]);
2429    let (initial, reset_scan_start) = if first_reset {
2430        // The first sample is irrelevant after an immediate reset. Adopt the
2431        // second sample's layout so an incompatible pre-reset layout is ignored.
2432        (samples[1].zero_like(), 2)
2433    } else {
2434        (samples[0].clone(), 1)
2435    };
2436    let mut result = samples.last()?.sub(&initial)?;
2437    for index in reset_scan_start..samples.len() {
2438        if samples[index].detect_counter_reset(
2439            &samples[index - 1],
2440            timestamps[index - 1],
2441            timestamps[index],
2442        ) {
2443            result = result.add(&samples[index - 1])?;
2444        }
2445    }
2446    Some(result.into_gauge())
2447}
2448
2449fn idelta_value(
2450    samples: &[NativeHistogram],
2451    is_rate: bool,
2452    previous_ts: i64,
2453    current_ts: i64,
2454    sampled_interval_secs: f64,
2455) -> Option<NativeHistogram> {
2456    if samples.len() < 2 {
2457        return None;
2458    }
2459    let previous = &samples[samples.len() - 2];
2460    let current = samples.last()?;
2461    let result = if is_rate && current.detect_counter_reset(previous, previous_ts, current_ts) {
2462        current.clone()
2463    } else {
2464        current.sub(previous)?
2465    };
2466    Some(
2467        if is_rate {
2468            result.scale(1.0 / sampled_interval_secs)
2469        } else {
2470            result
2471        }
2472        .into_gauge(),
2473    )
2474}
2475
2476fn native_extrapolated_rate<const IS_COUNTER: bool, const IS_RATE: bool>(
2477    input: &[ColumnarValue],
2478    range_length: i64,
2479    func_name: &'static str,
2480    collector: Option<PromqlAnnotationCollector>,
2481) -> DfResult<ColumnarValue> {
2482    if input.len() != 4 {
2483        return Err(DataFusionError::Plan(format!(
2484            "{func_name} function should have 4 inputs"
2485        )));
2486    }
2487
2488    let ts_dict = extract_range_dict(
2489        &input[0],
2490        func_name,
2491        "timestamp range vector",
2492        &DataType::Timestamp(TimeUnit::Millisecond, None),
2493    )?;
2494    let value_dict = extract_range_dict(
2495        &input[1],
2496        func_name,
2497        "value range vector",
2498        &native_histogram_arrow_type(),
2499    )?;
2500    let eval_ts = extract_array(&input[2])?;
2501    let eval_ts = eval_ts
2502        .as_any()
2503        .downcast_ref::<TimestampMillisecondArray>()
2504        .ok_or_else(|| {
2505            DataFusionError::Execution(format!(
2506                "{func_name}: expect evaluation timestamp vector as Timestamp(Millisecond), found {}",
2507                eval_ts.data_type()
2508            ))
2509        })?;
2510
2511    let keys = ts_dict.keys().values();
2512    if value_dict.keys().values() != keys || eval_ts.len() != keys.len() {
2513        return Err(DataFusionError::Execution(format!(
2514            "{func_name}: timestamp, value, and evaluation ranges should have the same layout"
2515        )));
2516    }
2517
2518    let all_timestamps = ts_dict
2519        .values()
2520        .as_any()
2521        .downcast_ref::<TimestampMillisecondArray>()
2522        .expect("validated timestamp range")
2523        .values();
2524    let all_histograms = value_dict
2525        .values()
2526        .as_any()
2527        .downcast_ref::<StructArray>()
2528        .expect("validated native histogram range");
2529    let range_length_secs = range_length as f64 / 1000.0;
2530    let mut result = Vec::with_capacity(keys.len());
2531
2532    for index in 0..keys.len() {
2533        let (raw_offset, raw_length) = unpack(keys[index]);
2534        let offset = raw_offset as usize;
2535        let length = raw_length as usize;
2536        if length == 0 {
2537            result.push(None);
2538            continue;
2539        }
2540
2541        let mut samples = Vec::with_capacity(length);
2542        let mut has_null = false;
2543        for row in offset..offset + length {
2544            let Some(histogram) = read_histogram(all_histograms, row)? else {
2545                has_null = true;
2546                break;
2547            };
2548            samples.push(histogram);
2549        }
2550        if has_null {
2551            result.push(None);
2552            continue;
2553        }
2554
2555        let first_ts = all_timestamps[offset];
2556        let last_ts = all_timestamps[offset + length - 1];
2557        let range_end = eval_ts.value(index);
2558        let range_start = range_end - range_length;
2559        let synthetic_zero_timestamp = IS_COUNTER
2560            .then_some(samples[0].start_timestamp)
2561            .flatten()
2562            .filter(|start| *start != 0 && range_start < *start && *start < first_ts);
2563        let synthetic_zero_start = synthetic_zero_timestamp.is_some();
2564        if length < 2 && !synthetic_zero_start {
2565            result.push(None);
2566            continue;
2567        }
2568
2569        let wrong_flavor = if IS_COUNTER {
2570            samples
2571                .iter()
2572                .any(|histogram| histogram.reset_hint == GAUGE_RESET_HINT)
2573        } else {
2574            samples[0].reset_hint != GAUGE_RESET_HINT
2575                || samples[samples.len() - 1].reset_hint != GAUGE_RESET_HINT
2576        };
2577        if wrong_flavor {
2578            let expected = if IS_COUNTER { "counter" } else { "gauge" };
2579            record_warning(
2580                &collector,
2581                format!("{func_name}: native histogram input should be a {expected} histogram"),
2582            );
2583        }
2584
2585        let timestamps = &all_timestamps[offset..offset + length];
2586        for pair in samples.windows(2) {
2587            record_custom_reconciliation(&collector, func_name, &pair[0], &pair[1]);
2588        }
2589        let mut histogram = if length == 1 {
2590            samples[0].clone()
2591        } else if let Some(histogram) = histogram_delta(&samples, timestamps, IS_COUNTER) {
2592            histogram
2593        } else {
2594            record_warning(
2595                &collector,
2596                format!("{func_name}: dropped native histogram range with incompatible schemas"),
2597            );
2598            result.push(None);
2599            continue;
2600        };
2601        if synthetic_zero_start && length > 1 {
2602            let Some(with_synthetic_zero) = histogram.add(&samples[0]) else {
2603                record_warning(
2604                    &collector,
2605                    format!(
2606                        "{func_name}: dropped native histogram range with incompatible schemas"
2607                    ),
2608                );
2609                result.push(None);
2610                continue;
2611            };
2612            histogram = with_synthetic_zero;
2613        }
2614
2615        let real_sampled_interval_ms = (last_ts - first_ts) as f64;
2616        let sampled_interval_ms = synthetic_zero_timestamp
2617            .map(|start| (last_ts - start) as f64)
2618            .unwrap_or(real_sampled_interval_ms);
2619        if sampled_interval_ms <= 0.0 {
2620            result.push(None);
2621            continue;
2622        }
2623        let average_interval_ms = if length > 1 {
2624            real_sampled_interval_ms / (length - 1) as f64
2625        } else {
2626            0.0
2627        };
2628        let mut duration_to_start_ms = if synthetic_zero_start {
2629            0.0
2630        } else {
2631            (first_ts - range_start) as f64
2632        };
2633        let duration_to_end_ms = (range_end - last_ts) as f64;
2634
2635        if IS_COUNTER && !synthetic_zero_start && histogram.count > 0.0 && samples[0].count >= 0.0 {
2636            let duration_to_zero = sampled_interval_ms * (samples[0].count / histogram.count);
2637            if duration_to_zero < duration_to_start_ms {
2638                duration_to_start_ms = duration_to_zero;
2639            }
2640        }
2641
2642        let extrapolation_threshold = average_interval_ms * 1.1;
2643        let mut extrapolated_interval_ms = sampled_interval_ms;
2644        if duration_to_start_ms < extrapolation_threshold {
2645            extrapolated_interval_ms += duration_to_start_ms;
2646        } else {
2647            extrapolated_interval_ms += average_interval_ms / 2.0;
2648        }
2649        if duration_to_end_ms < extrapolation_threshold {
2650            extrapolated_interval_ms += duration_to_end_ms;
2651        } else {
2652            extrapolated_interval_ms += average_interval_ms / 2.0;
2653        }
2654
2655        let mut factor = extrapolated_interval_ms / sampled_interval_ms;
2656        if IS_RATE {
2657            factor /= range_length_secs;
2658        }
2659        histogram = histogram.scale(factor).into_gauge();
2660        result.push(Some(histogram));
2661    }
2662
2663    Ok(ColumnarValue::Array(build_histogram_array(&result)))
2664}
2665
2666fn create_native_extrapolated_udf<const IS_COUNTER: bool, const IS_RATE: bool>(
2667    name: &'static str,
2668    collector: Option<PromqlAnnotationCollector>,
2669) -> ScalarUDF {
2670    let input_types = vec![
2671        RangeArray::convert_data_type(DataType::Timestamp(TimeUnit::Millisecond, None)),
2672        RangeArray::convert_data_type(native_histogram_arrow_type()),
2673        DataType::Timestamp(TimeUnit::Millisecond, None),
2674        DataType::Int64,
2675    ];
2676    create_udf(
2677        name,
2678        input_types,
2679        native_histogram_arrow_type(),
2680        Volatility::Volatile,
2681        Arc::new(move |input: &[ColumnarValue]| {
2682            let range_length = extract_array(&input[3])?;
2683            let range_length = range_length
2684                .as_any()
2685                .downcast_ref::<Int64Array>()
2686                .ok_or_else(|| {
2687                    DataFusionError::Execution(format!(
2688                        "{name}: expect Int64 as range length type, found {}",
2689                        range_length.data_type()
2690                    ))
2691                })?;
2692            if range_length.is_empty() || range_length.is_null(0) {
2693                return Err(DataFusionError::Execution(format!(
2694                    "{name}: range length must contain a non-null Int64 value"
2695                )));
2696            }
2697            native_extrapolated_rate::<IS_COUNTER, IS_RATE>(
2698                input,
2699                range_length.value(0),
2700                name,
2701                collector.clone(),
2702            )
2703        }) as _,
2704    )
2705}
2706
2707pub struct NativeHistogramDelta;
2708pub struct NativeHistogramRate;
2709pub struct NativeHistogramIncrease;
2710
2711impl NativeHistogramDelta {
2712    pub const fn name() -> &'static str {
2713        "prom_native_histogram_delta"
2714    }
2715
2716    pub fn scalar_udf() -> ScalarUDF {
2717        Self::scalar_udf_with_collector(None)
2718    }
2719
2720    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
2721        create_native_extrapolated_udf::<false, false>(Self::name(), collector)
2722    }
2723}
2724
2725impl NativeHistogramRate {
2726    pub const fn name() -> &'static str {
2727        "prom_native_histogram_rate"
2728    }
2729
2730    pub fn scalar_udf() -> ScalarUDF {
2731        Self::scalar_udf_with_collector(None)
2732    }
2733
2734    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
2735        create_native_extrapolated_udf::<true, true>(Self::name(), collector)
2736    }
2737}
2738
2739impl NativeHistogramIncrease {
2740    pub const fn name() -> &'static str {
2741        "prom_native_histogram_increase"
2742    }
2743
2744    pub fn scalar_udf() -> ScalarUDF {
2745        Self::scalar_udf_with_collector(None)
2746    }
2747
2748    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
2749        create_native_extrapolated_udf::<true, false>(Self::name(), collector)
2750    }
2751}
2752
2753fn native_idelta<const IS_RATE: bool>(
2754    input: &[ColumnarValue],
2755    func_name: &'static str,
2756    collector: Option<PromqlAnnotationCollector>,
2757) -> DfResult<ColumnarValue> {
2758    if input.len() != 2 {
2759        return Err(DataFusionError::Plan(format!(
2760            "{func_name} function should have 2 inputs"
2761        )));
2762    }
2763
2764    let ts_range = extract_range_dict(
2765        &input[0],
2766        func_name,
2767        "timestamp range vector",
2768        &DataType::Timestamp(TimeUnit::Millisecond, None),
2769    )?;
2770    let value_range = extract_range_dict(
2771        &input[1],
2772        func_name,
2773        "value range vector",
2774        &native_histogram_arrow_type(),
2775    )?;
2776
2777    if ts_range.keys().values() != value_range.keys().values() {
2778        return Err(DataFusionError::Execution(format!(
2779            "{func_name}: timestamp and value ranges should have the same window layout"
2780        )));
2781    }
2782
2783    let ts_values = ts_range
2784        .values()
2785        .as_any()
2786        .downcast_ref::<TimestampMillisecondArray>()
2787        .expect("validated timestamp range")
2788        .values();
2789    let histograms = value_range
2790        .values()
2791        .as_any()
2792        .downcast_ref::<StructArray>()
2793        .expect("validated native histogram range");
2794    let mut result = Vec::with_capacity(ts_range.keys().len());
2795
2796    for key in ts_range.keys().values() {
2797        let (offset, length) = unpack(*key);
2798        let offset = offset as usize;
2799        let length = length as usize;
2800        if length < 2 {
2801            result.push(None);
2802            continue;
2803        }
2804
2805        let mut samples = Vec::with_capacity(2);
2806        let mut has_null = false;
2807        for row in offset + length - 2..offset + length {
2808            let Some(histogram) = read_histogram(histograms, row)? else {
2809                has_null = true;
2810                break;
2811            };
2812            samples.push(histogram);
2813        }
2814        if has_null {
2815            result.push(None);
2816            continue;
2817        }
2818
2819        let wrong_flavor = samples.iter().any(|histogram| {
2820            if IS_RATE {
2821                histogram.reset_hint == GAUGE_RESET_HINT
2822            } else {
2823                histogram.reset_hint != GAUGE_RESET_HINT
2824            }
2825        });
2826        if wrong_flavor {
2827            let expected = if IS_RATE { "counter" } else { "gauge" };
2828            record_warning(
2829                &collector,
2830                format!("{func_name}: native histogram input should be a {expected} histogram"),
2831            );
2832        }
2833
2834        let sampled_interval_secs =
2835            (ts_values[offset + length - 1] - ts_values[offset + length - 2]) as f64 / 1000.0;
2836        if sampled_interval_secs <= 0.0 {
2837            result.push(None);
2838            continue;
2839        }
2840        record_custom_reconciliation(&collector, func_name, &samples[0], &samples[1]);
2841        let value = idelta_value(
2842            &samples,
2843            IS_RATE,
2844            ts_values[offset + length - 2],
2845            ts_values[offset + length - 1],
2846            sampled_interval_secs,
2847        );
2848        if value.is_none() {
2849            record_warning(
2850                &collector,
2851                format!("{func_name}: dropped native histogram range with incompatible schemas"),
2852            );
2853        }
2854        result.push(value);
2855    }
2856
2857    Ok(ColumnarValue::Array(build_histogram_array(&result)))
2858}
2859
2860fn create_native_idelta_udf<const IS_RATE: bool>(
2861    name: &'static str,
2862    collector: Option<PromqlAnnotationCollector>,
2863) -> ScalarUDF {
2864    create_udf(
2865        name,
2866        vec![
2867            RangeArray::convert_data_type(DataType::Timestamp(TimeUnit::Millisecond, None)),
2868            RangeArray::convert_data_type(native_histogram_arrow_type()),
2869        ],
2870        native_histogram_arrow_type(),
2871        Volatility::Volatile,
2872        Arc::new(move |input: &[ColumnarValue]| {
2873            native_idelta::<IS_RATE>(input, name, collector.clone())
2874        }) as _,
2875    )
2876}
2877
2878pub struct NativeHistogramIDelta;
2879pub struct NativeHistogramIRate;
2880
2881impl NativeHistogramIDelta {
2882    pub const fn name() -> &'static str {
2883        "prom_native_histogram_idelta"
2884    }
2885
2886    pub fn scalar_udf() -> ScalarUDF {
2887        Self::scalar_udf_with_collector(None)
2888    }
2889
2890    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
2891        create_native_idelta_udf::<false>(Self::name(), collector)
2892    }
2893}
2894
2895impl NativeHistogramIRate {
2896    pub const fn name() -> &'static str {
2897        "prom_native_histogram_irate"
2898    }
2899
2900    pub fn scalar_udf() -> ScalarUDF {
2901        Self::scalar_udf_with_collector(None)
2902    }
2903
2904    pub fn scalar_udf_with_collector(collector: Option<PromqlAnnotationCollector>) -> ScalarUDF {
2905        create_native_idelta_udf::<true>(Self::name(), collector)
2906    }
2907}
2908
2909#[cfg(test)]
2910mod tests {
2911    use datafusion::arrow::datatypes::Field;
2912    use datafusion_common::config::ConfigOptions;
2913    use datafusion_expr::ScalarFunctionArgs;
2914
2915    use super::*;
2916
2917    fn sample_histogram(count: f64, sum: f64, positive_buckets: Vec<f64>) -> NativeHistogram {
2918        NativeHistogram {
2919            schema: 0,
2920            zero_threshold: 0.0,
2921            sum,
2922            reset_hint: UNKNOWN_COUNTER_RESET_HINT,
2923            start_timestamp: None,
2924            custom_values: Vec::new(),
2925            positive_spans: vec![Span {
2926                offset: 0,
2927                length: positive_buckets.len() as i32,
2928            }],
2929            negative_spans: Vec::new(),
2930            count,
2931            zero_count: 0.0,
2932            positive_buckets,
2933            negative_buckets: Vec::new(),
2934        }
2935    }
2936
2937    fn run_scalar_udf(udf: ScalarUDF, input: Vec<ColumnarValue>) -> f64 {
2938        let result = run_udf(udf, input, DataType::Float64);
2939        extract_array(&result)
2940            .unwrap()
2941            .as_any()
2942            .downcast_ref::<Float64Array>()
2943            .unwrap()
2944            .value(0)
2945    }
2946
2947    fn run_udf(udf: ScalarUDF, input: Vec<ColumnarValue>, return_type: DataType) -> ColumnarValue {
2948        let arg_fields = input
2949            .iter()
2950            .enumerate()
2951            .map(|(idx, input)| Arc::new(Field::new(format!("arg_{idx}"), input.data_type(), true)))
2952            .collect();
2953        let args = ScalarFunctionArgs {
2954            args: input,
2955            arg_fields,
2956            number_rows: 1,
2957            return_field: Arc::new(Field::new("result", return_type, true)),
2958            config_options: Arc::new(ConfigOptions::default()),
2959        };
2960
2961        udf.invoke_with_args(args).unwrap()
2962    }
2963
2964    fn run_histogram_udf(udf: ScalarUDF, input: Vec<ColumnarValue>) -> NativeHistogram {
2965        let result = run_udf(udf, input, native_histogram_arrow_type());
2966        let array = extract_array(&result)
2967            .unwrap()
2968            .as_any()
2969            .downcast_ref::<StructArray>()
2970            .unwrap()
2971            .clone();
2972        read_histogram(&array, 0).unwrap().unwrap()
2973    }
2974
2975    fn evaluated_histogram(
2976        accumulator: &mut NativeHistogramAggregateAccumulator,
2977    ) -> NativeHistogram {
2978        let ScalarValue::Struct(array) = accumulator.evaluate().unwrap() else {
2979            panic!("native histogram accumulator returned a non-struct value");
2980        };
2981        read_histogram(&array, 0).unwrap().unwrap()
2982    }
2983
2984    fn histogram_range_input(values: Vec<Option<NativeHistogram>>) -> Vec<ColumnarValue> {
2985        let timestamps = Arc::new(TimestampMillisecondArray::from_iter(
2986            (0..values.len()).map(|idx| Some((idx as i64 + 1) * 1000)),
2987        ));
2988        let histograms = build_histogram_array(&values);
2989        let range = [(0, values.len() as u32)];
2990        let ts_range = RangeArray::from_ranges(timestamps, range).unwrap();
2991        let value_range = RangeArray::from_ranges(histograms, range).unwrap();
2992
2993        vec![
2994            ColumnarValue::Array(Arc::new(ts_range.into_dict())),
2995            ColumnarValue::Array(Arc::new(value_range.into_dict())),
2996        ]
2997    }
2998
2999    fn mixed_range_input(
3000        name: &str,
3001        floats: Vec<Option<f64>>,
3002        histograms: Vec<Option<NativeHistogram>>,
3003    ) -> Vec<ColumnarValue> {
3004        assert_eq!(floats.len(), histograms.len());
3005        let timestamps = Arc::new(TimestampMillisecondArray::from_iter(
3006            (0..floats.len()).map(|idx| Some((idx as i64 + 1) * 1000)),
3007        ));
3008        let floats = Arc::new(Float64Array::from(floats));
3009        let histograms = build_histogram_array(&histograms);
3010        let range = [(0, u32::try_from(floats.len()).unwrap())];
3011        vec![
3012            ColumnarValue::Scalar(ScalarValue::Utf8(Some(name.to_string()))),
3013            ColumnarValue::Array(Arc::new(
3014                RangeArray::from_ranges(timestamps, range)
3015                    .unwrap()
3016                    .into_dict(),
3017            )),
3018            ColumnarValue::Array(Arc::new(
3019                RangeArray::from_ranges(floats, range).unwrap().into_dict(),
3020            )),
3021            ColumnarValue::Array(Arc::new(
3022                RangeArray::from_ranges(histograms, range)
3023                    .unwrap()
3024                    .into_dict(),
3025            )),
3026        ]
3027    }
3028
3029    fn mixed_float_result(udf: ScalarUDF, input: Vec<ColumnarValue>) -> Option<f64> {
3030        let result = run_udf(udf, input, DataType::Float64);
3031        let result = extract_array(&result).unwrap();
3032        let result = result.as_any().downcast_ref::<Float64Array>().unwrap();
3033        result.is_valid(0).then(|| result.value(0))
3034    }
3035
3036    fn mixed_histogram_result(
3037        udf: ScalarUDF,
3038        input: Vec<ColumnarValue>,
3039    ) -> Option<NativeHistogram> {
3040        let result = run_udf(udf, input, native_histogram_arrow_type());
3041        let result = extract_array(&result).unwrap();
3042        let result = result.as_any().downcast_ref::<StructArray>().unwrap();
3043        read_histogram(result, 0).unwrap()
3044    }
3045
3046    fn run_histogram_range_udf(
3047        udf: ScalarUDF,
3048        histograms: Vec<NativeHistogram>,
3049    ) -> NativeHistogram {
3050        run_histogram_udf(
3051            udf,
3052            histogram_range_input(histograms.into_iter().map(Some).collect()),
3053        )
3054    }
3055
3056    fn run_float_range_udf(
3057        udf: ScalarUDF,
3058        histograms: Vec<Option<NativeHistogram>>,
3059    ) -> Option<f64> {
3060        let result = run_udf(udf, histogram_range_input(histograms), DataType::Float64);
3061        let result = extract_array(&result).unwrap();
3062        let result = result.as_any().downcast_ref::<Float64Array>().unwrap();
3063        (!result.is_null(0)).then(|| result.value(0))
3064    }
3065
3066    fn run_extrapolated_histogram_udf(
3067        udf: ScalarUDF,
3068        histograms: Vec<NativeHistogram>,
3069    ) -> NativeHistogram {
3070        let range_length = histograms.len() as i64 * 1000;
3071        let timestamps = (0..histograms.len())
3072            .map(|idx| (idx as i64 + 1) * 1000)
3073            .collect();
3074        extrapolated_histogram_result(udf, timestamps, histograms, range_length, range_length)
3075            .unwrap()
3076    }
3077
3078    fn extrapolated_histogram_result(
3079        udf: ScalarUDF,
3080        timestamps: Vec<i64>,
3081        histograms: Vec<NativeHistogram>,
3082        range_end: i64,
3083        range_length: i64,
3084    ) -> Option<NativeHistogram> {
3085        assert_eq!(timestamps.len(), histograms.len());
3086        let range = [(0, u32::try_from(histograms.len()).unwrap())];
3087        let timestamps = Arc::new(TimestampMillisecondArray::from(timestamps));
3088        let histograms =
3089            build_histogram_array(&histograms.into_iter().map(Some).collect::<Vec<_>>());
3090        let mut input = vec![
3091            ColumnarValue::Array(Arc::new(
3092                RangeArray::from_ranges(timestamps, range)
3093                    .unwrap()
3094                    .into_dict(),
3095            )),
3096            ColumnarValue::Array(Arc::new(
3097                RangeArray::from_ranges(histograms, range)
3098                    .unwrap()
3099                    .into_dict(),
3100            )),
3101        ];
3102        input.push(ColumnarValue::Array(Arc::new(
3103            TimestampMillisecondArray::from(vec![range_end]),
3104        )));
3105        input.push(ColumnarValue::Array(Arc::new(Int64Array::from(vec![
3106            range_length,
3107        ]))));
3108        let result = run_udf(udf, input, native_histogram_arrow_type());
3109        let result = extract_array(&result).unwrap();
3110        let result = result.as_any().downcast_ref::<StructArray>().unwrap();
3111        read_histogram(result, 0).unwrap()
3112    }
3113
3114    fn collected_warnings(collector: &PromqlAnnotationCollector) -> Vec<String> {
3115        let mut warnings = Vec::new();
3116        collector.append_to(&mut warnings, &mut Vec::new());
3117        warnings
3118    }
3119
3120    fn collected_infos(collector: &PromqlAnnotationCollector) -> Vec<String> {
3121        let mut infos = Vec::new();
3122        collector.append_to(&mut Vec::new(), &mut infos);
3123        infos
3124    }
3125
3126    #[test]
3127    fn quantile_and_fraction_report_nan_observations() {
3128        let histogram = sample_histogram(10.0, f64::NAN, vec![8.0]);
3129        let histogram_arg =
3130            || ColumnarValue::Array(build_histogram_array(&[Some(histogram.clone())]));
3131
3132        let quantile_collector = PromqlAnnotationCollector::default();
3133        let skewed = run_scalar_udf(
3134            NativeHistogramQuantile::scalar_udf_with_collector(Some(quantile_collector.clone())),
3135            vec![
3136                histogram_arg(),
3137                ColumnarValue::Scalar(ScalarValue::Float64(Some(0.5))),
3138            ],
3139        );
3140        assert!(skewed.is_finite());
3141        let nan = run_scalar_udf(
3142            NativeHistogramQuantile::scalar_udf_with_collector(Some(quantile_collector.clone())),
3143            vec![
3144                histogram_arg(),
3145                ColumnarValue::Scalar(ScalarValue::Float64(Some(0.9))),
3146            ],
3147        );
3148        assert!(nan.is_nan());
3149        let infos = collected_infos(&quantile_collector);
3150        assert!(
3151            infos
3152                .iter()
3153                .any(|info| info.ends_with("result is skewed higher"))
3154        );
3155        assert!(infos.iter().any(|info| info.ends_with("result is NaN")));
3156
3157        let fraction_collector = PromqlAnnotationCollector::default();
3158        assert_eq!(
3159            run_scalar_udf(
3160                NativeHistogramFraction::scalar_udf_with_collector(Some(
3161                    fraction_collector.clone(),
3162                )),
3163                vec![
3164                    histogram_arg(),
3165                    ColumnarValue::Scalar(ScalarValue::Float64(Some(f64::NEG_INFINITY))),
3166                    ColumnarValue::Scalar(ScalarValue::Float64(Some(f64::INFINITY))),
3167                ],
3168            ),
3169            0.8
3170        );
3171        assert_eq!(
3172            collected_infos(&fraction_collector),
3173            vec![
3174                "input to histogram_fraction has NaN observations, which are excluded from all fractions"
3175                    .to_string()
3176            ]
3177        );
3178    }
3179
3180    #[test]
3181    fn mixed_ranges_follow_prometheus_sample_type_semantics() {
3182        let first = sample_histogram(1.0, 1.0, vec![1.0]);
3183        let second = sample_histogram(3.0, 3.0, vec![3.0]);
3184        let collector = PromqlAnnotationCollector::default();
3185
3186        let mut rate = mixed_range_input(
3187            "rate",
3188            vec![Some(1.0), None, Some(3.0)],
3189            vec![None, Some(first.clone()), None],
3190        );
3191        rate.push(ColumnarValue::Array(Arc::new(
3192            TimestampMillisecondArray::from(vec![3000]),
3193        )));
3194        rate.push(ColumnarValue::Array(Arc::new(Int64Array::from(vec![3000]))));
3195        assert_eq!(
3196            mixed_float_result(MixedRange::float_udf(Some(collector.clone())), rate.clone()),
3197            None
3198        );
3199        assert_eq!(
3200            mixed_histogram_result(MixedRange::histogram_udf(Some(collector.clone())), rate),
3201            None
3202        );
3203        assert!(
3204            collected_warnings(&collector)
3205                .iter()
3206                .any(|warning| warning.contains("mix of float and native histogram"))
3207        );
3208
3209        let pure_rate = |floats, histograms| {
3210            let mut input = mixed_range_input("rate", floats, histograms);
3211            input.push(ColumnarValue::Array(Arc::new(
3212                TimestampMillisecondArray::from(vec![3000]),
3213            )));
3214            input.push(ColumnarValue::Array(Arc::new(Int64Array::from(vec![3000]))));
3215            input
3216        };
3217        let pure_float = pure_rate(
3218            vec![Some(1.0), Some(2.0), Some(3.0)],
3219            vec![None, None, None],
3220        );
3221        assert_eq!(
3222            mixed_float_result(MixedRange::float_udf(None), pure_float.clone()),
3223            Some(1.0)
3224        );
3225        assert_eq!(
3226            mixed_histogram_result(MixedRange::histogram_udf(None), pure_float),
3227            None
3228        );
3229        let pure_histogram = pure_rate(
3230            vec![None, None, None],
3231            vec![
3232                Some(sample_histogram(1.0, 1.0, vec![1.0])),
3233                Some(sample_histogram(2.0, 2.0, vec![2.0])),
3234                Some(sample_histogram(3.0, 3.0, vec![3.0])),
3235            ],
3236        );
3237        assert_eq!(
3238            mixed_float_result(MixedRange::float_udf(None), pure_histogram.clone()),
3239            None
3240        );
3241        assert_eq!(
3242            mixed_histogram_result(MixedRange::histogram_udf(None), pure_histogram)
3243                .unwrap()
3244                .count,
3245            1.0
3246        );
3247
3248        let idelta = mixed_range_input(
3249            "idelta",
3250            vec![Some(10.0), None, None],
3251            vec![None, Some(first.clone()), Some(second.clone())],
3252        );
3253        assert_eq!(
3254            mixed_float_result(MixedRange::float_udf(None), idelta.clone()),
3255            None
3256        );
3257        assert_eq!(
3258            mixed_histogram_result(MixedRange::histogram_udf(None), idelta)
3259                .unwrap()
3260                .count,
3261            2.0
3262        );
3263
3264        let alternating = || {
3265            mixed_range_input(
3266                "changes",
3267                vec![Some(1.0), None, None, Some(1.0)],
3268                vec![None, Some(first.clone()), Some(first.clone()), None],
3269            )
3270        };
3271        assert_eq!(
3272            mixed_float_result(MixedRange::float_udf(None), alternating()),
3273            Some(2.0)
3274        );
3275        let mut resets = alternating();
3276        resets[0] = ColumnarValue::Scalar(ScalarValue::Utf8(Some("resets".to_string())));
3277        assert_eq!(
3278            mixed_float_result(MixedRange::float_udf(None), resets),
3279            Some(2.0)
3280        );
3281
3282        for (name, expected) in [
3283            ("count_over_time", Some(4.0)),
3284            ("present_over_time", Some(1.0)),
3285            ("absent_over_time", None),
3286        ] {
3287            let mut input = alternating();
3288            input[0] = ColumnarValue::Scalar(ScalarValue::Utf8(Some(name.to_string())));
3289            assert_eq!(
3290                mixed_float_result(MixedRange::float_udf(None), input),
3291                expected,
3292                "{name}"
3293            );
3294        }
3295
3296        let last = mixed_range_input(
3297            "last_over_time",
3298            vec![None, Some(4.0)],
3299            vec![Some(first.clone()), None],
3300        );
3301        assert_eq!(
3302            mixed_float_result(MixedRange::float_udf(None), last.clone()),
3303            Some(4.0)
3304        );
3305        assert_eq!(
3306            mixed_histogram_result(MixedRange::histogram_udf(None), last),
3307            None
3308        );
3309
3310        let collector = PromqlAnnotationCollector::default();
3311        let histogram_only_min =
3312            mixed_range_input("min_over_time", vec![None], vec![Some(first.clone())]);
3313        assert_eq!(
3314            mixed_float_result(
3315                MixedRange::float_udf(Some(collector.clone())),
3316                histogram_only_min,
3317            ),
3318            None
3319        );
3320        assert!(collected_infos(&collector).is_empty());
3321
3322        let min = mixed_range_input(
3323            "min_over_time",
3324            vec![Some(3.0), None, Some(1.0)],
3325            vec![None, Some(first), None],
3326        );
3327        assert_eq!(
3328            mixed_float_result(MixedRange::float_udf(Some(collector.clone())), min),
3329            Some(1.0)
3330        );
3331        assert!(
3332            collected_infos(&collector)
3333                .iter()
3334                .any(|info| info.contains("ignored native histogram"))
3335        );
3336    }
3337
3338    #[test]
3339    fn count_sum_and_avg_read_struct() {
3340        let histograms = vec![Some(sample_histogram(6.0, 10.0, vec![2.0, 4.0]))];
3341        let array = build_histogram_array(&histograms);
3342        let input = vec![ColumnarValue::Array(array)];
3343
3344        let count = run_scalar_udf(NativeHistogramCount::scalar_udf(), input.clone());
3345        assert_eq!(count, 6.0);
3346
3347        let sum = run_scalar_udf(NativeHistogramSum::scalar_udf(), input.clone());
3348        assert_eq!(sum, 10.0);
3349
3350        let avg = run_scalar_udf(NativeHistogramAvg::scalar_udf(), input);
3351        assert_eq!(avg, 10.0 / 6.0);
3352    }
3353
3354    #[test]
3355    fn quantile_uses_bucket_bounds() {
3356        let histogram = sample_histogram(6.0, 10.0, vec![2.0, 4.0]);
3357        assert_eq!(histogram.quantile(0.0), 0.5);
3358        assert!(histogram.quantile(0.5) > 1.0);
3359        assert!(histogram.quantile(0.5) < 2.0);
3360    }
3361
3362    #[test]
3363    fn comparison_observes_explicit_sparse_zero_buckets() {
3364        let mut left = sample_histogram(1.0, 1.0, vec![1.0, 0.0]);
3365        left.reset_hint = COUNTER_RESET_HINT;
3366        left.start_timestamp = Some(1000);
3367        let mut right = sample_histogram(1.0, 1.0, vec![1.0]);
3368        right.reset_hint = NOT_COUNTER_RESET_HINT;
3369        right.start_timestamp = Some(2000);
3370
3371        let result = run_udf(
3372            NativeHistogramEq::scalar_udf(),
3373            vec![
3374                ColumnarValue::Array(build_histogram_array(&[Some(left)])),
3375                ColumnarValue::Array(build_histogram_array(&[Some(right)])),
3376            ],
3377            DataType::Boolean,
3378        );
3379        let values = extract_array(&result).unwrap();
3380        let values = values.as_any().downcast_ref::<BooleanArray>().unwrap();
3381        assert!(!values.value(0));
3382    }
3383
3384    #[test]
3385    fn unary_minus_returns_gauge_histogram() {
3386        let result = run_histogram_udf(
3387            NativeHistogramNeg::scalar_udf(),
3388            vec![ColumnarValue::Array(build_histogram_array(&[Some(
3389                sample_histogram(2.0, 3.0, vec![2.0]),
3390            )]))],
3391        );
3392
3393        assert_eq!(result.reset_hint, GAUGE_RESET_HINT);
3394        assert_eq!(result.count, -2.0);
3395        assert_eq!(result.sum, -3.0);
3396        assert_eq!(result.positive_buckets, vec![-2.0]);
3397    }
3398
3399    #[test]
3400    fn histogram_over_time_functions_preserve_reset_hints() {
3401        let mut first = sample_histogram(1.0, 1.0, vec![1.0]);
3402        first.reset_hint = COUNTER_RESET_HINT;
3403        let mut second = sample_histogram(2.0, 2.0, vec![2.0]);
3404        second.reset_hint = COUNTER_RESET_HINT;
3405
3406        let result = run_histogram_range_udf(
3407            NativeHistogramSumOverTime::scalar_udf(),
3408            vec![first.clone(), second.clone()],
3409        );
3410        assert_eq!(result.reset_hint, COUNTER_RESET_HINT);
3411        assert_eq!(result.count, 3.0);
3412        assert_eq!(result.sum, 3.0);
3413        assert_eq!(result.positive_buckets, vec![3.0]);
3414
3415        let result = run_histogram_range_udf(
3416            NativeHistogramAvgOverTime::scalar_udf(),
3417            vec![first.clone(), second.clone()],
3418        );
3419        assert_eq!(result.reset_hint, COUNTER_RESET_HINT);
3420        assert_eq!(result.count, 1.5);
3421        assert_eq!(result.sum, 1.5);
3422        assert_eq!(result.positive_buckets, vec![1.5]);
3423
3424        let result = run_histogram_range_udf(
3425            NativeHistogramLastOverTime::scalar_udf(),
3426            vec![first, second],
3427        );
3428        assert_eq!(result.reset_hint, COUNTER_RESET_HINT);
3429        assert_eq!(result.count, 2.0);
3430        assert_eq!(result.sum, 2.0);
3431        assert_eq!(result.positive_buckets, vec![2.0]);
3432    }
3433
3434    #[test]
3435    fn histogram_averages_avoid_sum_overflow() {
3436        let large = sample_histogram(1.0e308, 1.0e308, vec![1.0e308]);
3437
3438        let range_average = run_histogram_range_udf(
3439            NativeHistogramAvgOverTime::scalar_udf(),
3440            vec![large.clone(), large.clone()],
3441        );
3442        assert_eq!(range_average.count, 1.0e308);
3443        assert_eq!(range_average.sum, 1.0e308);
3444        assert_eq!(range_average.positive_buckets, vec![1.0e308]);
3445
3446        let mut aggregate =
3447            NativeHistogramAggregateAccumulator::new(NativeHistogramAggregateKind::Avg, None);
3448        aggregate.push_histogram(large.clone(), 1).unwrap();
3449        aggregate.push_histogram(large, 1).unwrap();
3450        let aggregate_average = evaluated_histogram(&mut aggregate);
3451        assert_eq!(aggregate_average.count, 1.0e308);
3452        assert_eq!(aggregate_average.sum, 1.0e308);
3453        assert_eq!(aggregate_average.positive_buckets, vec![1.0e308]);
3454    }
3455
3456    #[test]
3457    fn histogram_average_partial_states_are_weighted() {
3458        let mut first =
3459            NativeHistogramAggregateAccumulator::new(NativeHistogramAggregateKind::Avg, None);
3460        first
3461            .push_histogram(sample_histogram(1.0, 1.0, vec![1.0]), 1)
3462            .unwrap();
3463        first
3464            .push_histogram(sample_histogram(3.0, 3.0, vec![3.0]), 1)
3465            .unwrap();
3466        let first_state = first
3467            .state()
3468            .unwrap()
3469            .into_iter()
3470            .map(|value| value.to_array_of_size(1).unwrap())
3471            .collect::<Vec<_>>();
3472
3473        let mut second =
3474            NativeHistogramAggregateAccumulator::new(NativeHistogramAggregateKind::Avg, None);
3475        second
3476            .push_histogram(sample_histogram(8.0, 8.0, vec![8.0]), 1)
3477            .unwrap();
3478        let second_state = second
3479            .state()
3480            .unwrap()
3481            .into_iter()
3482            .map(|value| value.to_array_of_size(1).unwrap())
3483            .collect::<Vec<_>>();
3484
3485        let mut merged =
3486            NativeHistogramAggregateAccumulator::new(NativeHistogramAggregateKind::Avg, None);
3487        merged.merge_batch(&first_state).unwrap();
3488        merged.merge_batch(&second_state).unwrap();
3489        let average = evaluated_histogram(&mut merged);
3490        assert_eq!(average.count, 4.0);
3491        assert_eq!(average.sum, 4.0);
3492        assert_eq!(average.positive_buckets, vec![4.0]);
3493    }
3494
3495    #[test]
3496    fn histogram_average_rejects_sample_count_overflow() {
3497        let mut aggregate =
3498            NativeHistogramAggregateAccumulator::new(NativeHistogramAggregateKind::Avg, None);
3499        aggregate.value = Some(sample_histogram(1.0, 1.0, vec![1.0]));
3500        aggregate.count = u64::MAX;
3501
3502        let error = aggregate
3503            .push_histogram(sample_histogram(1.0, 1.0, vec![1.0]), 1)
3504            .unwrap_err();
3505        assert!(error.to_string().contains("sample count overflow"));
3506    }
3507
3508    #[test]
3509    fn presence_only_range_functions_preserve_null_semantics() {
3510        let first = sample_histogram(1.0, 1.0, vec![1.0]);
3511        let second = sample_histogram(2.0, 2.0, vec![2.0]);
3512        assert_eq!(
3513            run_float_range_udf(
3514                NativeHistogramCountOverTime::scalar_udf(),
3515                vec![Some(first.clone()), Some(second.clone())],
3516            ),
3517            Some(2.0)
3518        );
3519        assert_eq!(
3520            run_float_range_udf(
3521                NativeHistogramPresentOverTime::scalar_udf(),
3522                vec![Some(first.clone()), Some(second.clone())],
3523            ),
3524            Some(1.0)
3525        );
3526        assert_eq!(
3527            run_float_range_udf(
3528                NativeHistogramCountOverTime::scalar_udf(),
3529                vec![None, Some(second.clone())],
3530            ),
3531            None
3532        );
3533
3534        let result = run_udf(
3535            NativeHistogramLastOverTime::scalar_udf(),
3536            histogram_range_input(vec![None, Some(second)]),
3537            native_histogram_arrow_type(),
3538        );
3539        let result = extract_array(&result).unwrap();
3540        let result = result.as_any().downcast_ref::<StructArray>().unwrap();
3541        assert!(result.is_null(0));
3542    }
3543
3544    #[test]
3545    fn resets_counts_histogram_flavor_transitions() {
3546        let mut counter = sample_histogram(1.0, 1.0, vec![1.0]);
3547        counter.reset_hint = NOT_COUNTER_RESET_HINT;
3548        let mut gauge = sample_histogram(2.0, 2.0, vec![2.0]);
3549        gauge.reset_hint = GAUGE_RESET_HINT;
3550        assert_eq!(
3551            run_float_range_udf(
3552                NativeHistogramResets::scalar_udf(),
3553                vec![Some(counter), Some(gauge)],
3554            ),
3555            Some(1.0)
3556        );
3557
3558        let mut gauge = sample_histogram(1.0, 1.0, vec![1.0]);
3559        gauge.reset_hint = GAUGE_RESET_HINT;
3560        let mut counter = sample_histogram(2.0, 2.0, vec![2.0]);
3561        counter.reset_hint = NOT_COUNTER_RESET_HINT;
3562        assert_eq!(
3563            run_float_range_udf(
3564                NativeHistogramResets::scalar_udf(),
3565                vec![Some(gauge), Some(counter)],
3566            ),
3567            Some(1.0)
3568        );
3569    }
3570
3571    #[test]
3572    fn wrong_flavor_functions_record_warnings() {
3573        let mut gauge_first = sample_histogram(1.0, 1.0, vec![1.0]);
3574        gauge_first.reset_hint = GAUGE_RESET_HINT;
3575        let mut gauge_last = sample_histogram(2.0, 2.0, vec![2.0]);
3576        gauge_last.reset_hint = GAUGE_RESET_HINT;
3577        let mut counter_first = sample_histogram(1.0, 1.0, vec![1.0]);
3578        counter_first.reset_hint = NOT_COUNTER_RESET_HINT;
3579        let mut counter_last = sample_histogram(2.0, 2.0, vec![2.0]);
3580        counter_last.reset_hint = NOT_COUNTER_RESET_HINT;
3581        let collector = PromqlAnnotationCollector::default();
3582
3583        run_extrapolated_histogram_udf(
3584            NativeHistogramRate::scalar_udf_with_collector(Some(collector.clone())),
3585            vec![gauge_first.clone(), gauge_last.clone()],
3586        );
3587        run_extrapolated_histogram_udf(
3588            NativeHistogramDelta::scalar_udf_with_collector(Some(collector.clone())),
3589            vec![counter_first.clone(), counter_last.clone()],
3590        );
3591        run_histogram_range_udf(
3592            NativeHistogramIRate::scalar_udf_with_collector(Some(collector.clone())),
3593            vec![gauge_first, gauge_last],
3594        );
3595        run_histogram_range_udf(
3596            NativeHistogramIDelta::scalar_udf_with_collector(Some(collector.clone())),
3597            vec![counter_first, counter_last],
3598        );
3599
3600        let warnings = collected_warnings(&collector);
3601        for expected in [
3602            format!(
3603                "{}: native histogram input should be a counter histogram",
3604                NativeHistogramRate::name()
3605            ),
3606            format!(
3607                "{}: native histogram input should be a gauge histogram",
3608                NativeHistogramDelta::name()
3609            ),
3610            format!(
3611                "{}: native histogram input should be a counter histogram",
3612                NativeHistogramIRate::name()
3613            ),
3614            format!(
3615                "{}: native histogram input should be a gauge histogram",
3616                NativeHistogramIDelta::name()
3617            ),
3618        ] {
3619            assert!(warnings.contains(&expected), "missing warning: {expected}");
3620        }
3621    }
3622
3623    #[test]
3624    fn subtraction_records_reset_hint_contradictions_for_incompatible_histograms() {
3625        for incompatible in [false, true] {
3626            let mut left = sample_histogram(2.0, 2.0, vec![2.0]);
3627            left.reset_hint = COUNTER_RESET_HINT;
3628            let mut right = sample_histogram(1.0, 1.0, vec![1.0]);
3629            right.reset_hint = NOT_COUNTER_RESET_HINT;
3630            if incompatible {
3631                right.schema = CUSTOM_BUCKETS_SCHEMA;
3632                right.custom_values = vec![1.0];
3633            }
3634            let collector = PromqlAnnotationCollector::default();
3635
3636            run_udf(
3637                NativeHistogramSub::scalar_udf_with_collector(Some(collector.clone())),
3638                vec![
3639                    ColumnarValue::Array(build_histogram_array(&[Some(left)])),
3640                    ColumnarValue::Array(build_histogram_array(&[Some(right)])),
3641                ],
3642                native_histogram_arrow_type(),
3643            );
3644
3645            assert!(collected_warnings(&collector).contains(&format!(
3646                "{}: native histogram counter reset hints contradict",
3647                NativeHistogramSub::name()
3648            )));
3649        }
3650    }
3651
3652    #[test]
3653    fn counter_reset_hint_history_survives_folds_and_state_merges() {
3654        let mut reset = sample_histogram(1.0, 1.0, vec![1.0]);
3655        reset.reset_hint = COUNTER_RESET_HINT;
3656        let mut unknown = sample_histogram(2.0, 2.0, vec![2.0]);
3657        unknown.reset_hint = UNKNOWN_COUNTER_RESET_HINT;
3658        let mut not_reset = sample_histogram(3.0, 3.0, vec![3.0]);
3659        not_reset.reset_hint = NOT_COUNTER_RESET_HINT;
3660
3661        let range_collector = PromqlAnnotationCollector::default();
3662        assert!(
3663            range_fold_histograms(
3664                vec![reset.clone(), unknown.clone(), not_reset.clone()],
3665                NativeHistogramAggregateKind::Sum,
3666                NativeHistogramSumOverTime::name(),
3667                &Some(range_collector.clone()),
3668            )
3669            .is_some()
3670        );
3671        assert!(collected_warnings(&range_collector).contains(&format!(
3672            "{}: native histogram counter reset hints contradict",
3673            NativeHistogramSumOverTime::name()
3674        )));
3675
3676        for kind in [
3677            NativeHistogramAggregateKind::Sum,
3678            NativeHistogramAggregateKind::Avg,
3679        ] {
3680            let mut first_partial = NativeHistogramAggregateAccumulator::new(kind, None);
3681            first_partial.push_histogram(reset.clone(), 1).unwrap();
3682            first_partial.push_histogram(unknown.clone(), 1).unwrap();
3683            let first_states = first_partial
3684                .state()
3685                .unwrap()
3686                .into_iter()
3687                .map(|value| value.to_array_of_size(1).unwrap())
3688                .collect::<Vec<_>>();
3689
3690            let mut second_partial = NativeHistogramAggregateAccumulator::new(kind, None);
3691            second_partial.push_histogram(not_reset.clone(), 1).unwrap();
3692            let second_states = second_partial
3693                .state()
3694                .unwrap()
3695                .into_iter()
3696                .map(|value| value.to_array_of_size(1).unwrap())
3697                .collect::<Vec<_>>();
3698
3699            let collector = PromqlAnnotationCollector::default();
3700            let mut merged =
3701                NativeHistogramAggregateAccumulator::new(kind, Some(collector.clone()));
3702            merged.merge_batch(&first_states).unwrap();
3703            merged.merge_batch(&second_states).unwrap();
3704            assert!(collected_warnings(&collector).contains(&format!(
3705                "{}: native histogram counter reset hints contradict",
3706                kind.name()
3707            )));
3708        }
3709    }
3710
3711    #[test]
3712    fn incompatible_aggregates_do_not_hide_reset_hint_contradictions() {
3713        let mut reset = sample_histogram(1.0, 1.0, vec![1.0]);
3714        reset.reset_hint = COUNTER_RESET_HINT;
3715        let mut incompatible_reset = sample_histogram(2.0, 2.0, vec![2.0]);
3716        incompatible_reset.schema = CUSTOM_BUCKETS_SCHEMA;
3717        incompatible_reset.custom_values = vec![1.0];
3718        incompatible_reset.reset_hint = COUNTER_RESET_HINT;
3719        let mut not_reset = sample_histogram(3.0, 3.0, vec![3.0]);
3720        not_reset.reset_hint = NOT_COUNTER_RESET_HINT;
3721
3722        for kind in [
3723            NativeHistogramAggregateKind::Sum,
3724            NativeHistogramAggregateKind::Avg,
3725        ] {
3726            for histograms in [
3727                [reset.clone(), incompatible_reset.clone(), not_reset.clone()],
3728                [reset.clone(), not_reset.clone(), incompatible_reset.clone()],
3729            ] {
3730                let collector = PromqlAnnotationCollector::default();
3731                let mut aggregate =
3732                    NativeHistogramAggregateAccumulator::new(kind, Some(collector.clone()));
3733                for histogram in histograms {
3734                    aggregate.push_histogram(histogram, 1).unwrap();
3735                }
3736
3737                assert!(aggregate.dropped_incompatible);
3738                let warnings = collected_warnings(&collector);
3739                assert!(warnings.contains(&format!(
3740                    "{}: dropped native histogram aggregate with incompatible schemas",
3741                    kind.name()
3742                )));
3743                assert!(warnings.contains(&format!(
3744                    "{}: native histogram counter reset hints contradict",
3745                    kind.name()
3746                )));
3747            }
3748
3749            let mut dropped_partial = NativeHistogramAggregateAccumulator::new(kind, None);
3750            dropped_partial.push_histogram(reset.clone(), 1).unwrap();
3751            dropped_partial
3752                .push_histogram(incompatible_reset.clone(), 1)
3753                .unwrap();
3754            let dropped_states = dropped_partial
3755                .state()
3756                .unwrap()
3757                .into_iter()
3758                .map(|value| value.to_array_of_size(1).unwrap())
3759                .collect::<Vec<_>>();
3760
3761            let mut opposing_partial = NativeHistogramAggregateAccumulator::new(kind, None);
3762            opposing_partial
3763                .push_histogram(not_reset.clone(), 1)
3764                .unwrap();
3765            let opposing_states = opposing_partial
3766                .state()
3767                .unwrap()
3768                .into_iter()
3769                .map(|value| value.to_array_of_size(1).unwrap())
3770                .collect::<Vec<_>>();
3771
3772            for (first, second) in [
3773                (&dropped_states, &opposing_states),
3774                (&opposing_states, &dropped_states),
3775            ] {
3776                let collector = PromqlAnnotationCollector::default();
3777                let mut merged =
3778                    NativeHistogramAggregateAccumulator::new(kind, Some(collector.clone()));
3779                merged.merge_batch(first).unwrap();
3780                merged.merge_batch(second).unwrap();
3781
3782                assert!(merged.dropped_incompatible);
3783                assert!(collected_warnings(&collector).contains(&format!(
3784                    "{}: native histogram counter reset hints contradict",
3785                    kind.name()
3786                )));
3787            }
3788        }
3789
3790        for (kind, name) in [
3791            (
3792                NativeHistogramAggregateKind::Sum,
3793                NativeHistogramSumOverTime::name(),
3794            ),
3795            (
3796                NativeHistogramAggregateKind::Avg,
3797                NativeHistogramAvgOverTime::name(),
3798            ),
3799        ] {
3800            let collector = PromqlAnnotationCollector::default();
3801            assert!(
3802                range_fold_histograms(
3803                    vec![reset.clone(), incompatible_reset.clone(), not_reset.clone()],
3804                    kind,
3805                    name,
3806                    &Some(collector.clone()),
3807                )
3808                .is_none()
3809            );
3810            let warnings = collected_warnings(&collector);
3811            assert!(warnings.contains(&format!(
3812                "{name}: dropped native histogram range with incompatible schemas"
3813            )));
3814            assert!(warnings.contains(&format!(
3815                "{name}: native histogram counter reset hints contradict"
3816            )));
3817        }
3818    }
3819
3820    #[test]
3821    fn histogram_aggregate_accumulator_accounts_for_heap_allocations() {
3822        let histogram = sample_histogram(3.0, 3.0, vec![1.0, 2.0]);
3823        let heap_size = histogram.custom_values.capacity() * size_of::<f64>()
3824            + histogram.positive_spans.capacity() * size_of::<Span>()
3825            + histogram.negative_spans.capacity() * size_of::<Span>()
3826            + histogram.positive_buckets.capacity() * size_of::<f64>()
3827            + histogram.negative_buckets.capacity() * size_of::<f64>();
3828        let mut accumulator =
3829            NativeHistogramAggregateAccumulator::new(NativeHistogramAggregateKind::Sum, None);
3830        let empty_size = accumulator.size();
3831
3832        accumulator.push_histogram(histogram, 1).unwrap();
3833
3834        assert!(heap_size > 0);
3835        assert_eq!(accumulator.size(), empty_size + heap_size);
3836    }
3837
3838    #[test]
3839    fn absent_over_time_handles_histogram_ranges() {
3840        let values = vec![Some(sample_histogram(1.0, 1.0, vec![1.0]))];
3841        let timestamps = Arc::new(TimestampMillisecondArray::from_iter([Some(1000)]));
3842        let histograms = build_histogram_array(&values);
3843        let ranges = [(0, 1), (0, 0)];
3844        let ts_range = RangeArray::from_ranges(timestamps, ranges).unwrap();
3845        let value_range = RangeArray::from_ranges(histograms, ranges).unwrap();
3846
3847        let result = run_udf(
3848            NativeHistogramAbsentOverTime::scalar_udf(),
3849            vec![
3850                ColumnarValue::Array(Arc::new(ts_range.into_dict())),
3851                ColumnarValue::Array(Arc::new(value_range.into_dict())),
3852            ],
3853            DataType::Float64,
3854        );
3855        let result = extract_array(&result).unwrap();
3856        let result = result.as_any().downcast_ref::<Float64Array>().unwrap();
3857
3858        assert!(result.is_null(0));
3859        assert_eq!(result.value(1), 1.0);
3860    }
3861
3862    #[test]
3863    fn delta_requires_exact_layout() {
3864        let first = sample_histogram(2.0, 3.0, vec![1.0, 1.0]);
3865        let last = sample_histogram(5.0, 8.0, vec![2.0, 3.0]);
3866        let delta = histogram_delta(&[first, last], &[0, 1], false).unwrap();
3867        assert_eq!(delta.count, 3.0);
3868        assert_eq!(delta.sum, 5.0);
3869        assert_eq!(delta.reset_hint, GAUGE_RESET_HINT);
3870        assert_eq!(delta.positive_buckets, vec![1.0, 2.0]);
3871    }
3872
3873    #[test]
3874    fn reset_hint_shortcuts_detection() {
3875        let previous = sample_histogram(6.0, 10.0, vec![2.0, 4.0]);
3876
3877        let mut current = sample_histogram(7.0, 12.0, vec![3.0, 4.0]);
3878        current.reset_hint = COUNTER_RESET_HINT;
3879        assert!(current.detect_reset(&previous));
3880
3881        let mut current = sample_histogram(5.0, 8.0, vec![1.0, 4.0]);
3882        current.reset_hint = NOT_COUNTER_RESET_HINT;
3883        assert!(!current.detect_reset(&previous));
3884    }
3885
3886    #[test]
3887    fn start_timestamp_detects_counter_reset() {
3888        let first = sample_histogram(6.0, 10.0, vec![2.0, 4.0]);
3889        let mut last = sample_histogram(7.0, 12.0, vec![3.0, 4.0]);
3890        last.start_timestamp = Some(1500);
3891
3892        let delta = histogram_delta(&[first.clone(), last.clone()], &[1000, 2000], true).unwrap();
3893        assert_eq!(delta.count, 7.0);
3894        assert_eq!(delta.sum, 12.0);
3895
3896        let idelta = idelta_value(&[first, last], true, 1000, 2000, 1.0).unwrap();
3897        assert_eq!(idelta.count, 7.0);
3898        assert_eq!(idelta.sum, 12.0);
3899    }
3900
3901    #[test]
3902    fn extrapolated_rate_uses_start_timestamp_synthetic_zero() {
3903        let mut single = sample_histogram(1.0, 1.0, vec![1.0]);
3904        single.start_timestamp = Some(1_000);
3905
3906        let rate = extrapolated_histogram_result(
3907            NativeHistogramRate::scalar_udf(),
3908            vec![2_000],
3909            vec![single.clone()],
3910            3_000,
3911            3_000,
3912        )
3913        .unwrap();
3914        assert_eq!(rate.count, 1.0 / 3.0);
3915        assert_eq!(rate.sum, 1.0 / 3.0);
3916
3917        let increase = extrapolated_histogram_result(
3918            NativeHistogramIncrease::scalar_udf(),
3919            vec![2_000],
3920            vec![single],
3921            3_000,
3922            3_000,
3923        )
3924        .unwrap();
3925        assert_eq!(increase.count, 1.0);
3926        assert_eq!(increase.sum, 1.0);
3927
3928        let mut first = sample_histogram(2.0, 2.0, vec![2.0]);
3929        first.start_timestamp = Some(1_000);
3930        let last = sample_histogram(4.0, 4.0, vec![4.0]);
3931        let increase = extrapolated_histogram_result(
3932            NativeHistogramIncrease::scalar_udf(),
3933            vec![2_000, 3_000],
3934            vec![first, last],
3935            3_000,
3936            3_000,
3937        )
3938        .unwrap();
3939        assert_eq!(increase.count, 4.0);
3940        assert_eq!(increase.sum, 4.0);
3941    }
3942
3943    #[test]
3944    fn extrapolated_rate_requires_strictly_in_range_start_timestamp() {
3945        for (start_timestamp, range_end, range_length) in [
3946            (0, 3_000, 3_000),
3947            (500, 3_000, 2_000),
3948            (1_000, 3_000, 2_000),
3949            (2_000, 3_000, 3_000),
3950            (2_500, 3_000, 3_000),
3951        ] {
3952            let mut sample = sample_histogram(1.0, 1.0, vec![1.0]);
3953            sample.start_timestamp = Some(start_timestamp);
3954            assert!(
3955                extrapolated_histogram_result(
3956                    NativeHistogramRate::scalar_udf(),
3957                    vec![2_000],
3958                    vec![sample],
3959                    range_end,
3960                    range_length,
3961                )
3962                .is_none(),
3963                "start_timestamp={start_timestamp}"
3964            );
3965        }
3966
3967        let mut gauge = sample_histogram(1.0, 1.0, vec![1.0]);
3968        gauge.start_timestamp = Some(1_000);
3969        gauge.reset_hint = GAUGE_RESET_HINT;
3970        assert!(
3971            extrapolated_histogram_result(
3972                NativeHistogramDelta::scalar_udf(),
3973                vec![2_000],
3974                vec![gauge],
3975                3_000,
3976                3_000,
3977            )
3978            .is_none()
3979        );
3980    }
3981
3982    #[test]
3983    fn first_reset_ignores_incompatible_pre_reset_layout() {
3984        let first = sample_histogram(10.0, 10.0, vec![10.0]);
3985        let second = NativeHistogram {
3986            schema: CUSTOM_BUCKETS_SCHEMA,
3987            zero_threshold: 0.0,
3988            sum: 2.0,
3989            reset_hint: COUNTER_RESET_HINT,
3990            start_timestamp: None,
3991            custom_values: vec![1.0],
3992            positive_spans: vec![Span {
3993                offset: 0,
3994                length: 1,
3995            }],
3996            negative_spans: Vec::new(),
3997            count: 2.0,
3998            zero_count: 0.0,
3999            positive_buckets: vec![2.0],
4000            negative_buckets: Vec::new(),
4001        };
4002
4003        let delta = histogram_delta(&[first, second], &[1_000, 2_000], true).unwrap();
4004        assert_eq!(delta.schema, CUSTOM_BUCKETS_SCHEMA);
4005        assert_eq!(delta.count, 2.0);
4006        assert_eq!(delta.sum, 2.0);
4007        assert_eq!(delta.positive_buckets, vec![2.0]);
4008    }
4009
4010    #[test]
4011    fn counter_delta_handles_reset_segment_boundaries() {
4012        let first = sample_histogram(5.0, 5.0, vec![5.0]);
4013        let second = sample_histogram(7.0, 7.0, vec![7.0]);
4014        let mut reset = sample_histogram(2.0, 2.0, vec![2.0]);
4015        reset.reset_hint = COUNTER_RESET_HINT;
4016        let last = sample_histogram(4.0, 4.0, vec![4.0]);
4017
4018        let delta = histogram_delta(
4019            &[first, second, reset, last],
4020            &[1_000, 2_000, 3_000, 4_000],
4021            true,
4022        )
4023        .unwrap();
4024        assert_eq!(delta.count, 6.0);
4025        assert_eq!(delta.sum, 6.0);
4026        assert_eq!(delta.positive_buckets, vec![6.0]);
4027    }
4028}