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