1use 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#[derive(Debug, Snafu)]
35#[snafu(display("{message}"))]
36pub struct NativeHistogramError {
37 message: String,
38}
39
40type Result<T> = std::result::Result<T, NativeHistogramError>;
41
42pub 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
60pub 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
147pub 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 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(¤t_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 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 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 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}