1mod encoding;
23
24use std::collections::BTreeMap;
25use std::sync::Arc;
26
27use datafusion::arrow::array::{
28 Array, ArrayRef, Float64Array, Int32Array, Int64Array, ListArray, PrimitiveArray, StructArray,
29 TimestampMillisecondArray,
30};
31use datafusion::arrow::buffer::NullBuffer;
32use datafusion::arrow::datatypes::{
33 ArrowPrimitiveType, DataType as ArrowDataType, Field, Float64Type, Int32Type, Int64Type,
34 TimestampMillisecondType,
35};
36use datafusion_common::{DataFusionError, Result as DfResult};
37use datatypes::data_type::{ConcreteDataType, DataType};
38use datatypes::types::{StructField, StructType};
39pub use encoding::{
40 NativeHistogramError, decode_native_histogram_for_remote_read, encode_native_histogram,
41 native_histogram_column_schema,
42};
43use once_cell::sync::Lazy;
44
45use crate::prelude::greptime_native_histogram;
46use crate::prometheus::format_prometheus_float;
47
48pub const NATIVE_HISTOGRAM_FIELD: &str = "greptime_native_histogram";
49pub const SCHEMA_FIELD: &str = "schema";
50pub const ZERO_THRESHOLD_FIELD: &str = "zero_threshold";
51pub const SUM_FIELD: &str = "sum";
52pub const RESET_HINT_FIELD: &str = "reset_hint";
53pub const START_TIMESTAMP_FIELD: &str = "start_timestamp";
54pub const CUSTOM_VALUES_FIELD: &str = "custom_values";
55pub const POSITIVE_SPAN_OFFSETS_FIELD: &str = "positive_span_offsets";
56pub const POSITIVE_SPAN_LENGTHS_FIELD: &str = "positive_span_lengths";
57pub const NEGATIVE_SPAN_OFFSETS_FIELD: &str = "negative_span_offsets";
58pub const NEGATIVE_SPAN_LENGTHS_FIELD: &str = "negative_span_lengths";
59pub const COUNT_I64_FIELD: &str = "count_i64";
60pub const ZERO_COUNT_I64_FIELD: &str = "zero_count_i64";
61pub const POSITIVE_BUCKETS_I64_FIELD: &str = "positive_buckets_i64";
62pub const NEGATIVE_BUCKETS_I64_FIELD: &str = "negative_buckets_i64";
63pub const COUNT_F64_FIELD: &str = "count_f64";
64pub const ZERO_COUNT_F64_FIELD: &str = "zero_count_f64";
65pub const POSITIVE_BUCKETS_F64_FIELD: &str = "positive_buckets_f64";
66pub const NEGATIVE_BUCKETS_F64_FIELD: &str = "negative_buckets_f64";
67
68pub const NATIVE_HISTOGRAM_FIELD_NAMES: &[&str] = &[
71 SCHEMA_FIELD,
72 ZERO_THRESHOLD_FIELD,
73 SUM_FIELD,
74 RESET_HINT_FIELD,
75 START_TIMESTAMP_FIELD,
76 CUSTOM_VALUES_FIELD,
77 POSITIVE_SPAN_OFFSETS_FIELD,
78 POSITIVE_SPAN_LENGTHS_FIELD,
79 NEGATIVE_SPAN_OFFSETS_FIELD,
80 NEGATIVE_SPAN_LENGTHS_FIELD,
81 COUNT_I64_FIELD,
82 ZERO_COUNT_I64_FIELD,
83 POSITIVE_BUCKETS_I64_FIELD,
84 NEGATIVE_BUCKETS_I64_FIELD,
85 COUNT_F64_FIELD,
86 ZERO_COUNT_F64_FIELD,
87 POSITIVE_BUCKETS_F64_FIELD,
88 NEGATIVE_BUCKETS_F64_FIELD,
89];
90
91static NATIVE_HISTOGRAM_VALUE_TYPE: Lazy<ConcreteDataType> = Lazy::new(|| {
92 let fields = NATIVE_HISTOGRAM_FIELD_NAMES
93 .iter()
94 .filter_map(|name| {
95 let data_type = native_histogram_field_type(name)?;
96 Some(StructField::new((*name).to_string(), data_type, true))
97 })
98 .collect();
99 ConcreteDataType::struct_datatype(StructType::new(Arc::new(fields)))
100});
101
102pub fn native_histogram_field_type(name: &str) -> Option<ConcreteDataType> {
104 match name {
105 SCHEMA_FIELD | RESET_HINT_FIELD => Some(ConcreteDataType::int32_datatype()),
106 ZERO_THRESHOLD_FIELD | SUM_FIELD | COUNT_F64_FIELD | ZERO_COUNT_F64_FIELD => {
107 Some(ConcreteDataType::float64_datatype())
108 }
109 START_TIMESTAMP_FIELD => Some(ConcreteDataType::timestamp_millisecond_datatype()),
110 CUSTOM_VALUES_FIELD | POSITIVE_BUCKETS_F64_FIELD | NEGATIVE_BUCKETS_F64_FIELD => Some(
111 ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::float64_datatype())),
112 ),
113 POSITIVE_SPAN_OFFSETS_FIELD | NEGATIVE_SPAN_OFFSETS_FIELD => Some(
114 ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::int32_datatype())),
115 ),
116 POSITIVE_SPAN_LENGTHS_FIELD | NEGATIVE_SPAN_LENGTHS_FIELD => Some(
117 ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::int32_datatype())),
118 ),
119 COUNT_I64_FIELD | ZERO_COUNT_I64_FIELD => Some(ConcreteDataType::int64_datatype()),
120 POSITIVE_BUCKETS_I64_FIELD | NEGATIVE_BUCKETS_I64_FIELD => Some(
121 ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::int64_datatype())),
122 ),
123 _ => None,
124 }
125}
126
127pub fn native_histogram_value_type() -> &'static ConcreteDataType {
129 &NATIVE_HISTOGRAM_VALUE_TYPE
130}
131
132pub fn is_native_histogram_value_type(data_type: &ConcreteDataType) -> bool {
134 data_type == native_histogram_value_type()
135}
136
137pub fn is_native_histogram_value_schema(name: &str, data_type: &ConcreteDataType) -> bool {
139 name == greptime_native_histogram() && is_native_histogram_value_type(data_type)
140}
141
142#[cfg(any(test, feature = "testing"))]
143pub mod testing {
144 use api::helper::pb_value_to_value_ref;
145 use api::v1::{Value, value::ValueData};
146 use datafusion::arrow::array::{AsArray, StructArray};
147 use datatypes::vectors::Helper;
148
149 use crate::native_histogram::{native_histogram_column_schema, native_histogram_value_type};
150
151 pub fn histogram_array(values: &[Option<ValueData>]) -> StructArray {
153 let schema = native_histogram_column_schema().unwrap();
154 let rows = values
155 .iter()
156 .map(|value_data| {
157 datatypes::value::Value::from(pb_value_to_value_ref(
158 &Value {
159 value_data: value_data.clone(),
160 },
161 schema.datatype_extension.as_ref(),
162 ))
163 })
164 .collect::<Vec<_>>();
165 Helper::try_from_row_into_vector(&rows, native_histogram_value_type())
166 .unwrap()
167 .to_arrow_array()
168 .as_struct()
169 .clone()
170 }
171}
172
173pub const CUSTOM_BUCKETS_SCHEMA: i32 = -53;
175const MIN_EXPONENTIAL_SCHEMA: i32 = -4;
176const MAX_EXPONENTIAL_SCHEMA: i32 = 8;
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183pub enum CounterResetHint {
184 Unknown,
186 CounterReset,
188 NotCounterReset,
190 Gauge,
192 Unrecognized(i32),
194}
195
196impl From<i32> for CounterResetHint {
197 fn from(value: i32) -> Self {
198 match value {
199 0 => Self::Unknown,
200 1 => Self::CounterReset,
201 2 => Self::NotCounterReset,
202 3 => Self::Gauge,
203 value => Self::Unrecognized(value),
204 }
205 }
206}
207
208impl From<CounterResetHint> for i32 {
209 fn from(value: CounterResetHint) -> Self {
210 match value {
211 CounterResetHint::Unknown => 0,
212 CounterResetHint::CounterReset => 1,
213 CounterResetHint::NotCounterReset => 2,
214 CounterResetHint::Gauge => 3,
215 CounterResetHint::Unrecognized(value) => value,
216 }
217 }
218}
219
220pub const UNKNOWN_COUNTER_RESET_HINT: CounterResetHint = CounterResetHint::Unknown;
222pub const COUNTER_RESET_HINT: CounterResetHint = CounterResetHint::CounterReset;
224pub const NOT_COUNTER_RESET_HINT: CounterResetHint = CounterResetHint::NotCounterReset;
226pub const GAUGE_RESET_HINT: CounterResetHint = CounterResetHint::Gauge;
228
229#[derive(Clone, Debug, PartialEq)]
231pub struct Span {
232 pub offset: i32,
234 pub length: i32,
236}
237
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
240enum BoundaryRule {
241 OpenLeft = 0,
242 OpenRight = 1,
243 ClosedBoth = 3,
244}
245
246#[derive(Clone, Debug, PartialEq)]
248struct Bucket {
249 lower: f64,
250 upper: f64,
251 count: f64,
252 boundary_rule: BoundaryRule,
253}
254
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
257pub enum NativeHistogramQuantileInfo {
258 NaNSkew,
260 NaNResult,
262}
263
264#[derive(Clone, Debug, PartialEq)]
269pub struct NativeHistogram {
270 pub schema: i32,
272 pub zero_threshold: f64,
274 pub sum: f64,
276 pub reset_hint: CounterResetHint,
278 pub start_timestamp: Option<i64>,
280 pub custom_values: Vec<f64>,
282 pub positive_spans: Vec<Span>,
284 pub negative_spans: Vec<Span>,
286 pub count: f64,
288 pub zero_count: f64,
290 pub positive_buckets: Vec<f64>,
292 pub negative_buckets: Vec<f64>,
294}
295
296fn format_promql_histogram_float(value: f64) -> String {
298 let abs = value.abs();
299 if !value.is_finite() || value == 0.0 || (1e-4..1e6).contains(&abs) {
300 return format_prometheus_float(value);
301 }
302
303 let scientific = format!("{value:e}");
304 let Some((mantissa, exponent)) = scientific.rsplit_once('e') else {
305 return scientific;
306 };
307 let (sign, exponent) = if let Some(exponent) = exponent.strip_prefix('-') {
308 ('-', exponent)
309 } else {
310 ('+', exponent.strip_prefix('+').unwrap_or(exponent))
311 };
312 format!("{mantissa}e{sign}{exponent:0>2}")
313}
314
315impl NativeHistogram {
316 fn uses_custom_buckets(&self) -> bool {
317 self.schema == CUSTOM_BUCKETS_SCHEMA
318 }
319
320 fn compatible_with(&self, other: &Self) -> bool {
321 self.schema == other.schema
322 && self.zero_threshold == other.zero_threshold
323 && self.custom_values == other.custom_values
324 }
325
326 pub fn zero_like(&self) -> Self {
328 let mut result = self.clone();
329 result.count = 0.0;
330 result.zero_count = 0.0;
331 result.sum = 0.0;
332 result.positive_buckets.fill(0.0);
333 result.negative_buckets.fill(0.0);
334 result
335 }
336
337 fn combine_exact(
338 &self,
339 other: &Self,
340 reset_hint: CounterResetHint,
341 op: impl Fn(f64, f64) -> f64 + Copy,
342 ) -> Option<Self> {
343 if !self.compatible_with(other) {
344 return None;
345 }
346
347 let mut result = self.clone();
348 result.count = op(result.count, other.count);
349 result.zero_count = op(result.zero_count, other.zero_count);
350 result.sum = op(result.sum, other.sum);
351 result.reset_hint = reset_hint;
352 (result.positive_spans, result.positive_buckets) = merge_side(
353 &self.positive_spans,
354 &self.positive_buckets,
355 &other.positive_spans,
356 &other.positive_buckets,
357 op,
358 )?;
359 (result.negative_spans, result.negative_buckets) = merge_side(
360 &self.negative_spans,
361 &self.negative_buckets,
362 &other.negative_spans,
363 &other.negative_buckets,
364 op,
365 )?;
366 Some(result)
367 }
368
369 pub fn add(&self, other: &Self) -> Option<Self> {
373 let reset_hint = add_reset_hint(self.reset_hint, other.reset_hint);
374 let (left, right) = self.reconcile(other)?;
375 left.combine_exact(&right, reset_hint, |left, right| left + right)?
376 .compact()
377 }
378
379 pub fn sub(&self, other: &Self) -> Option<Self> {
383 let (left, right) = self.reconcile(other)?;
384 left.combine_exact(&right, CounterResetHint::Gauge, |left, right| left - right)?
385 .compact()
386 }
387
388 pub fn negated(self) -> Self {
390 self.scale(-1.0)
391 }
392
393 pub fn into_gauge(mut self) -> Self {
395 self.reset_hint = CounterResetHint::Gauge;
396 self
397 }
398
399 pub fn counter_reset_hints_contradict(&self, other: &Self) -> bool {
401 matches!(
402 (self.reset_hint, other.reset_hint),
403 (
404 CounterResetHint::CounterReset,
405 CounterResetHint::NotCounterReset
406 ) | (
407 CounterResetHint::NotCounterReset,
408 CounterResetHint::CounterReset
409 )
410 )
411 }
412
413 pub fn needs_custom_reconciliation(&self, other: &Self) -> bool {
415 self.uses_custom_buckets()
416 && other.uses_custom_buckets()
417 && self.custom_values != other.custom_values
418 }
419
420 pub fn promql_eq(&self, other: &Self) -> bool {
424 self.schema == other.schema
425 && self.zero_threshold == other.zero_threshold
426 && self.custom_values == other.custom_values
427 && self.count.to_bits() == other.count.to_bits()
428 && self.zero_count.to_bits() == other.zero_count.to_bits()
429 && self.sum.to_bits() == other.sum.to_bits()
430 && side_layout_equal(
431 &self.positive_spans,
432 &self.positive_buckets,
433 &other.positive_spans,
434 &other.positive_buckets,
435 )
436 && side_layout_equal(
437 &self.negative_spans,
438 &self.negative_buckets,
439 &other.negative_spans,
440 &other.negative_buckets,
441 )
442 }
443
444 pub fn promql_string(&self) -> String {
446 let mut parts = vec![
447 format!("count:{}", format_promql_histogram_float(self.count)),
448 format!("sum:{}", format_promql_histogram_float(self.sum)),
449 ];
450 if let Some(buckets) = self.all_buckets() {
451 parts.extend(
452 buckets
453 .into_iter()
454 .filter(|bucket| bucket.count != 0.0)
455 .map(|bucket| {
456 let (left, right) = match bucket.boundary_rule {
457 BoundaryRule::OpenLeft => ("(", "]"),
458 BoundaryRule::OpenRight => ("[", ")"),
459 BoundaryRule::ClosedBoth => ("[", "]"),
460 };
461 format!(
462 "{}{},{}{}:{}",
463 left,
464 format_promql_histogram_float(bucket.lower),
465 format_promql_histogram_float(bucket.upper),
466 right,
467 format_promql_histogram_float(bucket.count)
468 )
469 }),
470 );
471 }
472 format!("{{{}}}", parts.join(", "))
473 }
474
475 pub fn estimated_stdvar(&self) -> f64 {
477 if self.count == 0.0 {
478 return f64::NAN;
479 }
480 let mean = self.sum / self.count;
481 let Some(buckets) = self.all_buckets() else {
482 return f64::NAN;
483 };
484 buckets
485 .into_iter()
486 .map(|bucket| {
487 let midpoint = self.bucket_midpoint(&bucket);
488 bucket.count * (midpoint - mean).powi(2)
489 })
490 .sum::<f64>()
491 / self.count
492 }
493
494 pub fn estimated_stddev(&self) -> f64 {
496 self.estimated_stdvar().sqrt()
497 }
498
499 pub fn scale(mut self, factor: f64) -> Self {
503 self.count *= factor;
504 self.zero_count *= factor;
505 self.sum *= factor;
506 for count in &mut self.positive_buckets {
507 *count *= factor;
508 }
509 for count in &mut self.negative_buckets {
510 *count *= factor;
511 }
512 if factor < 0.0 {
513 self.reset_hint = CounterResetHint::Gauge;
514 }
515 self
516 }
517
518 pub fn divide_by(mut self, divisor: f64) -> Self {
522 self.count /= divisor;
523 self.zero_count /= divisor;
524 self.sum /= divisor;
525 if divisor == 0.0 {
526 self.positive_spans.clear();
527 self.positive_buckets.clear();
528 self.negative_spans.clear();
529 self.negative_buckets.clear();
530 } else {
531 for count in &mut self.positive_buckets {
532 *count /= divisor;
533 }
534 for count in &mut self.negative_buckets {
535 *count /= divisor;
536 }
537 }
538 if divisor < 0.0 {
539 self.reset_hint = CounterResetHint::Gauge;
540 }
541 self
542 }
543
544 fn compact(mut self) -> Option<Self> {
545 let (spans, buckets) = compact_side(&self.positive_spans, &self.positive_buckets)?;
546 self.positive_spans = spans;
547 self.positive_buckets = buckets;
548 let (spans, buckets) = compact_side(&self.negative_spans, &self.negative_buckets)?;
549 self.negative_spans = spans;
550 self.negative_buckets = buckets;
551 Some(self)
552 }
553
554 pub fn detect_reset(&self, previous: &Self) -> bool {
556 match self.reset_hint {
557 CounterResetHint::CounterReset => return true,
558 CounterResetHint::NotCounterReset => return false,
559 CounterResetHint::Unknown
560 | CounterResetHint::Gauge
561 | CounterResetHint::Unrecognized(_) => {}
562 }
563
564 if self.count < previous.count {
565 return true;
566 }
567
568 match (self.uses_custom_buckets(), previous.uses_custom_buckets()) {
569 (true, true) => {
570 let Some((current, previous)) = reconcile_custom(self, previous) else {
571 return true;
572 };
573 current.zero_count < previous.zero_count
574 || current.side_has_reset(true, &previous)
575 || current.side_has_reset(false, &previous)
576 }
577 (true, false) | (false, true) => true,
578 (false, false) => {
579 if self.schema > previous.schema || self.zero_threshold < previous.zero_threshold {
580 return true;
581 }
582
583 let mut previous = previous.clone();
584 if self.zero_threshold > previous.zero_threshold {
585 let Some(expanded) = previous.expanded_zero_threshold(self.zero_threshold)
586 else {
587 return true;
588 };
589 if expanded != self.zero_threshold
590 || previous.grow_zero_threshold(self.zero_threshold).is_none()
591 {
592 return true;
593 }
594 }
595 let Some(previous) = previous.copy_to_schema(self.schema) else {
596 return true;
597 };
598
599 self.zero_count < previous.zero_count
600 || self.side_has_reset(true, &previous)
601 || self.side_has_reset(false, &previous)
602 }
603 }
604 }
605
606 fn detect_start_timestamp_reset(
607 &self,
608 previous: &Self,
609 previous_ts: i64,
610 current_ts: i64,
611 ) -> bool {
612 let current_start = self.start_timestamp.unwrap_or_default();
613 if current_start == 0 || current_start >= current_ts || current_start < previous_ts {
614 return false;
615 }
616 if current_start > previous_ts {
617 return true;
618 }
619
620 let previous_start = previous.start_timestamp.unwrap_or_default();
621 previous_start <= previous_ts && previous_start != 0 && previous_start != previous_ts
622 }
623
624 pub fn detect_counter_reset(&self, previous: &Self, previous_ts: i64, current_ts: i64) -> bool {
626 self.detect_start_timestamp_reset(previous, previous_ts, current_ts)
627 || self.detect_reset(previous)
628 }
629
630 fn all_buckets(&self) -> Option<Vec<Bucket>> {
631 let mut buckets = self.side_buckets(false)?;
632 buckets.reverse();
633 if self.zero_count != 0.0 {
634 buckets.push(Bucket {
635 lower: -self.zero_threshold,
636 upper: self.zero_threshold,
637 count: self.zero_count,
638 boundary_rule: BoundaryRule::ClosedBoth,
639 });
640 }
641 buckets.extend(self.side_buckets(true)?);
642 Some(buckets)
643 }
644
645 fn side_buckets(&self, positive: bool) -> Option<Vec<Bucket>> {
646 let (spans, counts) = if positive {
647 (&self.positive_spans, &self.positive_buckets)
648 } else {
649 (&self.negative_spans, &self.negative_buckets)
650 };
651
652 let mut result = Vec::with_capacity(counts.len());
653 for (idx, count) in side_bucket_indices(spans)?.into_iter().zip(counts) {
654 let upper = get_bound(idx, self.schema, &self.custom_values)?;
655 let lower = get_bound(idx.checked_sub(1)?, self.schema, &self.custom_values)?;
656 if positive {
657 result.push(Bucket {
658 lower,
659 upper,
660 count: *count,
661 boundary_rule: if self.uses_custom_buckets() && idx == 0 {
662 BoundaryRule::ClosedBoth
663 } else {
664 BoundaryRule::OpenLeft
665 },
666 });
667 } else {
668 result.push(Bucket {
669 lower: -upper,
670 upper: -lower,
671 count: *count,
672 boundary_rule: BoundaryRule::OpenRight,
673 });
674 }
675 }
676 Some(result)
677 }
678
679 pub fn quantile(&self, q: f64) -> f64 {
684 self.quantile_with_info(q).0
685 }
686
687 pub fn quantile_with_info(&self, q: f64) -> (f64, Option<NativeHistogramQuantileInfo>) {
689 if q < 0.0 {
690 return (f64::NEG_INFINITY, None);
691 }
692 if q > 1.0 {
693 return (f64::INFINITY, None);
694 }
695 if self.count == 0.0 || q.is_nan() {
696 return (f64::NAN, None);
697 }
698
699 let Some(mut buckets) = self.all_buckets() else {
700 return (f64::NAN, None);
701 };
702 let bucket_total = buckets.iter().map(|bucket| bucket.count).sum::<f64>();
703 let has_nan_observations = self.sum.is_nan() && bucket_total < self.count;
704 let info = has_nan_observations.then_some(NativeHistogramQuantileInfo::NaNSkew);
705 let rank = q * self.count;
706 let mut count = 0.0;
707 for bucket in &mut buckets {
708 if bucket.count == 0.0 {
709 continue;
710 }
711 count += bucket.count;
712 if count < rank {
713 continue;
714 }
715
716 if !self.uses_custom_buckets() && bucket.lower < 0.0 && bucket.upper > 0.0 {
717 if self.negative_buckets.is_empty() && !self.positive_buckets.is_empty() {
718 bucket.lower = 0.0;
719 } else if self.positive_buckets.is_empty() && !self.negative_buckets.is_empty() {
720 bucket.upper = 0.0;
721 }
722 } else if self.uses_custom_buckets() {
723 if bucket.lower == f64::NEG_INFINITY {
724 if bucket.upper <= 0.0 {
725 return (bucket.upper, info);
726 }
727 bucket.lower = 0.0;
728 } else if bucket.upper == f64::INFINITY {
729 return (bucket.lower, info);
730 }
731 }
732
733 let rank_in_bucket = rank - (count - bucket.count);
734 let fraction = rank_in_bucket / bucket.count;
735 if self.uses_custom_buckets() || (bucket.lower <= 0.0 && bucket.upper >= 0.0) {
736 return (
737 bucket.lower + (bucket.upper - bucket.lower) * fraction,
738 info,
739 );
740 }
741
742 let log_lower = bucket.lower.abs().log2();
743 let log_upper = bucket.upper.abs().log2();
744 if bucket.lower > 0.0 {
745 return (
746 2.0_f64.powf(log_lower + (log_upper - log_lower) * fraction),
747 info,
748 );
749 }
750 return (
751 -2.0_f64.powf(log_upper + (log_lower - log_upper) * (1.0 - fraction)),
752 info,
753 );
754 }
755
756 let info = if self.sum.is_nan() && count < self.count {
757 Some(if count < rank {
758 NativeHistogramQuantileInfo::NaNResult
759 } else {
760 NativeHistogramQuantileInfo::NaNSkew
761 })
762 } else {
763 None
764 };
765 (f64::NAN, info)
766 }
767
768 pub fn fraction(&self, lower: f64, upper: f64) -> f64 {
770 self.fraction_with_info(lower, upper).0
771 }
772
773 pub fn fraction_with_info(&self, lower: f64, upper: f64) -> (f64, bool) {
775 if self.count == 0.0 || lower.is_nan() || upper.is_nan() {
776 return (f64::NAN, false);
777 }
778 if lower >= upper {
779 return (0.0, false);
780 }
781
782 let Some(mut buckets) = self.all_buckets() else {
783 return (f64::NAN, false);
784 };
785 let count = if self.sum.is_nan() {
786 buckets.iter().map(|bucket| bucket.count).sum()
787 } else {
788 self.count
789 };
790
791 let mut rank = 0.0;
792 let mut lower_rank = 0.0;
793 let mut upper_rank = 0.0;
794 let mut lower_set = false;
795 let mut upper_set = false;
796
797 for bucket in &mut buckets {
798 let zero_bucket = bucket.lower <= 0.0 && bucket.upper >= 0.0;
799 if zero_bucket {
800 if self.negative_buckets.is_empty() && !self.positive_buckets.is_empty() {
801 bucket.lower = 0.0;
802 } else if self.positive_buckets.is_empty() && !self.negative_buckets.is_empty() {
803 bucket.upper = 0.0;
804 }
805 }
806
807 if !lower_set && bucket.lower >= lower {
808 lower_rank = rank;
809 lower_set = true;
810 }
811 if !upper_set && bucket.lower >= upper {
812 upper_rank = rank;
813 upper_set = true;
814 }
815 if lower_set && upper_set {
816 break;
817 }
818 if !lower_set && bucket.lower < lower && bucket.upper > lower {
819 lower_rank = self.interpolate_rank(bucket, rank, lower, zero_bucket);
820 lower_set = true;
821 }
822 if !upper_set && bucket.lower < upper && bucket.upper > upper {
823 upper_rank = self.interpolate_rank(bucket, rank, upper, zero_bucket);
824 upper_set = true;
825 }
826 if lower_set && upper_set {
827 break;
828 }
829 rank += bucket.count;
830 }
831
832 if !lower_set || lower_rank > count {
833 lower_rank = count;
834 }
835 if !upper_set || upper_rank > count {
836 upper_rank = count;
837 }
838
839 (
840 (upper_rank - lower_rank) / self.count,
841 self.sum.is_nan() && count < self.count,
842 )
843 }
844
845 pub fn to_prometheus_buckets(&self) -> Option<Vec<(u8, String, String, String)>> {
847 Some(
848 self.all_buckets()?
849 .into_iter()
850 .filter(|bucket| bucket.count != 0.0)
851 .map(|bucket| {
852 (
853 bucket.boundary_rule as u8,
854 format_prometheus_float(bucket.lower),
855 format_prometheus_float(bucket.upper),
856 format_prometheus_float(bucket.count),
857 )
858 })
859 .collect(),
860 )
861 }
862
863 fn interpolate_rank(&self, bucket: &Bucket, rank: f64, value: f64, zero_bucket: bool) -> f64 {
864 if self.uses_custom_buckets() || zero_bucket {
865 if bucket.lower == f64::NEG_INFINITY {
866 return bucket.count;
867 }
868 return rank + bucket.count * (value - bucket.lower) / (bucket.upper - bucket.lower);
869 }
870
871 let log_lower = bucket.lower.abs().log2();
872 let log_upper = bucket.upper.abs().log2();
873 let log_value = value.abs().log2();
874 let fraction = if value > 0.0 {
875 (log_value - log_lower) / (log_upper - log_lower)
876 } else {
877 1.0 - ((log_value - log_upper) / (log_lower - log_upper))
878 };
879 rank + bucket.count * fraction
880 }
881
882 fn bucket_midpoint(&self, bucket: &Bucket) -> f64 {
883 if self.uses_custom_buckets() {
884 return (bucket.lower + bucket.upper) / 2.0;
885 }
886 if bucket.lower <= 0.0 && bucket.upper >= 0.0 {
887 return 0.0;
888 }
889 if bucket.upper < 0.0 {
890 -((bucket.lower.abs() * bucket.upper.abs()).sqrt())
891 } else {
892 (bucket.lower * bucket.upper).sqrt()
893 }
894 }
895
896 fn side_has_reset(&self, positive: bool, previous: &Self) -> bool {
897 let (current_spans, current_buckets, previous_spans, previous_buckets) = if positive {
898 (
899 &self.positive_spans,
900 &self.positive_buckets,
901 &previous.positive_spans,
902 &previous.positive_buckets,
903 )
904 } else {
905 (
906 &self.negative_spans,
907 &self.negative_buckets,
908 &previous.negative_spans,
909 &previous.negative_buckets,
910 )
911 };
912 let Some(current) = side_counts(current_spans, current_buckets) else {
913 return true;
914 };
915 let Some(previous) = side_counts(previous_spans, previous_buckets) else {
916 return true;
917 };
918 previous.keys().chain(current.keys()).any(|idx| {
919 current.get(idx).copied().unwrap_or_default()
920 < previous.get(idx).copied().unwrap_or_default()
921 })
922 }
923
924 fn reconcile(&self, other: &Self) -> Option<(Self, Self)> {
925 match (self.uses_custom_buckets(), other.uses_custom_buckets()) {
926 (true, true) => reconcile_custom(self, other),
927 (false, false) => reconcile_exponential(self, other),
928 _ => None,
929 }
930 }
931}
932
933fn reconcile_exponential(
934 left: &NativeHistogram,
935 right: &NativeHistogram,
936) -> Option<(NativeHistogram, NativeHistogram)> {
937 let schema = left.schema.min(right.schema);
938 let mut left = left.copy_to_schema(schema)?;
939 let mut right = right.copy_to_schema(schema)?;
940 let mut zero_threshold = left.zero_threshold.max(right.zero_threshold);
941 loop {
942 let expanded = left
943 .expanded_zero_threshold(zero_threshold)?
944 .max(right.expanded_zero_threshold(zero_threshold)?);
945 if expanded == zero_threshold {
946 break;
947 }
948 zero_threshold = expanded;
949 }
950 left.grow_zero_threshold(zero_threshold)?;
951 right.grow_zero_threshold(zero_threshold)?;
952 Some((left.compact()?, right.compact()?))
953}
954
955fn reconcile_custom(
956 left: &NativeHistogram,
957 right: &NativeHistogram,
958) -> Option<(NativeHistogram, NativeHistogram)> {
959 let custom_values = if left.custom_values == right.custom_values {
960 left.custom_values.clone()
961 } else {
962 left.custom_values
963 .iter()
964 .copied()
965 .filter(|value| right.custom_values.contains(value))
966 .collect()
967 };
968
969 Some((
970 left.copy_to_custom_values(custom_values.clone())?
971 .compact()?,
972 right.copy_to_custom_values(custom_values)?.compact()?,
973 ))
974}
975
976impl NativeHistogram {
977 fn copy_to_schema(&self, target_schema: i32) -> Option<Self> {
978 if self.uses_custom_buckets()
979 || !(MIN_EXPONENTIAL_SCHEMA..=MAX_EXPONENTIAL_SCHEMA).contains(&target_schema)
980 || target_schema > self.schema
981 {
982 return None;
983 }
984 if target_schema == self.schema {
985 return Some(self.clone());
986 }
987
988 let mut result = self.clone();
989 result.schema = target_schema;
990 (result.positive_spans, result.positive_buckets) = reduce_side(
991 &self.positive_spans,
992 &self.positive_buckets,
993 self.schema,
994 target_schema,
995 )?;
996 (result.negative_spans, result.negative_buckets) = reduce_side(
997 &self.negative_spans,
998 &self.negative_buckets,
999 self.schema,
1000 target_schema,
1001 )?;
1002 Some(result)
1003 }
1004
1005 fn copy_to_custom_values(&self, custom_values: Vec<f64>) -> Option<Self> {
1006 if !self.uses_custom_buckets() {
1007 return None;
1008 }
1009 if self.custom_values == custom_values {
1010 return Some(self.clone());
1011 }
1012
1013 let mut result = self.clone();
1014 result.custom_values = custom_values.clone();
1015 result.negative_spans.clear();
1016 result.negative_buckets.clear();
1017 (result.positive_spans, result.positive_buckets) = map_custom_side(
1018 &self.positive_spans,
1019 &self.positive_buckets,
1020 &self.custom_values,
1021 &custom_values,
1022 )?;
1023 Some(result)
1024 }
1025
1026 fn grow_zero_threshold(&mut self, zero_threshold: f64) -> Option<()> {
1027 if self.uses_custom_buckets() || zero_threshold == self.zero_threshold {
1028 self.zero_threshold = zero_threshold;
1029 return Some(());
1030 }
1031
1032 let (spans, buckets, zero_count) = fold_zero_side(
1033 &self.positive_spans,
1034 &self.positive_buckets,
1035 self.schema,
1036 zero_threshold,
1037 )?;
1038 self.positive_spans = spans;
1039 self.positive_buckets = buckets;
1040 self.zero_count += zero_count;
1041
1042 let (spans, buckets, zero_count) = fold_zero_side(
1043 &self.negative_spans,
1044 &self.negative_buckets,
1045 self.schema,
1046 zero_threshold,
1047 )?;
1048 self.negative_spans = spans;
1049 self.negative_buckets = buckets;
1050 self.zero_count += zero_count;
1051 self.zero_threshold = zero_threshold;
1052 Some(())
1053 }
1054
1055 fn expanded_zero_threshold(&self, mut zero_threshold: f64) -> Option<f64> {
1056 if self.uses_custom_buckets() {
1057 return Some(zero_threshold);
1058 }
1059 zero_threshold = expand_zero_threshold_side(
1060 &self.positive_spans,
1061 &self.positive_buckets,
1062 self.schema,
1063 zero_threshold,
1064 )?;
1065 expand_zero_threshold_side(
1066 &self.negative_spans,
1067 &self.negative_buckets,
1068 self.schema,
1069 zero_threshold,
1070 )
1071 }
1072}
1073
1074fn add_reset_hint(left: CounterResetHint, right: CounterResetHint) -> CounterResetHint {
1075 if left == CounterResetHint::Gauge || right == CounterResetHint::Gauge {
1076 CounterResetHint::Gauge
1077 } else if left == right {
1078 left
1079 } else {
1080 CounterResetHint::Unknown
1081 }
1082}
1083
1084fn side_bucket_indices(spans: &[Span]) -> Option<Vec<i32>> {
1085 let mut indices = Vec::new();
1086 let mut current_index = 0i32;
1087 let mut first = true;
1088 for (span_index, span) in spans.iter().enumerate() {
1089 if span_index > 0 && span.offset < 0 {
1090 return None;
1091 }
1092 if first {
1093 current_index = span.offset;
1094 first = false;
1095 } else {
1096 current_index = current_index.checked_add(span.offset)?;
1097 }
1098 for _ in 0..span.length {
1099 indices.push(current_index);
1100 current_index = current_index.checked_add(1)?;
1101 }
1102 }
1103 Some(indices)
1104}
1105
1106fn span_bucket_len(spans: &[Span]) -> Option<usize> {
1107 spans
1108 .iter()
1109 .try_fold(0usize, |sum, span| sum.checked_add(span.length as usize))
1110}
1111
1112fn spans_from_indices_counts(values: Vec<(i32, f64)>) -> Option<(Vec<Span>, Vec<f64>)> {
1113 let mut spans = Vec::<Span>::new();
1114 let mut buckets = Vec::new();
1115 let mut previous_index = None::<i32>;
1116
1117 for (idx, count) in values {
1118 if count == 0.0 {
1119 continue;
1120 }
1121 match (spans.last_mut(), previous_index) {
1122 (Some(span), Some(previous)) if previous.checked_add(1) == Some(idx) => {
1123 span.length = span.length.checked_add(1)?;
1124 }
1125 (_, Some(previous)) => {
1126 spans.push(Span {
1127 offset: idx.checked_sub(previous)?.checked_sub(1)?,
1128 length: 1,
1129 });
1130 }
1131 (_, None) => {
1132 spans.push(Span {
1133 offset: idx,
1134 length: 1,
1135 });
1136 }
1137 }
1138 buckets.push(count);
1139 previous_index = Some(idx);
1140 }
1141
1142 Some((spans, buckets))
1143}
1144
1145fn side_counts(spans: &[Span], buckets: &[f64]) -> Option<BTreeMap<i32, f64>> {
1146 let mut values = BTreeMap::new();
1147 for (idx, count) in side_bucket_indices(spans)?.into_iter().zip(buckets) {
1148 if *count != 0.0 {
1149 values.insert(idx, *count);
1150 }
1151 }
1152 Some(values)
1153}
1154
1155fn side_layout_equal(
1156 left_spans: &[Span],
1157 left_buckets: &[f64],
1158 right_spans: &[Span],
1159 right_buckets: &[f64],
1160) -> bool {
1161 if span_bucket_len(left_spans) != Some(left_buckets.len())
1162 || span_bucket_len(right_spans) != Some(right_buckets.len())
1163 {
1164 return false;
1165 }
1166 let Some(left_indices) = side_bucket_indices(left_spans) else {
1167 return false;
1168 };
1169 let Some(right_indices) = side_bucket_indices(right_spans) else {
1170 return false;
1171 };
1172 left_indices == right_indices
1173 && left_buckets
1174 .iter()
1175 .zip(right_buckets)
1176 .all(|(left, right)| left.to_bits() == right.to_bits())
1177}
1178
1179fn merge_side(
1180 left_spans: &[Span],
1181 left_buckets: &[f64],
1182 right_spans: &[Span],
1183 right_buckets: &[f64],
1184 op: impl Fn(f64, f64) -> f64,
1185) -> Option<(Vec<Span>, Vec<f64>)> {
1186 let left = side_counts(left_spans, left_buckets)?;
1187 let right = side_counts(right_spans, right_buckets)?;
1188 let mut values = BTreeMap::new();
1189 for idx in left.keys().chain(right.keys()) {
1190 values.insert(
1191 *idx,
1192 op(
1193 left.get(idx).copied().unwrap_or_default(),
1194 right.get(idx).copied().unwrap_or_default(),
1195 ),
1196 );
1197 }
1198 spans_from_indices_counts(values.into_iter().collect())
1199}
1200
1201fn reduce_side(
1202 spans: &[Span],
1203 buckets: &[f64],
1204 schema: i32,
1205 target_schema: i32,
1206) -> Option<(Vec<Span>, Vec<f64>)> {
1207 let factor = 1_i32.checked_shl((schema - target_schema) as u32)?;
1208 let mut values = std::collections::BTreeMap::<i32, f64>::new();
1209 for (idx, count) in side_bucket_indices(spans)?.into_iter().zip(buckets) {
1210 let target_idx = ceil_div(idx, factor);
1211 *values.entry(target_idx).or_default() += *count;
1212 }
1213 spans_from_indices_counts(values.into_iter().collect())
1214}
1215
1216fn compact_side(spans: &[Span], buckets: &[f64]) -> Option<(Vec<Span>, Vec<f64>)> {
1217 let indices = side_bucket_indices(spans)?;
1218 spans_from_indices_counts(indices.into_iter().zip(buckets.iter().copied()).collect())
1219}
1220
1221fn map_custom_side(
1222 spans: &[Span],
1223 buckets: &[f64],
1224 old_values: &[f64],
1225 new_values: &[f64],
1226) -> Option<(Vec<Span>, Vec<f64>)> {
1227 let mut values = std::collections::BTreeMap::<i32, f64>::new();
1228 for (idx, count) in side_bucket_indices(spans)?.into_iter().zip(buckets) {
1229 let upper = get_bound(idx, CUSTOM_BUCKETS_SCHEMA, old_values)?;
1230 let target_idx = new_values
1231 .iter()
1232 .position(|value| *value >= upper)
1233 .unwrap_or(new_values.len()) as i32;
1234 *values.entry(target_idx).or_default() += *count;
1235 }
1236 spans_from_indices_counts(values.into_iter().collect())
1237}
1238
1239fn fold_zero_side(
1240 spans: &[Span],
1241 buckets: &[f64],
1242 schema: i32,
1243 zero_threshold: f64,
1244) -> Option<(Vec<Span>, Vec<f64>, f64)> {
1245 let mut kept = Vec::new();
1246 let mut zero_count = 0.0;
1247 for (idx, count) in side_bucket_indices(spans)?.into_iter().zip(buckets) {
1248 if get_bound(idx, schema, &[])? <= zero_threshold {
1249 zero_count += *count;
1250 } else {
1251 kept.push((idx, *count));
1252 }
1253 }
1254 let (spans, buckets) = spans_from_indices_counts(kept)?;
1255 Some((spans, buckets, zero_count))
1256}
1257
1258fn expand_zero_threshold_side(
1259 spans: &[Span],
1260 buckets: &[f64],
1261 schema: i32,
1262 mut zero_threshold: f64,
1263) -> Option<f64> {
1264 for (idx, count) in side_bucket_indices(spans)?.into_iter().zip(buckets) {
1265 if *count == 0.0 {
1266 continue;
1267 }
1268 let lower = get_bound(idx.checked_sub(1)?, schema, &[])?;
1269 let upper = get_bound(idx, schema, &[])?;
1270 if lower < zero_threshold && zero_threshold < upper {
1271 zero_threshold = upper;
1272 }
1273 }
1274 Some(zero_threshold)
1275}
1276
1277fn ceil_div(value: i32, divisor: i32) -> i32 {
1278 value.div_euclid(divisor) + i32::from(value.rem_euclid(divisor) != 0)
1279}
1280
1281pub fn exponential_overflow_bucket_index(schema: i32) -> Option<i32> {
1283 if !(MIN_EXPONENTIAL_SCHEMA..=MAX_EXPONENTIAL_SCHEMA).contains(&schema) {
1284 return None;
1285 }
1286 let last_finite = if schema >= 0 {
1287 1024_i64.checked_shl(schema as u32)?
1288 } else {
1289 1024_i64.checked_shr((-schema) as u32)?
1290 };
1291 i32::try_from(last_finite.checked_add(1)?).ok()
1292}
1293
1294fn get_bound(idx: i32, schema: i32, custom_values: &[f64]) -> Option<f64> {
1295 if schema == CUSTOM_BUCKETS_SCHEMA {
1296 return match idx {
1297 -1 => Some(f64::NEG_INFINITY),
1298 idx if idx == custom_values.len() as i32 => Some(f64::INFINITY),
1299 idx if idx >= 0 && (idx as usize) < custom_values.len() => {
1300 Some(custom_values[idx as usize])
1301 }
1302 _ => None,
1303 };
1304 }
1305
1306 let overflow_index = exponential_overflow_bucket_index(schema)?;
1307 if idx > overflow_index {
1308 return None;
1309 }
1310 if idx == overflow_index {
1311 return Some(f64::INFINITY);
1312 }
1313 if idx == overflow_index - 1 {
1314 return Some(f64::MAX);
1315 }
1316 if schema < 0 {
1317 let exponent = i64::from(idx).checked_shl((-schema) as u32)?;
1318 let Ok(exponent) = i32::try_from(exponent) else {
1319 return Some(0.0);
1320 };
1321 return Some(2.0_f64.powi(exponent));
1322 }
1323 let exponent = idx as f64 / (1u32 << schema) as f64;
1324 Some(2.0_f64.powf(exponent))
1325}
1326
1327pub fn native_histogram_arrow_type() -> ArrowDataType {
1329 native_histogram_value_type().as_arrow_type()
1330}
1331
1332fn struct_child<'a>(array: &'a StructArray, name: &str) -> DfResult<&'a ArrayRef> {
1333 let index = array
1334 .fields()
1335 .iter()
1336 .position(|field| field.name() == name)
1337 .ok_or_else(|| {
1338 DataFusionError::Execution(format!("native histogram missing field {name}"))
1339 })?;
1340 Ok(array.column(index))
1341}
1342
1343fn primitive_child<'a, T>(array: &'a StructArray, name: &str) -> DfResult<&'a PrimitiveArray<T>>
1344where
1345 T: ArrowPrimitiveType,
1346{
1347 let child = struct_child(array, name)?;
1348 child
1349 .as_any()
1350 .downcast_ref::<PrimitiveArray<T>>()
1351 .ok_or_else(|| {
1352 DataFusionError::Execution(format!(
1353 "native histogram field {name} has invalid type {}",
1354 child.data_type()
1355 ))
1356 })
1357}
1358
1359fn list_child<'a>(array: &'a StructArray, name: &str) -> DfResult<&'a ListArray> {
1360 let child = struct_child(array, name)?;
1361 child.as_any().downcast_ref::<ListArray>().ok_or_else(|| {
1362 DataFusionError::Execution(format!(
1363 "native histogram field {name} has invalid type {}",
1364 child.data_type()
1365 ))
1366 })
1367}
1368
1369fn required_primitive<T>(array: &StructArray, name: &str, row: usize) -> DfResult<T::Native>
1370where
1371 T: ArrowPrimitiveType,
1372{
1373 let values = primitive_child::<T>(array, name)?;
1374 if values.is_null(row) {
1375 return Err(DataFusionError::Execution(format!(
1376 "native histogram field {name} is null"
1377 )));
1378 }
1379 Ok(values.value(row))
1380}
1381
1382fn optional_primitive<T>(array: &StructArray, name: &str, row: usize) -> DfResult<Option<T::Native>>
1383where
1384 T: ArrowPrimitiveType,
1385{
1386 let values = primitive_child::<T>(array, name)?;
1387 Ok((!values.is_null(row)).then(|| values.value(row)))
1388}
1389
1390fn list_values<T>(array: &StructArray, name: &str, row: usize) -> DfResult<Vec<T::Native>>
1391where
1392 T: ArrowPrimitiveType,
1393{
1394 let list = list_child(array, name)?;
1395 if list.is_null(row) {
1396 return Ok(Vec::new());
1397 }
1398
1399 let values = list.value(row);
1400 let values = values
1401 .as_any()
1402 .downcast_ref::<PrimitiveArray<T>>()
1403 .ok_or_else(|| {
1404 DataFusionError::Execution(format!(
1405 "native histogram list field {name} has invalid value type {}",
1406 values.data_type()
1407 ))
1408 })?;
1409
1410 values
1411 .iter()
1412 .map(|value| {
1413 value.ok_or_else(|| {
1414 DataFusionError::Execution(format!(
1415 "native histogram list field {name} contains null"
1416 ))
1417 })
1418 })
1419 .collect()
1420}
1421
1422fn read_spans(offsets: Vec<i32>, lengths: Vec<i32>, name: &str) -> DfResult<Vec<Span>> {
1423 if offsets.len() != lengths.len() {
1424 return Err(DataFusionError::Execution(format!(
1425 "native histogram {name} span offsets and lengths mismatch: {} vs {}",
1426 offsets.len(),
1427 lengths.len()
1428 )));
1429 }
1430 offsets
1431 .into_iter()
1432 .zip(lengths)
1433 .map(|(offset, length)| {
1434 if length < 0 {
1435 return Err(DataFusionError::Execution(format!(
1436 "native histogram {name} span has negative length {length}"
1437 )));
1438 }
1439 Ok(Span { offset, length })
1440 })
1441 .collect()
1442}
1443
1444fn check_span_bucket_count(spans: &[Span], buckets: usize, name: &str) -> DfResult<()> {
1445 let span_len = span_bucket_len(spans).ok_or_else(|| {
1446 DataFusionError::Execution(format!("native histogram {name} spans overflow"))
1447 })?;
1448 if span_len != buckets {
1449 return Err(DataFusionError::Execution(format!(
1450 "native histogram {name} spans describe {span_len} buckets, found {buckets}"
1451 )));
1452 }
1453 Ok(())
1454}
1455
1456pub fn read_histogram(array: &StructArray, row: usize) -> DfResult<Option<NativeHistogram>> {
1461 if array.is_null(row) {
1462 return Ok(None);
1463 }
1464
1465 let schema = required_primitive::<Int32Type>(array, SCHEMA_FIELD, row)?;
1466 let positive_spans = read_spans(
1467 list_values::<Int32Type>(array, POSITIVE_SPAN_OFFSETS_FIELD, row)?,
1468 list_values::<Int32Type>(array, POSITIVE_SPAN_LENGTHS_FIELD, row)?,
1469 "positive",
1470 )?;
1471 let negative_spans = read_spans(
1472 list_values::<Int32Type>(array, NEGATIVE_SPAN_OFFSETS_FIELD, row)?,
1473 list_values::<Int32Type>(array, NEGATIVE_SPAN_LENGTHS_FIELD, row)?,
1474 "negative",
1475 )?;
1476
1477 let (count, zero_count, positive_buckets, negative_buckets) =
1478 if let Some(count) = optional_primitive::<Float64Type>(array, COUNT_F64_FIELD, row)? {
1479 (
1480 count,
1481 optional_primitive::<Float64Type>(array, ZERO_COUNT_F64_FIELD, row)?
1482 .unwrap_or_default(),
1483 list_values::<Float64Type>(array, POSITIVE_BUCKETS_F64_FIELD, row)?,
1484 list_values::<Float64Type>(array, NEGATIVE_BUCKETS_F64_FIELD, row)?,
1485 )
1486 } else {
1487 (
1488 required_primitive::<Int64Type>(array, COUNT_I64_FIELD, row)? as f64,
1489 optional_primitive::<Int64Type>(array, ZERO_COUNT_I64_FIELD, row)?
1490 .unwrap_or_default() as f64,
1491 list_values::<Int64Type>(array, POSITIVE_BUCKETS_I64_FIELD, row)?
1492 .into_iter()
1493 .map(|value| value as f64)
1494 .collect(),
1495 list_values::<Int64Type>(array, NEGATIVE_BUCKETS_I64_FIELD, row)?
1496 .into_iter()
1497 .map(|value| value as f64)
1498 .collect(),
1499 )
1500 };
1501
1502 check_span_bucket_count(&positive_spans, positive_buckets.len(), "positive")?;
1503 check_span_bucket_count(&negative_spans, negative_buckets.len(), "negative")?;
1504
1505 Ok(Some(NativeHistogram {
1506 schema,
1507 zero_threshold: required_primitive::<Float64Type>(array, ZERO_THRESHOLD_FIELD, row)?,
1508 sum: required_primitive::<Float64Type>(array, SUM_FIELD, row)?,
1509 reset_hint: required_primitive::<Int32Type>(array, RESET_HINT_FIELD, row)?.into(),
1510 start_timestamp: optional_primitive::<TimestampMillisecondType>(
1511 array,
1512 START_TIMESTAMP_FIELD,
1513 row,
1514 )?,
1515 custom_values: list_values::<Float64Type>(array, CUSTOM_VALUES_FIELD, row)?,
1516 positive_spans,
1517 negative_spans,
1518 count,
1519 zero_count,
1520 positive_buckets,
1521 negative_buckets,
1522 }))
1523}
1524
1525fn list_opt<T>(values: Vec<T>) -> Option<Vec<Option<T>>> {
1526 Some(values.into_iter().map(Some).collect())
1527}
1528
1529pub fn build_histogram_array(values: &[Option<NativeHistogram>]) -> ArrayRef {
1535 let mut schemas = Vec::with_capacity(values.len());
1536 let mut zero_thresholds = Vec::with_capacity(values.len());
1537 let mut sums = Vec::with_capacity(values.len());
1538 let mut reset_hints = Vec::with_capacity(values.len());
1539 let mut start_timestamps = Vec::with_capacity(values.len());
1540 let mut custom_values = Vec::with_capacity(values.len());
1541 let mut positive_span_offsets = Vec::with_capacity(values.len());
1542 let mut positive_span_lengths = Vec::with_capacity(values.len());
1543 let mut negative_span_offsets = Vec::with_capacity(values.len());
1544 let mut negative_span_lengths = Vec::with_capacity(values.len());
1545 let mut count_i64 = Vec::<Option<i64>>::with_capacity(values.len());
1546 let mut zero_count_i64 = Vec::<Option<i64>>::with_capacity(values.len());
1547 let mut positive_buckets_i64 = Vec::with_capacity(values.len());
1548 let mut negative_buckets_i64 = Vec::with_capacity(values.len());
1549 let mut count_f64 = Vec::with_capacity(values.len());
1550 let mut zero_count_f64 = Vec::with_capacity(values.len());
1551 let mut positive_buckets_f64 = Vec::with_capacity(values.len());
1552 let mut negative_buckets_f64 = Vec::with_capacity(values.len());
1553 let mut validity = Vec::with_capacity(values.len());
1554
1555 for value in values {
1556 validity.push(value.is_some());
1557 if let Some(histogram) = value {
1558 schemas.push(Some(histogram.schema));
1559 zero_thresholds.push(Some(histogram.zero_threshold));
1560 sums.push(Some(histogram.sum));
1561 reset_hints.push(Some(i32::from(histogram.reset_hint)));
1562 start_timestamps.push(histogram.start_timestamp);
1563 custom_values.push(list_opt(histogram.custom_values.clone()));
1564 positive_span_offsets.push(list_opt(
1565 histogram
1566 .positive_spans
1567 .iter()
1568 .map(|span| span.offset)
1569 .collect(),
1570 ));
1571 positive_span_lengths.push(list_opt(
1572 histogram
1573 .positive_spans
1574 .iter()
1575 .map(|span| span.length)
1576 .collect(),
1577 ));
1578 negative_span_offsets.push(list_opt(
1579 histogram
1580 .negative_spans
1581 .iter()
1582 .map(|span| span.offset)
1583 .collect(),
1584 ));
1585 negative_span_lengths.push(list_opt(
1586 histogram
1587 .negative_spans
1588 .iter()
1589 .map(|span| span.length)
1590 .collect(),
1591 ));
1592 count_i64.push(None);
1593 zero_count_i64.push(None);
1594 positive_buckets_i64.push(list_opt(Vec::<i64>::new()));
1595 negative_buckets_i64.push(list_opt(Vec::<i64>::new()));
1596 count_f64.push(Some(histogram.count));
1597 zero_count_f64.push(Some(histogram.zero_count));
1598 positive_buckets_f64.push(list_opt(histogram.positive_buckets.clone()));
1599 negative_buckets_f64.push(list_opt(histogram.negative_buckets.clone()));
1600 } else {
1601 schemas.push(None);
1602 zero_thresholds.push(None);
1603 sums.push(None);
1604 reset_hints.push(None);
1605 start_timestamps.push(None);
1606 custom_values.push(None);
1607 positive_span_offsets.push(None);
1608 positive_span_lengths.push(None);
1609 negative_span_offsets.push(None);
1610 negative_span_lengths.push(None);
1611 count_i64.push(None);
1612 zero_count_i64.push(None);
1613 positive_buckets_i64.push(None);
1614 negative_buckets_i64.push(None);
1615 count_f64.push(None);
1616 zero_count_f64.push(None);
1617 positive_buckets_f64.push(None);
1618 negative_buckets_f64.push(None);
1619 }
1620 }
1621
1622 let named_arrays: Vec<(&str, ArrayRef)> = vec![
1623 (SCHEMA_FIELD, Arc::new(Int32Array::from(schemas))),
1624 (
1625 ZERO_THRESHOLD_FIELD,
1626 Arc::new(Float64Array::from(zero_thresholds)),
1627 ),
1628 (SUM_FIELD, Arc::new(Float64Array::from(sums))),
1629 (RESET_HINT_FIELD, Arc::new(Int32Array::from(reset_hints))),
1630 (
1631 START_TIMESTAMP_FIELD,
1632 Arc::new(TimestampMillisecondArray::from_iter(start_timestamps)),
1633 ),
1634 (
1635 CUSTOM_VALUES_FIELD,
1636 Arc::new(ListArray::from_iter_primitive::<Float64Type, _, _>(
1637 custom_values,
1638 )),
1639 ),
1640 (
1641 POSITIVE_SPAN_OFFSETS_FIELD,
1642 Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(
1643 positive_span_offsets,
1644 )),
1645 ),
1646 (
1647 POSITIVE_SPAN_LENGTHS_FIELD,
1648 Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(
1649 positive_span_lengths,
1650 )),
1651 ),
1652 (
1653 NEGATIVE_SPAN_OFFSETS_FIELD,
1654 Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(
1655 negative_span_offsets,
1656 )),
1657 ),
1658 (
1659 NEGATIVE_SPAN_LENGTHS_FIELD,
1660 Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(
1661 negative_span_lengths,
1662 )),
1663 ),
1664 (COUNT_I64_FIELD, Arc::new(Int64Array::from(count_i64))),
1665 (
1666 ZERO_COUNT_I64_FIELD,
1667 Arc::new(Int64Array::from(zero_count_i64)),
1668 ),
1669 (
1670 POSITIVE_BUCKETS_I64_FIELD,
1671 Arc::new(ListArray::from_iter_primitive::<Int64Type, _, _>(
1672 positive_buckets_i64,
1673 )),
1674 ),
1675 (
1676 NEGATIVE_BUCKETS_I64_FIELD,
1677 Arc::new(ListArray::from_iter_primitive::<Int64Type, _, _>(
1678 negative_buckets_i64,
1679 )),
1680 ),
1681 (COUNT_F64_FIELD, Arc::new(Float64Array::from(count_f64))),
1682 (
1683 ZERO_COUNT_F64_FIELD,
1684 Arc::new(Float64Array::from(zero_count_f64)),
1685 ),
1686 (
1687 POSITIVE_BUCKETS_F64_FIELD,
1688 Arc::new(ListArray::from_iter_primitive::<Float64Type, _, _>(
1689 positive_buckets_f64,
1690 )),
1691 ),
1692 (
1693 NEGATIVE_BUCKETS_F64_FIELD,
1694 Arc::new(ListArray::from_iter_primitive::<Float64Type, _, _>(
1695 negative_buckets_f64,
1696 )),
1697 ),
1698 ];
1699 let (fields, arrays): (Vec<_>, Vec<_>) = named_arrays
1700 .into_iter()
1701 .map(|(name, array)| (Field::new(name, array.data_type().clone(), true), array))
1702 .unzip();
1703
1704 Arc::new(StructArray::new(
1705 fields.into(),
1706 arrays,
1707 Some(NullBuffer::from(validity)),
1708 ))
1709}
1710
1711#[cfg(test)]
1712mod tests {
1713 use super::*;
1714
1715 fn histogram(positive_spans: Vec<Span>, positive_buckets: Vec<f64>) -> NativeHistogram {
1716 let count = positive_buckets.iter().sum();
1717 NativeHistogram {
1718 schema: 0,
1719 zero_threshold: 0.0,
1720 sum: count,
1721 reset_hint: CounterResetHint::CounterReset,
1722 start_timestamp: None,
1723 custom_values: Vec::new(),
1724 positive_spans,
1725 negative_spans: Vec::new(),
1726 count,
1727 zero_count: 0.0,
1728 positive_buckets,
1729 negative_buckets: Vec::new(),
1730 }
1731 }
1732
1733 #[test]
1734 fn promql_string_matches_prometheus_float_format() {
1735 assert_eq!(format_promql_histogram_float(0.0001), "0.0001");
1736 assert_eq!(format_promql_histogram_float(1_000_000.0), "1e+06");
1737
1738 let histogram = NativeHistogram {
1739 schema: CUSTOM_BUCKETS_SCHEMA,
1740 zero_threshold: 0.0,
1741 sum: 2_349_209.324,
1742 reset_hint: CounterResetHint::Unknown,
1743 start_timestamp: None,
1744 custom_values: vec![0.00001],
1745 positive_spans: vec![Span {
1746 offset: 0,
1747 length: 2,
1748 }],
1749 negative_spans: Vec::new(),
1750 count: 3.0,
1751 zero_count: 0.0,
1752 positive_buckets: vec![1.0, 2.0],
1753 negative_buckets: Vec::new(),
1754 };
1755
1756 assert_eq!(
1757 histogram.promql_string(),
1758 "{count:3, sum:2.349209324e+06, [-Inf,1e-05]:1, (1e-05,+Inf]:2}"
1759 );
1760 }
1761
1762 #[test]
1763 fn span_rebuild_checks_offsets_and_preserves_empty_input() {
1764 assert_eq!(
1765 spans_from_indices_counts(Vec::new()),
1766 Some((Vec::new(), Vec::new()))
1767 );
1768 assert_eq!(
1769 spans_from_indices_counts(vec![(2, 1.0), (3, 2.0), (5, 3.0)]),
1770 Some((
1771 vec![
1772 Span {
1773 offset: 2,
1774 length: 2,
1775 },
1776 Span {
1777 offset: 1,
1778 length: 1,
1779 },
1780 ],
1781 vec![1.0, 2.0, 3.0],
1782 ))
1783 );
1784 assert_eq!(
1785 spans_from_indices_counts(vec![(i32::MIN, 1.0), (i32::MAX, 2.0)]),
1786 None
1787 );
1788 }
1789
1790 #[test]
1791 fn arrow_round_trip_preserves_unrecognized_reset_hint() {
1792 let mut expected = histogram(
1793 vec![Span {
1794 offset: 0,
1795 length: 1,
1796 }],
1797 vec![1.0],
1798 );
1799 expected.reset_hint = CounterResetHint::Unrecognized(42);
1800
1801 let array = build_histogram_array(&[Some(expected.clone())]);
1802 assert_eq!(array.data_type(), &native_histogram_arrow_type());
1803 let array = array.as_any().downcast_ref::<StructArray>().unwrap();
1804
1805 assert_eq!(read_histogram(array, 0).unwrap(), Some(expected));
1806 assert!(
1807 primitive_child::<Int64Type>(array, COUNT_I64_FIELD)
1808 .unwrap()
1809 .is_null(0)
1810 );
1811 assert_eq!(
1812 primitive_child::<Float64Type>(array, COUNT_F64_FIELD)
1813 .unwrap()
1814 .value(0),
1815 1.0
1816 );
1817 }
1818
1819 #[test]
1820 fn add_uses_sparse_bucket_union() {
1821 let left = histogram(
1822 vec![Span {
1823 offset: 0,
1824 length: 1,
1825 }],
1826 vec![1.0],
1827 );
1828 let right = histogram(
1829 vec![Span {
1830 offset: 2,
1831 length: 1,
1832 }],
1833 vec![2.0],
1834 );
1835
1836 let result = left.add(&right).unwrap();
1837 assert_eq!(
1838 result.positive_spans,
1839 vec![
1840 Span {
1841 offset: 0,
1842 length: 1,
1843 },
1844 Span {
1845 offset: 1,
1846 length: 1,
1847 },
1848 ]
1849 );
1850 assert_eq!(result.positive_buckets, vec![1.0, 2.0]);
1851 }
1852
1853 #[test]
1854 fn add_combines_reset_hints_for_compatible_empty_payloads() {
1855 let mut empty = histogram(Vec::new(), Vec::new());
1856 empty.reset_hint = CounterResetHint::Gauge;
1857 let mut populated = histogram(
1858 vec![Span {
1859 offset: 0,
1860 length: 1,
1861 }],
1862 vec![1.0],
1863 );
1864 populated.reset_hint = CounterResetHint::NotCounterReset;
1865
1866 assert_eq!(
1867 empty.add(&populated).unwrap().reset_hint,
1868 CounterResetHint::Gauge
1869 );
1870 assert_eq!(
1871 populated.add(&empty).unwrap().reset_hint,
1872 CounterResetHint::Gauge
1873 );
1874 }
1875
1876 #[test]
1877 fn empty_arithmetic_rejects_incompatible_schemas() {
1878 let exponential = histogram(Vec::new(), Vec::new());
1879 let mut custom = histogram(
1880 vec![Span {
1881 offset: 0,
1882 length: 1,
1883 }],
1884 vec![2.0],
1885 );
1886 custom.schema = CUSTOM_BUCKETS_SCHEMA;
1887 custom.custom_values = vec![1.0];
1888
1889 assert!(exponential.add(&custom).is_none());
1890 assert!(custom.add(&exponential).is_none());
1891 assert!(exponential.sub(&custom).is_none());
1892 assert!(custom.sub(&exponential).is_none());
1893 }
1894
1895 #[test]
1896 fn empty_custom_histogram_still_reconciles_bounds() {
1897 let mut empty = histogram(Vec::new(), Vec::new());
1898 empty.schema = CUSTOM_BUCKETS_SCHEMA;
1899 empty.custom_values = vec![1.0];
1900 let mut populated = histogram(
1901 vec![Span {
1902 offset: 0,
1903 length: 1,
1904 }],
1905 vec![2.0],
1906 );
1907 populated.schema = CUSTOM_BUCKETS_SCHEMA;
1908 populated.custom_values = vec![2.0];
1909
1910 let result = empty.add(&populated).unwrap();
1911 assert!(result.custom_values.is_empty());
1912 assert_eq!(result.positive_buckets, vec![2.0]);
1913 }
1914
1915 #[test]
1916 fn zero_threshold_expands_to_populated_bucket_boundary() {
1917 let left = NativeHistogram {
1918 zero_threshold: 0.5,
1919 ..histogram(
1920 vec![Span {
1921 offset: 0,
1922 length: 1,
1923 }],
1924 vec![1.0],
1925 )
1926 };
1927 let right = NativeHistogram {
1928 zero_threshold: 0.75,
1929 ..histogram(
1930 vec![Span {
1931 offset: 1,
1932 length: 1,
1933 }],
1934 vec![1.0],
1935 )
1936 };
1937
1938 let result = left.add(&right).unwrap();
1939 assert_eq!(result.zero_threshold, 1.0);
1940 assert_eq!(result.zero_count, 1.0);
1941 assert_eq!(result.positive_buckets, vec![1.0]);
1942 }
1943
1944 #[test]
1945 fn custom_reconciliation_without_shared_bounds_uses_overflow_bucket() {
1946 let mut left = histogram(
1947 vec![Span {
1948 offset: 0,
1949 length: 1,
1950 }],
1951 vec![1.0],
1952 );
1953 left.schema = CUSTOM_BUCKETS_SCHEMA;
1954 left.custom_values = vec![1.0];
1955
1956 let mut right = histogram(
1957 vec![Span {
1958 offset: 0,
1959 length: 1,
1960 }],
1961 vec![2.0],
1962 );
1963 right.schema = CUSTOM_BUCKETS_SCHEMA;
1964 right.custom_values = vec![2.0];
1965
1966 let result = left.add(&right).unwrap();
1967 assert!(result.custom_values.is_empty());
1968 assert_eq!(
1969 result.positive_spans,
1970 vec![Span {
1971 offset: 0,
1972 length: 1,
1973 }]
1974 );
1975 assert_eq!(result.positive_buckets, vec![3.0]);
1976 }
1977
1978 #[test]
1979 fn promql_eq_ignores_metadata_but_compares_sparse_layout() {
1980 let mut left = histogram(
1981 vec![Span {
1982 offset: 0,
1983 length: 2,
1984 }],
1985 vec![1.0, 0.0],
1986 );
1987 left.reset_hint = CounterResetHint::CounterReset;
1988 left.start_timestamp = Some(1000);
1989
1990 let mut right = histogram(
1991 vec![Span {
1992 offset: 0,
1993 length: 1,
1994 }],
1995 vec![1.0],
1996 );
1997 right.reset_hint = CounterResetHint::NotCounterReset;
1998 right.start_timestamp = Some(2000);
1999
2000 assert!(!left.promql_eq(&right));
2001
2002 right.positive_spans = vec![
2003 Span {
2004 offset: 0,
2005 length: 0,
2006 },
2007 Span {
2008 offset: 0,
2009 length: 1,
2010 },
2011 Span {
2012 offset: 42,
2013 length: 0,
2014 },
2015 ];
2016 left.positive_spans = vec![Span {
2017 offset: 0,
2018 length: 1,
2019 }];
2020 left.positive_buckets = vec![1.0];
2021 assert!(left.promql_eq(&right));
2022 }
2023
2024 #[test]
2025 fn promql_eq_compares_payload_floats_by_bits() {
2026 let nan = f64::from_bits(0x7ff8_0000_0000_0001);
2027 let other_nan = f64::from_bits(0x7ff8_0000_0000_0002);
2028 let mut left = histogram(
2029 vec![Span {
2030 offset: 0,
2031 length: 1,
2032 }],
2033 vec![nan],
2034 );
2035 left.count = nan;
2036 left.zero_count = nan;
2037 left.sum = nan;
2038 let right = left.clone();
2039
2040 assert!(left.promql_eq(&right));
2041
2042 for changed in [
2043 NativeHistogram {
2044 count: other_nan,
2045 ..right.clone()
2046 },
2047 NativeHistogram {
2048 zero_count: other_nan,
2049 ..right.clone()
2050 },
2051 NativeHistogram {
2052 sum: other_nan,
2053 ..right.clone()
2054 },
2055 NativeHistogram {
2056 positive_buckets: vec![other_nan],
2057 ..right.clone()
2058 },
2059 ] {
2060 assert!(!left.promql_eq(&changed));
2061 }
2062
2063 let undecodable = histogram(
2064 vec![Span {
2065 offset: i32::MAX,
2066 length: 1,
2067 }],
2068 vec![1.0],
2069 );
2070 assert!(!undecodable.promql_eq(&undecodable.clone()));
2071
2072 let mut mismatched = histogram(
2073 vec![Span {
2074 offset: 0,
2075 length: 2,
2076 }],
2077 vec![1.0],
2078 );
2079 mismatched.count = 1.0;
2080 mismatched.sum = 1.0;
2081 assert!(!mismatched.promql_eq(&mismatched.clone()));
2082
2083 let invalid_offset = histogram(
2084 vec![
2085 Span {
2086 offset: 0,
2087 length: 1,
2088 },
2089 Span {
2090 offset: -1,
2091 length: 1,
2092 },
2093 ],
2094 vec![1.0, 1.0],
2095 );
2096 assert!(!invalid_offset.promql_eq(&invalid_offset.clone()));
2097 }
2098
2099 #[test]
2100 fn detect_reset_preserves_layout_direction() {
2101 let mut previous = histogram(
2102 vec![Span {
2103 offset: 1,
2104 length: 1,
2105 }],
2106 vec![1.0],
2107 );
2108 previous.reset_hint = CounterResetHint::Unknown;
2109
2110 let mut higher_resolution = previous.clone();
2111 higher_resolution.schema = 1;
2112 higher_resolution.positive_spans[0].offset = 2;
2113 assert!(higher_resolution.detect_reset(&previous));
2114
2115 let mut lower_resolution = previous.clone();
2116 lower_resolution.schema = 0;
2117 lower_resolution.positive_spans[0].offset = 1;
2118 let mut previous_higher_resolution = higher_resolution;
2119 previous_higher_resolution.reset_hint = CounterResetHint::Unknown;
2120 assert!(!lower_resolution.detect_reset(&previous_higher_resolution));
2121
2122 let mut smaller_zero_threshold = previous.clone();
2123 smaller_zero_threshold.zero_threshold = 0.5;
2124 previous.zero_threshold = 1.0;
2125 assert!(smaller_zero_threshold.detect_reset(&previous));
2126
2127 let mut previous = histogram(
2128 vec![Span {
2129 offset: 0,
2130 length: 1,
2131 }],
2132 vec![1.0],
2133 );
2134 previous.reset_hint = CounterResetHint::Unknown;
2135 let mut split_bucket = histogram(Vec::new(), Vec::new());
2136 split_bucket.reset_hint = CounterResetHint::Unknown;
2137 split_bucket.count = 1.0;
2138 split_bucket.zero_count = 1.0;
2139 split_bucket.zero_threshold = 0.75;
2140 assert!(split_bucket.detect_reset(&previous));
2141
2142 split_bucket.zero_threshold = 1.0;
2143 assert!(!split_bucket.detect_reset(&previous));
2144 }
2145
2146 #[test]
2147 fn extreme_bucket_index_returns_none_instead_of_overflowing() {
2148 assert_eq!(ceil_div(i32::MIN, 2), i32::MIN / 2);
2149
2150 let mut histogram = histogram(
2151 vec![Span {
2152 offset: i32::MIN,
2153 length: 1,
2154 }],
2155 vec![1.0],
2156 );
2157 assert!(histogram.side_buckets(true).is_none());
2158
2159 histogram.schema = 1;
2160 let reduced = histogram.copy_to_schema(0).unwrap();
2161 assert_eq!(reduced.positive_spans[0].offset, i32::MIN / 2);
2162 }
2163
2164 #[test]
2165 fn exponential_bucket_bounds_stop_after_overflow_bucket() {
2166 for schema in [-4, 0, 8] {
2167 let overflow = exponential_overflow_bucket_index(schema).unwrap();
2168 assert_eq!(get_bound(overflow - 1, schema, &[]), Some(f64::MAX));
2169 assert_eq!(get_bound(overflow, schema, &[]), Some(f64::INFINITY));
2170 assert_eq!(get_bound(overflow + 1, schema, &[]), None);
2171 }
2172 assert_eq!(exponential_overflow_bucket_index(-5), None);
2173 assert_eq!(exponential_overflow_bucket_index(9), None);
2174 assert_eq!(get_bound(i32::MIN, -4, &[]), Some(0.0));
2175 }
2176
2177 #[test]
2178 fn custom_bucket_midpoints_preserve_infinite_bounds() {
2179 let mut histogram = histogram(Vec::new(), Vec::new());
2180 histogram.schema = CUSTOM_BUCKETS_SCHEMA;
2181
2182 assert_eq!(
2183 histogram.bucket_midpoint(&Bucket {
2184 lower: f64::NEG_INFINITY,
2185 upper: -1.0,
2186 count: 1.0,
2187 boundary_rule: BoundaryRule::OpenLeft,
2188 }),
2189 f64::NEG_INFINITY
2190 );
2191 assert_eq!(
2192 histogram.bucket_midpoint(&Bucket {
2193 lower: 1.0,
2194 upper: f64::INFINITY,
2195 count: 1.0,
2196 boundary_rule: BoundaryRule::OpenLeft,
2197 }),
2198 f64::INFINITY
2199 );
2200 assert!(
2201 histogram
2202 .bucket_midpoint(&Bucket {
2203 lower: f64::NEG_INFINITY,
2204 upper: f64::INFINITY,
2205 count: 1.0,
2206 boundary_rule: BoundaryRule::OpenLeft,
2207 })
2208 .is_nan()
2209 );
2210 }
2211
2212 #[test]
2213 fn fraction_excludes_nan_observations() {
2214 let mut histogram = histogram(
2215 vec![Span {
2216 offset: 0,
2217 length: 1,
2218 }],
2219 vec![8.0],
2220 );
2221 histogram.count = 10.0;
2222 histogram.sum = f64::NAN;
2223
2224 assert_eq!(
2225 histogram.fraction_with_info(f64::NEG_INFINITY, f64::INFINITY),
2226 (0.8, true)
2227 );
2228 }
2229
2230 #[test]
2231 fn quantile_reports_nan_observation_effect() {
2232 let mut histogram = histogram(
2233 vec![Span {
2234 offset: 0,
2235 length: 1,
2236 }],
2237 vec![8.0],
2238 );
2239 histogram.count = 10.0;
2240 histogram.sum = f64::NAN;
2241
2242 let (skewed, info) = histogram.quantile_with_info(0.5);
2243 assert!(skewed.is_finite());
2244 assert_eq!(info, Some(NativeHistogramQuantileInfo::NaNSkew));
2245
2246 let (nan, info) = histogram.quantile_with_info(0.9);
2247 assert!(nan.is_nan());
2248 assert_eq!(info, Some(NativeHistogramQuantileInfo::NaNResult));
2249
2250 histogram.count = 8.0;
2251 assert_eq!(histogram.quantile_with_info(0.5).1, None);
2252
2253 histogram.count = 3.0;
2254 histogram.positive_spans.clear();
2255 histogram.positive_buckets.clear();
2256 let (nan, info) = histogram.quantile_with_info(0.0);
2257 assert!(nan.is_nan());
2258 assert_eq!(info, Some(NativeHistogramQuantileInfo::NaNSkew));
2259
2260 histogram.schema = CUSTOM_BUCKETS_SCHEMA;
2261 histogram.count = 10.0;
2262 histogram.positive_spans = vec![Span {
2263 offset: 0,
2264 length: 1,
2265 }];
2266 histogram.positive_buckets = vec![8.0];
2267 for (bound, offset, expected) in [(-1.0, 0, -1.0), (1.0, 1, 1.0)] {
2268 histogram.custom_values = vec![bound];
2269 histogram.positive_spans[0].offset = offset;
2270 assert_eq!(
2271 histogram.quantile_with_info(0.5),
2272 (expected, Some(NativeHistogramQuantileInfo::NaNSkew))
2273 );
2274 }
2275 }
2276
2277 #[test]
2278 fn subtraction_returns_gauge_histogram() {
2279 let left = histogram(
2280 vec![Span {
2281 offset: 0,
2282 length: 1,
2283 }],
2284 vec![3.0],
2285 );
2286 let right = histogram(
2287 vec![Span {
2288 offset: 0,
2289 length: 1,
2290 }],
2291 vec![1.0],
2292 );
2293
2294 let result = left.sub(&right).unwrap();
2295 assert_eq!(result.reset_hint, CounterResetHint::Gauge);
2296 assert_eq!(result.positive_buckets, vec![2.0]);
2297 }
2298
2299 #[test]
2300 fn subtraction_treats_compatible_empty_left_as_zero() {
2301 let left = histogram(vec![], vec![]);
2302 let right = histogram(
2303 vec![Span {
2304 offset: 0,
2305 length: 1,
2306 }],
2307 vec![2.0],
2308 );
2309
2310 let result = left.sub(&right).unwrap();
2311 assert_eq!(result.reset_hint, CounterResetHint::Gauge);
2312 assert_eq!(result.count, -2.0);
2313 assert_eq!(result.sum, -2.0);
2314 assert_eq!(result.positive_buckets, vec![-2.0]);
2315 }
2316
2317 #[test]
2318 fn subtraction_rejects_incompatible_empty_left() {
2319 let left = histogram(vec![], vec![]);
2320 let mut right = histogram(
2321 vec![Span {
2322 offset: 0,
2323 length: 1,
2324 }],
2325 vec![2.0],
2326 );
2327 right.schema = CUSTOM_BUCKETS_SCHEMA;
2328 right.custom_values = vec![1.0];
2329
2330 assert!(left.sub(&right).is_none());
2331 }
2332}
2333
2334pub const NATIVE_HISTOGRAM_SUBFIELD_ID_BASE: i32 = 0x5000_0000;
2354
2355pub const NATIVE_HISTOGRAM_SUBFIELD_ID_STRIDE: i32 = 64;
2358
2359pub const NATIVE_HISTOGRAM_LIST_ELEMENT_OFFSET: i32 = 32;
2361
2362pub fn native_histogram_subfield_id(column_id: i32, name: &str) -> Option<i32> {
2366 let idx = subfield_index(name)?;
2367 NATIVE_HISTOGRAM_SUBFIELD_ID_BASE
2368 .checked_add(column_id.checked_mul(NATIVE_HISTOGRAM_SUBFIELD_ID_STRIDE)?)
2369 .and_then(|v| v.checked_add(idx))
2370}
2371
2372pub fn native_histogram_list_element_id(column_id: i32, name: &str) -> Option<i32> {
2376 let idx = subfield_index(name)?;
2377 NATIVE_HISTOGRAM_SUBFIELD_ID_BASE
2378 .checked_add(column_id.checked_mul(NATIVE_HISTOGRAM_SUBFIELD_ID_STRIDE)?)
2379 .and_then(|v| v.checked_add(NATIVE_HISTOGRAM_LIST_ELEMENT_OFFSET))
2380 .and_then(|v| v.checked_add(idx))
2381}
2382
2383fn subfield_index(name: &str) -> Option<i32> {
2384 NATIVE_HISTOGRAM_FIELD_NAMES
2385 .iter()
2386 .position(|n| *n == name)
2387 .map(|i| i as i32)
2388}
2389
2390#[cfg(test)]
2391mod subfield_id_tests {
2392 use super::*;
2393
2394 #[test]
2395 fn subfield_ids_are_namespaced_and_disjoint() {
2396 let sum_idx = 2;
2398 let custom_values_idx = 5;
2399
2400 assert_eq!(
2403 native_histogram_subfield_id(1, SUM_FIELD),
2404 Some(NATIVE_HISTOGRAM_SUBFIELD_ID_BASE + NATIVE_HISTOGRAM_SUBFIELD_ID_STRIDE + sum_idx,)
2405 );
2406 assert_eq!(
2408 native_histogram_list_element_id(1, CUSTOM_VALUES_FIELD),
2409 Some(
2410 NATIVE_HISTOGRAM_SUBFIELD_ID_BASE
2411 + NATIVE_HISTOGRAM_SUBFIELD_ID_STRIDE
2412 + NATIVE_HISTOGRAM_LIST_ELEMENT_OFFSET
2413 + custom_values_idx,
2414 )
2415 );
2416 assert_ne!(
2418 native_histogram_subfield_id(1, SUM_FIELD),
2419 native_histogram_subfield_id(7, SUM_FIELD)
2420 );
2421 assert_eq!(native_histogram_subfield_id(1, "not_a_field"), None);
2423 }
2424
2425 #[test]
2426 fn subfield_ids_overflow_returns_none() {
2427 assert_eq!(native_histogram_subfield_id(12_582_912, SUM_FIELD), None);
2432 assert_eq!(
2433 native_histogram_list_element_id(12_582_912, CUSTOM_VALUES_FIELD),
2434 None
2435 );
2436 assert!(native_histogram_subfield_id(12_582_911, SUM_FIELD).is_some());
2438 }
2439}