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