Skip to main content

common_query/
native_histogram.rs

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