1use 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
751pub 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
1441pub 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 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
1892fn 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
1915fn 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
1937fn 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#[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 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#[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, ¤t) {
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 (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}