Skip to main content

common_function/
uddsketch_compat.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Compatibility decoder for UDDSketch states written before the canonical v1 format.
16
17use 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}