1use rand::rngs::SmallRng;
2use rand::{Rng, SeedableRng};
3use std::collections::{HashMap, HashSet};
4use std::fmt::Write as _;
5
6use crate::{ValueHintBindingAccumulator, profile_builder::infer_data_type_streaming};
7use dataprof_core::{SemanticHintBinding, SemanticHintKind, SemanticHints, char_len};
8use dataprof_metrics::analysis::inference::is_null_like_token;
9use dataprof_metrics::{
10 CardinalityEstimator, NumericAccumulator, RowCompletenessSummary, RowDuplicateSummary,
11 value_matches_hint,
12};
13
14#[derive(Debug, Clone)]
28pub struct StreamReservoirSampler {
29 reservoir: Vec<String>,
30 capacity: usize,
31 count: u64,
32 rng: SmallRng,
33}
34
35impl StreamReservoirSampler {
36 const DEFAULT_SEED: u64 = 0xDA7A_900D_F00D_5EED;
37
38 pub fn new(capacity: usize) -> Self {
39 let capacity = capacity.max(1);
40 Self {
41 reservoir: Vec::with_capacity(capacity.min(1024)),
42 capacity,
43 count: 0,
44 rng: SmallRng::seed_from_u64(Self::DEFAULT_SEED),
48 }
49 }
50
51 #[cfg(test)]
52 pub fn seed(capacity: usize, seed: u64) -> Self {
53 Self {
54 reservoir: Vec::with_capacity(capacity.min(1024)),
55 capacity,
56 count: 0,
57 rng: SmallRng::seed_from_u64(seed),
58 }
59 }
60
61 #[inline]
62 pub fn offer(&mut self, value: String) {
63 self.count += 1;
64 if self.reservoir.len() < self.capacity {
65 self.reservoir.push(value);
66 } else {
67 let index = self.rng.random_range(0..self.count as usize);
68 if index < self.capacity {
69 self.reservoir[index] = value;
70 }
71 }
72 }
73
74 pub fn shrink_to(&mut self, new_capacity: usize) {
82 let new_capacity = new_capacity.max(1);
83 let retained = self.reservoir.len();
84 if retained > new_capacity {
85 for slot in 0..new_capacity {
86 let pick = self.rng.random_range(slot..retained);
87 self.reservoir.swap(slot, pick);
88 }
89 self.reservoir.truncate(new_capacity);
90 }
91 self.capacity = new_capacity;
92 self.reservoir.shrink_to_fit();
93 }
94
95 pub fn samples(&self) -> &[String] {
96 &self.reservoir
97 }
98
99 pub fn memory_usage_bytes(&self) -> usize {
100 self.reservoir
101 .iter()
102 .map(|value| std::mem::size_of::<String>() + value.capacity())
103 .sum()
104 }
105
106 pub fn merge(&mut self, other: &StreamReservoirSampler) {
123 if other.count == 0 {
124 return;
125 }
126
127 let available_self = self.reservoir.len();
128 let available_other = other.reservoir.len();
129 let target = self.capacity.min(available_self + available_other);
130
131 let mut take_self = 0usize;
132 let mut take_other = 0usize;
133 let mut population_self = self.count;
134 let mut population_other = other.count;
135
136 for _ in 0..target {
137 let from_self = if take_self == available_self {
142 false
143 } else if take_other == available_other {
144 true
145 } else {
146 self.rng.random_range(0..population_self + population_other) < population_self
147 };
148
149 if from_self {
150 take_self += 1;
151 population_self -= 1;
152 } else {
153 take_other += 1;
154 population_other -= 1;
155 }
156 }
157
158 let mut merged: Vec<String> = std::mem::take(&mut self.reservoir);
161 for index in 0..take_self {
162 let swap_with = self.rng.random_range(index..available_self);
163 merged.swap(index, swap_with);
164 }
165 merged.truncate(take_self);
166
167 let mut indices: Vec<usize> = (0..available_other).collect();
168 for index in 0..take_other {
169 let swap_with = self.rng.random_range(index..available_other);
170 indices.swap(index, swap_with);
171 }
172 merged.extend(
173 indices[..take_other]
174 .iter()
175 .map(|&index| other.reservoir[index].clone()),
176 );
177
178 self.reservoir = merged;
179 self.count += other.count;
180 }
181}
182
183#[derive(Debug, Clone)]
184pub struct TextLengthStats {
185 pub min_length: usize,
186 pub max_length: usize,
187 pub avg_length: f64,
188 welford: NumericAccumulator,
189 histogram: [u64; 32],
190}
191
192impl TextLengthStats {
193 pub fn new() -> Self {
194 Self {
195 min_length: usize::MAX,
196 max_length: 0,
197 avg_length: 0.0,
198 welford: NumericAccumulator::new(),
199 histogram: [0u64; 32],
200 }
201 }
202
203 pub fn update(&mut self, length: usize) {
204 self.min_length = self.min_length.min(length);
205 self.max_length = self.max_length.max(length);
206 self.welford.update(length as f64);
207 self.avg_length = self.welford.mean();
208
209 let bucket = if length == 0 {
210 0
211 } else {
212 (usize::BITS - length.leading_zeros()).min(31) as usize
213 };
214 self.histogram[bucket] += 1;
215 }
216
217 pub fn merge(&mut self, other: &TextLengthStats) {
218 if other.welford.count() == 0 {
219 return;
220 }
221 if self.welford.count() == 0 {
222 *self = other.clone();
223 return;
224 }
225
226 self.min_length = self.min_length.min(other.min_length);
227 self.max_length = self.max_length.max(other.max_length);
228 self.welford.merge(&other.welford);
229 self.avg_length = self.welford.mean();
230
231 for (left, right) in self.histogram.iter_mut().zip(other.histogram.iter()) {
232 *left += *right;
233 }
234 }
235
236 pub fn empty() -> Self {
237 Self {
238 min_length: 0,
239 max_length: 0,
240 avg_length: 0.0,
241 welford: NumericAccumulator::new(),
242 histogram: [0u64; 32],
243 }
244 }
245}
246
247impl Default for TextLengthStats {
248 fn default() -> Self {
249 Self::new()
250 }
251}
252
253#[derive(Debug, Clone)]
254pub struct StreamingStatistics {
255 pub count: usize,
256 pub null_count: usize,
257 pub min: f64,
258 pub max: f64,
259 welford: NumericAccumulator,
260 distinct: CardinalityEstimator,
264 sampler: StreamReservoirSampler,
265 text_length_tracker: TextLengthStats,
266 date_match_count: usize,
267}
268
269impl StreamingStatistics {
270 pub fn new() -> Self {
271 Self {
272 count: 0,
273 null_count: 0,
274 min: f64::INFINITY,
275 max: f64::NEG_INFINITY,
276 welford: NumericAccumulator::new(),
277 distinct: CardinalityEstimator::new(),
278 sampler: StreamReservoirSampler::new(10_000),
279 text_length_tracker: TextLengthStats::new(),
280 date_match_count: 0,
281 }
282 }
283
284 pub fn with_sample_capacity(max_sample: usize) -> Self {
285 Self {
286 sampler: StreamReservoirSampler::new(max_sample),
287 ..Self::new()
288 }
289 }
290
291 pub fn update(&mut self, value: &str) {
292 self.count += 1;
293
294 if is_null_like_token(value) {
295 self.null_count += 1;
296 return;
297 }
298
299 self.distinct.insert(value);
300 self.sampler.offer(value.to_string());
301 self.text_length_tracker.update(char_len(value));
303 if value_matches_hint(value, SemanticHintKind::Temporal) {
304 self.date_match_count += 1;
305 }
306
307 if let Some(number) = value
308 .trim()
309 .parse::<f64>()
310 .ok()
311 .filter(|num| num.is_finite())
312 {
313 self.welford.update(number);
314 self.min = self.min.min(number);
315 self.max = self.max.max(number);
316 }
317 }
318
319 pub fn merge(&mut self, other: &StreamingStatistics) {
320 self.count += other.count;
321 self.null_count += other.null_count;
322
323 if other.min < self.min {
324 self.min = other.min;
325 }
326 if other.max > self.max {
327 self.max = other.max;
328 }
329
330 self.welford.merge(&other.welford);
331 self.distinct.merge(&other.distinct);
332 self.sampler.merge(&other.sampler);
333 self.text_length_tracker.merge(&other.text_length_tracker);
334 self.date_match_count += other.date_match_count;
335 }
336
337 pub fn mean(&self) -> f64 {
338 self.welford.mean()
339 }
340
341 pub fn variance(&self) -> f64 {
342 self.welford.population_variance()
343 }
344
345 pub fn std_dev(&self) -> f64 {
346 self.welford.population_std_dev()
347 }
348
349 pub fn unique_count(&self) -> usize {
356 self.distinct.estimate()
357 }
358
359 pub fn unique_count_is_approximate(&self) -> bool {
360 self.distinct.is_approximate()
361 }
362
363 pub fn unique_count_lower_bound(&self) -> usize {
365 self.distinct.distinct_lower_bound()
366 }
367
368 pub fn spill_distinct(&mut self) {
371 self.distinct.spill();
372 }
373
374 pub fn exact_distinct_bytes(&self) -> usize {
377 self.distinct.exact_bytes()
378 }
379
380 pub fn sample_values(&self) -> &[String] {
381 self.sampler.samples()
382 }
383
384 pub fn date_match_count(&self) -> usize {
386 self.date_match_count
387 }
388
389 pub fn exact_numeric_aggregates(
396 &self,
397 ) -> Option<crate::profile_builder::ExactNumericAggregates> {
398 let count = self.welford.count();
399 if count == 0 {
400 return None;
401 }
402 Some(crate::profile_builder::ExactNumericAggregates {
403 min: self.min,
404 max: self.max,
405 mean: self.welford.mean(),
406 std_dev: self.welford.sample_std_dev(),
407 variance: self.welford.sample_variance(),
408 count: count as usize,
409 })
410 }
411
412 pub fn text_length_stats(&self) -> TextLengthStats {
413 if self.text_length_tracker.welford.count() == 0 {
414 return TextLengthStats::empty();
415 }
416 self.text_length_tracker.clone()
417 }
418
419 pub fn reduce_sample_capacity(&mut self) {
420 self.sampler.shrink_to(self.sampler.capacity / 2);
421 }
422
423 pub fn memory_usage_bytes(&self) -> usize {
424 let struct_size = std::mem::size_of::<Self>();
425 let distinct_size = self.distinct.memory_usage_bytes();
426 let reservoir_size = self.sampler.memory_usage_bytes();
427
428 struct_size + distinct_size + reservoir_size
429 }
430}
431
432impl Default for StreamingStatistics {
433 fn default() -> Self {
434 Self::new()
435 }
436}
437
438#[derive(Debug, Default)]
453pub struct RowSignature {
454 buffer: String,
455 len_through_last_present: usize,
457}
458
459impl RowSignature {
460 pub fn push_field(&mut self, value: &str) {
470 let _ = write!(self.buffer, "{}:", value.len());
471 self.buffer.push_str(value);
472 if !value.is_empty() {
473 self.len_through_last_present = self.buffer.len();
474 }
475 }
476
477 pub fn finish(mut self) -> String {
479 self.buffer.truncate(self.len_through_last_present);
480 self.buffer
481 }
482}
483
484#[derive(Debug, Clone, Default)]
493pub struct RowUniquenessTracker {
494 rows_seen: usize,
495 distinct: CardinalityEstimator,
496 incomparable_signatures: bool,
506}
507
508impl RowUniquenessTracker {
509 pub fn observe(&mut self, signature: String) {
510 self.rows_seen += 1;
511 self.distinct.insert_owned(signature);
512 }
513
514 pub fn rows_seen(&self) -> usize {
515 self.rows_seen
516 }
517
518 pub fn duplicate_rows(&self) -> usize {
520 self.rows_seen.saturating_sub(self.distinct.estimate())
521 }
522
523 pub fn is_approximate(&self) -> bool {
524 self.distinct.is_approximate()
525 }
526
527 pub fn mark_incomparable(&mut self) {
530 self.incomparable_signatures = true;
531 }
532
533 pub fn merge(&mut self, other: &RowUniquenessTracker) {
534 self.rows_seen += other.rows_seen;
535 self.distinct.merge(&other.distinct);
536 self.incomparable_signatures |= other.incomparable_signatures;
537 }
538
539 pub fn memory_usage_bytes(&self) -> usize {
540 self.distinct.memory_usage_bytes()
541 }
542
543 pub fn exact_bytes(&self) -> usize {
545 self.distinct.exact_bytes()
546 }
547
548 pub fn spill(&mut self) {
551 self.distinct.spill();
552 }
553
554 pub fn summary(&self) -> Option<RowDuplicateSummary> {
558 if self.rows_seen == 0 || self.incomparable_signatures {
559 return None;
560 }
561 Some(RowDuplicateSummary {
562 duplicate_rows: self.duplicate_rows(),
563 rows_checked: self.rows_seen,
564 approximate: self.is_approximate(),
565 max_duplicate_rows: self
566 .rows_seen
567 .saturating_sub(self.distinct.distinct_lower_bound()),
568 })
569 }
570}
571
572#[derive(Debug, Clone, Default)]
579pub struct RowCompletenessTracker {
580 rows_seen: usize,
581 complete_rows: usize,
582}
583
584impl RowCompletenessTracker {
585 pub fn observe(&mut self, had_null: bool) {
587 self.rows_seen += 1;
588 if !had_null {
589 self.complete_rows += 1;
590 }
591 }
592
593 pub fn invalidate_completed_rows(&mut self) {
596 self.complete_rows = 0;
597 }
598
599 pub fn observe_aligned_columns(&mut self, columns: &[&[String]], total_rows: usize) {
606 if columns.is_empty() {
607 return;
608 }
609 for row_index in 0..total_rows {
610 let had_null = columns.iter().any(|cells| {
611 cells
612 .get(row_index)
613 .is_none_or(|value| is_null_like_token(value))
614 });
615 self.observe(had_null);
616 }
617 }
618
619 pub fn merge(&mut self, other: &RowCompletenessTracker) {
620 self.rows_seen += other.rows_seen;
621 self.complete_rows += other.complete_rows;
622 }
623
624 pub fn summary(&self) -> Option<RowCompletenessSummary> {
626 if self.rows_seen == 0 {
627 return None;
628 }
629 Some(RowCompletenessSummary {
630 complete_rows: self.complete_rows,
631 rows_checked: self.rows_seen,
632 })
633 }
634}
635
636pub struct StreamingColumnCollection {
637 columns: HashMap<String, StreamingStatistics>,
638 ordered_names: Vec<String>,
639 memory_limit_bytes: usize,
640 row_tracker: RowUniquenessTracker,
641 completeness_tracker: RowCompletenessTracker,
642 hint_bindings: ValueHintBindingAccumulator,
643}
644
645impl StreamingColumnCollection {
646 pub fn new() -> Self {
647 Self {
648 columns: HashMap::new(),
649 ordered_names: Vec::new(),
650 memory_limit_bytes: 100 * 1024 * 1024,
651 row_tracker: RowUniquenessTracker::default(),
652 completeness_tracker: RowCompletenessTracker::default(),
653 hint_bindings: ValueHintBindingAccumulator::default(),
654 }
655 }
656
657 pub fn memory_limit(limit_mb: usize) -> Self {
658 Self {
659 columns: HashMap::new(),
660 ordered_names: Vec::new(),
661 memory_limit_bytes: limit_mb * 1024 * 1024,
662 row_tracker: RowUniquenessTracker::default(),
663 completeness_tracker: RowCompletenessTracker::default(),
664 hint_bindings: ValueHintBindingAccumulator::default(),
665 }
666 }
667
668 pub fn with_semantic_hints(mut self, hints: &SemanticHints) -> Self {
670 self.hint_bindings = ValueHintBindingAccumulator::new(hints);
671 self
672 }
673
674 pub fn init_columns(&mut self, headers: &[String]) {
675 for header in headers {
676 if !self.columns.contains_key(header) {
677 self.columns
678 .insert(header.clone(), StreamingStatistics::default());
679 self.ordered_names.push(header.clone());
680 }
681 }
682 }
683
684 pub fn init_column_with_missing(&mut self, header: &str, prior_rows: usize) {
691 if self.columns.contains_key(header) {
692 return;
693 }
694
695 let stats = StreamingStatistics {
696 count: prior_rows,
697 null_count: prior_rows,
698 ..Default::default()
699 };
700 self.columns.insert(header.to_string(), stats);
701 self.ordered_names.push(header.to_string());
702 if prior_rows > 0 {
703 self.completeness_tracker.invalidate_completed_rows();
704 }
705 }
706
707 pub fn process_record<I>(&mut self, headers: &[String], values: I)
708 where
709 I: IntoIterator<Item = String>,
710 {
711 let mut row_signature = RowSignature::default();
712 let mut row_has_null = false;
713 let mut values = values.into_iter();
714
715 for header in headers {
719 let value = values.next().unwrap_or_default();
720 row_signature.push_field(&value);
721
722 if !self.columns.contains_key(header) {
723 self.ordered_names.push(header.clone());
724 }
725 let stats = self.columns.entry(header.to_string()).or_default();
726 stats.update(&value);
727 row_has_null |= is_null_like_token(&value);
730 self.hint_bindings.observe(header, &value);
731 }
732
733 if !headers.is_empty() {
734 self.row_tracker.observe(row_signature.finish());
735 self.completeness_tracker.observe(row_has_null);
736 }
737 }
738
739 pub fn row_duplicate_summary(&self) -> Option<RowDuplicateSummary> {
741 self.row_tracker.summary()
742 }
743
744 pub fn row_completeness_summary(&self) -> Option<RowCompletenessSummary> {
746 self.completeness_tracker.summary()
747 }
748
749 pub fn semantic_hint_bindings(&self) -> Vec<SemanticHintBinding> {
751 self.hint_bindings
752 .bindings(self.ordered_names.iter().map(String::as_str))
753 }
754
755 pub fn get_column_stats(&self, column_name: &str) -> Option<&StreamingStatistics> {
756 self.columns.get(column_name)
757 }
758
759 pub fn column_names(&self) -> Vec<String> {
760 self.ordered_names.clone()
761 }
762
763 pub fn retain_columns(&mut self, names: &[String]) {
770 let selected = names.iter().map(String::as_str).collect::<HashSet<_>>();
771 self.columns
772 .retain(|name, _| selected.contains(name.as_str()));
773 self.ordered_names
774 .retain(|name| selected.contains(name.as_str()));
775 }
776
777 pub fn memory_usage_bytes(&self) -> usize {
778 self.columns
779 .values()
780 .map(|stats| stats.memory_usage_bytes())
781 .sum::<usize>()
782 + self.row_tracker.memory_usage_bytes()
783 }
784
785 pub fn is_memory_pressure(&self) -> bool {
786 self.memory_usage_bytes() > (self.memory_limit_bytes * 80 / 100)
787 }
788
789 pub fn reduce_memory_usage(&mut self) {
790 let limit = self.memory_limit_bytes * 80 / 100;
805 let mut usage = self.memory_usage_bytes();
806 if usage <= limit {
807 return;
808 }
809 usage -= self.spill_distinct_sets(usage - limit);
810 if usage > limit {
811 usage -= self.row_tracker.exact_bytes();
812 self.row_tracker.spill();
813 }
814 if usage > limit {
815 for stats in self.columns.values_mut() {
816 stats.reduce_sample_capacity();
817 }
818 }
819 }
820
821 fn spill_distinct_sets(&mut self, excess: usize) -> usize {
824 let mut by_size: Vec<(usize, usize)> = self
825 .ordered_names
826 .iter()
827 .enumerate()
828 .filter_map(|(position, name)| {
829 let bytes = self.columns.get(name)?.exact_distinct_bytes();
830 (bytes > 0).then_some((position, bytes))
831 })
832 .collect();
833 by_size.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
834 let mut freed = 0;
835 for (position, bytes) in by_size {
836 if freed >= excess {
837 break;
838 }
839 let name = &self.ordered_names[position];
840 if let Some(stats) = self.columns.get_mut(name) {
841 stats.spill_distinct();
842 freed += bytes;
843 }
844 }
845 freed
846 }
847
848 pub fn column_type_fingerprint(&self) -> u64 {
853 use std::collections::hash_map::DefaultHasher;
854 use std::hash::{Hash, Hasher};
855
856 let mut hasher = DefaultHasher::new();
857 let mut names: Vec<&String> = self.columns.keys().collect();
858 names.sort();
859 for name in names {
860 let stats = &self.columns[name];
861 let data_type = infer_data_type_streaming(stats);
862 name.hash(&mut hasher);
863 std::mem::discriminant(&data_type).hash(&mut hasher);
864 }
865 hasher.finish()
866 }
867
868 pub fn merge(&mut self, other: StreamingColumnCollection) {
893 let self_rows = self.row_tracker.rows_seen();
894 let other_rows = other.row_tracker.rows_seen();
895
896 let StreamingColumnCollection {
897 columns,
898 ordered_names,
899 memory_limit_bytes: _,
900 row_tracker,
901 mut completeness_tracker,
902 hint_bindings,
903 } = other;
904
905 let schemas_match = self.ordered_names == ordered_names;
909
910 let mut other_lacks_columns = false;
913 for (column_name, stats) in self.columns.iter_mut() {
914 if columns.contains_key(column_name) {
915 continue;
916 }
917 other_lacks_columns = true;
918 stats.count += other_rows;
919 stats.null_count += other_rows;
920 }
921
922 let mut self_lacks_columns = false;
926 for column_name in &ordered_names {
927 if !self.columns.contains_key(column_name) {
928 self.ordered_names.push(column_name.clone());
929 self_lacks_columns = true;
930 }
931 }
932
933 for (column_name, other_stats) in columns {
934 match self.columns.get_mut(&column_name) {
935 Some(existing_stats) => existing_stats.merge(&other_stats),
936 None => {
937 let mut stats = other_stats;
938 stats.count += self_rows;
940 stats.null_count += self_rows;
941 self.columns.insert(column_name, stats);
942 }
943 }
944 }
945
946 if self_lacks_columns && self_rows > 0 {
947 self.completeness_tracker.invalidate_completed_rows();
948 }
949 if other_lacks_columns && other_rows > 0 {
950 completeness_tracker.invalidate_completed_rows();
951 }
952
953 self.completeness_tracker.merge(&completeness_tracker);
954 self.row_tracker.merge(&row_tracker);
955 if self_rows > 0 && other_rows > 0 && !schemas_match {
958 self.row_tracker.mark_incomparable();
959 }
960 self.hint_bindings.merge(&hint_bindings);
961 }
962}
963
964impl Default for StreamingColumnCollection {
965 fn default() -> Self {
966 Self::new()
967 }
968}
969
970#[cfg(test)]
971mod row_tracker_tests {
972 use super::*;
973
974 fn record(collection: &mut StreamingColumnCollection, headers: &[String], values: &[&str]) {
975 collection.process_record(headers, values.iter().map(|v| v.to_string()));
976 }
977
978 fn completeness(collection: &StreamingColumnCollection) -> (usize, usize) {
979 let summary = collection
980 .row_completeness_summary()
981 .expect("rows were observed");
982 (summary.complete_rows, summary.rows_checked)
983 }
984
985 #[test]
986 fn test_complete_rows_count_rows_not_null_cells() {
987 let headers = vec!["a".to_string(), "b".to_string()];
988 let mut collection = StreamingColumnCollection::new();
989 record(&mut collection, &headers, &["", ""]);
993 record(&mut collection, &headers, &["x", "1"]);
994 record(&mut collection, &headers, &["y", "2"]);
995 record(&mut collection, &headers, &["z", "3"]);
996
997 assert_eq!(completeness(&collection), (3, 4));
998 }
999
1000 #[test]
1001 fn test_null_like_tokens_make_a_row_incomplete() {
1002 let headers = vec!["a".to_string(), "b".to_string()];
1003 let mut collection = StreamingColumnCollection::new();
1004 record(&mut collection, &headers, &["x", "NULL"]);
1007 record(&mut collection, &headers, &["y", "NaN"]);
1008 record(&mut collection, &headers, &["z", " "]);
1009 record(&mut collection, &headers, &["w", "3"]);
1010
1011 assert_eq!(completeness(&collection), (1, 4));
1012 }
1013
1014 #[test]
1015 fn test_ragged_rows_are_incomplete() {
1016 let headers = vec!["a".to_string(), "b".to_string()];
1017 let mut collection = StreamingColumnCollection::new();
1018 record(&mut collection, &headers, &["x"]);
1019 record(&mut collection, &headers, &["y", "1"]);
1020
1021 assert_eq!(completeness(&collection), (1, 2));
1022 }
1023
1024 #[test]
1025 fn test_a_late_column_makes_earlier_rows_incomplete() {
1026 let mut collection = StreamingColumnCollection::new();
1027 let first = vec!["a".to_string()];
1028 record(&mut collection, &first, &["x"]);
1029 record(&mut collection, &first, &["y"]);
1030 assert_eq!(completeness(&collection), (2, 2));
1031
1032 collection.init_column_with_missing("b", 2);
1035 let both = vec!["a".to_string(), "b".to_string()];
1036 record(&mut collection, &both, &["z", "1"]);
1037
1038 assert_eq!(completeness(&collection), (1, 3));
1039 }
1040
1041 #[test]
1042 fn test_no_rows_means_no_completeness_summary() {
1043 let collection = StreamingColumnCollection::new();
1044 assert!(collection.row_completeness_summary().is_none());
1045 }
1046
1047 #[test]
1048 fn test_aligned_columns_count_the_same_complete_rows() {
1049 let a: Vec<String> = ["", "x", "y"].iter().map(|v| v.to_string()).collect();
1050 let b: Vec<String> = ["", "1", "2"].iter().map(|v| v.to_string()).collect();
1051 let mut tracker = RowCompletenessTracker::default();
1052 tracker.observe_aligned_columns(&[a.as_slice(), b.as_slice()], 3);
1053
1054 let summary = tracker.summary().expect("rows were observed");
1055 assert_eq!((summary.complete_rows, summary.rows_checked), (2, 3));
1056 }
1057
1058 #[test]
1059 fn test_aligned_columns_treat_a_short_column_as_missing() {
1060 let a: Vec<String> = ["x", "y"].iter().map(|v| v.to_string()).collect();
1061 let b: Vec<String> = ["1"].iter().map(|v| v.to_string()).collect();
1062 let mut tracker = RowCompletenessTracker::default();
1063 tracker.observe_aligned_columns(&[a.as_slice(), b.as_slice()], 2);
1064
1065 let summary = tracker.summary().expect("rows were observed");
1066 assert_eq!((summary.complete_rows, summary.rows_checked), (1, 2));
1067 }
1068
1069 #[test]
1070 fn test_exact_duplicates_including_null_rows() {
1071 let headers = vec!["a".to_string(), "b".to_string()];
1072 let mut collection = StreamingColumnCollection::new();
1073 record(&mut collection, &headers, &["x", ""]);
1074 record(&mut collection, &headers, &["x", ""]);
1075 record(&mut collection, &headers, &["x", "1"]);
1076 record(&mut collection, &headers, &["", ""]);
1077
1078 let summary = collection
1079 .row_duplicate_summary()
1080 .expect("rows were observed");
1081 assert_eq!(summary.rows_checked, 4);
1082 assert_eq!(summary.duplicate_rows, 1);
1085 assert!(!summary.approximate);
1086 }
1087
1088 #[test]
1089 fn test_field_boundaries_are_unambiguous() {
1090 let headers = vec!["a".to_string(), "b".to_string()];
1091 let mut collection = StreamingColumnCollection::new();
1092 record(&mut collection, &headers, &["ab", "c"]);
1093 record(&mut collection, &headers, &["a", "bc"]);
1094
1095 let summary = collection
1096 .row_duplicate_summary()
1097 .expect("rows were observed");
1098 assert_eq!(
1099 summary.duplicate_rows, 0,
1100 "different field splits must not collide"
1101 );
1102 }
1103
1104 #[test]
1105 fn test_ragged_rows_normalize_missing_trailing_fields() {
1106 let headers = vec!["a".to_string(), "b".to_string()];
1107 let mut collection = StreamingColumnCollection::new();
1108 record(&mut collection, &headers, &["x"]);
1109 record(&mut collection, &headers, &["x", ""]);
1110
1111 let summary = collection
1112 .row_duplicate_summary()
1113 .expect("rows were observed");
1114 assert_eq!(summary.rows_checked, 2);
1115 assert_eq!(summary.duplicate_rows, 1);
1116 assert_eq!(
1117 collection
1118 .get_column_stats("b")
1119 .expect("column b")
1120 .null_count,
1121 2
1122 );
1123 }
1124
1125 #[test]
1126 fn test_trailing_null_like_token_is_not_trimmed() {
1127 let headers = vec!["a".to_string(), "b".to_string()];
1132 let mut collection = StreamingColumnCollection::new();
1133 record(&mut collection, &headers, &["x", "null"]);
1134 record(&mut collection, &headers, &["x", " "]);
1135 record(&mut collection, &headers, &["x", ""]);
1136
1137 let summary = collection
1138 .row_duplicate_summary()
1139 .expect("rows were observed");
1140 assert_eq!(summary.duplicate_rows, 0);
1141 }
1142
1143 #[test]
1144 fn test_late_column_does_not_split_identical_rows() {
1145 let mut collection = StreamingColumnCollection::new();
1149 let short = vec!["a".to_string()];
1150 let wide = vec!["a".to_string(), "b".to_string()];
1151
1152 record(&mut collection, &short, &["x"]);
1153 collection.init_column_with_missing("b", 1);
1154 record(&mut collection, &wide, &["x", "y"]);
1155 record(&mut collection, &wide, &["x", ""]);
1156
1157 let summary = collection
1158 .row_duplicate_summary()
1159 .expect("rows were observed");
1160 assert_eq!(summary.rows_checked, 3);
1161 assert_eq!(
1162 summary.duplicate_rows, 1,
1163 "rows 1 and 3 hold the same data; they differ only in how many \
1164 columns had been discovered when each was read"
1165 );
1166 }
1167
1168 #[test]
1169 fn test_late_column_in_the_middle_does_not_split_identical_rows() {
1170 let mut collection = StreamingColumnCollection::new();
1173 let one = vec!["a".to_string()];
1174 let two = vec!["a".to_string(), "b".to_string()];
1175 let three = vec!["a".to_string(), "b".to_string(), "c".to_string()];
1176
1177 record(&mut collection, &one, &["1"]);
1178 collection.init_column_with_missing("b", 1);
1179 record(&mut collection, &two, &["1", "2"]);
1180 collection.init_column_with_missing("c", 2);
1181 record(&mut collection, &three, &["1", "", "3"]);
1182 record(&mut collection, &three, &["1", "", ""]);
1183
1184 let summary = collection
1185 .row_duplicate_summary()
1186 .expect("rows were observed");
1187 assert_eq!(summary.rows_checked, 4);
1188 assert_eq!(summary.duplicate_rows, 1, "row 4 repeats row 1");
1189 }
1190
1191 #[test]
1192 fn test_no_rows_means_no_summary() {
1193 let collection = StreamingColumnCollection::new();
1194 assert!(collection.row_duplicate_summary().is_none());
1195 }
1196
1197 #[test]
1198 fn a_column_longer_than_the_reservoir_keeps_an_exact_count() {
1199 let mut constant = StreamingStatistics::new();
1203 let mut ids = StreamingStatistics::new();
1204 for id in 0..50_000 {
1205 constant.update("v1");
1206 ids.update(&id.to_string());
1207 }
1208 assert!(!constant.unique_count_is_approximate());
1209 assert_eq!(constant.unique_count(), 1);
1210 assert!(!ids.unique_count_is_approximate());
1211 assert_eq!(ids.unique_count(), 50_000);
1212 }
1213
1214 #[test]
1215 fn memory_pressure_spills_the_largest_exact_set_first() {
1216 let headers = vec!["id".to_string(), "flag".to_string()];
1217 let mut collection = StreamingColumnCollection::memory_limit(8);
1221 for id in 0..300_000 {
1222 let flag = if id % 2 == 0 { "yes" } else { "no" };
1223 record(&mut collection, &headers, &[&id.to_string(), flag]);
1224 }
1225 assert!(
1226 collection.is_memory_pressure(),
1227 "300,000 exact fingerprints no longer exceed the limit; this test reaches nothing"
1228 );
1229 collection.reduce_memory_usage();
1230
1231 let id = collection.get_column_stats("id").unwrap();
1232 let flag = collection.get_column_stats("flag").unwrap();
1233 assert!(id.unique_count_is_approximate(), "the large set spills");
1234 assert!(!flag.unique_count_is_approximate(), "the small one is kept");
1235 assert_eq!(flag.unique_count(), 2);
1236 assert_eq!(id.sample_values().len(), 10_000);
1239 assert_eq!(flag.sample_values().len(), 10_000);
1240 }
1241
1242 #[test]
1243 fn memory_pressure_spills_the_row_tracker_before_shrinking_reservoirs() {
1244 let headers = vec!["id".to_string(), "flag".to_string(), "blank".to_string()];
1245 let mut collection = StreamingColumnCollection::memory_limit(5);
1248 for id in 0..300_000 {
1249 let flag = if id % 2 == 0 { "yes" } else { "no" };
1250 record(&mut collection, &headers, &[&id.to_string(), flag, ""]);
1251 }
1252 collection.reduce_memory_usage();
1253
1254 let rows = collection.row_duplicate_summary().unwrap();
1255 assert!(rows.approximate, "the row tracker spills");
1256 assert!(!collection.is_memory_pressure());
1257 let blank = collection.get_column_stats("blank").unwrap();
1260 assert!(!blank.unique_count_is_approximate());
1261 assert_eq!(blank.unique_count(), 0);
1262 for name in ["id", "flag"] {
1263 let stats = collection.get_column_stats(name).unwrap();
1264 assert!(stats.unique_count_is_approximate(), "{name} spills first");
1265 assert_eq!(stats.sample_values().len(), 10_000, "{name} reservoir kept");
1266 }
1267 }
1268
1269 #[test]
1270 fn test_spills_to_approximate_beyond_distinct_threshold() {
1271 let headers = vec!["n".to_string()];
1272 let mut collection = StreamingColumnCollection::new();
1273 let distinct = dataprof_metrics::EXACT_CARDINALITY_THRESHOLD + 500;
1274 for i in 0..distinct {
1275 record(&mut collection, &headers, &[&i.to_string()]);
1276 }
1277 for i in 0..distinct {
1279 record(&mut collection, &headers, &[&i.to_string()]);
1280 }
1281
1282 let summary = collection
1283 .row_duplicate_summary()
1284 .expect("rows were observed");
1285 assert!(summary.approximate, "past the threshold the count is HLL");
1286 assert_eq!(summary.rows_checked, distinct * 2);
1287 let error = (summary.duplicate_rows as f64 - distinct as f64).abs() / distinct as f64;
1288 assert!(
1289 error < 0.05,
1290 "estimated {} duplicates for {distinct} true, off by {error:.4}",
1291 summary.duplicate_rows
1292 );
1293 }
1294
1295 #[test]
1296 fn test_merge_combines_row_trackers() {
1297 let headers = vec!["a".to_string()];
1298 let mut left = StreamingColumnCollection::new();
1299 let mut right = StreamingColumnCollection::new();
1300 record(&mut left, &headers, &["x"]);
1301 record(&mut left, &headers, &["y"]);
1302 record(&mut right, &headers, &["x"]);
1303
1304 left.merge(right);
1305 let summary = left.row_duplicate_summary().expect("rows were observed");
1306 assert_eq!(summary.rows_checked, 3);
1307 assert_eq!(summary.duplicate_rows, 1);
1308 }
1309
1310 #[test]
1311 fn test_merge_keeps_columns_only_the_other_side_saw() {
1312 let mut left = StreamingColumnCollection::new();
1313 let mut right = StreamingColumnCollection::new();
1314 record(&mut left, &["a".to_string()], &["1"]);
1315 record(&mut right, &["b".to_string()], &["2"]);
1316
1317 left.merge(right);
1318
1319 assert_eq!(left.column_names(), vec!["a".to_string(), "b".to_string()]);
1322 assert!(left.get_column_stats("b").is_some());
1323 }
1324
1325 #[test]
1326 fn test_merge_pads_columns_the_other_side_never_saw() {
1327 let left_headers = vec!["a".to_string()];
1328 let right_headers = vec!["a".to_string(), "b".to_string()];
1329 let mut left = StreamingColumnCollection::new();
1330 let mut right = StreamingColumnCollection::new();
1331 record(&mut left, &left_headers, &["1"]);
1332 record(&mut left, &left_headers, &["2"]);
1333 record(&mut right, &right_headers, &["3", "x"]);
1334
1335 left.merge(right);
1336
1337 let b = left.get_column_stats("b").expect("column b survived");
1338 assert_eq!(b.count, 3);
1340 assert_eq!(b.null_count, 2);
1341
1342 let a = left.get_column_stats("a").expect("column a");
1343 assert_eq!(a.count, 3);
1344 assert_eq!(a.null_count, 0);
1345 }
1346
1347 #[test]
1348 fn test_merge_combines_completeness() {
1349 let headers = vec!["a".to_string(), "b".to_string()];
1350 let mut left = StreamingColumnCollection::new();
1351 let mut right = StreamingColumnCollection::new();
1352 record(&mut left, &headers, &["1", "2"]);
1353 record(&mut left, &headers, &["3", ""]);
1354 record(&mut right, &headers, &["4", "5"]);
1355
1356 left.merge(right);
1357
1358 assert_eq!(completeness(&left), (2, 3));
1359 }
1360
1361 #[test]
1362 fn test_merge_invalidates_completeness_for_the_narrower_side() {
1363 let left_headers = vec!["a".to_string()];
1364 let right_headers = vec!["a".to_string(), "b".to_string()];
1365 let mut left = StreamingColumnCollection::new();
1366 let mut right = StreamingColumnCollection::new();
1367 record(&mut left, &left_headers, &["1"]);
1368 record(&mut right, &right_headers, &["2", "y"]);
1369
1370 left.merge(right);
1371
1372 assert_eq!(completeness(&left), (1, 2));
1374 }
1375
1376 #[test]
1377 fn test_merge_withholds_duplicates_when_schemas_differ() {
1378 let mut left = StreamingColumnCollection::new();
1379 let mut right = StreamingColumnCollection::new();
1380 record(&mut left, &["a".to_string()], &["x"]);
1381 record(&mut right, &["b".to_string()], &["x"]);
1382
1383 left.merge(right);
1384
1385 assert!(left.row_duplicate_summary().is_none());
1388 }
1389
1390 #[test]
1391 fn test_merge_withholds_duplicates_when_column_order_differs() {
1392 let mut left = StreamingColumnCollection::new();
1393 let mut right = StreamingColumnCollection::new();
1394 record(&mut left, &["a".to_string(), "b".to_string()], &["x", "y"]);
1395 record(&mut right, &["b".to_string(), "a".to_string()], &["x", "y"]);
1396
1397 left.merge(right);
1398
1399 assert!(left.row_duplicate_summary().is_none());
1402 }
1403
1404 #[test]
1405 fn test_merge_keeps_duplicates_when_schemas_match() {
1406 let headers = vec!["a".to_string(), "b".to_string()];
1407 let mut left = StreamingColumnCollection::new();
1408 let mut right = StreamingColumnCollection::new();
1409 record(&mut left, &headers, &["x", "y"]);
1410 record(&mut right, &headers, &["x", "y"]);
1411
1412 left.merge(right);
1413
1414 let summary = left
1415 .row_duplicate_summary()
1416 .expect("one schema on both sides");
1417 assert_eq!(summary.rows_checked, 2);
1418 assert_eq!(summary.duplicate_rows, 1);
1419 }
1420
1421 #[test]
1422 fn test_merge_into_an_empty_accumulator_keeps_duplicates() {
1423 let headers = vec!["a".to_string()];
1424 let mut accumulator = StreamingColumnCollection::new();
1425 let mut partition = StreamingColumnCollection::new();
1426 record(&mut partition, &headers, &["x"]);
1427 record(&mut partition, &headers, &["x"]);
1428
1429 accumulator.merge(partition);
1432
1433 let summary = accumulator
1434 .row_duplicate_summary()
1435 .expect("the empty side contributed no signatures");
1436 assert_eq!(summary.rows_checked, 2);
1437 assert_eq!(summary.duplicate_rows, 1);
1438 }
1439
1440 #[test]
1445 fn test_merged_partitions_match_a_single_pass() {
1446 let headers = vec!["id".to_string(), "city".to_string(), "score".to_string()];
1447 let rows: Vec<[String; 3]> = (0..300)
1448 .map(|index| {
1449 [
1450 format!("{}", index % 10),
1451 ["Rome", "Milan", "Turin", ""][index % 4].to_string(),
1452 format!("{}.5", index % 5),
1453 ]
1454 })
1455 .collect();
1456
1457 let mut single = StreamingColumnCollection::new();
1458 for row in &rows {
1459 single.process_record(&headers, row.iter().cloned());
1460 }
1461
1462 let mut partitions: Vec<StreamingColumnCollection> =
1464 (0..7).map(|_| StreamingColumnCollection::new()).collect();
1465 for (index, row) in rows.iter().enumerate() {
1466 partitions[index % 7].process_record(&headers, row.iter().cloned());
1467 }
1468 let mut merged = partitions.remove(0);
1469 for partition in partitions {
1470 merged.merge(partition);
1471 }
1472
1473 assert_eq!(merged.column_names(), single.column_names());
1474 for name in single.column_names() {
1475 let expected = single.get_column_stats(&name).expect("single-pass column");
1476 let actual = merged.get_column_stats(&name).expect("merged column");
1477 assert_eq!(actual.count, expected.count, "count for {name}");
1478 assert_eq!(actual.null_count, expected.null_count, "nulls for {name}");
1479 assert_eq!(
1480 actual.unique_count(),
1481 expected.unique_count(),
1482 "distinct for {name}"
1483 );
1484 assert_eq!(actual.min, expected.min, "min for {name}");
1485 assert_eq!(actual.max, expected.max, "max for {name}");
1486 assert!(
1487 (actual.mean() - expected.mean()).abs() < 1e-9,
1488 "mean for {name}"
1489 );
1490 assert!(
1491 (actual.variance() - expected.variance()).abs() < 1e-9,
1492 "variance for {name}"
1493 );
1494 }
1495
1496 let merged_rows = merged.row_completeness_summary().expect("rows observed");
1497 let single_rows = single.row_completeness_summary().expect("rows observed");
1498 assert_eq!(merged_rows.rows_checked, single_rows.rows_checked);
1499 assert_eq!(merged_rows.complete_rows, single_rows.complete_rows);
1500
1501 let merged_dupes = merged.row_duplicate_summary().expect("rows observed");
1502 let single_dupes = single.row_duplicate_summary().expect("rows observed");
1503 assert_eq!(merged_dupes.rows_checked, single_dupes.rows_checked);
1504 assert_eq!(merged_dupes.duplicate_rows, single_dupes.duplicate_rows);
1505 }
1506}
1507
1508#[cfg(test)]
1509mod tests {
1510 use super::*;
1511 use dataprof_metrics::HyperLogLog;
1512
1513 #[test]
1514 fn test_streaming_statistics() {
1515 let mut stats = StreamingStatistics::new();
1516
1517 stats.update("10.5");
1518 stats.update("20.0");
1519 stats.update("15.5");
1520 stats.update("");
1521
1522 assert_eq!(stats.count, 4);
1523 assert_eq!(stats.null_count, 1);
1524 assert_eq!(stats.unique_count(), 3);
1525 assert!(!stats.unique_count_is_approximate());
1526 assert!((stats.mean() - 15.333333333333334).abs() < 1e-10);
1527 assert_eq!(stats.min, 10.5);
1528 assert_eq!(stats.max, 20.0);
1529 }
1530
1531 #[test]
1532 fn test_streaming_statistics_merge() {
1533 let mut stats1 = StreamingStatistics::new();
1534 stats1.update("10");
1535 stats1.update("20");
1536
1537 let mut stats2 = StreamingStatistics::new();
1538 stats2.update("30");
1539 stats2.update("40");
1540
1541 stats1.merge(&stats2);
1542
1543 assert_eq!(stats1.count, 4);
1544 assert_eq!(stats1.unique_count(), 4);
1545 assert!(!stats1.unique_count_is_approximate());
1546 assert!((stats1.mean() - 25.0).abs() < 1e-10);
1547 assert_eq!(stats1.min, 10.0);
1548 assert_eq!(stats1.max, 40.0);
1549 }
1550
1551 #[test]
1552 fn test_reservoir_merge_weights_sides_by_population() {
1553 let mut large = StreamReservoirSampler::new(100);
1554 for index in 0..10_000 {
1555 large.offer(format!("large-{index}"));
1556 }
1557 let mut small = StreamReservoirSampler::new(100);
1558 for index in 0..100 {
1559 small.offer(format!("small-{index}"));
1560 }
1561
1562 large.merge(&small);
1563
1564 assert_eq!(large.samples().len(), 100);
1565 let from_small = large
1566 .samples()
1567 .iter()
1568 .filter(|value| value.starts_with("small-"))
1569 .count();
1570 assert!(
1574 from_small <= 10,
1575 "small partition took {from_small} of 100 slots"
1576 );
1577 }
1578
1579 #[test]
1580 fn test_reservoir_merge_keeps_every_value_when_capacity_allows() {
1581 let mut left = StreamReservoirSampler::new(100);
1582 left.offer("a".to_string());
1583 left.offer("b".to_string());
1584 let mut right = StreamReservoirSampler::new(100);
1585 right.offer("c".to_string());
1586
1587 left.merge(&right);
1588
1589 let mut samples: Vec<String> = left.samples().to_vec();
1590 samples.sort();
1591 assert_eq!(samples, vec!["a", "b", "c"]);
1592 assert_eq!(left.count, 3);
1593 }
1594
1595 #[test]
1596 fn test_column_collection() {
1597 let mut collection = StreamingColumnCollection::new();
1598 let headers = vec!["name".to_string(), "age".to_string()];
1599
1600 collection.process_record(&headers, vec!["Alice".to_string(), "25".to_string()]);
1601 collection.process_record(&headers, vec!["Bob".to_string(), "30".to_string()]);
1602
1603 let age_stats = collection.get_column_stats("age").unwrap();
1604 assert_eq!(age_stats.count, 2);
1605 assert!((age_stats.mean() - 27.5).abs() < 1e-10);
1606 }
1607
1608 #[test]
1609 fn the_distinct_count_does_not_depend_on_the_reservoir() {
1610 let mut stats = StreamingStatistics::with_sample_capacity(2);
1614 for value in ["a", "b", "c", "a"] {
1615 stats.update(value);
1616 }
1617 assert_eq!(stats.unique_count(), 3);
1618 assert!(!stats.unique_count_is_approximate());
1619
1620 stats.spill_distinct();
1621 assert!(stats.unique_count_is_approximate());
1622 assert_eq!(stats.unique_count(), 3);
1623 }
1624
1625 #[test]
1626 fn test_default_reservoir_sampling_is_deterministic() {
1627 let mut left = StreamReservoirSampler::new(10);
1628 let mut right = StreamReservoirSampler::new(10);
1629 for value in 0..1_000 {
1630 left.offer(value.to_string());
1631 right.offer(value.to_string());
1632 }
1633
1634 assert_eq!(left.samples(), right.samples());
1635 }
1636
1637 #[test]
1638 fn shrinking_a_reservoir_keeps_a_uniform_subset_not_its_head() {
1639 let mut sampler = StreamReservoirSampler::new(1_000);
1642 for value in 0..1_000 {
1643 sampler.offer(value.to_string());
1644 }
1645 sampler.shrink_to(100);
1646
1647 let retained: Vec<usize> = sampler
1648 .samples()
1649 .iter()
1650 .map(|value| value.parse().unwrap())
1651 .collect();
1652 assert_eq!(retained.len(), 100);
1653 let mean = retained.iter().sum::<usize>() as f64 / retained.len() as f64;
1654 assert!(
1655 (350.0..650.0).contains(&mean),
1656 "mean {mean} of {retained:?}"
1657 );
1658 let mut distinct = retained.clone();
1659 distinct.sort_unstable();
1660 distinct.dedup();
1661 assert_eq!(distinct.len(), 100, "a value was kept twice");
1662 }
1663
1664 #[test]
1665 fn test_reservoir_zero_capacity_still_retains_a_sample() {
1666 let mut sampler = StreamReservoirSampler::new(0);
1667 sampler.offer("value".to_string());
1668
1669 assert_eq!(sampler.samples(), ["value"]);
1670 }
1671
1672 #[test]
1673 fn test_late_column_is_backfilled_as_missing() {
1674 let mut collection = StreamingColumnCollection::new();
1675 collection.init_column_with_missing("late", 3);
1676 collection.process_record(&["late".to_string()], ["value".to_string()]);
1677
1678 let stats = collection.get_column_stats("late").unwrap();
1679 assert_eq!(stats.count, 4);
1680 assert_eq!(stats.null_count, 3);
1681 assert_eq!(stats.unique_count(), 1);
1682 }
1683
1684 #[test]
1685 fn test_welford_accuracy() {
1686 let mut accumulator = NumericAccumulator::new();
1687 for value in 1..=1000 {
1688 accumulator.update(value as f64);
1689 }
1690 let expected_mean = 500.5;
1691 let expected_variance = (1000.0 * 1000.0 - 1.0) / 12.0;
1692 assert!((accumulator.mean() - expected_mean).abs() < 1e-6);
1693 assert!((accumulator.population_variance() - expected_variance).abs() < 1.0);
1694 }
1695
1696 #[test]
1697 fn test_welford_merge() {
1698 let mut left = NumericAccumulator::new();
1699 let mut right = NumericAccumulator::new();
1700 let mut full = NumericAccumulator::new();
1701
1702 for value in 1..=500 {
1703 left.update(value as f64);
1704 full.update(value as f64);
1705 }
1706 for value in 501..=1000 {
1707 right.update(value as f64);
1708 full.update(value as f64);
1709 }
1710
1711 left.merge(&right);
1712 assert!((left.mean() - full.mean()).abs() < 1e-10);
1713 assert!((left.population_variance() - full.population_variance()).abs() < 1e-6);
1714 }
1715
1716 #[test]
1717 fn test_hll_cardinality() {
1718 let mut counter = HyperLogLog::new();
1719 let total = 100_000;
1720 for index in 0..total {
1721 counter.insert(&format!("item_{index}"));
1722 }
1723 let estimate = counter.count();
1724 let error = (estimate as f64 - total as f64).abs() / total as f64;
1725 assert!(error < 0.05);
1726 }
1727
1728 #[test]
1729 fn test_reservoir_uniformity() {
1730 let mut sampler = StreamReservoirSampler::seed(1000, 42);
1731 let total = 100_000;
1732 for index in 0..total {
1733 sampler.offer(index.to_string());
1734 }
1735
1736 assert_eq!(sampler.samples().len(), 1000);
1737 let values: Vec<usize> = sampler
1738 .samples()
1739 .iter()
1740 .map(|value| value.parse().unwrap())
1741 .collect();
1742 let max_value = *values.iter().max().unwrap();
1743 assert!(max_value > total / 2);
1744 }
1745
1746 #[test]
1747 fn test_text_length_stats_streaming() {
1748 let mut stats = TextLengthStats::new();
1749 for &length in &[3, 5, 10, 1, 7] {
1750 stats.update(length);
1751 }
1752 assert_eq!(stats.min_length, 1);
1753 assert_eq!(stats.max_length, 10);
1754 assert!((stats.avg_length - 5.2).abs() < 1e-10);
1755 }
1756
1757 #[test]
1758 fn test_memory_usage_bounded() {
1759 let mut stats = StreamingStatistics::new();
1760 for index in 0..50_000 {
1761 stats.update(&format!("value_{index}"));
1762 }
1763 let usage = stats.memory_usage_bytes();
1764 assert!(usage < 1_000_000);
1765 }
1766}