Skip to main content

common_query/native_histogram/
encoding.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
15use api::greptime_proto::io::prometheus::write::v2::histogram::{Count, ZeroCount};
16use api::greptime_proto::io::prometheus::write::v2::{BucketSpan, Histogram};
17use api::helper::ColumnDataTypeWrapper;
18use api::prom_store::remote;
19use api::v1::value::ValueData;
20use api::v1::{ColumnSchema, ListValue, SemanticType, Value};
21use datafusion::arrow::array::{Array, StructArray};
22use datafusion::arrow::datatypes::{Float64Type, Int32Type, Int64Type, TimestampMillisecondType};
23use snafu::{Snafu, ensure};
24
25use crate::native_histogram::{
26    self, CUSTOM_BUCKETS_SCHEMA, MAX_EXPONENTIAL_SCHEMA, NATIVE_HISTOGRAM_FIELD_NAMES,
27    exponential_overflow_bucket_index, native_histogram_value_type,
28};
29use crate::prelude::greptime_native_histogram;
30
31const MAX_REDUCIBLE_NATIVE_HISTOGRAM_SCHEMA: i32 = 52;
32
33/// Error returned while validating or encoding a native histogram.
34#[derive(Debug, Snafu)]
35#[snafu(display("{message}"))]
36pub struct NativeHistogramError {
37    message: String,
38}
39
40type Result<T> = std::result::Result<T, NativeHistogramError>;
41
42/// Returns the canonical column schema for a native histogram value.
43pub fn native_histogram_column_schema() -> Result<ColumnSchema> {
44    let (datatype, datatype_extension) =
45        ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
46            .map_err(|error| NativeHistogramError {
47                message: format!("native histogram type cannot be encoded: {error}"),
48            })?
49            .into_parts();
50
51    Ok(ColumnSchema {
52        column_name: greptime_native_histogram().to_string(),
53        datatype: datatype as i32,
54        semantic_type: SemanticType::Field as i32,
55        datatype_extension,
56        options: None,
57    })
58}
59
60/// Validates and encodes a Prometheus histogram into the canonical Struct value.
61pub fn encode_native_histogram(histogram: &Histogram) -> Result<ValueData> {
62    let uses_float_counts = native_histogram_uses_float_counts(histogram)?;
63    validate_native_histogram(histogram, uses_float_counts)?;
64
65    let mut items = Vec::with_capacity(NATIVE_HISTOGRAM_FIELD_NAMES.len());
66    let positive_span_lengths = i32_span_lengths("positive", &histogram.positive_spans)?;
67    let negative_span_lengths = i32_span_lengths("negative", &histogram.negative_spans)?;
68    items.extend([
69        pb_value(ValueData::I32Value(histogram.schema)),
70        pb_value(ValueData::F64Value(histogram.zero_threshold)),
71        pb_value(ValueData::F64Value(histogram.sum)),
72        pb_value(ValueData::I32Value(histogram.reset_hint)),
73        optional_pb_value((histogram.start_timestamp != 0).then_some(
74            ValueData::TimestampMillisecondValue(histogram.start_timestamp),
75        )),
76        f64_list_value(histogram.custom_values.iter().copied()),
77        i32_list_value(histogram.positive_spans.iter().map(|span| span.offset)),
78        i32_list_value(positive_span_lengths),
79        i32_list_value(histogram.negative_spans.iter().map(|span| span.offset)),
80        i32_list_value(negative_span_lengths),
81    ]);
82
83    if uses_float_counts {
84        let count = match histogram.count.as_ref() {
85            Some(Count::CountFloat(count)) => *count,
86            _ => 0.0,
87        };
88        let zero_count = match histogram.zero_count.as_ref() {
89            Some(ZeroCount::ZeroCountFloat(zero_count)) => *zero_count,
90            _ => 0.0,
91        };
92        validate_float_native_histogram_counts(
93            count,
94            zero_count,
95            &histogram.positive_counts,
96            &histogram.negative_counts,
97        )?;
98        items.extend([
99            null_pb_value(),
100            null_pb_value(),
101            i64_list_value(std::iter::empty()),
102            i64_list_value(std::iter::empty()),
103            pb_value(ValueData::F64Value(count)),
104            pb_value(ValueData::F64Value(zero_count)),
105            f64_list_value(histogram.positive_counts.iter().copied()),
106            f64_list_value(histogram.negative_counts.iter().copied()),
107        ]);
108    } else {
109        let count = match histogram.count.as_ref() {
110            Some(Count::CountInt(count)) => *count,
111            _ => 0,
112        };
113        let zero_count = match histogram.zero_count.as_ref() {
114            Some(ZeroCount::ZeroCountInt(zero_count)) => *zero_count,
115            _ => 0,
116        };
117        let positive_buckets = bucket_counts_from_deltas(&histogram.positive_deltas)?;
118        let negative_buckets = bucket_counts_from_deltas(&histogram.negative_deltas)?;
119        validate_integer_native_histogram_counts(
120            count,
121            zero_count,
122            histogram.sum,
123            &positive_buckets,
124            &negative_buckets,
125        )?;
126        let count = i64::try_from(count).map_err(|_| NativeHistogramError {
127            message: format!("native histogram integer count {count} overflows i64"),
128        })?;
129        let zero_count = i64::try_from(zero_count).map_err(|_| NativeHistogramError {
130            message: format!("native histogram integer zero_count {zero_count} overflows i64"),
131        })?;
132        items.extend([
133            pb_value(ValueData::I64Value(count)),
134            pb_value(ValueData::I64Value(zero_count)),
135            i64_list_value(positive_buckets),
136            i64_list_value(negative_buckets),
137            null_pb_value(),
138            null_pb_value(),
139            f64_list_value(std::iter::empty()),
140            f64_list_value(std::iter::empty()),
141        ]);
142    }
143
144    Ok(ValueData::StructValue(api::v1::StructValue { items }))
145}
146
147/// Decodes a stored histogram without rounding integer counts through `f64`.
148///
149/// Null structs are absent samples. Malformed or sampled-wire-incompatible
150/// values are errors. The sampled format has no field for `start_timestamp`.
151pub fn decode_native_histogram_for_remote_read(
152    array: &StructArray,
153    row: usize,
154    timestamp: i64,
155) -> Result<Option<remote::Histogram>> {
156    use crate::native_histogram::{
157        COUNT_F64_FIELD, COUNT_I64_FIELD, CUSTOM_VALUES_FIELD, NEGATIVE_BUCKETS_F64_FIELD,
158        NEGATIVE_BUCKETS_I64_FIELD, NEGATIVE_SPAN_LENGTHS_FIELD, NEGATIVE_SPAN_OFFSETS_FIELD,
159        POSITIVE_BUCKETS_F64_FIELD, POSITIVE_BUCKETS_I64_FIELD, POSITIVE_SPAN_LENGTHS_FIELD,
160        POSITIVE_SPAN_OFFSETS_FIELD, RESET_HINT_FIELD, SCHEMA_FIELD, START_TIMESTAMP_FIELD,
161        SUM_FIELD, ZERO_COUNT_F64_FIELD, ZERO_COUNT_I64_FIELD, ZERO_THRESHOLD_FIELD,
162    };
163
164    ensure!(
165        row < array.len(),
166        NativeHistogramSnafu {
167            message: "native histogram row is out of bounds"
168        }
169    );
170    if array.is_null(row) {
171        return Ok(None);
172    }
173    let invalid_field = |error| NativeHistogramError {
174        message: format!("invalid stored native histogram: {error}"),
175    };
176    let read_spans = |name, offsets, lengths| -> Result<Vec<remote::BucketSpan>> {
177        let offsets = native_histogram::list_values::<Int32Type>(array, offsets, row)
178            .map_err(invalid_field)?;
179        let lengths = native_histogram::list_values::<Int32Type>(array, lengths, row)
180            .map_err(invalid_field)?;
181        native_histogram::read_spans(offsets, lengths, name)
182            .map_err(invalid_field)?
183            .into_iter()
184            .map(|span| {
185                Ok(remote::BucketSpan {
186                    offset: span.offset,
187                    length: u32::try_from(span.length).map_err(|_| NativeHistogramError {
188                        message: format!("native histogram {name} span length is negative"),
189                    })?,
190                })
191            })
192            .collect()
193    };
194    let mut histogram = remote::Histogram {
195        schema: native_histogram::required_primitive::<Int32Type>(array, SCHEMA_FIELD, row)
196            .map_err(invalid_field)?,
197        zero_threshold: native_histogram::required_primitive::<Float64Type>(
198            array,
199            ZERO_THRESHOLD_FIELD,
200            row,
201        )
202        .map_err(invalid_field)?,
203        sum: native_histogram::required_primitive::<Float64Type>(array, SUM_FIELD, row)
204            .map_err(invalid_field)?,
205        reset_hint: native_histogram::required_primitive::<Int32Type>(array, RESET_HINT_FIELD, row)
206            .map_err(invalid_field)?,
207        custom_values: native_histogram::list_values::<Float64Type>(
208            array,
209            CUSTOM_VALUES_FIELD,
210            row,
211        )
212        .map_err(invalid_field)?,
213        positive_spans: read_spans(
214            "positive",
215            POSITIVE_SPAN_OFFSETS_FIELD,
216            POSITIVE_SPAN_LENGTHS_FIELD,
217        )?,
218        negative_spans: read_spans(
219            "negative",
220            NEGATIVE_SPAN_OFFSETS_FIELD,
221            NEGATIVE_SPAN_LENGTHS_FIELD,
222        )?,
223        timestamp,
224        ..Default::default()
225    };
226    // Validate the stored child even though the sampled wire format cannot carry it.
227    native_histogram::optional_primitive::<TimestampMillisecondType>(
228        array,
229        START_TIMESTAMP_FIELD,
230        row,
231    )
232    .map_err(invalid_field)?;
233    let count_i64 = native_histogram::optional_primitive::<Int64Type>(array, COUNT_I64_FIELD, row)
234        .map_err(invalid_field)?;
235    let count_f64 =
236        native_histogram::optional_primitive::<Float64Type>(array, COUNT_F64_FIELD, row)
237            .map_err(invalid_field)?;
238    let zero_i64 =
239        native_histogram::optional_primitive::<Int64Type>(array, ZERO_COUNT_I64_FIELD, row)
240            .map_err(invalid_field)?;
241    let zero_f64 =
242        native_histogram::optional_primitive::<Float64Type>(array, ZERO_COUNT_F64_FIELD, row)
243            .map_err(invalid_field)?;
244    let positive_i64 =
245        native_histogram::list_values::<Int64Type>(array, POSITIVE_BUCKETS_I64_FIELD, row)
246            .map_err(invalid_field)?;
247    let negative_i64 =
248        native_histogram::list_values::<Int64Type>(array, NEGATIVE_BUCKETS_I64_FIELD, row)
249            .map_err(invalid_field)?;
250    let positive_f64 =
251        native_histogram::list_values::<Float64Type>(array, POSITIVE_BUCKETS_F64_FIELD, row)
252            .map_err(invalid_field)?;
253    let negative_f64 =
254        native_histogram::list_values::<Float64Type>(array, NEGATIVE_BUCKETS_F64_FIELD, row)
255            .map_err(invalid_field)?;
256
257    let (zero_count_is_zero, positive_len, negative_len) = match (count_i64, count_f64) {
258        (Some(count), None) => {
259            ensure!(
260                zero_f64.is_none() && positive_f64.is_empty() && negative_f64.is_empty(),
261                NativeHistogramSnafu {
262                    message: "integer native histogram has populated float fields"
263                }
264            );
265            let count = u64::try_from(count).map_err(|_| NativeHistogramError {
266                message: "native histogram integer count is negative".to_string(),
267            })?;
268            let zero_count =
269                u64::try_from(zero_i64.unwrap_or_default()).map_err(|_| NativeHistogramError {
270                    message: "native histogram integer zero_count is negative".to_string(),
271                })?;
272            validate_integer_native_histogram_counts(
273                count,
274                zero_count,
275                histogram.sum,
276                &positive_i64,
277                &negative_i64,
278            )?;
279            histogram.count = Some(remote::histogram::Count::CountInt(count));
280            histogram.zero_count = Some(remote::histogram::ZeroCount::ZeroCountInt(zero_count));
281            histogram.positive_deltas = bucket_deltas_from_counts(&positive_i64)?;
282            histogram.negative_deltas = bucket_deltas_from_counts(&negative_i64)?;
283            (zero_count == 0, positive_i64.len(), negative_i64.len())
284        }
285        (None, Some(count)) => {
286            ensure!(
287                zero_i64.is_none() && positive_i64.is_empty() && negative_i64.is_empty(),
288                NativeHistogramSnafu {
289                    message: "float native histogram has populated integer fields"
290                }
291            );
292            let zero_count = zero_f64.unwrap_or_default();
293            validate_float_native_histogram_counts(
294                count,
295                zero_count,
296                &positive_f64,
297                &negative_f64,
298            )?;
299            histogram.count = Some(remote::histogram::Count::CountFloat(count));
300            histogram.zero_count = Some(remote::histogram::ZeroCount::ZeroCountFloat(zero_count));
301            histogram.positive_counts = positive_f64;
302            histogram.negative_counts = negative_f64;
303            (
304                zero_count == 0.0,
305                histogram.positive_counts.len(),
306                histogram.negative_counts.len(),
307            )
308        }
309        _ => {
310            return NativeHistogramSnafu {
311                message: "stored native histogram must have exactly one populated count family",
312            }
313            .fail();
314        }
315    };
316    validate_native_histogram_layout(
317        histogram.schema,
318        histogram.zero_threshold,
319        zero_count_is_zero,
320        &histogram.custom_values,
321        (
322            histogram
323                .positive_spans
324                .iter()
325                .map(|s| (s.offset, s.length)),
326            positive_len,
327        ),
328        (
329            histogram
330                .negative_spans
331                .iter()
332                .map(|s| (s.offset, s.length)),
333            negative_len,
334        ),
335    )?;
336    Ok(Some(histogram))
337}
338
339fn bucket_deltas_from_counts(counts: &[i64]) -> Result<Vec<i64>> {
340    let mut previous = 0i64;
341    counts
342        .iter()
343        .map(|&count| {
344            let delta = count
345                .checked_sub(previous)
346                .ok_or_else(|| NativeHistogramError {
347                    message: "native histogram bucket delta overflows i64".to_string(),
348                })?;
349            previous = count;
350            Ok(delta)
351        })
352        .collect()
353}
354
355fn validate_native_histogram(histogram: &Histogram, uses_float_counts: bool) -> Result<()> {
356    let (positive_buckets, negative_buckets) = if uses_float_counts {
357        (
358            histogram.positive_counts.len(),
359            histogram.negative_counts.len(),
360        )
361    } else {
362        (
363            histogram.positive_deltas.len(),
364            histogram.negative_deltas.len(),
365        )
366    };
367    validate_native_histogram_layout(
368        histogram.schema,
369        histogram.zero_threshold,
370        native_histogram_zero_count_is_zero(histogram),
371        &histogram.custom_values,
372        (
373            histogram
374                .positive_spans
375                .iter()
376                .map(|s| (s.offset, s.length)),
377            positive_buckets,
378        ),
379        (
380            histogram
381                .negative_spans
382                .iter()
383                .map(|s| (s.offset, s.length)),
384            negative_buckets,
385        ),
386    )
387}
388
389fn validate_native_histogram_layout(
390    schema: i32,
391    zero_threshold: f64,
392    zero_count_is_zero: bool,
393    custom_values: &[f64],
394    positive: (impl ExactSizeIterator<Item = (i32, u32)> + Clone, usize),
395    negative: (impl ExactSizeIterator<Item = (i32, u32)> + Clone, usize),
396) -> Result<()> {
397    let exponential_overflow_index = validate_native_histogram_schema(schema)?;
398    validate_native_histogram_custom_values(schema, custom_values)?;
399    if schema == CUSTOM_BUCKETS_SCHEMA {
400        ensure!(
401            zero_threshold == 0.0 && zero_count_is_zero,
402            NativeHistogramSnafu {
403                message: "custom native histogram must not use a zero bucket"
404            }
405        );
406        ensure!(
407            negative.0.len() == 0 && negative.1 == 0,
408            NativeHistogramSnafu {
409                message: "custom native histogram must not use negative buckets"
410            }
411        );
412    }
413
414    let bucket_index_range = if let Some(overflow_index) = exponential_overflow_index {
415        (i32::MIN, overflow_index)
416    } else {
417        (
418            0,
419            i32::try_from(custom_values.len()).map_err(|_| NativeHistogramError {
420                message: "custom native histogram has too many custom_values".to_string(),
421            })?,
422        )
423    };
424    let positive_extent =
425        validate_native_histogram_spans("positive", positive.0, positive.1, bucket_index_range)?;
426    validate_native_histogram_spans("negative", negative.0, negative.1, bucket_index_range)?;
427
428    if schema == CUSTOM_BUCKETS_SCHEMA {
429        let max_extent =
430            bucket_index_range
431                .1
432                .checked_add(1)
433                .ok_or_else(|| NativeHistogramError {
434                    message: "custom native histogram extent overflows i32".to_string(),
435                })?;
436        ensure!(
437            positive_extent <= max_extent,
438            NativeHistogramSnafu {
439                message: "custom native histogram span extent exceeds custom_values plus the overflow bucket"
440            }
441        );
442    }
443
444    Ok(())
445}
446
447fn validate_native_histogram_schema(schema: i32) -> Result<Option<i32>> {
448    if schema == CUSTOM_BUCKETS_SCHEMA {
449        return Ok(None);
450    }
451
452    if let Some(overflow_index) = exponential_overflow_bucket_index(schema) {
453        return Ok(Some(overflow_index));
454    }
455
456    if (MAX_EXPONENTIAL_SCHEMA + 1..=MAX_REDUCIBLE_NATIVE_HISTOGRAM_SCHEMA).contains(&schema) {
457        Err(NativeHistogramError {
458            message: format!("native histogram schema {schema} must be reduced before ingestion"),
459        })
460    } else {
461        Err(NativeHistogramError {
462            message: format!("native histogram schema {schema} is unsupported"),
463        })
464    }
465}
466
467fn validate_native_histogram_custom_values(schema: i32, custom_values: &[f64]) -> Result<()> {
468    if schema != CUSTOM_BUCKETS_SCHEMA {
469        ensure!(
470            custom_values.is_empty(),
471            NativeHistogramSnafu {
472                message: "standard native histogram must not use custom_values"
473            }
474        );
475        return Ok(());
476    }
477
478    for value in custom_values {
479        ensure!(
480            !value.is_nan() && *value != f64::INFINITY,
481            NativeHistogramSnafu {
482                message: "custom native histogram custom_values must not contain +Inf or NaN"
483            }
484        );
485    }
486    for values in custom_values.windows(2) {
487        ensure!(
488            values[0] < values[1],
489            NativeHistogramSnafu {
490                message: "custom native histogram custom_values must be sorted"
491            }
492        );
493    }
494
495    Ok(())
496}
497
498fn validate_native_histogram_spans(
499    name: &str,
500    spans: impl Iterator<Item = (i32, u32)> + Clone,
501    bucket_count: usize,
502    bucket_index_range: (i32, i32),
503) -> Result<i32> {
504    let span_len = spans.clone().try_fold(0usize, |sum, (_, length)| {
505        let length = usize::try_from(length).map_err(|_| NativeHistogramError {
506            message: format!("native histogram {name} span length exceeds usize"),
507        })?;
508        sum.checked_add(length).ok_or_else(|| NativeHistogramError {
509            message: format!("native histogram {name} spans overflow"),
510        })
511    })?;
512    ensure!(
513        span_len == bucket_count,
514        NativeHistogramSnafu {
515            message: format!(
516                "native histogram {name} spans describe {span_len} buckets, found {bucket_count}"
517            )
518        }
519    );
520
521    let mut current_index = 0i32;
522    for (span_index, (offset, length)) in spans.enumerate() {
523        ensure!(
524            offset >= 0 || (span_index == 0 && bucket_index_range.0 == i32::MIN),
525            NativeHistogramSnafu {
526                message: format!(
527                    "native histogram {name} span {} has negative offset {}",
528                    span_index + 1,
529                    offset
530                )
531            }
532        );
533        ensure!(
534            span_index != 0 || offset != i32::MIN,
535            NativeHistogramSnafu {
536                message: format!(
537                    "native histogram {name} initial span offset i32::MIN is incompatible with Remote Read"
538                )
539            }
540        );
541        current_index = if span_index == 0 {
542            offset
543        } else {
544            current_index
545                .checked_add(offset)
546                .ok_or_else(|| NativeHistogramError {
547                    message: format!("native histogram {name} span index overflows i32"),
548                })?
549        };
550
551        for _ in 0..length {
552            ensure!(
553                (bucket_index_range.0..=bucket_index_range.1).contains(&current_index),
554                NativeHistogramSnafu {
555                    message: format!(
556                        "native histogram {name} bucket index {current_index} is out of range"
557                    )
558                }
559            );
560            current_index = current_index
561                .checked_add(1)
562                .ok_or_else(|| NativeHistogramError {
563                    message: format!("native histogram {name} span index overflows i32"),
564                })?;
565        }
566    }
567
568    Ok(current_index)
569}
570
571fn validate_float_native_histogram_counts(
572    count: f64,
573    zero_count: f64,
574    positive_buckets: &[f64],
575    negative_buckets: &[f64],
576) -> Result<()> {
577    ensure!(
578        count >= 0.0 || count.is_nan(),
579        NativeHistogramSnafu {
580            message: "native histogram float count must not be negative"
581        }
582    );
583
584    ensure!(
585        zero_count >= 0.0 || zero_count.is_nan(),
586        NativeHistogramSnafu {
587            message: "native histogram float zero_count must not be negative"
588        }
589    );
590
591    for (name, counts) in [
592        ("positive", positive_buckets),
593        ("negative", negative_buckets),
594    ] {
595        for (index, count) in counts.iter().enumerate() {
596            ensure!(
597                *count >= 0.0 || count.is_nan(),
598                NativeHistogramSnafu {
599                    message: format!(
600                        "native histogram {name} bucket {} count must not be negative",
601                        index + 1
602                    )
603                }
604            );
605        }
606    }
607
608    Ok(())
609}
610
611fn validate_integer_native_histogram_counts(
612    count: u64,
613    zero_count: u64,
614    sum: f64,
615    positive_buckets: &[i64],
616    negative_buckets: &[i64],
617) -> Result<()> {
618    let bucket_count =
619        positive_buckets
620            .iter()
621            .chain(negative_buckets)
622            .try_fold(zero_count, |total, bucket| {
623                let bucket = u64::try_from(*bucket).map_err(|_| NativeHistogramError {
624                    message: "native histogram bucket count is negative".to_string(),
625                })?;
626                total
627                    .checked_add(bucket)
628                    .ok_or_else(|| NativeHistogramError {
629                        message: "native histogram bucket total overflows u64".to_string(),
630                    })
631            })?;
632    ensure!(
633        if sum.is_nan() {
634            bucket_count <= count
635        } else {
636            bucket_count == count
637        },
638        NativeHistogramSnafu {
639            message: format!(
640                "native histogram has {bucket_count} observations in buckets, count is {count}"
641            )
642        }
643    );
644
645    Ok(())
646}
647
648fn native_histogram_zero_count_is_zero(histogram: &Histogram) -> bool {
649    match histogram.zero_count.as_ref() {
650        Some(ZeroCount::ZeroCountInt(zero_count)) => *zero_count == 0,
651        Some(ZeroCount::ZeroCountFloat(zero_count)) => *zero_count == 0.0,
652        None => true,
653    }
654}
655
656fn native_histogram_uses_float_counts(histogram: &Histogram) -> Result<bool> {
657    let uses_float_count = matches!(histogram.count, Some(Count::CountFloat(_)))
658        || matches!(histogram.zero_count, Some(ZeroCount::ZeroCountFloat(_)));
659    let uses_int_count = matches!(histogram.count, Some(Count::CountInt(_)))
660        || matches!(histogram.zero_count, Some(ZeroCount::ZeroCountInt(_)));
661    let uses_float_buckets =
662        !histogram.positive_counts.is_empty() || !histogram.negative_counts.is_empty();
663    let uses_int_buckets =
664        !histogram.positive_deltas.is_empty() || !histogram.negative_deltas.is_empty();
665
666    ensure!(
667        !matches!(
668            (&histogram.count, &histogram.zero_count),
669            (Some(Count::CountInt(_)), Some(ZeroCount::ZeroCountFloat(_)))
670                | (Some(Count::CountFloat(_)), Some(ZeroCount::ZeroCountInt(_)))
671        ),
672        NativeHistogramSnafu {
673            message: "native histogram count and zero_count must use the same integer or float family"
674        }
675    );
676    ensure!(
677        !(uses_float_buckets && uses_int_buckets),
678        NativeHistogramSnafu {
679            message: "native histogram bucket counts must use either integer deltas or float counts"
680        }
681    );
682    ensure!(
683        !(uses_float_count && uses_int_buckets),
684        NativeHistogramSnafu {
685            message: "float native histogram must not use integer bucket deltas"
686        }
687    );
688    ensure!(
689        !(uses_int_count && uses_float_buckets),
690        NativeHistogramSnafu {
691            message: "integer native histogram must not use float bucket counts"
692        }
693    );
694
695    Ok(uses_float_count || uses_float_buckets)
696}
697
698fn pb_value(value_data: ValueData) -> Value {
699    optional_pb_value(Some(value_data))
700}
701
702fn null_pb_value() -> Value {
703    optional_pb_value(None)
704}
705
706fn optional_pb_value(value_data: Option<ValueData>) -> Value {
707    Value { value_data }
708}
709
710fn list_value(values: impl IntoIterator<Item = ValueData>) -> Value {
711    pb_value(ValueData::ListValue(ListValue {
712        items: values.into_iter().map(pb_value).collect(),
713    }))
714}
715
716fn i32_list_value(values: impl IntoIterator<Item = i32>) -> Value {
717    list_value(values.into_iter().map(ValueData::I32Value))
718}
719
720fn i32_span_lengths(name: &str, spans: &[BucketSpan]) -> Result<Vec<i32>> {
721    spans
722        .iter()
723        .map(|span| {
724            i32::try_from(span.length).map_err(|_| NativeHistogramError {
725                message: format!(
726                    "native histogram {name} span length {} overflows i32",
727                    span.length
728                ),
729            })
730        })
731        .collect()
732}
733
734fn i64_list_value(values: impl IntoIterator<Item = i64>) -> Value {
735    list_value(values.into_iter().map(ValueData::I64Value))
736}
737
738fn f64_list_value(values: impl IntoIterator<Item = f64>) -> Value {
739    list_value(values.into_iter().map(ValueData::F64Value))
740}
741
742fn bucket_counts_from_deltas(deltas: &[i64]) -> Result<Vec<i64>> {
743    let mut count = 0_i64;
744    let mut buckets = Vec::with_capacity(deltas.len());
745
746    for delta in deltas {
747        count = count
748            .checked_add(*delta)
749            .ok_or_else(|| NativeHistogramError {
750                message: "native histogram bucket count overflows i64".to_string(),
751            })?;
752        ensure!(
753            count >= 0,
754            NativeHistogramSnafu {
755                message: "native histogram bucket count is negative"
756            }
757        );
758        buckets.push(count);
759    }
760
761    Ok(buckets)
762}
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767
768    use std::sync::Arc;
769
770    use datafusion::arrow::array::{ArrayRef, Float64Array, Int32Array};
771    use datafusion::arrow::datatypes::Field;
772
773    use crate::native_histogram::testing::histogram_array;
774    use crate::native_histogram::*;
775
776    fn empty_histogram(float: bool) -> Histogram {
777        Histogram {
778            count: Some(if float {
779                Count::CountFloat(0.0)
780            } else {
781                Count::CountInt(0)
782            }),
783            zero_count: Some(if float {
784                ZeroCount::ZeroCountFloat(0.0)
785            } else {
786                ZeroCount::ZeroCountInt(0)
787            }),
788            ..Default::default()
789        }
790    }
791
792    fn stored(histogram: &Histogram) -> StructArray {
793        histogram_array(&[Some(encode_native_histogram(histogram).unwrap())])
794    }
795
796    fn modified(histogram: &Histogram, changes: &[(&str, Value)]) -> StructArray {
797        let ValueData::StructValue(mut value) = encode_native_histogram(histogram).unwrap() else {
798            unreachable!()
799        };
800        for (name, replacement) in changes {
801            let index = NATIVE_HISTOGRAM_FIELD_NAMES
802                .iter()
803                .position(|field| field == name)
804                .unwrap();
805            value.items[index] = replacement.clone();
806        }
807        histogram_array(&[Some(ValueData::StructValue(value))])
808    }
809
810    fn assert_invalid(array: &StructArray, message: &str) {
811        let error = decode_native_histogram_for_remote_read(array, 0, 123).unwrap_err();
812        assert!(
813            error.to_string().contains(message),
814            "{error}: expected {message}"
815        );
816    }
817
818    #[test]
819    fn native_histogram_remote_read_preserves_integer_counts_and_deltas() {
820        for count in [(1u64 << 53) + 1, i64::MAX as u64] {
821            let histogram = Histogram {
822                count: Some(Count::CountInt(count)),
823                zero_count: Some(ZeroCount::ZeroCountInt(count - 12)),
824                positive_spans: vec![
825                    BucketSpan {
826                        offset: -2,
827                        length: 2,
828                    },
829                    BucketSpan {
830                        offset: 3,
831                        length: 1,
832                    },
833                ],
834                negative_spans: vec![
835                    BucketSpan {
836                        offset: -3,
837                        length: 1,
838                    },
839                    BucketSpan {
840                        offset: 2,
841                        length: 2,
842                    },
843                ],
844                positive_deltas: vec![1, 2, -1],
845                negative_deltas: vec![1, 2, -1],
846                sum: 10.0,
847                reset_hint: 42,
848                start_timestamp: 123,
849                ..Default::default()
850            };
851            let array = stored(&histogram);
852            assert_eq!(
853                native_histogram::list_values::<Int64Type>(&array, POSITIVE_BUCKETS_I64_FIELD, 0)
854                    .unwrap(),
855                vec![1, 3, 2]
856            );
857            assert_eq!(
858                native_histogram::list_values::<Int64Type>(&array, NEGATIVE_BUCKETS_I64_FIELD, 0)
859                    .unwrap(),
860                vec![1, 3, 2]
861            );
862            let decoded = decode_native_histogram_for_remote_read(&array, 0, -2)
863                .unwrap()
864                .unwrap();
865            assert_eq!(
866                decoded.count,
867                Some(remote::histogram::Count::CountInt(count))
868            );
869            assert_eq!(
870                decoded.zero_count,
871                Some(remote::histogram::ZeroCount::ZeroCountInt(count - 12))
872            );
873            assert_eq!(decoded.positive_deltas, vec![1, 2, -1]);
874            assert_eq!(decoded.negative_deltas, vec![1, 2, -1]);
875            assert_eq!(
876                decoded
877                    .positive_spans
878                    .iter()
879                    .map(|s| (s.offset, s.length))
880                    .collect::<Vec<_>>(),
881                vec![(-2, 2), (3, 1)]
882            );
883            assert_eq!(decoded.reset_hint, 42);
884            assert_eq!(decoded.timestamp, -2);
885        }
886        // A NaN sum permits unobserved NaNs; an incorrect [3, 2] delta vector
887        // would also pass a total check, so assert the deltas independently.
888        let histogram = Histogram {
889            count: Some(Count::CountInt(8)),
890            sum: f64::NAN,
891            positive_spans: vec![BucketSpan {
892                offset: 0,
893                length: 2,
894            }],
895            positive_deltas: vec![3, -1],
896            ..Default::default()
897        };
898        let decoded = decode_native_histogram_for_remote_read(&stored(&histogram), 0, 0)
899            .unwrap()
900            .unwrap();
901        assert_eq!(decoded.positive_deltas, vec![3, -1]);
902    }
903
904    #[test]
905    fn native_histogram_remote_read_zero_families_and_float_bits() {
906        for float in [false, true] {
907            let histogram = empty_histogram(float);
908            let array = modified(
909                &histogram,
910                &[
911                    (
912                        if float {
913                            ZERO_COUNT_F64_FIELD
914                        } else {
915                            ZERO_COUNT_I64_FIELD
916                        },
917                        null_pb_value(),
918                    ),
919                    (POSITIVE_BUCKETS_I64_FIELD, null_pb_value()),
920                    (POSITIVE_BUCKETS_F64_FIELD, null_pb_value()),
921                ],
922            );
923            let decoded = decode_native_histogram_for_remote_read(&array, 0, 1)
924                .unwrap()
925                .unwrap();
926            assert_eq!(
927                matches!(
928                    decoded.count,
929                    Some(remote::histogram::Count::CountFloat(0.0))
930                ),
931                float
932            );
933            assert_eq!(
934                matches!(decoded.count, Some(remote::histogram::Count::CountInt(0))),
935                !float
936            );
937            assert_eq!(
938                matches!(
939                    decoded.zero_count,
940                    Some(remote::histogram::ZeroCount::ZeroCountFloat(0.0))
941                ),
942                float
943            );
944            assert_eq!(
945                matches!(
946                    decoded.zero_count,
947                    Some(remote::histogram::ZeroCount::ZeroCountInt(0))
948                ),
949                !float
950            );
951            for bits in [0x7ff8_0000_0000_0123, 0x7ff0_0000_0000_0002] {
952                let histogram = Histogram {
953                    sum: f64::from_bits(bits),
954                    ..histogram.clone()
955                };
956                let decoded = decode_native_histogram_for_remote_read(&stored(&histogram), 0, 1)
957                    .unwrap()
958                    .unwrap();
959                assert_eq!(decoded.sum.to_bits(), bits);
960            }
961        }
962        let histogram = Histogram {
963            count: Some(Count::CountFloat(0.3)),
964            positive_spans: vec![BucketSpan {
965                offset: 0,
966                length: 2,
967            }],
968            positive_counts: vec![0.1, 0.2],
969            ..empty_histogram(true)
970        };
971        let decoded = decode_native_histogram_for_remote_read(&stored(&histogram), 0, 1)
972            .unwrap()
973            .unwrap();
974        assert_eq!(decoded.positive_counts, vec![0.1, 0.2]);
975        for value in [f64::INFINITY, f64::from_bits(0x7ff8_0000_0000_0123)] {
976            let histogram = Histogram {
977                count: Some(Count::CountFloat(value)),
978                zero_count: Some(ZeroCount::ZeroCountFloat(value)),
979                positive_counts: vec![value, value],
980                ..histogram.clone()
981            };
982            let decoded = decode_native_histogram_for_remote_read(&stored(&histogram), 0, 1)
983                .unwrap()
984                .unwrap();
985            let Some(remote::histogram::Count::CountFloat(count)) = decoded.count else {
986                panic!()
987            };
988            assert_eq!(count.to_bits(), value.to_bits());
989            assert_eq!(decoded.positive_counts[0].to_bits(), value.to_bits());
990        }
991        assert!(
992            decode_native_histogram_for_remote_read(&histogram_array(&[None]), 0, 0)
993                .unwrap()
994                .is_none()
995        );
996    }
997
998    #[test]
999    fn native_histogram_remote_read_rejects_invalid_stored_children() {
1000        let integer = empty_histogram(false);
1001        let float = empty_histogram(true);
1002        for field in [
1003            SCHEMA_FIELD,
1004            ZERO_THRESHOLD_FIELD,
1005            SUM_FIELD,
1006            RESET_HINT_FIELD,
1007        ] {
1008            assert_invalid(&modified(&integer, &[(field, null_pb_value())]), "is null");
1009        }
1010        for (histogram, changes, message) in [
1011            (
1012                &integer,
1013                vec![(COUNT_I64_FIELD, null_pb_value())],
1014                "exactly one",
1015            ),
1016            (
1017                &integer,
1018                vec![(COUNT_F64_FIELD, pb_value(ValueData::F64Value(0.0)))],
1019                "exactly one",
1020            ),
1021            (
1022                &integer,
1023                vec![(ZERO_COUNT_F64_FIELD, pb_value(ValueData::F64Value(0.0)))],
1024                "populated float",
1025            ),
1026            (
1027                &integer,
1028                vec![(POSITIVE_BUCKETS_F64_FIELD, f64_list_value([0.0]))],
1029                "populated float",
1030            ),
1031            (
1032                &integer,
1033                vec![(NEGATIVE_BUCKETS_F64_FIELD, f64_list_value([0.0]))],
1034                "populated float",
1035            ),
1036            (
1037                &float,
1038                vec![(ZERO_COUNT_I64_FIELD, pb_value(ValueData::I64Value(0)))],
1039                "populated integer",
1040            ),
1041            (
1042                &float,
1043                vec![(POSITIVE_BUCKETS_I64_FIELD, i64_list_value([0]))],
1044                "populated integer",
1045            ),
1046            (
1047                &float,
1048                vec![(NEGATIVE_BUCKETS_I64_FIELD, i64_list_value([0]))],
1049                "populated integer",
1050            ),
1051            (
1052                &integer,
1053                vec![(COUNT_I64_FIELD, pb_value(ValueData::I64Value(-1)))],
1054                "count is negative",
1055            ),
1056            (
1057                &integer,
1058                vec![(ZERO_COUNT_I64_FIELD, pb_value(ValueData::I64Value(-1)))],
1059                "zero_count is negative",
1060            ),
1061            (
1062                &integer,
1063                vec![(POSITIVE_BUCKETS_I64_FIELD, i64_list_value([-1]))],
1064                "count is negative",
1065            ),
1066            (
1067                &integer,
1068                vec![(NEGATIVE_BUCKETS_I64_FIELD, i64_list_value([-1]))],
1069                "count is negative",
1070            ),
1071            (
1072                &float,
1073                vec![(COUNT_F64_FIELD, pb_value(ValueData::F64Value(-1.0)))],
1074                "must not be negative",
1075            ),
1076            (
1077                &float,
1078                vec![(ZERO_COUNT_F64_FIELD, pb_value(ValueData::F64Value(-1.0)))],
1079                "must not be negative",
1080            ),
1081            (
1082                &float,
1083                vec![(POSITIVE_BUCKETS_F64_FIELD, f64_list_value([-1.0]))],
1084                "must not be negative",
1085            ),
1086            (
1087                &float,
1088                vec![(NEGATIVE_BUCKETS_F64_FIELD, f64_list_value([-1.0]))],
1089                "must not be negative",
1090            ),
1091            (
1092                &integer,
1093                vec![
1094                    (COUNT_I64_FIELD, pb_value(ValueData::I64Value(1))),
1095                    (
1096                        POSITIVE_BUCKETS_I64_FIELD,
1097                        i64_list_value([i64::MAX, i64::MAX, 3]),
1098                    ),
1099                ],
1100                "overflows u64",
1101            ),
1102            (
1103                &integer,
1104                vec![(COUNT_I64_FIELD, pb_value(ValueData::I64Value(1)))],
1105                "observations",
1106            ),
1107            (
1108                &integer,
1109                vec![(SCHEMA_FIELD, pb_value(ValueData::I32Value(53)))],
1110                "unsupported",
1111            ),
1112            (
1113                &integer,
1114                vec![(POSITIVE_SPAN_OFFSETS_FIELD, i32_list_value([0]))],
1115                "offsets and lengths mismatch",
1116            ),
1117            (
1118                &integer,
1119                vec![
1120                    (POSITIVE_SPAN_OFFSETS_FIELD, i32_list_value([0])),
1121                    (POSITIVE_SPAN_LENGTHS_FIELD, i32_list_value([-1])),
1122                ],
1123                "negative length",
1124            ),
1125            (
1126                &integer,
1127                vec![
1128                    (POSITIVE_SPAN_OFFSETS_FIELD, i32_list_value([0])),
1129                    (POSITIVE_SPAN_LENGTHS_FIELD, i32_list_value([1])),
1130                ],
1131                "describe 1 buckets",
1132            ),
1133            (
1134                &integer,
1135                vec![
1136                    (POSITIVE_SPAN_OFFSETS_FIELD, i32_list_value([i32::MAX, 1])),
1137                    (POSITIVE_SPAN_LENGTHS_FIELD, i32_list_value([0, 0])),
1138                ],
1139                "overflows i32",
1140            ),
1141            (
1142                &integer,
1143                vec![(
1144                    CUSTOM_VALUES_FIELD,
1145                    pb_value(ValueData::ListValue(ListValue {
1146                        items: vec![null_pb_value()],
1147                    })),
1148                )],
1149                "contains null",
1150            ),
1151        ] {
1152            assert_invalid(&modified(histogram, &changes), message);
1153        }
1154        let array = stored(&integer);
1155        let mut fields = array.fields().to_vec();
1156        let mut children = array.columns().to_vec();
1157        fields.remove(0);
1158        children.remove(0);
1159        assert_invalid(
1160            &StructArray::new(fields.into(), children, None),
1161            "missing field schema",
1162        );
1163        let mut fields = array.fields().to_vec();
1164        let mut children = array.columns().to_vec();
1165        children[0] = Arc::new(Float64Array::from(vec![0.0])) as ArrayRef;
1166        fields[0] = Arc::new(Field::new(
1167            SCHEMA_FIELD,
1168            children[0].data_type().clone(),
1169            true,
1170        ));
1171        assert_invalid(
1172            &StructArray::new(fields.into(), children, None),
1173            "invalid type",
1174        );
1175        // Even inactive children have to retain their declared types.
1176        let mut fields = array.fields().to_vec();
1177        let mut children = array.columns().to_vec();
1178        children[14] = Arc::new(Int32Array::from(vec![None])) as ArrayRef;
1179        fields[14] = Arc::new(Field::new(
1180            COUNT_F64_FIELD,
1181            children[14].data_type().clone(),
1182            true,
1183        ));
1184        assert_invalid(
1185            &StructArray::new(fields.into(), children, None),
1186            "invalid type",
1187        );
1188    }
1189
1190    #[test]
1191    fn native_histogram_remote_read_schema_rules_match_ingestion() {
1192        for float in [false, true] {
1193            let base = empty_histogram(float);
1194            for bounds in [
1195                vec![f64::NAN],
1196                vec![f64::INFINITY],
1197                vec![1.0, 1.0],
1198                vec![2.0, 1.0],
1199            ] {
1200                let invalid = Histogram {
1201                    schema: CUSTOM_BUCKETS_SCHEMA,
1202                    custom_values: bounds.clone(),
1203                    ..base.clone()
1204                };
1205                assert!(encode_native_histogram(&invalid).is_err());
1206                assert_invalid(
1207                    &modified(
1208                        &base,
1209                        &[
1210                            (
1211                                SCHEMA_FIELD,
1212                                pb_value(ValueData::I32Value(CUSTOM_BUCKETS_SCHEMA)),
1213                            ),
1214                            (CUSTOM_VALUES_FIELD, f64_list_value(bounds)),
1215                        ],
1216                    ),
1217                    "custom_values",
1218                );
1219            }
1220            let invalid = Histogram {
1221                custom_values: vec![1.0],
1222                ..base.clone()
1223            };
1224            assert!(encode_native_histogram(&invalid).is_err());
1225            assert_invalid(
1226                &modified(&base, &[(CUSTOM_VALUES_FIELD, f64_list_value([1.0]))]),
1227                "must not use custom_values",
1228            );
1229            let custom = Histogram {
1230                schema: CUSTOM_BUCKETS_SCHEMA,
1231                custom_values: vec![-f64::INFINITY, 1.0],
1232                ..base
1233            };
1234            let decoded = decode_native_histogram_for_remote_read(&stored(&custom), 0, 0)
1235                .unwrap()
1236                .unwrap();
1237            assert_eq!(decoded.custom_values, vec![-f64::INFINITY, 1.0]);
1238            let invalid = Histogram {
1239                zero_threshold: 1.0,
1240                ..custom.clone()
1241            };
1242            assert!(encode_native_histogram(&invalid).is_err());
1243            assert_invalid(
1244                &modified(
1245                    &custom,
1246                    &[(ZERO_THRESHOLD_FIELD, pb_value(ValueData::F64Value(1.0)))],
1247                ),
1248                "must not use a zero bucket",
1249            );
1250            let (count_field, zero_field, bucket_field, one, buckets) = if float {
1251                (
1252                    COUNT_F64_FIELD,
1253                    ZERO_COUNT_F64_FIELD,
1254                    NEGATIVE_BUCKETS_F64_FIELD,
1255                    pb_value(ValueData::F64Value(1.0)),
1256                    f64_list_value([1.0]),
1257                )
1258            } else {
1259                (
1260                    COUNT_I64_FIELD,
1261                    ZERO_COUNT_I64_FIELD,
1262                    NEGATIVE_BUCKETS_I64_FIELD,
1263                    pb_value(ValueData::I64Value(1)),
1264                    i64_list_value([1]),
1265                )
1266            };
1267            let invalid = Histogram {
1268                count: Some(if float {
1269                    Count::CountFloat(1.0)
1270                } else {
1271                    Count::CountInt(1)
1272                }),
1273                zero_count: Some(if float {
1274                    ZeroCount::ZeroCountFloat(1.0)
1275                } else {
1276                    ZeroCount::ZeroCountInt(1)
1277                }),
1278                ..custom.clone()
1279            };
1280            assert!(encode_native_histogram(&invalid).is_err());
1281            assert_invalid(
1282                &modified(
1283                    &custom,
1284                    &[(count_field, one.clone()), (zero_field, one.clone())],
1285                ),
1286                "must not use a zero bucket",
1287            );
1288            for length in [0, 1] {
1289                let mut invalid = custom.clone();
1290                invalid.negative_spans = vec![BucketSpan { offset: 1, length }];
1291                let mut changes = vec![
1292                    (NEGATIVE_SPAN_OFFSETS_FIELD, i32_list_value([1])),
1293                    (NEGATIVE_SPAN_LENGTHS_FIELD, i32_list_value([length as i32])),
1294                ];
1295                if length == 1 {
1296                    invalid.count = Some(if float {
1297                        Count::CountFloat(1.0)
1298                    } else {
1299                        Count::CountInt(1)
1300                    });
1301                    if float {
1302                        invalid.negative_counts = vec![1.0];
1303                    } else {
1304                        invalid.negative_deltas = vec![1];
1305                    }
1306                    changes.extend([(count_field, one.clone()), (bucket_field, buckets.clone())]);
1307                }
1308                assert!(encode_native_histogram(&invalid).is_err());
1309                assert_invalid(
1310                    &modified(&custom, &changes),
1311                    "must not use negative buckets",
1312                );
1313            }
1314        }
1315    }
1316
1317    #[test]
1318    fn native_histogram_remote_read_span_rules_match_ingestion() {
1319        for float in [false, true] {
1320            let custom = Histogram {
1321                schema: CUSTOM_BUCKETS_SCHEMA,
1322                custom_values: vec![1.0, 2.0],
1323                ..empty_histogram(float)
1324            };
1325            for custom_values in [vec![], custom.custom_values.clone()] {
1326                let overflow_index = custom_values.len() as i32;
1327                for (spans, accepted) in [
1328                    (vec![(-1, 0)], false),
1329                    (vec![(overflow_index + 2, 0)], false),
1330                    (vec![(overflow_index, 0), (2, 0)], false),
1331                    (vec![(overflow_index + 1, 0)], true),
1332                    (vec![(overflow_index, 0), (1, 0)], true),
1333                    (vec![(overflow_index, 1)], true),
1334                    (vec![(overflow_index + 1, 1)], false),
1335                ] {
1336                    let bucket_count = spans.iter().map(|(_, length)| *length).sum::<u32>();
1337                    let base = Histogram {
1338                        custom_values: custom_values.clone(),
1339                        positive_spans: vec![BucketSpan {
1340                            offset: 0,
1341                            length: bucket_count,
1342                        }],
1343                        positive_counts: if float {
1344                            vec![0.0; bucket_count as usize]
1345                        } else {
1346                            vec![]
1347                        },
1348                        positive_deltas: if float {
1349                            vec![]
1350                        } else {
1351                            vec![0; bucket_count as usize]
1352                        },
1353                        ..custom.clone()
1354                    };
1355                    let write = Histogram {
1356                        positive_spans: spans
1357                            .iter()
1358                            .map(|&(offset, length)| BucketSpan { offset, length })
1359                            .collect(),
1360                        ..base.clone()
1361                    };
1362                    assert_eq!(encode_native_histogram(&write).is_ok(), accepted);
1363                    let array = modified(
1364                        &base,
1365                        &[
1366                            (
1367                                POSITIVE_SPAN_OFFSETS_FIELD,
1368                                i32_list_value(spans.iter().map(|s| s.0)),
1369                            ),
1370                            (
1371                                POSITIVE_SPAN_LENGTHS_FIELD,
1372                                i32_list_value(spans.iter().map(|s| s.1 as i32)),
1373                            ),
1374                        ],
1375                    );
1376                    assert_eq!(
1377                        decode_native_histogram_for_remote_read(&array, 0, 0).is_ok(),
1378                        accepted
1379                    );
1380                }
1381            }
1382            for negative in [false, true] {
1383                for length in [0, 1] {
1384                    let mut base = empty_histogram(float);
1385                    let spans = vec![BucketSpan { offset: 0, length }];
1386                    if negative {
1387                        base.negative_spans = spans;
1388                    } else {
1389                        base.positive_spans = spans;
1390                    }
1391                    if length == 1 {
1392                        if float {
1393                            if negative {
1394                                base.negative_counts = vec![0.0];
1395                            } else {
1396                                base.positive_counts = vec![0.0];
1397                            }
1398                        } else if negative {
1399                            base.negative_deltas = vec![0];
1400                        } else {
1401                            base.positive_deltas = vec![0];
1402                        }
1403                    }
1404                    for offset in [i32::MIN, i32::MIN + 1, -2, 0] {
1405                        let mut write = base.clone();
1406                        let offset_field = if negative {
1407                            write.negative_spans[0].offset = offset;
1408                            NEGATIVE_SPAN_OFFSETS_FIELD
1409                        } else {
1410                            write.positive_spans[0].offset = offset;
1411                            POSITIVE_SPAN_OFFSETS_FIELD
1412                        };
1413                        if offset == i32::MIN {
1414                            assert!(encode_native_histogram(&write).is_err());
1415                            assert_invalid(
1416                                &modified(&base, &[(offset_field, i32_list_value([offset]))]),
1417                                "initial span offset i32::MIN",
1418                            );
1419                        } else {
1420                            assert!(
1421                                decode_native_histogram_for_remote_read(&stored(&write), 0, 0)
1422                                    .unwrap()
1423                                    .is_some()
1424                            );
1425                        }
1426                    }
1427                }
1428            }
1429            let base = empty_histogram(float);
1430            // Out-of-range buckets are different from valid empty end cursors.
1431            for offset in [-1, 3] {
1432                let custom = Histogram {
1433                    positive_spans: vec![BucketSpan {
1434                        offset: 0,
1435                        length: 1,
1436                    }],
1437                    positive_counts: if float { vec![0.0] } else { vec![] },
1438                    positive_deltas: if float { vec![] } else { vec![0] },
1439                    ..custom.clone()
1440                };
1441                assert_invalid(
1442                    &modified(
1443                        &custom,
1444                        &[(POSITIVE_SPAN_OFFSETS_FIELD, i32_list_value([offset]))],
1445                    ),
1446                    if offset < 0 {
1447                        "negative offset"
1448                    } else {
1449                        "out of range"
1450                    },
1451                );
1452            }
1453            let exponential = Histogram {
1454                positive_spans: vec![BucketSpan {
1455                    offset: 0,
1456                    length: 1,
1457                }],
1458                positive_counts: if float { vec![0.0] } else { vec![] },
1459                positive_deltas: if float { vec![] } else { vec![0] },
1460                ..base
1461            };
1462            assert_invalid(
1463                &modified(
1464                    &exponential,
1465                    &[(POSITIVE_SPAN_OFFSETS_FIELD, i32_list_value([i32::MAX]))],
1466                ),
1467                "out of range",
1468            );
1469        }
1470    }
1471
1472    #[test]
1473    fn shared_errors_are_protocol_neutral() {
1474        let error = encode_native_histogram(&Histogram {
1475            schema: 9,
1476            ..Default::default()
1477        })
1478        .unwrap_err();
1479
1480        assert_eq!(
1481            error.to_string(),
1482            "native histogram schema 9 must be reduced before ingestion"
1483        );
1484        assert!(!error.to_string().contains("remote write"));
1485        assert!(!error.to_string().contains("OTLP"));
1486    }
1487}