This is an automated email from the ASF dual-hosted git repository.

tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 9b695cb  feat: return Option from intersection to_sketch before first 
update (#211)
9b695cb is described below

commit 9b695cb701f5df233569ead1112ad650ae3885ad
Author: Vedant Madane <[email protected]>
AuthorDate: Tue Aug 25 07:03:48 2026 +0530

    feat: return Option from intersection to_sketch before first update (#211)
    
    Signed-off-by: Vedant Madane <[email protected]>
    Co-authored-by: tison <[email protected]>
---
 datasketches/src/thetafamily/theta/intersection.rs | 27 ++++++-------
 datasketches/src/thetafamily/tuple/intersection.rs | 31 ++++++++-------
 datasketches/tests/theta_test/intersection.rs      | 45 ++++++++++------------
 datasketches/tests/tuple_test/intersection.rs      | 21 +++++-----
 4 files changed, 60 insertions(+), 64 deletions(-)

diff --git a/datasketches/src/thetafamily/theta/intersection.rs 
b/datasketches/src/thetafamily/theta/intersection.rs
index 3f2f0f8..5186e18 100644
--- a/datasketches/src/thetafamily/theta/intersection.rs
+++ b/datasketches/src/thetafamily/theta/intersection.rs
@@ -25,8 +25,9 @@ use crate::thetacommon::intersection::IntersectionState;
 
 /// Stateful intersection operator for Theta sketches.
 ///
-/// Before the first [`update`](Self::update), the result is undefined; use
-/// [`has_result`](Self::has_result) to check.
+/// A newly created operator has no result. [`has_result`](Self::has_result) 
returns `false` and
+/// [`to_sketch`](Self::to_sketch) returns `None` until the first successful
+/// [`update`](Self::update).
 #[derive(Debug)]
 pub struct ThetaIntersection {
     state: IntersectionState<ThetaEntry, NoopIntersectionPolicy>,
@@ -63,7 +64,7 @@ impl ThetaIntersection {
         self.state.update(sketch)
     }
 
-    /// Returns whether this operator has received at least one update.
+    /// Returns `true` after the first successful [`update`](Self::update).
     pub fn has_result(&self) -> bool {
         self.state.has_result()
     }
@@ -73,18 +74,18 @@ impl ThetaIntersection {
         size_of::<Self>() + self.state.estimated_size()
     }
 
-    /// Returns the intersection result as a compact theta sketch.
+    /// Returns the current intersection as a compact theta sketch.
     ///
-    /// # Panics
+    /// Returns `None` until the first successful [`update`](Self::update). 
After that, returns
+    /// `Some` even when the intersection is empty.
     ///
-    /// Panics if called before the first [`update`](Self::update).
-    pub fn to_sketch(&self, ordered: bool) -> CompactThetaSketch {
-        assert!(
-            self.state.has_result(),
-            "ThetaIntersection::to_sketch() called before first update()"
-        );
+    /// If `ordered` is `true`, retained hashes are sorted in ascending order.
+    pub fn to_sketch(&self, ordered: bool) -> Option<CompactThetaSketch> {
+        if !self.state.has_result() {
+            return None;
+        }
         let parts = self.state.to_compact_parts(ordered);
-        CompactThetaSketch::from_parts(
+        Some(CompactThetaSketch::from_parts(
             parts
                 .entries
                 .into_iter()
@@ -94,6 +95,6 @@ impl ThetaIntersection {
             parts.seed_hash,
             parts.ordered,
             parts.empty,
-        )
+        ))
     }
 }
diff --git a/datasketches/src/thetafamily/tuple/intersection.rs 
b/datasketches/src/thetafamily/tuple/intersection.rs
index a93712c..c337f33 100644
--- a/datasketches/src/thetafamily/tuple/intersection.rs
+++ b/datasketches/src/thetafamily/tuple/intersection.rs
@@ -38,8 +38,9 @@ use crate::tuple::sketch::TupleSketchView;
 /// `P` is the [`SummaryCombinePolicy`] applied to keys present in more than 
one input. There is no
 /// default policy (see the module docs), so one must be supplied at 
construction.
 ///
-/// Before the first [`update`](Self::update), the result is undefined; use
-/// [`has_result`](Self::has_result) to check.
+/// A newly created operator has no result. [`has_result`](Self::has_result) 
returns `false` and
+/// [`to_sketch`](Self::to_sketch) returns `None` until the first successful
+/// [`update`](Self::update).
 ///
 /// # Examples
 ///
@@ -79,7 +80,7 @@ use crate::tuple::sketch::TupleSketchView;
 /// intersection.update(&a).unwrap();
 /// intersection.update(&b).unwrap();
 ///
-/// let result = intersection.to_sketch(true);
+/// let result = intersection.to_sketch(true).unwrap();
 /// assert_eq!(result.num_retained(), 1); // only "shared"
 /// assert_eq!(result.iter().next().unwrap().1, &7); // 3 + 4
 /// ```
@@ -128,7 +129,7 @@ where
         self.state.update(sketch)
     }
 
-    /// Returns whether this operator has received at least one update.
+    /// Returns `true` after the first successful [`update`](Self::update).
     pub fn has_result(&self) -> bool {
         self.state.has_result()
     }
@@ -138,28 +139,26 @@ where
         size_of::<Self>() + self.state.estimated_size()
     }
 
-    /// Returns the intersection result as a compact Tuple sketch.
+    /// Returns the current intersection as a compact Tuple sketch.
     ///
-    /// If `ordered` is `true`, retained entries are sorted ascending by hash.
+    /// Returns `None` until the first successful [`update`](Self::update). 
After that, returns
+    /// `Some` even when the intersection is empty.
     ///
-    /// # Panics
-    ///
-    /// Panics if called before the first [`update`](Self::update).
-    pub fn to_sketch(&self, ordered: bool) -> CompactTupleSketch<P::Summary>
+    /// If `ordered` is `true`, retained entries are sorted in ascending hash 
order.
+    pub fn to_sketch(&self, ordered: bool) -> 
Option<CompactTupleSketch<P::Summary>>
     where
         P::Summary: Clone,
     {
-        assert!(
-            self.state.has_result(),
-            "TupleIntersection::to_sketch() called before first update()"
-        );
+        if !self.state.has_result() {
+            return None;
+        }
         let parts = self.state.to_compact_parts(ordered);
-        CompactTupleSketch::from_parts(
+        Some(CompactTupleSketch::from_parts(
             parts.entries,
             parts.theta,
             parts.seed_hash,
             parts.ordered,
             parts.empty,
-        )
+        ))
     }
 }
diff --git a/datasketches/tests/theta_test/intersection.rs 
b/datasketches/tests/theta_test/intersection.rs
index 65f9d70..6e9a103 100644
--- a/datasketches/tests/theta_test/intersection.rs
+++ b/datasketches/tests/theta_test/intersection.rs
@@ -37,16 +37,13 @@ fn test_has_result_state_machine() {
     assert!(!i.has_result());
     i.update(&a).unwrap();
     assert!(i.has_result());
-    assert!(i.to_sketch(true).estimate() >= 1.0);
+    assert!(i.to_sketch(true).unwrap().estimate() >= 1.0);
 }
 
 #[test]
-fn test_result_before_update_panics() {
+fn test_result_before_first_update_returns_none() {
     let i = ThetaIntersection::with_seed(123);
-    let result = std::panic::catch_unwind(|| {
-        i.to_sketch(true);
-    });
-    assert!(result.is_err());
+    assert!(i.to_sketch(true).is_none());
 }
 
 #[test]
@@ -63,7 +60,7 @@ fn test_update_accepts_compact_sketch() {
     i.update(&a.compact(true)).unwrap();
     i.update(&b).unwrap();
 
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
     assert!(r.estimate() == 1.0);
     assert!(r.is_ordered());
 
@@ -74,7 +71,7 @@ fn test_update_accepts_compact_sketch() {
 
     i.update(&c.compact(false)).unwrap();
 
-    let r = i.to_sketch(false);
+    let r = i.to_sketch(false).unwrap();
     assert!(r.estimate() == 0.0);
     assert!(!r.is_ordered());
 }
@@ -86,7 +83,7 @@ fn test_seed_mismatch_behaviour_for_empty_sketch() {
 
     i.update(&empty_other_seed).unwrap();
     assert!(i.has_result());
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
     assert!(r.is_empty());
 }
 
@@ -110,7 +107,7 @@ fn test_terminal_empty_state_ignores_future_updates() {
     i.update(&empty).unwrap();
     i.update(&non_empty).unwrap();
 
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
     assert!(r.is_empty());
 }
 
@@ -123,7 +120,7 @@ fn test_to_sketch_unordered_is_not_ordered() {
     let mut i = ThetaIntersection::default();
     i.update(&a).unwrap();
 
-    let r = i.to_sketch(false);
+    let r = i.to_sketch(false).unwrap();
     assert!(!r.is_ordered());
 }
 
@@ -133,14 +130,14 @@ fn test_empty_update_twice() {
     let mut i = ThetaIntersection::default();
 
     i.update(&empty).unwrap();
-    let r1 = i.to_sketch(true);
+    let r1 = i.to_sketch(true).unwrap();
     assert_eq!(r1.num_retained(), 0);
     assert!(r1.is_empty());
     assert!(!r1.is_estimation_mode());
     assert_eq!(r1.estimate(), 0.0);
 
     i.update(&empty).unwrap();
-    let r2 = i.to_sketch(true);
+    let r2 = i.to_sketch(true).unwrap();
     assert_eq!(r2.num_retained(), 0);
     assert!(r2.is_empty());
     assert!(!r2.is_estimation_mode());
@@ -156,7 +153,7 @@ fn test_non_empty_no_retained_keys() {
 
     let mut i = ThetaIntersection::default();
     i.update(&s).unwrap();
-    let r1 = i.to_sketch(true);
+    let r1 = i.to_sketch(true).unwrap();
     assert_eq!(r1.num_retained(), 0);
     assert!(!r1.is_empty());
     assert!(r1.is_estimation_mode());
@@ -164,7 +161,7 @@ fn test_non_empty_no_retained_keys() {
     assert_eq!(r1.estimate(), 0.0);
 
     i.update(&s).unwrap();
-    let r2 = i.to_sketch(true);
+    let r2 = i.to_sketch(true).unwrap();
     assert_eq!(r2.num_retained(), 0);
     assert!(!r2.is_empty());
     assert!(r2.is_estimation_mode());
@@ -180,7 +177,7 @@ fn test_exact_half_overlap_unordered() {
     let mut i = ThetaIntersection::default();
     i.update(&s1).unwrap();
     i.update(&s2).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(!r.is_empty());
     assert!(!r.is_estimation_mode());
@@ -195,7 +192,7 @@ fn test_exact_half_overlap_ordered() {
     let mut i = ThetaIntersection::default();
     i.update(&s1.compact(true)).unwrap();
     i.update(&s2.compact(true)).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(!r.is_empty());
     assert!(!r.is_estimation_mode());
@@ -210,7 +207,7 @@ fn test_exact_disjoint_unordered() {
     let mut i = ThetaIntersection::default();
     i.update(&s1).unwrap();
     i.update(&s2).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(r.is_empty());
     assert!(!r.is_estimation_mode());
@@ -225,7 +222,7 @@ fn test_exact_disjoint_ordered() {
     let mut i = ThetaIntersection::default();
     i.update(&s1.compact(true)).unwrap();
     i.update(&s2.compact(true)).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(r.is_empty());
     assert!(!r.is_estimation_mode());
@@ -240,7 +237,7 @@ fn test_estimation_half_overlap_unordered() {
     let mut i = ThetaIntersection::default();
     i.update(&s1).unwrap();
     i.update(&s2).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(!r.is_empty());
     assert!(r.is_estimation_mode());
@@ -255,7 +252,7 @@ fn test_estimation_half_overlap_ordered() {
     let mut i = ThetaIntersection::default();
     i.update(&s1.compact(true)).unwrap();
     i.update(&s2.compact(true)).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(!r.is_empty());
     assert!(r.is_estimation_mode());
@@ -272,7 +269,7 @@ fn 
test_estimation_half_overlap_ordered_deserialized_compact() {
     let mut i = ThetaIntersection::default();
     i.update(&c1).unwrap();
     i.update(&c2).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(!r.is_empty());
     assert!(r.is_estimation_mode());
@@ -287,7 +284,7 @@ fn test_estimation_disjoint_unordered() {
     let mut i = ThetaIntersection::default();
     i.update(&s1).unwrap();
     i.update(&s2).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(!r.is_empty());
     assert!(r.is_estimation_mode());
@@ -302,7 +299,7 @@ fn test_estimation_disjoint_ordered() {
     let mut i = ThetaIntersection::default();
     i.update(&s1.compact(true)).unwrap();
     i.update(&s2.compact(true)).unwrap();
-    let r = i.to_sketch(true);
+    let r = i.to_sketch(true).unwrap();
 
     assert!(!r.is_empty());
     assert!(r.is_estimation_mode());
diff --git a/datasketches/tests/tuple_test/intersection.rs 
b/datasketches/tests/tuple_test/intersection.rs
index ea6e5af..9dd181f 100644
--- a/datasketches/tests/tuple_test/intersection.rs
+++ b/datasketches/tests/tuple_test/intersection.rs
@@ -49,14 +49,13 @@ fn has_result_tracks_the_first_update() {
     assert!(!intersection.has_result());
     intersection.update(&sketch).unwrap();
     assert!(intersection.has_result());
-    assert_eq!(intersection.to_sketch(true).num_retained(), 10);
+    assert_eq!(intersection.to_sketch(true).unwrap().num_retained(), 10);
 }
 
 #[test]
-#[should_panic(expected = "before first update")]
-fn result_before_first_update_panics() {
+fn result_before_first_update_returns_none() {
     let intersection = TupleIntersection::new(SumPolicy);
-    let _ = intersection.to_sketch(true);
+    assert!(intersection.to_sketch(true).is_none());
 }
 
 #[test]
@@ -71,7 +70,7 @@ fn overlap_combines_summaries() {
     let mut intersection = TupleIntersection::new(SumPolicy);
     intersection.update(&a).unwrap();
     intersection.update(&b).unwrap();
-    let result = intersection.to_sketch(true);
+    let result = intersection.to_sketch(true).unwrap();
 
     assert_eq!(result.num_retained(), 1);
     assert_eq!(result.iter().next().unwrap().1, &7);
@@ -86,7 +85,7 @@ fn accepts_mutable_and_compact_inputs() {
     intersection.update(&a).unwrap();
     intersection.update(&b.compact(true)).unwrap();
 
-    assert_eq!(intersection.to_sketch(true).num_retained(), 500);
+    assert_eq!(intersection.to_sketch(true).unwrap().num_retained(), 500);
 }
 
 #[test]
@@ -100,7 +99,7 @@ fn disjoint_result_is_terminally_empty() {
     intersection.update(&b).unwrap();
     intersection.update(&later).unwrap();
 
-    let result = intersection.to_sketch(true);
+    let result = intersection.to_sketch(true).unwrap();
     assert!(result.is_empty());
     assert_eq!(result.num_retained(), 0);
 }
@@ -124,7 +123,7 @@ fn 
logically_non_empty_input_without_retained_entries_is_preserved() {
 
     let mut intersection = TupleIntersection::new(SumPolicy);
     intersection.update(&sketch).unwrap();
-    let result = intersection.to_sketch(true);
+    let result = intersection.to_sketch(true).unwrap();
 
     assert!(!result.is_empty());
     assert_eq!(result.num_retained(), 0);
@@ -151,8 +150,8 @@ fn result_ordering_follows_the_request() {
     let mut intersection = TupleIntersection::new(SumPolicy);
     intersection.update(&input).unwrap();
 
-    assert!(intersection.to_sketch(true).is_ordered());
-    assert!(!intersection.to_sketch(false).is_ordered());
+    assert!(intersection.to_sketch(true).unwrap().is_ordered());
+    assert!(!intersection.to_sketch(false).unwrap().is_ordered());
 }
 
 #[test]
@@ -169,7 +168,7 @@ fn estimation_bounds_cover_the_true_intersection() {
     let mut intersection = TupleIntersection::new(SumPolicy);
     intersection.update(&a).unwrap();
     intersection.update(&b).unwrap();
-    let result = intersection.to_sketch(true);
+    let result = intersection.to_sketch(true).unwrap();
     let lower = result.lower_bound(NumStdDev::Three);
     let upper = result.upper_bound(NumStdDev::Three);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to