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.
97pub(crate) fn erase<S: TupleSummary>(value: S) -> RustSummary {
98 RustSummary::new(Box::new(Adapter::new(value)))
99}
100
101/// Recovers an owned `S` from a summary that crossed back from C++.
102///
103/// [`erase`] is the sole producer of erased summaries, and it is only ever
104/// called with `Adapter<S>` for the `S` of the calling sketch, so a mismatch
105/// here means an internal invariant broke, not user error.
106pub(crate) fn unerase<S: TupleSummary>(summary: &RustSummary) -> S {
107 match summary.ops().as_any().downcast_ref::<Adapter<S>>() {
108 Some(adapter) => adapter.value.clone(),
109 None => panic!(
110 "apache-datasketches internal invariant violated: a generic Tuple summary \
111 of a different concrete type was returned from the sketch. This should be \
112 impossible through the public API; please report it."
113 ),
114 }
115}