apache_datasketches/tuple/generic/summary.rs
1use apache_datasketches_sys::tuple_generic::{RawSummaryOps, RustSummary};
2use std::any::Any;
3
4/// A user-defined per-entry summary for [`TupleSketch`](super::TupleSketch).
5///
6/// Implement this on your own type to get a Tuple sketch that carries it.
7///
8/// # Panics and the FFI boundary
9///
10/// [`union_combine`](Self::union_combine),
11/// [`intersection_combine`](Self::intersection_combine), and
12/// [`Clone::clone`] are invoked by C++. A panic cannot unwind across that
13/// boundary, and the underlying C++ combine has no way to report failure and
14/// roll back an insert, so a panic in any of those three **aborts the
15/// process** after printing a diagnostic. Make them total.
16///
17/// [`create`](Self::create) is different: it runs entirely in Rust before any
18/// C++ call, so a panic there is an ordinary Rust panic that propagates to
19/// your caller.
20pub trait TupleSummary: Clone + Send + 'static {
21 /// The value passed to the sketch's `update_*` methods.
22 ///
23 /// May be unsized — `type Update = str` and `type Update = [f64]` both
24 /// work, so callers need not allocate to update.
25 type Update: ?Sized;
26
27 /// Builds a summary from a single update value.
28 fn create(update: &Self::Update) -> Self;
29
30 /// Merges `other` into `self` with union semantics. Used both when a key
31 /// is updated more than once and when two sketches are unioned.
32 fn union_combine(&mut self, other: &Self);
33
34 /// Merges `other` into `self` with intersection semantics.
35 ///
36 /// There is deliberately no default: upstream notes that no intersection
37 /// policy is sensible in general, and silently reusing union semantics
38 /// would be a correctness trap. If union semantics *are* what you want,
39 /// call `self.union_combine(other)` here explicitly.
40 fn intersection_combine(&mut self, other: &Self);
41}
42
43/// Erases a `TupleSummary` to the sys crate's `RawSummaryOps`.
44///
45/// Private: users never name this. It exists because cxx requires the opaque
46/// `extern "Rust"` type to live in the crate that declares the bridge, so the
47/// ergonomic trait here has to be adapted to the minimal trait there.
48pub(crate) struct Adapter<S: TupleSummary> {
49 value: S,
50}
51
52impl<S: TupleSummary> Adapter<S> {
53 fn new(value: S) -> Self {
54 Self { value }
55 }
56
57 /// Recovers `&S` from an erased operand.
58 ///
59 /// The typed façade makes a mismatch unreachable: `TupleSketch<S>` (via
60 /// [`erase`]) is the only producer of erased summaries in this crate, so
61 /// every operand a combine callback sees was wrapped as `Adapter<S>` for
62 /// the same `S`. A failure here means an internal invariant broke, not
63 /// user error.
64 fn downcast(other: &dyn RawSummaryOps) -> &S {
65 match other.as_any().downcast_ref::<Adapter<S>>() {
66 Some(adapter) => &adapter.value,
67 None => panic!(
68 "apache-datasketches internal invariant violated: a generic Tuple \
69 summary of a different concrete type reached a combine callback. \
70 This should be impossible through the public API; please report it."
71 ),
72 }
73 }
74}
75
76impl<S: TupleSummary> RawSummaryOps for Adapter<S> {
77 fn clone_boxed(&self) -> Box<dyn RawSummaryOps + Send> {
78 Box::new(Adapter::new(self.value.clone()))
79 }
80
81 fn union_combine(&mut self, other: &dyn RawSummaryOps) {
82 let other = Self::downcast(other);
83 self.value.union_combine(other);
84 }
85
86 fn intersection_combine(&mut self, other: &dyn RawSummaryOps) {
87 let other = Self::downcast(other);
88 self.value.intersection_combine(other);
89 }
90
91 fn as_any(&self) -> &dyn Any {
92 self
93 }
94}
95
96/// Wraps a user summary in the opaque type that crosses the FFI boundary.
97///
98/// Allocates. On the per-update path prefer [`refill`], which reuses a box the
99/// sketch already owns.
100pub(crate) fn erase<S: TupleSummary>(value: S) -> RustSummary {
101 RustSummary::new(Box::new(Adapter::new(value)))
102}
103
104/// Overwrites an existing erased summary in place, without allocating.
105///
106/// This is what keeps the update path allocation-free. `erase` heap-allocates a
107/// `Box<dyn RawSummaryOps>`, and doing that per update meant a malloc/free on
108/// every call — including for keys that C++ then discarded, since the box is
109/// built before the FFI crossing and therefore before upstream's theta screen.
110/// Reusing one box across updates removes that entirely; a new *entry* still
111/// costs one allocation, but that is C++ cloning into the table, which is
112/// inherent.
113///
114/// Sound because C++ only borrows the value for the duration of the update
115/// call: upstream reads it to clone into a fresh entry or combine into an
116/// existing one, and never stores it. So the same box can back the next update.
117///
118/// The type mismatch is unreachable through the public API — the scratch box is
119/// created by `erase` from the same `S` as the sketch it lives on — so this
120/// signals a broken internal invariant, exactly as [`unerase`] does.
121pub(crate) fn refill<S: TupleSummary>(scratch: &mut RustSummary, value: S) {
122 // Upcast `dyn RawSummaryOps` to its `Any` supertrait rather than adding an
123 // `as_any_mut` to that trait. The trait is public, so a new required method
124 // would be a breaking change to a published crate -- and unnecessary:
125 // trait upcasting has been stable since Rust 1.86, which is exactly the
126 // workaround `as_any` was written to avoid needing.
127 let ops: &mut dyn Any = scratch.ops_mut();
128 match ops.downcast_mut::<Adapter<S>>() {
129 Some(adapter) => adapter.value = value,
130 None => panic!(
131 "apache-datasketches internal invariant violated: a generic Tuple sketch's \
132 scratch summary held a different concrete type than the sketch's own. This \
133 should be impossible through the public API; please report it."
134 ),
135 }
136}
137
138/// Recovers an owned `S` from a summary that crossed back from C++.
139///
140/// [`erase`] is the sole producer of erased summaries, and it is only ever
141/// called with `Adapter<S>` for the `S` of the calling sketch, so a mismatch
142/// here means an internal invariant broke, not user error.
143pub(crate) fn unerase<S: TupleSummary>(summary: &RustSummary) -> S {
144 match summary.ops().as_any().downcast_ref::<Adapter<S>>() {
145 Some(adapter) => adapter.value.clone(),
146 None => panic!(
147 "apache-datasketches internal invariant violated: a generic Tuple summary \
148 of a different concrete type was returned from the sketch. This should be \
149 impossible through the public API; please report it."
150 ),
151 }
152}