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