Skip to main content

apache_datasketches/tuple/
sketch.rs

1use crate::error::SketchError;
2use crate::tuple::builder::ResizeFactor;
3use apache_datasketches_sys::array_of_doubles_sketch::ffi as sys;
4use cxx::UniquePtr;
5
6/// A mutable, update-only ArrayOfDoubles Tuple sketch: estimates the number
7/// of distinct keys added via `update_*`, and carries a fixed-width array of
8/// `f64` values per retained key, summed on collision. Build one with
9/// [`ArrayOfDoublesSketchBuilder`](super::ArrayOfDoublesSketchBuilder).
10///
11/// Call [`Self::compact`] to produce an immutable, serializable
12/// [`super::CompactArrayOfDoublesSketch`] snapshot for storage, transmission,
13/// or use as input to a set operation.
14pub struct ArrayOfDoublesSketch {
15    pub(crate) inner: UniquePtr<sys::ArrayOfDoublesSketchShim>,
16    /// Cached copy of the shim's `get_num_values()`.
17    ///
18    /// Every `update_*` has to validate the caller's slice length (see
19    /// [`Self::check_values`]), and reading `num_values` back from C++ to do
20    /// so cost a full FFI crossing per update — on the order of the update
21    /// itself.
22    ///
23    /// Caching is sound because the value is fixed for the sketch's lifetime:
24    /// it lives in the C++ update policy, is set once when the builder
25    /// constructs the sketch, has no setter, and `reset()` clears the hash
26    /// table without touching the policy.
27    ///
28    /// **This invariant is what makes the cache correct, and the compiler will
29    /// not enforce it.** `from_parts` is currently the only constructor. Any
30    /// new one — a `deserialize`, or a `from_shim` wrapping a sketch built
31    /// elsewhere — must populate this field from the shim rather than assume a
32    /// value, or the cache silently disagrees with C++ and `check_values`
33    /// starts rejecting valid slices (or admitting short ones).
34    num_values: u8,
35}
36
37unsafe impl Send for ArrayOfDoublesSketch {}
38
39impl ArrayOfDoublesSketch {
40    pub(crate) fn from_parts(
41        lg_k: u8,
42        rf: ResizeFactor,
43        p: f32,
44        num_values: u8,
45    ) -> Result<Self, SketchError> {
46        if num_values == 0 {
47            return Err(SketchError::InvalidConfig(
48                "num_values must be at least 1".to_string(),
49            ));
50        }
51        let inner = sys::new_array_of_doubles_sketch(lg_k, rf.into(), p, num_values)
52            .map_err(|e| SketchError::InvalidConfig(e.what().to_string()))?;
53        Ok(Self { inner, num_values })
54    }
55
56    /// Validates that `values` has exactly [`Self::get_num_values`] elements.
57    ///
58    /// This check cannot be delegated to the C++ layer's exceptions the way
59    /// `lg_k`/`num_std_dev` validation is: upstream's update policy indexes
60    /// the supplied values blindly for `i in 0..num_values`, so a short slice
61    /// would be an out-of-bounds read rather than a graceful failure.
62    ///
63    /// Reads the cached [`Self::num_values`] rather than calling into C++, so
64    /// this costs no FFI crossing.
65    fn check_values(&self, values: &[f64]) -> Result<(), SketchError> {
66        let expected = self.num_values as usize;
67        if values.len() != expected {
68            return Err(SketchError::InvalidConfig(format!(
69                "expected {expected} values, got {}",
70                values.len()
71            )));
72        }
73        Ok(())
74    }
75
76    /// Adds a `u64` key with its associated values. Returns
77    /// [`SketchError::InvalidConfig`] unless
78    /// `values.len() == self.get_num_values()`.
79    pub fn update_u64(&mut self, key: u64, values: &[f64]) -> Result<(), SketchError> {
80        self.check_values(values)?;
81        self.inner.pin_mut().update_u64(key, values);
82        Ok(())
83    }
84
85    /// Adds an `i64` key with its associated values. See [`Self::update_u64`].
86    pub fn update_i64(&mut self, key: i64, values: &[f64]) -> Result<(), SketchError> {
87        self.check_values(values)?;
88        self.inner.pin_mut().update_i64(key, values);
89        Ok(())
90    }
91
92    /// Adds a `u32` key with its associated values. See [`Self::update_u64`].
93    pub fn update_u32(&mut self, key: u32, values: &[f64]) -> Result<(), SketchError> {
94        self.check_values(values)?;
95        self.inner.pin_mut().update_u32(key, values);
96        Ok(())
97    }
98
99    /// Adds an `i32` key with its associated values. See [`Self::update_u64`].
100    pub fn update_i32(&mut self, key: i32, values: &[f64]) -> Result<(), SketchError> {
101        self.check_values(values)?;
102        self.inner.pin_mut().update_i32(key, values);
103        Ok(())
104    }
105
106    /// Adds a `u16` key with its associated values. See [`Self::update_u64`].
107    pub fn update_u16(&mut self, key: u16, values: &[f64]) -> Result<(), SketchError> {
108        self.check_values(values)?;
109        self.inner.pin_mut().update_u16(key, values);
110        Ok(())
111    }
112
113    /// Adds an `i16` key with its associated values. See [`Self::update_u64`].
114    pub fn update_i16(&mut self, key: i16, values: &[f64]) -> Result<(), SketchError> {
115        self.check_values(values)?;
116        self.inner.pin_mut().update_i16(key, values);
117        Ok(())
118    }
119
120    /// Adds a `u8` key with its associated values. See [`Self::update_u64`].
121    pub fn update_u8(&mut self, key: u8, values: &[f64]) -> Result<(), SketchError> {
122        self.check_values(values)?;
123        self.inner.pin_mut().update_u8(key, values);
124        Ok(())
125    }
126
127    /// Adds an `i8` key with its associated values. See [`Self::update_u64`].
128    pub fn update_i8(&mut self, key: i8, values: &[f64]) -> Result<(), SketchError> {
129        self.check_values(values)?;
130        self.inner.pin_mut().update_i8(key, values);
131        Ok(())
132    }
133
134    /// Adds an `f64` key with its associated values. See [`Self::update_u64`].
135    pub fn update_f64(&mut self, key: f64, values: &[f64]) -> Result<(), SketchError> {
136        self.check_values(values)?;
137        self.inner.pin_mut().update_f64(key, values);
138        Ok(())
139    }
140
141    /// Adds a string key with its associated values. See [`Self::update_u64`].
142    pub fn update_str(&mut self, key: &str, values: &[f64]) -> Result<(), SketchError> {
143        self.check_values(values)?;
144        self.inner.pin_mut().update_str(key, values);
145        Ok(())
146    }
147
148    /// Adds an arbitrary byte-slice key with its associated values. See
149    /// [`Self::update_u64`].
150    pub fn update_bytes(&mut self, key: &[u8], values: &[f64]) -> Result<(), SketchError> {
151        self.check_values(values)?;
152        self.inner.pin_mut().update_bytes(key, values);
153        Ok(())
154    }
155
156    /// Removes retained entries in excess of the nominal size `k`, lowering
157    /// the theta threshold to do so.
158    ///
159    /// Note that this *does* shift [`Self::get_estimate`] — trimming lowers
160    /// theta, and the estimate is derived from the retained count and theta
161    /// together. Upstream only guarantees the excess entries are dropped.
162    pub fn trim(&mut self) {
163        self.inner.pin_mut().trim();
164    }
165
166    /// Resets this sketch to its initial, empty state. `num_values` is
167    /// preserved.
168    pub fn reset(&mut self) {
169        self.inner.pin_mut().reset();
170    }
171
172    /// Returns the current estimate of the number of distinct keys added.
173    pub fn get_estimate(&self) -> f64 {
174        self.inner.get_estimate()
175    }
176
177    /// Returns the lower bound of the confidence interval around
178    /// [`Self::get_estimate`], for the given number of standard deviations
179    /// (`1`, `2`, or `3`, corresponding to roughly 67%, 95%, and 99%
180    /// confidence). Returns [`SketchError::InvalidConfig`] for any other
181    /// value.
182    pub fn get_lower_bound(&self, num_std_dev: u8) -> Result<f64, SketchError> {
183        self.inner
184            .get_lower_bound(num_std_dev)
185            .map_err(|e| SketchError::InvalidConfig(e.what().to_string()))
186    }
187
188    /// Returns the upper bound of the confidence interval around
189    /// [`Self::get_estimate`]. See [`Self::get_lower_bound`] for the meaning
190    /// of `num_std_dev`.
191    pub fn get_upper_bound(&self, num_std_dev: u8) -> Result<f64, SketchError> {
192        self.inner
193            .get_upper_bound(num_std_dev)
194            .map_err(|e| SketchError::InvalidConfig(e.what().to_string()))
195    }
196
197    /// Returns `true` if no keys have been added to this sketch.
198    pub fn is_empty(&self) -> bool {
199        self.inner.is_empty()
200    }
201
202    /// Returns `true` if this sketch's theta threshold is below `1.0`
203    /// (i.e. it has begun sampling and [`Self::get_estimate`] is a
204    /// statistical estimate rather than an exact count).
205    pub fn is_estimation_mode(&self) -> bool {
206        self.inner.is_estimation_mode()
207    }
208
209    /// Returns `true` if this sketch's retained entries are sorted by hash
210    /// value.
211    pub fn is_ordered(&self) -> bool {
212        self.inner.is_ordered()
213    }
214
215    /// Returns the current theta threshold (`1.0` until sampling begins).
216    pub fn get_theta(&self) -> f64 {
217        self.inner.get_theta()
218    }
219
220    /// Returns the number of entries currently retained by this sketch.
221    pub fn get_num_retained(&self) -> u32 {
222        self.inner.get_num_retained()
223    }
224
225    /// Returns the fixed number of `f64` values each retained entry carries,
226    /// as configured at build time.
227    pub fn get_num_values(&self) -> u8 {
228        self.num_values
229    }
230
231    /// Iterates the retained entries as `(hash, values)` pairs, where
232    /// `values.len() == self.get_num_values()`.
233    ///
234    /// The entries are copied out of C++ in two FFI calls up front (cxx
235    /// cannot hand back a live C++ iterator), so each item owns its `Vec`
236    /// rather than borrowing from the sketch. Iteration order is unspecified
237    /// for an update sketch; compact it with `ordered = true` for
238    /// hash-ordered iteration.
239    pub fn entries(&self) -> impl Iterator<Item = (u64, Vec<f64>)> {
240        let num_values = self.num_values as usize;
241        let hashes: Vec<u64> = self.inner.entry_hashes().into_iter().collect();
242        let values: Vec<f64> = self.inner.entry_values().into_iter().collect();
243        let grouped: Vec<Vec<f64>> = if num_values == 0 {
244            Vec::new()
245        } else {
246            values.chunks(num_values).map(|c| c.to_vec()).collect()
247        };
248        hashes.into_iter().zip(grouped)
249    }
250
251    /// Produces an immutable, serializable
252    /// [`super::CompactArrayOfDoublesSketch`] snapshot of this sketch's
253    /// current state. If `ordered` is `true`, the snapshot's entries are
254    /// sorted by hash value.
255    pub fn compact(&self, ordered: bool) -> super::CompactArrayOfDoublesSketch {
256        super::CompactArrayOfDoublesSketch::from_shim(self.inner.compact(ordered))
257    }
258}