Skip to main content

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}