diff --git a/datasketches/src/thetafamily/theta/sketch.rs b/datasketches/src/thetafamily/theta/sketch.rs index 833ca7c..2c3bf56 100644 --- a/datasketches/src/thetafamily/theta/sketch.rs +++ b/datasketches/src/thetafamily/theta/sketch.rs @@ -814,7 +814,7 @@ impl CompactThetaSketch { theta: MAX_THETA, seed_hash, ordered: true, - empty: true, + empty: num_entries == 0, }) } V2_PREAMBLE_ESTIMATE => { diff --git a/datasketches/tests/serde_tests/theta.rs b/datasketches/tests/serde_tests/theta.rs index 1f0f09b..b09ec8f 100644 --- a/datasketches/tests/serde_tests/theta.rs +++ b/datasketches/tests/serde_tests/theta.rs @@ -18,6 +18,8 @@ use std::fs; use std::path::PathBuf; +use datasketches::codec::SketchBytes; +use datasketches::common::NumStdDev; use datasketches::error::ErrorKind; use datasketches::theta::CompactThetaSketch; use datasketches::theta::ThetaSketchBuilder; @@ -26,6 +28,24 @@ use googletest::prelude::near; use crate::serialization_test_data; +fn serialize_v2_exact(entries: &[u64]) -> Vec { + let current = ThetaSketchBuilder::default().build().compact(true); + let current_bytes = current.serialize(); + let mut bytes = SketchBytes::with_capacity((2 + entries.len()) * size_of::()); + bytes.write_u8(2); // preamble longs + bytes.write_u8(2); // serialization version + bytes.write_u8(current_bytes[2]); // theta family ID + bytes.write_u8(0); // unused + bytes.write_u16_le(0); // unused + bytes.write_u16_le(current.seed_hash()); + bytes.write_u32_le(entries.len() as u32); + bytes.write_u32_le(0); // unused + for &entry in entries { + bytes.write_u64_le(entry); + } + bytes.into_bytes() +} + fn test_sketch_file(path: PathBuf, expected_cardinality: usize, use_compressed_round_trip: bool) { let expected = expected_cardinality as f64; @@ -163,3 +183,38 @@ fn malformed_input_is_rejected() { let err = CompactThetaSketch::deserialize(&unsupported_version).unwrap_err(); assert_eq!(err.kind(), ErrorKind::InvalidData); } + +#[test] +fn test_v2_exact_non_empty_compatibility() { + let entries = [1, 7, 42]; + let sketch = CompactThetaSketch::deserialize(&serialize_v2_exact(&entries)).unwrap(); + + assert!(!sketch.is_empty()); + assert!(!sketch.is_estimation_mode()); + assert!(sketch.is_ordered()); + assert_eq!(sketch.num_retained(), entries.len()); + assert_eq!(sketch.estimate(), entries.len() as f64); + assert_eq!(sketch.lower_bound(NumStdDev::One), entries.len() as f64); + assert_eq!(sketch.upper_bound(NumStdDev::One), entries.len() as f64); + assert_eq!( + sketch.iter().map(|entry| entry.hash()).collect::>(), + entries + ); + + let restored = CompactThetaSketch::deserialize(&sketch.serialize()).unwrap(); + assert!(!restored.is_empty()); + assert_eq!(restored.num_retained(), entries.len()); + assert_eq!(restored.estimate(), entries.len() as f64); +} + +#[test] +fn test_v2_exact_zero_entries_remains_empty() { + let sketch = CompactThetaSketch::deserialize(&serialize_v2_exact(&[])).unwrap(); + + assert!(sketch.is_empty()); + assert!(!sketch.is_estimation_mode()); + assert_eq!(sketch.num_retained(), 0); + assert_eq!(sketch.estimate(), 0.0); + assert_eq!(sketch.lower_bound(NumStdDev::One), 0.0); + assert_eq!(sketch.upper_bound(NumStdDev::One), 0.0); +}