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