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