1use std::collections::{HashMap, HashSet};
18
19use bincode::Options;
20use serde::Deserialize;
21use uddsketch::{UddSketch, UddSketchRef};
22
23const MAX_BYTES: usize = 64 * 1024 * 1024;
24const MAX_BUCKETS: usize = 1_000_000;
25const HEADER_LEN: usize = 48;
26
27#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
28enum LegacyBucketKey {
29 Negative(i64),
30 Zero,
31 Positive(i64),
32 Invalid,
33}
34
35#[derive(Debug, Deserialize)]
36struct LegacyBucket {
37 count: u64,
38 next: LegacyBucketKey,
39}
40
41#[derive(Debug, Deserialize)]
42struct LegacyBucketStore {
43 map: HashMap<LegacyBucketKey, LegacyBucket>,
44 head: LegacyBucketKey,
45}
46
47#[derive(Debug, Deserialize)]
48struct LegacyUddSketch {
49 buckets: LegacyBucketStore,
50 alpha: f64,
51 gamma: f64,
52 compactions: u32,
53 max_buckets: u64,
54 count: u64,
55 sum: f64,
56}
57
58#[derive(Debug, Deserialize)]
59struct LegacyUddSketchState {
60 uddsketch: LegacyUddSketch,
61 initial_error: f64,
62}
63
64struct ValidatedLegacyUddSketch {
65 buckets: Vec<(LegacyBucketKey, u64)>,
66 alpha: f64,
67 gamma: f64,
68 count: u64,
69}
70
71pub(crate) fn decode(raw: &[u8]) -> Result<UddSketch, String> {
72 match UddSketch::decode(raw) {
73 Ok(sketch) => Ok(sketch),
74 Err(current_error) => decode_legacy_state(raw).map_err(|legacy_error| {
75 format!(
76 "canonical decode failed: {current_error}; legacy decode failed: {legacy_error}"
77 )
78 }),
79 }
80}
81
82pub(crate) fn quantile(raw: &[u8], quantile: f64) -> Result<Option<f64>, String> {
83 match UddSketchRef::parse(raw) {
84 Ok(sketch) => sketch.quantile(quantile).map_err(|error| error.to_string()),
85 Err(current_error) => decode_legacy_sketch(raw)
86 .and_then(|sketch| sketch.quantile(quantile))
87 .map_err(|legacy_error| {
88 format!(
89 "canonical decode failed: {current_error}; legacy decode failed: {legacy_error}"
90 )
91 }),
92 }
93}
94
95pub(crate) fn rank(raw: &[u8], value: f64) -> Result<Option<f64>, String> {
96 match UddSketchRef::parse(raw) {
97 Ok(sketch) => sketch.rank(value).map_err(|error| error.to_string()),
98 Err(current_error) => decode_legacy_sketch(raw)
99 .and_then(|sketch| sketch.rank(value))
100 .map_err(|legacy_error| {
101 format!(
102 "canonical decode failed: {current_error}; legacy decode failed: {legacy_error}"
103 )
104 }),
105 }
106}
107
108fn validate_legacy_input(raw: &[u8]) -> Result<(), String> {
109 if raw.len() > MAX_BYTES {
110 return Err("input exceeds the legacy decode byte limit".to_string());
111 }
112 let map_len = raw
113 .get(..8)
114 .and_then(|bytes| bytes.try_into().ok())
115 .map(u64::from_le_bytes)
116 .ok_or_else(|| "legacy input is truncated before the bucket count".to_string())?;
117 if map_len > MAX_BUCKETS as u64 {
118 return Err("legacy populated bucket count exceeds decode limit".to_string());
119 }
120 Ok(())
121}
122
123fn legacy_options() -> impl Options {
124 bincode::DefaultOptions::new()
125 .with_fixint_encoding()
126 .with_limit(MAX_BYTES as u64)
127 .reject_trailing_bytes()
128}
129
130fn decode_legacy_state(raw: &[u8]) -> Result<UddSketch, String> {
131 validate_legacy_input(raw)?;
132 let state = legacy_options()
133 .deserialize::<LegacyUddSketchState>(raw)
134 .map_err(|error| error.to_string())?;
135 let encoded = state.into_canonical()?;
136 UddSketch::decode(&encoded).map_err(|error| error.to_string())
137}
138
139fn decode_legacy_sketch(raw: &[u8]) -> Result<LegacyUddSketch, String> {
140 validate_legacy_input(raw)?;
141 legacy_options()
142 .deserialize::<LegacyUddSketchState>(raw)
143 .map(|state| state.uddsketch)
144 .or_else(|state_error| {
145 legacy_options()
146 .deserialize::<LegacyUddSketch>(raw)
147 .map_err(|sketch_error| {
148 format!(
149 "legacy state decode failed: {state_error}; legacy sketch decode failed: {sketch_error}"
150 )
151 })
152 })
153}
154
155impl LegacyUddSketchState {
156 fn into_canonical(self) -> Result<Vec<u8>, String> {
157 let sketch = self.uddsketch;
158 let max_buckets = u32::try_from(sketch.max_buckets)
159 .map_err(|_| "legacy maximum bucket count exceeds u32".to_string())?;
160 if !(7..=MAX_BUCKETS as u32).contains(&max_buckets) {
161 return Err("legacy maximum bucket count is outside supported limits".to_string());
162 }
163 let compactions = u8::try_from(sketch.compactions)
164 .map_err(|_| "legacy compaction count exceeds u8".to_string())?;
165 if compactions > 63 {
166 return Err("legacy compaction count exceeds 63".to_string());
167 }
168
169 let (expected_alpha, expected_gamma) = mapping(self.initial_error, compactions)?;
170 if sketch.alpha.to_bits() != expected_alpha.to_bits()
171 || sketch.gamma.to_bits() != expected_gamma.to_bits()
172 {
173 return Err("legacy mapping metadata is inconsistent".to_string());
174 }
175
176 let buckets = sketch.buckets.ordered()?;
177 if buckets.len() > max_buckets as usize {
178 return Err("legacy populated bucket count exceeds its configured limit".to_string());
179 }
180 let decoded_count = buckets.iter().try_fold(0_u64, |total, (_, count)| {
181 total
182 .checked_add(*count)
183 .ok_or_else(|| "legacy bucket count sum overflows u64".to_string())
184 })?;
185 if decoded_count != sketch.count {
186 return Err("legacy bucket counts do not match the value count".to_string());
187 }
188 if sketch.count == 0 && sketch.sum.to_bits() != 0.0_f64.to_bits() {
189 return Err("legacy empty sketch sum is not positive zero".to_string());
190 }
191
192 encode_canonical(
193 max_buckets,
194 self.initial_error,
195 compactions,
196 sketch.count,
197 sketch.sum,
198 &buckets,
199 )
200 }
201}
202
203impl LegacyUddSketch {
204 fn quantile(self, quantile: f64) -> Result<Option<f64>, String> {
205 if !quantile.is_finite() || !(0.0..=1.0).contains(&quantile) {
206 return Err("invalid quantile".to_string());
207 }
208 let validated = self.validate()?;
209 let count = validated.count;
210 if count == 0 {
211 return Ok(None);
212 }
213
214 let target = if quantile == 1.0 {
215 count
216 } else {
217 ((count as f64 * quantile) as u64)
218 .saturating_add(1)
219 .min(count)
220 };
221 let mut seen = 0_u64;
222 for (key, bucket_count) in validated.buckets {
223 seen += bucket_count;
224 if seen >= target {
225 return Ok(Some(legacy_bucket_value(
226 validated.alpha,
227 validated.gamma,
228 key,
229 )?));
230 }
231 }
232 Err("legacy bucket counts do not cover the quantile rank".to_string())
233 }
234
235 fn rank(self, value: f64) -> Result<Option<f64>, String> {
236 let validated = self.validate()?;
237 let count = validated.count;
238 if count == 0 {
239 return Ok(None);
240 }
241 if value.is_nan() {
242 return Err("invalid rank value".to_string());
243 }
244
245 let index = if value.is_infinite() {
246 i64::MAX
247 } else {
248 value.abs().log(validated.gamma).ceil() as i64
249 };
250 let target = if value == 0.0 {
251 LegacyBucketKey::Zero
252 } else if value.is_sign_negative() {
253 LegacyBucketKey::Negative(index)
254 } else {
255 LegacyBucketKey::Positive(index)
256 };
257 let mut below = 0.0;
258 for (key, bucket_count) in validated.buckets {
259 if key == target {
260 return Ok(Some((below + bucket_count as f64 / 2.0) / count as f64));
261 }
262 if key_lt(target, key) {
263 return Ok(Some(below / count as f64));
264 }
265 below += bucket_count as f64;
266 }
267 Ok(Some(1.0))
268 }
269
270 fn validate(self) -> Result<ValidatedLegacyUddSketch, String> {
271 if self.compactions >= 64 {
272 return Err("legacy compaction count must be below 64".to_string());
273 }
274 validate_current_mapping(self.alpha, self.gamma, self.compactions)?;
275 if self.max_buckets == 0 || self.max_buckets > MAX_BUCKETS as u64 {
276 return Err("legacy maximum bucket count is outside supported limits".to_string());
277 }
278
279 let buckets = self.buckets.ordered()?;
280 if buckets.len() > self.max_buckets as usize {
281 return Err("legacy populated bucket count exceeds its configured limit".to_string());
282 }
283 for (key, _) in &buckets {
284 if !legacy_bucket_key_is_attainable(self.gamma, *key) {
285 return Err("legacy bucket index cannot represent an f64".to_string());
286 }
287 }
288 let decoded_count = buckets.iter().try_fold(0_u64, |total, (_, count)| {
289 total
290 .checked_add(*count)
291 .ok_or_else(|| "legacy bucket count sum overflows u64".to_string())
292 })?;
293 if decoded_count != self.count {
294 return Err("legacy bucket counts do not match the value count".to_string());
295 }
296
297 Ok(ValidatedLegacyUddSketch {
298 buckets,
299 alpha: self.alpha,
300 gamma: self.gamma,
301 count: self.count,
302 })
303 }
304}
305
306fn validate_current_mapping(
307 mut alpha: f64,
308 mut gamma: f64,
309 compactions: u32,
310) -> Result<(), String> {
311 if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) {
312 return Err("legacy current error is outside [0, 1]".to_string());
313 }
314 if !gamma.is_finite() || gamma <= 1.0 {
315 return (alpha == 1.0 && gamma == f64::INFINITY && compactions >= 5)
316 .then_some(())
317 .ok_or_else(|| "legacy gamma must be greater than one".to_string());
318 }
319 if alpha == 1.0 {
320 return (compactions > 0 && 1.0 - 2.0 / (gamma + 1.0) == 1.0)
321 .then_some(())
322 .ok_or_else(|| "legacy saturated mapping metadata is inconsistent".to_string());
323 }
324
325 for _ in 0..compactions {
326 alpha /= 1.0 + (1.0 - alpha * alpha).sqrt();
327 gamma = gamma.sqrt();
328 }
329 let expected_gamma = (1.0 + alpha) / (1.0 - alpha);
330 let relative_difference = (gamma - expected_gamma).abs() / expected_gamma;
331 if relative_difference > 1e-10 {
332 return Err("legacy mapping metadata is inconsistent".to_string());
333 }
334 Ok(())
335}
336
337fn legacy_bucket_key_is_attainable(gamma: f64, key: LegacyBucketKey) -> bool {
338 let index = match key {
339 LegacyBucketKey::Zero => return true,
340 LegacyBucketKey::Negative(index) | LegacyBucketKey::Positive(index) => index,
341 LegacyBucketKey::Invalid => return false,
342 };
343 if index == i64::MAX {
344 return true;
345 }
346
347 let minimum = f64::from_bits(1).log(gamma).ceil() as i64;
348 let maximum = f64::MAX.log(gamma).ceil() as i64;
349 (minimum..=maximum).contains(&index)
350}
351
352fn legacy_bucket_value(alpha: f64, gamma: f64, key: LegacyBucketKey) -> Result<f64, String> {
353 let magnitude = |index: i64| gamma.powf(index as f64 - 1.0) * (1.0 + alpha);
354 match key {
355 LegacyBucketKey::Negative(index) => Ok(-magnitude(index)),
356 LegacyBucketKey::Zero => Ok(0.0),
357 LegacyBucketKey::Positive(index) => Ok(magnitude(index)),
358 LegacyBucketKey::Invalid => Err("legacy bucket chain contains the end marker".to_string()),
359 }
360}
361
362impl LegacyBucketStore {
363 fn ordered(self) -> Result<Vec<(LegacyBucketKey, u64)>, String> {
364 if self.map.len() > MAX_BUCKETS {
365 return Err("legacy populated bucket count exceeds decode limit".to_string());
366 }
367 if self.map.is_empty() {
368 if self.head != LegacyBucketKey::Invalid {
369 return Err("legacy empty bucket store has a nonempty head".to_string());
370 }
371 return Ok(Vec::new());
372 }
373
374 let mut buckets = Vec::with_capacity(self.map.len());
375 let mut visited = HashSet::with_capacity(self.map.len());
376 let mut key = self.head;
377 while key != LegacyBucketKey::Invalid {
378 if !visited.insert(key) {
379 return Err("legacy bucket chain contains a cycle".to_string());
380 }
381 let bucket = self
382 .map
383 .get(&key)
384 .ok_or_else(|| "legacy bucket chain references a missing bucket".to_string())?;
385 if bucket.count == 0 {
386 return Err("legacy bucket has a zero count".to_string());
387 }
388 buckets.push((key, bucket.count));
389 key = bucket.next;
390 }
391 if buckets.len() != self.map.len() || self.map.contains_key(&LegacyBucketKey::Invalid) {
392 return Err("legacy bucket store contains unreachable buckets".to_string());
393 }
394 if !buckets.windows(2).all(|pair| key_lt(pair[0].0, pair[1].0)) {
395 return Err("legacy bucket chain is not strictly ordered".to_string());
396 }
397 Ok(buckets)
398 }
399}
400
401fn mapping(initial_error: f64, compactions: u8) -> Result<(f64, f64), String> {
402 if !initial_error.is_finite() || !(1e-12..1.0).contains(&initial_error) {
403 return Err("legacy initial error is outside [1e-12, 1)".to_string());
404 }
405 let mut alpha = initial_error;
406 let mut gamma = (1.0 + initial_error) / (1.0 - initial_error);
407 for _ in 0..compactions {
408 gamma *= gamma;
409 alpha = 2.0 * alpha / (1.0 + alpha.powi(2));
410 }
411 Ok((alpha, gamma))
412}
413
414fn encode_canonical(
415 max_buckets: u32,
416 initial_error: f64,
417 compactions: u8,
418 count: u64,
419 sum: f64,
420 buckets: &[(LegacyBucketKey, u64)],
421) -> Result<Vec<u8>, String> {
422 let negative = buckets
423 .iter()
424 .filter_map(|(key, count)| match key {
425 LegacyBucketKey::Negative(index) => Some((*index, *count)),
426 _ => None,
427 })
428 .collect::<Vec<_>>();
429 let zero_count = buckets
430 .iter()
431 .find_map(|(key, count)| (*key == LegacyBucketKey::Zero).then_some(*count))
432 .unwrap_or(0);
433 let positive = buckets
434 .iter()
435 .filter_map(|(key, count)| match key {
436 LegacyBucketKey::Positive(index) => Some((*index, *count)),
437 _ => None,
438 })
439 .collect::<Vec<_>>();
440
441 let mut encoded = vec![0; HEADER_LEN];
442 put_varint(&mut encoded, negative.len() as u64);
443 put_varint(&mut encoded, zero_count);
444 put_varint(&mut encoded, positive.len() as u64);
445 encode_section(&mut encoded, &negative, true)?;
446 encode_section(&mut encoded, &positive, false)?;
447
448 let payload_len = u32::try_from(encoded.len() - HEADER_LEN)
449 .map_err(|_| "legacy canonical payload exceeds u32".to_string())?;
450 encoded[0..4].copy_from_slice(b"UDDS");
451 encoded[4] = 1;
452 encoded[6] = compactions;
453 encoded[8..12].copy_from_slice(&max_buckets.to_le_bytes());
454 encoded[12..16].copy_from_slice(&(buckets.len() as u32).to_le_bytes());
455 encoded[16..24].copy_from_slice(&initial_error.to_bits().to_le_bytes());
456 encoded[24..32].copy_from_slice(&count.to_le_bytes());
457 encoded[32..40].copy_from_slice(&sum.to_bits().to_le_bytes());
458 encoded[40..44].copy_from_slice(&payload_len.to_le_bytes());
459 Ok(encoded)
460}
461
462fn encode_section(
463 output: &mut Vec<u8>,
464 buckets: &[(i64, u64)],
465 descending: bool,
466) -> Result<(), String> {
467 let mut previous = None;
468 for &(index, count) in buckets {
469 let encoded_index = match previous {
470 None => zigzag(index),
471 Some((previous_index, _)) => {
472 let delta = if descending {
473 previous_index as i128 - index as i128
474 } else {
475 index as i128 - previous_index as i128
476 };
477 u64::try_from(delta)
478 .ok()
479 .filter(|delta| *delta != 0)
480 .ok_or_else(|| "legacy bucket indices are not strictly ordered".to_string())?
481 }
482 };
483 let encoded_count = match previous {
484 None => count,
485 Some((_, previous_count)) => zigzag(count.wrapping_sub(previous_count) as i64),
486 };
487 put_varint(output, encoded_index);
488 put_varint(output, encoded_count);
489 previous = Some((index, count));
490 }
491 Ok(())
492}
493
494fn put_varint(output: &mut Vec<u8>, value: u64) {
495 let mut buffer = [0; 9];
496 let len = vu128::encode_u64(&mut buffer, value);
497 output.extend_from_slice(&buffer[..len]);
498}
499
500const fn zigzag(value: i64) -> u64 {
501 (value.wrapping_shl(1) ^ (value >> 63)) as u64
502}
503
504fn key_lt(left: LegacyBucketKey, right: LegacyBucketKey) -> bool {
505 use LegacyBucketKey::*;
506 match (left, right) {
507 (Negative(left), Negative(right)) => left > right,
508 (Negative(_), Zero | Positive(_)) | (Zero, Positive(_)) => true,
509 (Positive(left), Positive(right)) => left < right,
510 _ => false,
511 }
512}
513
514#[cfg(test)]
515pub(crate) const LEGACY_STATE: &[u8] = &[
516 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
517 0, 0, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0,
518 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 116,
519 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0,
520 123, 20, 174, 71, 225, 122, 132, 63, 253, 74, 129, 90, 191, 82, 240, 63, 0, 0, 0, 0, 128, 0, 0,
521 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 63, 123, 20, 174, 71, 225, 122,
522 132, 63,
523];
524
525#[cfg(test)]
526pub(crate) const COMPACTED_LEGACY_SKETCH: &[u8] = &[
527 6, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 36, 0, 0, 0, 0, 0,
528 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0, 0, 0, 0,
529 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 254, 255, 255, 255, 255, 255, 255,
530 255, 29, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 2, 0, 0, 0,
531 2, 0, 0, 0, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0,
532 0, 3, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0,
533 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 254, 255, 255,
534 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 240, 63, 169, 137, 186, 120, 1, 63, 82, 71, 12, 0,
535 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 201, 0, 0, 0, 0, 0, 0, 0, 112, 103, 108, 212, 220, 81, 180, 84,
536];
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 fn legacy_rank_sketch() -> LegacyUddSketch {
543 LegacyUddSketch {
544 buckets: LegacyBucketStore {
545 map: HashMap::from([
546 (
547 LegacyBucketKey::Negative(2),
548 LegacyBucket {
549 count: 2,
550 next: LegacyBucketKey::Zero,
551 },
552 ),
553 (
554 LegacyBucketKey::Zero,
555 LegacyBucket {
556 count: 1,
557 next: LegacyBucketKey::Positive(2),
558 },
559 ),
560 (
561 LegacyBucketKey::Positive(2),
562 LegacyBucket {
563 count: 1,
564 next: LegacyBucketKey::Invalid,
565 },
566 ),
567 ]),
568 head: LegacyBucketKey::Negative(2),
569 },
570 alpha: 0.01,
571 gamma: (1.0 + 0.01) / (1.0 - 0.01),
572 compactions: 0,
573 max_buckets: 128,
574 count: 4,
575 sum: 0.0,
576 }
577 }
578
579 #[test]
580 fn legacy_rank_matches_canonical_rank() {
581 let mut sketch = UddSketch::new(128, 0.01).unwrap();
582 sketch
583 .add_batch(&[-10.0, -1.0, 0.0, 1.0, 10.0, f64::INFINITY])
584 .unwrap();
585 let encoded = sketch.encode().unwrap();
586
587 for value in [
588 f64::NEG_INFINITY,
589 -10.0,
590 -0.0,
591 0.0,
592 1.0,
593 10.0,
594 f64::INFINITY,
595 ] {
596 assert_eq!(rank(&encoded, value).unwrap(), sketch.rank(value).unwrap());
597 }
598 }
599
600 #[test]
601 fn legacy_rank_reads_wrapped_and_bare_legacy_states() {
602 let bare = &LEGACY_STATE[..LEGACY_STATE.len() - std::mem::size_of::<f64>()];
603
604 for value in [f64::NEG_INFINITY, 0.0, 0.99, f64::INFINITY] {
605 assert_eq!(
606 rank(LEGACY_STATE, value).unwrap(),
607 rank(bare, value).unwrap()
608 );
609 }
610 assert_eq!(rank(LEGACY_STATE, f64::NEG_INFINITY).unwrap(), Some(0.0));
611 assert_eq!(rank(LEGACY_STATE, f64::INFINITY).unwrap(), Some(1.0));
612 }
613
614 #[test]
615 fn legacy_rank_reads_compacted_bare_sketch() {
616 assert_eq!(
617 rank(COMPACTED_LEGACY_SKETCH, f64::NEG_INFINITY).unwrap(),
618 Some(0.0)
619 );
620 assert!(rank(COMPACTED_LEGACY_SKETCH, 1.0).unwrap().is_some());
621 assert_eq!(
622 rank(COMPACTED_LEGACY_SKETCH, f64::INFINITY).unwrap(),
623 Some(1.0)
624 );
625 }
626
627 #[test]
628 fn legacy_rank_handles_bucket_boundaries_and_empty_sketch() {
629 let gamma = legacy_rank_sketch().gamma;
630 for (value, expected) in [
631 (f64::NEG_INFINITY, 0.0),
632 (-1.0, 0.5),
633 (-0.0, 0.625),
634 (0.0, 0.625),
635 (1.0, 0.75),
636 (gamma.powi(2), 0.875),
637 (f64::INFINITY, 1.0),
638 ] {
639 assert_eq!(legacy_rank_sketch().rank(value).unwrap(), Some(expected));
640 }
641
642 let mut empty = legacy_rank_sketch();
643 empty.buckets.map.clear();
644 empty.buckets.head = LegacyBucketKey::Invalid;
645 empty.count = 0;
646 assert_eq!(empty.rank(0.0).unwrap(), None);
647 }
648
649 #[test]
650 fn legacy_rank_empty_sketch_returns_none_for_nan() {
651 let mut empty = legacy_rank_sketch();
652 empty.buckets.map.clear();
653 empty.buckets.head = LegacyBucketKey::Invalid;
654 empty.count = 0;
655
656 assert_eq!(empty.rank(f64::NAN).unwrap(), None);
657 }
658
659 #[test]
660 fn legacy_rank_accumulates_large_counts_with_canonical_rounding() {
661 let large_count = 1_u64 << 53;
662 let total_count = large_count + 3;
663 let sketch = LegacyUddSketch {
664 buckets: LegacyBucketStore {
665 map: HashMap::from([
666 (
667 LegacyBucketKey::Negative(3),
668 LegacyBucket {
669 count: large_count,
670 next: LegacyBucketKey::Negative(2),
671 },
672 ),
673 (
674 LegacyBucketKey::Negative(2),
675 LegacyBucket {
676 count: 1,
677 next: LegacyBucketKey::Negative(1),
678 },
679 ),
680 (
681 LegacyBucketKey::Negative(1),
682 LegacyBucket {
683 count: 1,
684 next: LegacyBucketKey::Zero,
685 },
686 ),
687 (
688 LegacyBucketKey::Zero,
689 LegacyBucket {
690 count: 1,
691 next: LegacyBucketKey::Invalid,
692 },
693 ),
694 ]),
695 head: LegacyBucketKey::Negative(3),
696 },
697 alpha: 0.01,
698 gamma: (1.0 + 0.01) / (1.0 - 0.01),
699 compactions: 0,
700 max_buckets: 128,
701 count: total_count,
702 sum: 0.0,
703 };
704 let mut below = 0.0;
705 for count in [large_count, 1, 1] {
706 below += count as f64;
707 }
708 let expected = (below + 0.5) / total_count as f64;
709
710 assert_eq!(sketch.rank(0.0).unwrap(), Some(expected));
711 }
712
713 #[test]
714 fn legacy_rank_rejects_nan_and_malformed_data() {
715 let bare_len = LEGACY_STATE.len() - std::mem::size_of::<f64>();
716 let bare = &LEGACY_STATE[..bare_len];
717 assert!(rank(bare, f64::NAN).is_err());
718
719 let mut invalid_mapping = bare.to_vec();
720 let alpha_offset = bare_len - 44;
721 invalid_mapping[alpha_offset..alpha_offset + 8].copy_from_slice(&f64::NAN.to_le_bytes());
722 let error = rank(&invalid_mapping, 0.0).unwrap_err();
723 assert!(error.contains("canonical decode failed"));
724 assert!(error.contains("legacy decode failed"));
725
726 let mut invalid_count = legacy_rank_sketch();
727 invalid_count.count += 1;
728 assert!(invalid_count.rank(0.0).is_err());
729
730 let mut invalid_bucket = legacy_rank_sketch();
731 let bucket = invalid_bucket
732 .buckets
733 .map
734 .remove(&LegacyBucketKey::Negative(2))
735 .unwrap();
736 invalid_bucket
737 .buckets
738 .map
739 .insert(LegacyBucketKey::Negative(i64::MIN), bucket);
740 invalid_bucket.buckets.head = LegacyBucketKey::Negative(i64::MIN);
741 assert!(invalid_bucket.rank(0.0).is_err());
742 }
743
744 #[test]
745 fn legacy_rank_accepts_saturated_mapping() {
746 let sketch = LegacyUddSketch {
747 buckets: LegacyBucketStore {
748 map: HashMap::from([(
749 LegacyBucketKey::Positive(0),
750 LegacyBucket {
751 count: 1,
752 next: LegacyBucketKey::Invalid,
753 },
754 )]),
755 head: LegacyBucketKey::Positive(0),
756 },
757 alpha: 1.0,
758 gamma: f64::INFINITY,
759 compactions: 63,
760 max_buckets: 7,
761 count: 1,
762 sum: f64::INFINITY,
763 };
764
765 assert_eq!(sketch.rank(1.0).unwrap(), Some(0.5));
766 }
767
768 #[test]
769 fn current_format_is_decoded_without_legacy_fallback() {
770 let mut sketch = UddSketch::new(128, 0.01).unwrap();
771 sketch.add(42.0).unwrap();
772 let expected = sketch.quantile(0.5).unwrap();
773 let encoded = sketch.encode().unwrap();
774
775 assert!(decode_legacy_state(&encoded).is_err());
776 assert_eq!(decode(&encoded).unwrap(), sketch);
777 assert_eq!(quantile(&encoded, 0.5).unwrap(), expected);
778 }
779
780 #[test]
781 fn legacy_decoder_rejects_oversized_bucket_count_before_deserializing() {
782 let mut encoded = LEGACY_STATE.to_vec();
783 encoded[..8].copy_from_slice(&((MAX_BUCKETS as u64) + 1).to_le_bytes());
784
785 assert_eq!(
786 decode_legacy_state(&encoded).unwrap_err(),
787 "legacy populated bucket count exceeds decode limit"
788 );
789 }
790
791 #[test]
792 fn legacy_decoder_reads_bare_sketch_from_scalar_callers() {
793 let bare_sketch = &LEGACY_STATE[..LEGACY_STATE.len() - std::mem::size_of::<f64>()];
794
795 assert_eq!(
796 quantile(bare_sketch, 0.5).unwrap(),
797 Some(0.9900000000000001)
798 );
799 }
800
801 #[test]
802 fn legacy_decoder_reads_compacted_bare_sketch() {
803 assert!(decode_legacy_state(COMPACTED_LEGACY_SKETCH).is_err());
804 assert!(quantile(COMPACTED_LEGACY_SKETCH, 0.5).unwrap().is_some());
805 }
806
807 #[test]
808 fn legacy_quantile_rejects_invalid_mapping() {
809 let bare_len = LEGACY_STATE.len() - std::mem::size_of::<f64>();
810 let mut invalid = LEGACY_STATE[..bare_len].to_vec();
811 let alpha_offset = bare_len - 44;
812 invalid[alpha_offset..alpha_offset + 8].copy_from_slice(&f64::NAN.to_le_bytes());
813
814 assert!(quantile(&invalid, 0.5).is_err());
815
816 let mut sketch = decode_legacy_sketch(&LEGACY_STATE[..bare_len]).unwrap();
817 sketch.gamma = 2.0;
818 assert!(sketch.quantile(0.5).is_err());
819
820 let mut sketch = decode_legacy_sketch(&LEGACY_STATE[..bare_len]).unwrap();
821 sketch.alpha = 1.0;
822 sketch.gamma = 2.0;
823 assert!(sketch.quantile(0.5).is_err());
824
825 let mut sketch = decode_legacy_sketch(&LEGACY_STATE[..bare_len]).unwrap();
826 sketch.alpha = 1e-12;
827 sketch.gamma = 1.0 + 1e-9;
828 sketch.compactions = 0;
829 assert!(sketch.quantile(0.5).is_err());
830
831 let mut sketch = decode_legacy_sketch(&LEGACY_STATE[..bare_len]).unwrap();
832 sketch.alpha = 1.0;
833 sketch.gamma = 1e20;
834 sketch.compactions = 0;
835 assert!(sketch.quantile(0.5).is_err());
836
837 let mut sketch = decode_legacy_sketch(&LEGACY_STATE[..bare_len]).unwrap();
838 sketch.alpha = 1.0;
839 sketch.gamma = f64::INFINITY;
840 sketch.compactions = 1;
841 assert!(validate_current_mapping(sketch.alpha, sketch.gamma, sketch.compactions).is_err());
842
843 let mut sketch = decode_legacy_sketch(&LEGACY_STATE[..bare_len]).unwrap();
844 sketch.alpha = 0.99999999;
845 sketch.gamma = 2.0;
846 sketch.compactions = 0;
847 assert!(sketch.quantile(0.5).is_err());
848
849 let mut sketch = decode_legacy_sketch(&LEGACY_STATE[..bare_len]).unwrap();
850 let LegacyBucketKey::Negative(index) = sketch.buckets.head else {
851 panic!("Expected negative head bucket");
852 };
853 let bucket = sketch
854 .buckets
855 .map
856 .remove(&LegacyBucketKey::Negative(index))
857 .unwrap();
858 sketch
859 .buckets
860 .map
861 .insert(LegacyBucketKey::Negative(i64::MIN), bucket);
862 sketch.buckets.head = LegacyBucketKey::Negative(i64::MIN);
863 assert!(sketch.quantile(0.5).is_err());
864 }
865
866 #[test]
867 fn legacy_quantile_accepts_saturated_mapping() {
868 let sketch = LegacyUddSketch {
869 buckets: LegacyBucketStore {
870 map: HashMap::from([(
871 LegacyBucketKey::Positive(0),
872 LegacyBucket {
873 count: 1,
874 next: LegacyBucketKey::Invalid,
875 },
876 )]),
877 head: LegacyBucketKey::Positive(0),
878 },
879 alpha: 1.0,
880 gamma: f64::INFINITY,
881 compactions: 63,
882 max_buckets: 7,
883 count: 1,
884 sum: f64::INFINITY,
885 };
886
887 assert_eq!(sketch.quantile(0.5).unwrap(), Some(0.0));
888 }
889
890 #[test]
891 fn legacy_quantile_handles_maximum_count_at_one() {
892 let sketch = LegacyUddSketch {
893 buckets: LegacyBucketStore {
894 map: HashMap::from([(
895 LegacyBucketKey::Zero,
896 LegacyBucket {
897 count: u64::MAX,
898 next: LegacyBucketKey::Invalid,
899 },
900 )]),
901 head: LegacyBucketKey::Zero,
902 },
903 alpha: 0.01,
904 gamma: (1.0 + 0.01) / (1.0 - 0.01),
905 compactions: 0,
906 max_buckets: 128,
907 count: u64::MAX,
908 sum: 0.0,
909 };
910
911 assert_eq!(sketch.quantile(1.0).unwrap(), Some(0.0));
912 }
913}