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, ParsedNumber, RowCompletenessSummary,
11 RowDuplicateSummary, 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: &str) {
63 self.count += 1;
64 if self.reservoir.len() < self.capacity {
65 self.reservoir.push(value.to_owned());
66 } else {
67 let index = self.rng.random_range(0..self.count as usize);
68 if index < self.capacity {
69 self.reservoir[index] = value.to_owned();
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);
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) = ParsedNumber::parse(value) {
308 self.welford.update_number(number);
309 self.min = self.min.min(number.to_f64());
310 self.max = self.max.max(number.to_f64());
311 }
312 }
313
314 pub fn merge(&mut self, other: &StreamingStatistics) {
315 self.count += other.count;
316 self.null_count += other.null_count;
317
318 if other.min < self.min {
319 self.min = other.min;
320 }
321 if other.max > self.max {
322 self.max = other.max;
323 }
324
325 self.welford.merge(&other.welford);
326 self.distinct.merge(&other.distinct);
327 self.sampler.merge(&other.sampler);
328 self.text_length_tracker.merge(&other.text_length_tracker);
329 self.date_match_count += other.date_match_count;
330 }
331
332 pub fn mean(&self) -> f64 {
333 self.welford.mean()
334 }
335
336 pub fn variance(&self) -> f64 {
337 self.welford.population_variance()
338 }
339
340 pub fn std_dev(&self) -> f64 {
341 self.welford.population_std_dev()
342 }
343
344 pub fn unique_count(&self) -> usize {
351 self.distinct.estimate()
352 }
353
354 pub fn unique_count_is_approximate(&self) -> bool {
355 self.distinct.is_approximate()
356 }
357
358 pub fn unique_count_lower_bound(&self) -> usize {
360 self.distinct.distinct_lower_bound()
361 }
362
363 pub fn spill_distinct(&mut self) {
366 self.distinct.spill();
367 }
368
369 pub fn exact_distinct_bytes(&self) -> usize {
372 self.distinct.exact_bytes()
373 }
374
375 pub fn sample_values(&self) -> &[String] {
376 self.sampler.samples()
377 }
378
379 pub fn date_match_count(&self) -> usize {
381 self.date_match_count
382 }
383
384 pub fn exact_numeric_aggregates(
391 &self,
392 ) -> Option<crate::profile_builder::ExactNumericAggregates> {
393 let count = self.welford.count();
394 if count == 0 {
395 return None;
396 }
397 Some(crate::profile_builder::ExactNumericAggregates {
398 min: self.min,
399 max: self.max,
400 mean: self.welford.mean(),
401 std_dev: self.welford.sample_std_dev(),
402 variance: self.welford.sample_variance(),
403 count: count as usize,
404 })
405 }
406
407 pub fn text_length_stats(&self) -> TextLengthStats {
408 if self.text_length_tracker.welford.count() == 0 {
409 return TextLengthStats::empty();
410 }
411 self.text_length_tracker.clone()
412 }
413
414 pub fn reduce_sample_capacity(&mut self) {
415 self.sampler.shrink_to(self.sampler.capacity / 2);
416 }
417
418 pub fn memory_usage_bytes(&self) -> usize {
419 let struct_size = std::mem::size_of::<Self>();
420 let distinct_size = self.distinct.memory_usage_bytes();
421 let reservoir_size = self.sampler.memory_usage_bytes();
422
423 struct_size + distinct_size + reservoir_size
424 }
425}
426
427impl Default for StreamingStatistics {
428 fn default() -> Self {
429 Self::new()
430 }
431}
432
433#[derive(Debug, Default)]
448pub struct RowSignature {
449 buffer: String,
450 len_through_last_present: usize,
452}
453
454impl RowSignature {
455 pub fn push_field(&mut self, value: &str) {
465 self.push_tagged(value, ':');
466 }
467
468 pub fn push_container_field(&mut self, text: &str) {
474 self.push_tagged(text, '#');
475 }
476
477 fn push_tagged(&mut self, value: &str, tag: char) {
479 let _ = write!(self.buffer, "{}{tag}", value.len());
480 self.buffer.push_str(value);
481 if !value.is_empty() {
482 self.len_through_last_present = self.buffer.len();
483 }
484 }
485
486 pub fn finish(mut self) -> String {
488 self.buffer.truncate(self.len_through_last_present);
489 self.buffer
490 }
491}
492
493#[derive(Debug, Clone, Default)]
502pub struct RowUniquenessTracker {
503 rows_seen: usize,
504 distinct: CardinalityEstimator,
505 incomparable_signatures: bool,
515}
516
517impl RowUniquenessTracker {
518 pub fn observe(&mut self, signature: String) {
519 self.rows_seen += 1;
520 self.distinct.insert_owned(signature);
521 }
522
523 pub fn rows_seen(&self) -> usize {
524 self.rows_seen
525 }
526
527 pub fn duplicate_rows(&self) -> usize {
529 self.rows_seen.saturating_sub(self.distinct.estimate())
530 }
531
532 pub fn is_approximate(&self) -> bool {
533 self.distinct.is_approximate()
534 }
535
536 pub fn mark_incomparable(&mut self) {
539 self.incomparable_signatures = true;
540 }
541
542 pub fn merge(&mut self, other: &RowUniquenessTracker) {
543 self.rows_seen += other.rows_seen;
544 self.distinct.merge(&other.distinct);
545 self.incomparable_signatures |= other.incomparable_signatures;
546 }
547
548 pub fn memory_usage_bytes(&self) -> usize {
549 self.distinct.memory_usage_bytes()
550 }
551
552 pub fn exact_bytes(&self) -> usize {
554 self.distinct.exact_bytes()
555 }
556
557 pub fn spill(&mut self) {
560 self.distinct.spill();
561 }
562
563 pub fn summary(&self) -> Option<RowDuplicateSummary> {
567 if self.rows_seen == 0 || self.incomparable_signatures {
568 return None;
569 }
570 Some(RowDuplicateSummary {
571 duplicate_rows: self.duplicate_rows(),
572 rows_checked: self.rows_seen,
573 approximate: self.is_approximate(),
574 max_duplicate_rows: self
575 .rows_seen
576 .saturating_sub(self.distinct.distinct_lower_bound()),
577 })
578 }
579}
580
581#[derive(Debug, Clone, Default)]
588pub struct RowCompletenessTracker {
589 rows_seen: usize,
590 complete_rows: usize,
591}
592
593impl RowCompletenessTracker {
594 pub fn observe(&mut self, had_null: bool) {
596 self.rows_seen += 1;
597 if !had_null {
598 self.complete_rows += 1;
599 }
600 }
601
602 pub fn invalidate_completed_rows(&mut self) {
605 self.complete_rows = 0;
606 }
607
608 pub fn observe_aligned_columns(&mut self, columns: &[&[String]], total_rows: usize) {
615 if columns.is_empty() {
616 return;
617 }
618 for row_index in 0..total_rows {
619 let had_null = columns.iter().any(|cells| {
620 cells
621 .get(row_index)
622 .is_none_or(|value| is_null_like_token(value))
623 });
624 self.observe(had_null);
625 }
626 }
627
628 pub fn merge(&mut self, other: &RowCompletenessTracker) {
629 self.rows_seen += other.rows_seen;
630 self.complete_rows += other.complete_rows;
631 }
632
633 pub fn summary(&self) -> Option<RowCompletenessSummary> {
635 if self.rows_seen == 0 {
636 return None;
637 }
638 Some(RowCompletenessSummary {
639 complete_rows: self.complete_rows,
640 rows_checked: self.rows_seen,
641 })
642 }
643}
644
645pub struct StreamingColumnCollection {
646 columns: HashMap<String, StreamingStatistics>,
647 ordered_names: Vec<String>,
648 memory_limit_bytes: usize,
649 row_tracker: RowUniquenessTracker,
650 completeness_tracker: RowCompletenessTracker,
651 hint_bindings: ValueHintBindingAccumulator,
652}
653
654impl StreamingColumnCollection {
655 pub fn new() -> Self {
656 Self {
657 columns: HashMap::new(),
658 ordered_names: Vec::new(),
659 memory_limit_bytes: 100 * 1024 * 1024,
660 row_tracker: RowUniquenessTracker::default(),
661 completeness_tracker: RowCompletenessTracker::default(),
662 hint_bindings: ValueHintBindingAccumulator::default(),
663 }
664 }
665
666 pub fn memory_limit(limit_mb: usize) -> Self {
667 Self {
668 columns: HashMap::new(),
669 ordered_names: Vec::new(),
670 memory_limit_bytes: limit_mb * 1024 * 1024,
671 row_tracker: RowUniquenessTracker::default(),
672 completeness_tracker: RowCompletenessTracker::default(),
673 hint_bindings: ValueHintBindingAccumulator::default(),
674 }
675 }
676
677 pub fn with_semantic_hints(mut self, hints: &SemanticHints) -> Self {
679 self.hint_bindings = ValueHintBindingAccumulator::new(hints);
680 self
681 }
682
683 pub fn init_columns(&mut self, headers: &[String]) {
684 for header in headers {
685 if !self.columns.contains_key(header) {
686 self.columns
687 .insert(header.clone(), StreamingStatistics::default());
688 self.ordered_names.push(header.clone());
689 }
690 }
691 }
692
693 pub fn init_column_with_missing(&mut self, header: &str, prior_rows: usize) {
700 if self.columns.contains_key(header) {
701 return;
702 }
703
704 let stats = StreamingStatistics {
705 count: prior_rows,
706 null_count: prior_rows,
707 ..Default::default()
708 };
709 self.columns.insert(header.to_string(), stats);
710 self.ordered_names.push(header.to_string());
711 if prior_rows > 0 {
712 self.completeness_tracker.invalidate_completed_rows();
713 }
714 }
715
716 pub fn process_record<I>(&mut self, headers: &[String], values: I)
717 where
718 I: IntoIterator<Item = String>,
719 {
720 self.process_record_with_containers(headers, values, &[]);
721 }
722
723 pub fn process_record_with_containers<I>(
729 &mut self,
730 headers: &[String],
731 values: I,
732 containers: &[usize],
733 ) where
734 I: IntoIterator<Item = String>,
735 {
736 let mut row_signature = RowSignature::default();
737 let mut row_has_null = false;
738 let mut values = values.into_iter();
739
740 for (position, header) in headers.iter().enumerate() {
744 let value = values.next().unwrap_or_default();
745 if containers.contains(&position) {
746 row_signature.push_container_field(&value);
747 } else {
748 row_signature.push_field(&value);
749 }
750
751 if let Some(stats) = self.columns.get_mut(header) {
752 stats.update(&value);
753 } else {
754 self.ordered_names.push(header.clone());
755 self.columns
756 .entry(header.clone())
757 .or_default()
758 .update(&value);
759 }
760 row_has_null |= is_null_like_token(&value);
763 self.hint_bindings.observe(header, &value);
764 }
765
766 self.row_tracker.observe(row_signature.finish());
770 self.completeness_tracker.observe(row_has_null);
771 }
772
773 pub fn row_duplicate_summary(&self) -> Option<RowDuplicateSummary> {
775 self.row_tracker.summary()
776 }
777
778 pub fn row_completeness_summary(&self) -> Option<RowCompletenessSummary> {
780 self.completeness_tracker.summary()
781 }
782
783 pub fn semantic_hint_bindings(&self) -> Vec<SemanticHintBinding> {
785 self.hint_bindings
786 .bindings(self.ordered_names.iter().map(String::as_str))
787 }
788
789 pub fn get_column_stats(&self, column_name: &str) -> Option<&StreamingStatistics> {
790 self.columns.get(column_name)
791 }
792
793 pub fn column_names(&self) -> Vec<String> {
794 self.ordered_names.clone()
795 }
796
797 pub fn retain_columns(&mut self, names: &[String]) {
804 let selected = names.iter().map(String::as_str).collect::<HashSet<_>>();
805 self.columns
806 .retain(|name, _| selected.contains(name.as_str()));
807 self.ordered_names
808 .retain(|name| selected.contains(name.as_str()));
809 }
810
811 pub fn memory_usage_bytes(&self) -> usize {
812 self.columns
813 .values()
814 .map(|stats| stats.memory_usage_bytes())
815 .sum::<usize>()
816 + self.row_tracker.memory_usage_bytes()
817 }
818
819 pub fn is_memory_pressure(&self) -> bool {
820 self.memory_usage_bytes() > (self.memory_limit_bytes * 80 / 100)
821 }
822
823 pub fn reduce_memory_usage(&mut self) {
824 let limit = self.memory_limit_bytes * 80 / 100;
839 let mut usage = self.memory_usage_bytes();
840 if usage <= limit {
841 return;
842 }
843 usage -= self.spill_distinct_sets(usage - limit);
844 if usage > limit {
845 usage -= self.row_tracker.exact_bytes();
846 self.row_tracker.spill();
847 }
848 if usage > limit {
849 for stats in self.columns.values_mut() {
850 stats.reduce_sample_capacity();
851 }
852 }
853 }
854
855 fn spill_distinct_sets(&mut self, excess: usize) -> usize {
858 let mut by_size: Vec<(usize, usize)> = self
859 .ordered_names
860 .iter()
861 .enumerate()
862 .filter_map(|(position, name)| {
863 let bytes = self.columns.get(name)?.exact_distinct_bytes();
864 (bytes > 0).then_some((position, bytes))
865 })
866 .collect();
867 by_size.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
868 let mut freed = 0;
869 for (position, bytes) in by_size {
870 if freed >= excess {
871 break;
872 }
873 let name = &self.ordered_names[position];
874 if let Some(stats) = self.columns.get_mut(name) {
875 stats.spill_distinct();
876 freed += bytes;
877 }
878 }
879 freed
880 }
881
882 pub fn column_type_fingerprint(&self) -> u64 {
887 use std::collections::hash_map::DefaultHasher;
888 use std::hash::{Hash, Hasher};
889
890 let mut hasher = DefaultHasher::new();
891 let mut names: Vec<&String> = self.columns.keys().collect();
892 names.sort();
893 for name in names {
894 let stats = &self.columns[name];
895 let data_type = infer_data_type_streaming(stats);
896 name.hash(&mut hasher);
897 std::mem::discriminant(&data_type).hash(&mut hasher);
898 }
899 hasher.finish()
900 }
901
902 pub fn merge(&mut self, other: StreamingColumnCollection) {
927 let self_rows = self.row_tracker.rows_seen();
928 let other_rows = other.row_tracker.rows_seen();
929
930 let StreamingColumnCollection {
931 columns,
932 ordered_names,
933 memory_limit_bytes: _,
934 row_tracker,
935 mut completeness_tracker,
936 hint_bindings,
937 } = other;
938
939 let schemas_match = self.ordered_names == ordered_names;
943
944 let mut other_lacks_columns = false;
947 for (column_name, stats) in self.columns.iter_mut() {
948 if columns.contains_key(column_name) {
949 continue;
950 }
951 other_lacks_columns = true;
952 stats.count += other_rows;
953 stats.null_count += other_rows;
954 }
955
956 let mut self_lacks_columns = false;
960 for column_name in &ordered_names {
961 if !self.columns.contains_key(column_name) {
962 self.ordered_names.push(column_name.clone());
963 self_lacks_columns = true;
964 }
965 }
966
967 for (column_name, other_stats) in columns {
968 match self.columns.get_mut(&column_name) {
969 Some(existing_stats) => existing_stats.merge(&other_stats),
970 None => {
971 let mut stats = other_stats;
972 stats.count += self_rows;
974 stats.null_count += self_rows;
975 self.columns.insert(column_name, stats);
976 }
977 }
978 }
979
980 if self_lacks_columns && self_rows > 0 {
981 self.completeness_tracker.invalidate_completed_rows();
982 }
983 if other_lacks_columns && other_rows > 0 {
984 completeness_tracker.invalidate_completed_rows();
985 }
986
987 self.completeness_tracker.merge(&completeness_tracker);
988 self.row_tracker.merge(&row_tracker);
989 if self_rows > 0 && other_rows > 0 && !schemas_match {
992 self.row_tracker.mark_incomparable();
993 }
994 self.hint_bindings.merge(&hint_bindings);
995 }
996}
997
998impl Default for StreamingColumnCollection {
999 fn default() -> Self {
1000 Self::new()
1001 }
1002}
1003
1004#[cfg(test)]
1005mod row_tracker_tests {
1006 use super::*;
1007
1008 fn record(collection: &mut StreamingColumnCollection, headers: &[String], values: &[&str]) {
1009 collection.process_record(headers, values.iter().map(|v| v.to_string()));
1010 }
1011
1012 fn completeness(collection: &StreamingColumnCollection) -> (usize, usize) {
1013 let summary = collection
1014 .row_completeness_summary()
1015 .expect("rows were observed");
1016 (summary.complete_rows, summary.rows_checked)
1017 }
1018
1019 #[test]
1020 fn test_complete_rows_count_rows_not_null_cells() {
1021 let headers = vec!["a".to_string(), "b".to_string()];
1022 let mut collection = StreamingColumnCollection::new();
1023 record(&mut collection, &headers, &["", ""]);
1027 record(&mut collection, &headers, &["x", "1"]);
1028 record(&mut collection, &headers, &["y", "2"]);
1029 record(&mut collection, &headers, &["z", "3"]);
1030
1031 assert_eq!(completeness(&collection), (3, 4));
1032 }
1033
1034 #[test]
1035 fn test_null_like_tokens_make_a_row_incomplete() {
1036 let headers = vec!["a".to_string(), "b".to_string()];
1037 let mut collection = StreamingColumnCollection::new();
1038 record(&mut collection, &headers, &["x", "NULL"]);
1041 record(&mut collection, &headers, &["y", "NaN"]);
1042 record(&mut collection, &headers, &["z", " "]);
1043 record(&mut collection, &headers, &["w", "3"]);
1044
1045 assert_eq!(completeness(&collection), (1, 4));
1046 }
1047
1048 #[test]
1049 fn test_ragged_rows_are_incomplete() {
1050 let headers = vec!["a".to_string(), "b".to_string()];
1051 let mut collection = StreamingColumnCollection::new();
1052 record(&mut collection, &headers, &["x"]);
1053 record(&mut collection, &headers, &["y", "1"]);
1054
1055 assert_eq!(completeness(&collection), (1, 2));
1056 }
1057
1058 #[test]
1059 fn test_a_late_column_makes_earlier_rows_incomplete() {
1060 let mut collection = StreamingColumnCollection::new();
1061 let first = vec!["a".to_string()];
1062 record(&mut collection, &first, &["x"]);
1063 record(&mut collection, &first, &["y"]);
1064 assert_eq!(completeness(&collection), (2, 2));
1065
1066 collection.init_column_with_missing("b", 2);
1069 let both = vec!["a".to_string(), "b".to_string()];
1070 record(&mut collection, &both, &["z", "1"]);
1071
1072 assert_eq!(completeness(&collection), (1, 3));
1073 }
1074
1075 #[test]
1076 fn test_no_rows_means_no_completeness_summary() {
1077 let collection = StreamingColumnCollection::new();
1078 assert!(collection.row_completeness_summary().is_none());
1079 }
1080
1081 #[test]
1082 fn test_aligned_columns_count_the_same_complete_rows() {
1083 let a: Vec<String> = ["", "x", "y"].iter().map(|v| v.to_string()).collect();
1084 let b: Vec<String> = ["", "1", "2"].iter().map(|v| v.to_string()).collect();
1085 let mut tracker = RowCompletenessTracker::default();
1086 tracker.observe_aligned_columns(&[a.as_slice(), b.as_slice()], 3);
1087
1088 let summary = tracker.summary().expect("rows were observed");
1089 assert_eq!((summary.complete_rows, summary.rows_checked), (2, 3));
1090 }
1091
1092 #[test]
1093 fn test_aligned_columns_treat_a_short_column_as_missing() {
1094 let a: Vec<String> = ["x", "y"].iter().map(|v| v.to_string()).collect();
1095 let b: Vec<String> = ["1"].iter().map(|v| v.to_string()).collect();
1096 let mut tracker = RowCompletenessTracker::default();
1097 tracker.observe_aligned_columns(&[a.as_slice(), b.as_slice()], 2);
1098
1099 let summary = tracker.summary().expect("rows were observed");
1100 assert_eq!((summary.complete_rows, summary.rows_checked), (1, 2));
1101 }
1102
1103 #[test]
1104 fn test_exact_duplicates_including_null_rows() {
1105 let headers = vec!["a".to_string(), "b".to_string()];
1106 let mut collection = StreamingColumnCollection::new();
1107 record(&mut collection, &headers, &["x", ""]);
1108 record(&mut collection, &headers, &["x", ""]);
1109 record(&mut collection, &headers, &["x", "1"]);
1110 record(&mut collection, &headers, &["", ""]);
1111
1112 let summary = collection
1113 .row_duplicate_summary()
1114 .expect("rows were observed");
1115 assert_eq!(summary.rows_checked, 4);
1116 assert_eq!(summary.duplicate_rows, 1);
1119 assert!(!summary.approximate);
1120 }
1121
1122 #[test]
1123 fn test_field_boundaries_are_unambiguous() {
1124 let headers = vec!["a".to_string(), "b".to_string()];
1125 let mut collection = StreamingColumnCollection::new();
1126 record(&mut collection, &headers, &["ab", "c"]);
1127 record(&mut collection, &headers, &["a", "bc"]);
1128
1129 let summary = collection
1130 .row_duplicate_summary()
1131 .expect("rows were observed");
1132 assert_eq!(
1133 summary.duplicate_rows, 0,
1134 "different field splits must not collide"
1135 );
1136 }
1137
1138 #[test]
1139 fn test_ragged_rows_normalize_missing_trailing_fields() {
1140 let headers = vec!["a".to_string(), "b".to_string()];
1141 let mut collection = StreamingColumnCollection::new();
1142 record(&mut collection, &headers, &["x"]);
1143 record(&mut collection, &headers, &["x", ""]);
1144
1145 let summary = collection
1146 .row_duplicate_summary()
1147 .expect("rows were observed");
1148 assert_eq!(summary.rows_checked, 2);
1149 assert_eq!(summary.duplicate_rows, 1);
1150 assert_eq!(
1151 collection
1152 .get_column_stats("b")
1153 .expect("column b")
1154 .null_count,
1155 2
1156 );
1157 }
1158
1159 #[test]
1160 fn test_trailing_null_like_token_is_not_trimmed() {
1161 let headers = vec!["a".to_string(), "b".to_string()];
1166 let mut collection = StreamingColumnCollection::new();
1167 record(&mut collection, &headers, &["x", "null"]);
1168 record(&mut collection, &headers, &["x", " "]);
1169 record(&mut collection, &headers, &["x", ""]);
1170
1171 let summary = collection
1172 .row_duplicate_summary()
1173 .expect("rows were observed");
1174 assert_eq!(summary.duplicate_rows, 0);
1175 }
1176
1177 #[test]
1178 fn test_late_column_does_not_split_identical_rows() {
1179 let mut collection = StreamingColumnCollection::new();
1183 let short = vec!["a".to_string()];
1184 let wide = vec!["a".to_string(), "b".to_string()];
1185
1186 record(&mut collection, &short, &["x"]);
1187 collection.init_column_with_missing("b", 1);
1188 record(&mut collection, &wide, &["x", "y"]);
1189 record(&mut collection, &wide, &["x", ""]);
1190
1191 let summary = collection
1192 .row_duplicate_summary()
1193 .expect("rows were observed");
1194 assert_eq!(summary.rows_checked, 3);
1195 assert_eq!(
1196 summary.duplicate_rows, 1,
1197 "rows 1 and 3 hold the same data; they differ only in how many \
1198 columns had been discovered when each was read"
1199 );
1200 }
1201
1202 #[test]
1203 fn test_late_column_in_the_middle_does_not_split_identical_rows() {
1204 let mut collection = StreamingColumnCollection::new();
1207 let one = vec!["a".to_string()];
1208 let two = vec!["a".to_string(), "b".to_string()];
1209 let three = vec!["a".to_string(), "b".to_string(), "c".to_string()];
1210
1211 record(&mut collection, &one, &["1"]);
1212 collection.init_column_with_missing("b", 1);
1213 record(&mut collection, &two, &["1", "2"]);
1214 collection.init_column_with_missing("c", 2);
1215 record(&mut collection, &three, &["1", "", "3"]);
1216 record(&mut collection, &three, &["1", "", ""]);
1217
1218 let summary = collection
1219 .row_duplicate_summary()
1220 .expect("rows were observed");
1221 assert_eq!(summary.rows_checked, 4);
1222 assert_eq!(summary.duplicate_rows, 1, "row 4 repeats row 1");
1223 }
1224
1225 #[test]
1226 fn test_no_rows_means_no_summary() {
1227 let collection = StreamingColumnCollection::new();
1228 assert!(collection.row_duplicate_summary().is_none());
1229 }
1230
1231 #[test]
1232 fn a_column_longer_than_the_reservoir_keeps_an_exact_count() {
1233 let mut constant = StreamingStatistics::new();
1237 let mut ids = StreamingStatistics::new();
1238 for id in 0..50_000 {
1239 constant.update("v1");
1240 ids.update(&id.to_string());
1241 }
1242 assert!(!constant.unique_count_is_approximate());
1243 assert_eq!(constant.unique_count(), 1);
1244 assert!(!ids.unique_count_is_approximate());
1245 assert_eq!(ids.unique_count(), 50_000);
1246 }
1247
1248 #[test]
1249 fn memory_pressure_spills_the_largest_exact_set_first() {
1250 let headers = vec!["id".to_string(), "flag".to_string()];
1251 let mut collection = StreamingColumnCollection::memory_limit(8);
1255 for id in 0..300_000 {
1256 let flag = if id % 2 == 0 { "yes" } else { "no" };
1257 record(&mut collection, &headers, &[&id.to_string(), flag]);
1258 }
1259 assert!(
1260 collection.is_memory_pressure(),
1261 "300,000 exact fingerprints no longer exceed the limit; this test reaches nothing"
1262 );
1263 collection.reduce_memory_usage();
1264
1265 let id = collection.get_column_stats("id").unwrap();
1266 let flag = collection.get_column_stats("flag").unwrap();
1267 assert!(id.unique_count_is_approximate(), "the large set spills");
1268 assert!(!flag.unique_count_is_approximate(), "the small one is kept");
1269 assert_eq!(flag.unique_count(), 2);
1270 assert_eq!(id.sample_values().len(), 10_000);
1273 assert_eq!(flag.sample_values().len(), 10_000);
1274 }
1275
1276 #[test]
1277 fn memory_pressure_spills_the_row_tracker_before_shrinking_reservoirs() {
1278 let headers = vec!["id".to_string(), "flag".to_string(), "blank".to_string()];
1279 let mut collection = StreamingColumnCollection::memory_limit(5);
1282 for id in 0..300_000 {
1283 let flag = if id % 2 == 0 { "yes" } else { "no" };
1284 record(&mut collection, &headers, &[&id.to_string(), flag, ""]);
1285 }
1286 collection.reduce_memory_usage();
1287
1288 let rows = collection.row_duplicate_summary().unwrap();
1289 assert!(rows.approximate, "the row tracker spills");
1290 assert!(!collection.is_memory_pressure());
1291 let blank = collection.get_column_stats("blank").unwrap();
1294 assert!(!blank.unique_count_is_approximate());
1295 assert_eq!(blank.unique_count(), 0);
1296 for name in ["id", "flag"] {
1297 let stats = collection.get_column_stats(name).unwrap();
1298 assert!(stats.unique_count_is_approximate(), "{name} spills first");
1299 assert_eq!(stats.sample_values().len(), 10_000, "{name} reservoir kept");
1300 }
1301 }
1302
1303 #[test]
1304 fn test_spills_to_approximate_beyond_distinct_threshold() {
1305 let headers = vec!["n".to_string()];
1306 let mut collection = StreamingColumnCollection::new();
1307 let distinct = dataprof_metrics::EXACT_CARDINALITY_THRESHOLD + 500;
1308 for i in 0..distinct {
1309 record(&mut collection, &headers, &[&i.to_string()]);
1310 }
1311 for i in 0..distinct {
1313 record(&mut collection, &headers, &[&i.to_string()]);
1314 }
1315
1316 let summary = collection
1317 .row_duplicate_summary()
1318 .expect("rows were observed");
1319 assert!(summary.approximate, "past the threshold the count is HLL");
1320 assert_eq!(summary.rows_checked, distinct * 2);
1321 let error = (summary.duplicate_rows as f64 - distinct as f64).abs() / distinct as f64;
1322 assert!(
1323 error < 0.05,
1324 "estimated {} duplicates for {distinct} true, off by {error:.4}",
1325 summary.duplicate_rows
1326 );
1327 }
1328
1329 #[test]
1330 fn test_merge_combines_row_trackers() {
1331 let headers = vec!["a".to_string()];
1332 let mut left = StreamingColumnCollection::new();
1333 let mut right = StreamingColumnCollection::new();
1334 record(&mut left, &headers, &["x"]);
1335 record(&mut left, &headers, &["y"]);
1336 record(&mut right, &headers, &["x"]);
1337
1338 left.merge(right);
1339 let summary = left.row_duplicate_summary().expect("rows were observed");
1340 assert_eq!(summary.rows_checked, 3);
1341 assert_eq!(summary.duplicate_rows, 1);
1342 }
1343
1344 #[test]
1345 fn test_merge_keeps_columns_only_the_other_side_saw() {
1346 let mut left = StreamingColumnCollection::new();
1347 let mut right = StreamingColumnCollection::new();
1348 record(&mut left, &["a".to_string()], &["1"]);
1349 record(&mut right, &["b".to_string()], &["2"]);
1350
1351 left.merge(right);
1352
1353 assert_eq!(left.column_names(), vec!["a".to_string(), "b".to_string()]);
1356 assert!(left.get_column_stats("b").is_some());
1357 }
1358
1359 #[test]
1360 fn test_merge_pads_columns_the_other_side_never_saw() {
1361 let left_headers = vec!["a".to_string()];
1362 let right_headers = vec!["a".to_string(), "b".to_string()];
1363 let mut left = StreamingColumnCollection::new();
1364 let mut right = StreamingColumnCollection::new();
1365 record(&mut left, &left_headers, &["1"]);
1366 record(&mut left, &left_headers, &["2"]);
1367 record(&mut right, &right_headers, &["3", "x"]);
1368
1369 left.merge(right);
1370
1371 let b = left.get_column_stats("b").expect("column b survived");
1372 assert_eq!(b.count, 3);
1374 assert_eq!(b.null_count, 2);
1375
1376 let a = left.get_column_stats("a").expect("column a");
1377 assert_eq!(a.count, 3);
1378 assert_eq!(a.null_count, 0);
1379 }
1380
1381 #[test]
1382 fn test_merge_combines_completeness() {
1383 let headers = vec!["a".to_string(), "b".to_string()];
1384 let mut left = StreamingColumnCollection::new();
1385 let mut right = StreamingColumnCollection::new();
1386 record(&mut left, &headers, &["1", "2"]);
1387 record(&mut left, &headers, &["3", ""]);
1388 record(&mut right, &headers, &["4", "5"]);
1389
1390 left.merge(right);
1391
1392 assert_eq!(completeness(&left), (2, 3));
1393 }
1394
1395 #[test]
1396 fn test_merge_invalidates_completeness_for_the_narrower_side() {
1397 let left_headers = vec!["a".to_string()];
1398 let right_headers = vec!["a".to_string(), "b".to_string()];
1399 let mut left = StreamingColumnCollection::new();
1400 let mut right = StreamingColumnCollection::new();
1401 record(&mut left, &left_headers, &["1"]);
1402 record(&mut right, &right_headers, &["2", "y"]);
1403
1404 left.merge(right);
1405
1406 assert_eq!(completeness(&left), (1, 2));
1408 }
1409
1410 #[test]
1411 fn test_merge_withholds_duplicates_when_schemas_differ() {
1412 let mut left = StreamingColumnCollection::new();
1413 let mut right = StreamingColumnCollection::new();
1414 record(&mut left, &["a".to_string()], &["x"]);
1415 record(&mut right, &["b".to_string()], &["x"]);
1416
1417 left.merge(right);
1418
1419 assert!(left.row_duplicate_summary().is_none());
1422 }
1423
1424 #[test]
1425 fn test_merge_withholds_duplicates_when_column_order_differs() {
1426 let mut left = StreamingColumnCollection::new();
1427 let mut right = StreamingColumnCollection::new();
1428 record(&mut left, &["a".to_string(), "b".to_string()], &["x", "y"]);
1429 record(&mut right, &["b".to_string(), "a".to_string()], &["x", "y"]);
1430
1431 left.merge(right);
1432
1433 assert!(left.row_duplicate_summary().is_none());
1436 }
1437
1438 #[test]
1439 fn test_merge_keeps_duplicates_when_schemas_match() {
1440 let headers = vec!["a".to_string(), "b".to_string()];
1441 let mut left = StreamingColumnCollection::new();
1442 let mut right = StreamingColumnCollection::new();
1443 record(&mut left, &headers, &["x", "y"]);
1444 record(&mut right, &headers, &["x", "y"]);
1445
1446 left.merge(right);
1447
1448 let summary = left
1449 .row_duplicate_summary()
1450 .expect("one schema on both sides");
1451 assert_eq!(summary.rows_checked, 2);
1452 assert_eq!(summary.duplicate_rows, 1);
1453 }
1454
1455 #[test]
1456 fn test_merge_into_an_empty_accumulator_keeps_duplicates() {
1457 let headers = vec!["a".to_string()];
1458 let mut accumulator = StreamingColumnCollection::new();
1459 let mut partition = StreamingColumnCollection::new();
1460 record(&mut partition, &headers, &["x"]);
1461 record(&mut partition, &headers, &["x"]);
1462
1463 accumulator.merge(partition);
1466
1467 let summary = accumulator
1468 .row_duplicate_summary()
1469 .expect("the empty side contributed no signatures");
1470 assert_eq!(summary.rows_checked, 2);
1471 assert_eq!(summary.duplicate_rows, 1);
1472 }
1473
1474 #[test]
1479 fn test_merged_partitions_match_a_single_pass() {
1480 let headers = vec!["id".to_string(), "city".to_string(), "score".to_string()];
1481 let rows: Vec<[String; 3]> = (0..300)
1482 .map(|index| {
1483 [
1484 format!("{}", index % 10),
1485 ["Rome", "Milan", "Turin", ""][index % 4].to_string(),
1486 format!("{}.5", index % 5),
1487 ]
1488 })
1489 .collect();
1490
1491 let mut single = StreamingColumnCollection::new();
1492 for row in &rows {
1493 single.process_record(&headers, row.iter().cloned());
1494 }
1495
1496 let mut partitions: Vec<StreamingColumnCollection> =
1498 (0..7).map(|_| StreamingColumnCollection::new()).collect();
1499 for (index, row) in rows.iter().enumerate() {
1500 partitions[index % 7].process_record(&headers, row.iter().cloned());
1501 }
1502 let mut merged = partitions.remove(0);
1503 for partition in partitions {
1504 merged.merge(partition);
1505 }
1506
1507 assert_eq!(merged.column_names(), single.column_names());
1508 for name in single.column_names() {
1509 let expected = single.get_column_stats(&name).expect("single-pass column");
1510 let actual = merged.get_column_stats(&name).expect("merged column");
1511 assert_eq!(actual.count, expected.count, "count for {name}");
1512 assert_eq!(actual.null_count, expected.null_count, "nulls for {name}");
1513 assert_eq!(
1514 actual.unique_count(),
1515 expected.unique_count(),
1516 "distinct for {name}"
1517 );
1518 assert_eq!(actual.min, expected.min, "min for {name}");
1519 assert_eq!(actual.max, expected.max, "max for {name}");
1520 assert!(
1521 (actual.mean() - expected.mean()).abs() < 1e-9,
1522 "mean for {name}"
1523 );
1524 assert!(
1525 (actual.variance() - expected.variance()).abs() < 1e-9,
1526 "variance for {name}"
1527 );
1528 }
1529
1530 let merged_rows = merged.row_completeness_summary().expect("rows observed");
1531 let single_rows = single.row_completeness_summary().expect("rows observed");
1532 assert_eq!(merged_rows.rows_checked, single_rows.rows_checked);
1533 assert_eq!(merged_rows.complete_rows, single_rows.complete_rows);
1534
1535 let merged_dupes = merged.row_duplicate_summary().expect("rows observed");
1536 let single_dupes = single.row_duplicate_summary().expect("rows observed");
1537 assert_eq!(merged_dupes.rows_checked, single_dupes.rows_checked);
1538 assert_eq!(merged_dupes.duplicate_rows, single_dupes.duplicate_rows);
1539 }
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544 use super::*;
1545 use dataprof_metrics::HyperLogLog;
1546
1547 #[test]
1548 fn test_streaming_statistics() {
1549 let mut stats = StreamingStatistics::new();
1550
1551 stats.update("10.5");
1552 stats.update("20.0");
1553 stats.update("15.5");
1554 stats.update("");
1555
1556 assert_eq!(stats.count, 4);
1557 assert_eq!(stats.null_count, 1);
1558 assert_eq!(stats.unique_count(), 3);
1559 assert!(!stats.unique_count_is_approximate());
1560 assert!((stats.mean() - 15.333333333333334).abs() < 1e-10);
1561 assert_eq!(stats.min, 10.5);
1562 assert_eq!(stats.max, 20.0);
1563 }
1564
1565 #[test]
1566 fn test_streaming_statistics_merge() {
1567 let mut stats1 = StreamingStatistics::new();
1568 stats1.update("10");
1569 stats1.update("20");
1570
1571 let mut stats2 = StreamingStatistics::new();
1572 stats2.update("30");
1573 stats2.update("40");
1574
1575 stats1.merge(&stats2);
1576
1577 assert_eq!(stats1.count, 4);
1578 assert_eq!(stats1.unique_count(), 4);
1579 assert!(!stats1.unique_count_is_approximate());
1580 assert!((stats1.mean() - 25.0).abs() < 1e-10);
1581 assert_eq!(stats1.min, 10.0);
1582 assert_eq!(stats1.max, 40.0);
1583 }
1584
1585 #[test]
1586 fn test_reservoir_merge_weights_sides_by_population() {
1587 let mut large = StreamReservoirSampler::new(100);
1588 for index in 0..10_000 {
1589 large.offer(&format!("large-{index}"));
1590 }
1591 let mut small = StreamReservoirSampler::new(100);
1592 for index in 0..100 {
1593 small.offer(&format!("small-{index}"));
1594 }
1595
1596 large.merge(&small);
1597
1598 assert_eq!(large.samples().len(), 100);
1599 let from_small = large
1600 .samples()
1601 .iter()
1602 .filter(|value| value.starts_with("small-"))
1603 .count();
1604 assert!(
1608 from_small <= 10,
1609 "small partition took {from_small} of 100 slots"
1610 );
1611 }
1612
1613 #[test]
1614 fn test_reservoir_merge_keeps_every_value_when_capacity_allows() {
1615 let mut left = StreamReservoirSampler::new(100);
1616 left.offer("a");
1617 left.offer("b");
1618 let mut right = StreamReservoirSampler::new(100);
1619 right.offer("c");
1620
1621 left.merge(&right);
1622
1623 let mut samples: Vec<String> = left.samples().to_vec();
1624 samples.sort();
1625 assert_eq!(samples, vec!["a", "b", "c"]);
1626 assert_eq!(left.count, 3);
1627 }
1628
1629 #[test]
1630 fn test_column_collection() {
1631 let mut collection = StreamingColumnCollection::new();
1632 let headers = vec!["name".to_string(), "age".to_string()];
1633
1634 collection.process_record(&headers, vec!["Alice".to_string(), "25".to_string()]);
1635 collection.process_record(&headers, vec!["Bob".to_string(), "30".to_string()]);
1636
1637 let age_stats = collection.get_column_stats("age").unwrap();
1638 assert_eq!(age_stats.count, 2);
1639 assert!((age_stats.mean() - 27.5).abs() < 1e-10);
1640 }
1641
1642 #[test]
1643 fn the_distinct_count_does_not_depend_on_the_reservoir() {
1644 let mut stats = StreamingStatistics::with_sample_capacity(2);
1648 for value in ["a", "b", "c", "a"] {
1649 stats.update(value);
1650 }
1651 assert_eq!(stats.unique_count(), 3);
1652 assert!(!stats.unique_count_is_approximate());
1653
1654 stats.spill_distinct();
1655 assert!(stats.unique_count_is_approximate());
1656 assert_eq!(stats.unique_count(), 3);
1657 }
1658
1659 #[test]
1660 fn test_default_reservoir_sampling_is_deterministic() {
1661 let mut left = StreamReservoirSampler::new(10);
1662 let mut right = StreamReservoirSampler::new(10);
1663 for value in 0..1_000 {
1664 left.offer(&value.to_string());
1665 right.offer(&value.to_string());
1666 }
1667
1668 assert_eq!(left.samples(), right.samples());
1669 }
1670
1671 #[test]
1672 fn shrinking_a_reservoir_keeps_a_uniform_subset_not_its_head() {
1673 let mut sampler = StreamReservoirSampler::new(1_000);
1676 for value in 0..1_000 {
1677 sampler.offer(&value.to_string());
1678 }
1679 sampler.shrink_to(100);
1680
1681 let retained: Vec<usize> = sampler
1682 .samples()
1683 .iter()
1684 .map(|value| value.parse().unwrap())
1685 .collect();
1686 assert_eq!(retained.len(), 100);
1687 let mean = retained.iter().sum::<usize>() as f64 / retained.len() as f64;
1688 assert!(
1689 (350.0..650.0).contains(&mean),
1690 "mean {mean} of {retained:?}"
1691 );
1692 let mut distinct = retained.clone();
1693 distinct.sort_unstable();
1694 distinct.dedup();
1695 assert_eq!(distinct.len(), 100, "a value was kept twice");
1696 }
1697
1698 #[test]
1699 fn test_reservoir_zero_capacity_still_retains_a_sample() {
1700 let mut sampler = StreamReservoirSampler::new(0);
1701 sampler.offer("value");
1702
1703 assert_eq!(sampler.samples(), ["value"]);
1704 }
1705
1706 #[test]
1707 fn test_late_column_is_backfilled_as_missing() {
1708 let mut collection = StreamingColumnCollection::new();
1709 collection.init_column_with_missing("late", 3);
1710 collection.process_record(&["late".to_string()], ["value".to_string()]);
1711
1712 let stats = collection.get_column_stats("late").unwrap();
1713 assert_eq!(stats.count, 4);
1714 assert_eq!(stats.null_count, 3);
1715 assert_eq!(stats.unique_count(), 1);
1716 }
1717
1718 #[test]
1719 fn test_welford_accuracy() {
1720 let mut accumulator = NumericAccumulator::new();
1721 for value in 1..=1000 {
1722 accumulator.update(value as f64);
1723 }
1724 let expected_mean = 500.5;
1725 let expected_variance = (1000.0 * 1000.0 - 1.0) / 12.0;
1726 assert!((accumulator.mean() - expected_mean).abs() < 1e-6);
1727 assert!((accumulator.population_variance() - expected_variance).abs() < 1.0);
1728 }
1729
1730 #[test]
1731 fn test_welford_merge() {
1732 let mut left = NumericAccumulator::new();
1733 let mut right = NumericAccumulator::new();
1734 let mut full = NumericAccumulator::new();
1735
1736 for value in 1..=500 {
1737 left.update(value as f64);
1738 full.update(value as f64);
1739 }
1740 for value in 501..=1000 {
1741 right.update(value as f64);
1742 full.update(value as f64);
1743 }
1744
1745 left.merge(&right);
1746 assert!((left.mean() - full.mean()).abs() < 1e-10);
1747 assert!((left.population_variance() - full.population_variance()).abs() < 1e-6);
1748 }
1749
1750 #[test]
1751 fn test_hll_cardinality() {
1752 let mut counter = HyperLogLog::new();
1753 let total = 100_000;
1754 for index in 0..total {
1755 counter.insert(&format!("item_{index}"));
1756 }
1757 let estimate = counter.count();
1758 let error = (estimate as f64 - total as f64).abs() / total as f64;
1759 assert!(error < 0.05);
1760 }
1761
1762 #[test]
1763 fn test_reservoir_uniformity() {
1764 let mut sampler = StreamReservoirSampler::seed(1000, 42);
1765 let total = 100_000;
1766 for index in 0..total {
1767 sampler.offer(&index.to_string());
1768 }
1769
1770 assert_eq!(sampler.samples().len(), 1000);
1771 let values: Vec<usize> = sampler
1772 .samples()
1773 .iter()
1774 .map(|value| value.parse().unwrap())
1775 .collect();
1776 let max_value = *values.iter().max().unwrap();
1777 assert!(max_value > total / 2);
1778 }
1779
1780 #[test]
1781 fn test_text_length_stats_streaming() {
1782 let mut stats = TextLengthStats::new();
1783 for &length in &[3, 5, 10, 1, 7] {
1784 stats.update(length);
1785 }
1786 assert_eq!(stats.min_length, 1);
1787 assert_eq!(stats.max_length, 10);
1788 assert!((stats.avg_length - 5.2).abs() < 1e-10);
1789 }
1790
1791 #[test]
1792 fn test_memory_usage_bounded() {
1793 let mut stats = StreamingStatistics::new();
1794 for index in 0..50_000 {
1795 stats.update(&format!("value_{index}"));
1796 }
1797 let usage = stats.memory_usage_bytes();
1798 assert!(usage < 1_000_000);
1799 }
1800}