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};
8use dataprof_metrics::analysis::inference::is_null_like_token;
9use dataprof_metrics::{
10 CardinalityEstimator, HyperLogLog, RowCompletenessSummary, RowDuplicateSummary,
11 value_matches_hint,
12};
13
14#[derive(Debug, Clone)]
24pub struct WelfordAccumulator {
25 count: u64,
26 mean: f64,
27 m2: f64,
28}
29
30impl WelfordAccumulator {
31 pub fn new() -> Self {
32 Self {
33 count: 0,
34 mean: 0.0,
35 m2: 0.0,
36 }
37 }
38
39 #[inline]
40 pub fn update(&mut self, value: f64) {
41 self.count += 1;
42 let delta = value - self.mean;
43 self.mean += delta / self.count as f64;
44 let delta2 = value - self.mean;
45 self.m2 += delta * delta2;
46 }
47
48 #[inline]
49 pub fn mean(&self) -> f64 {
50 if self.count == 0 { 0.0 } else { self.mean }
51 }
52
53 #[inline]
55 pub fn count(&self) -> u64 {
56 self.count
57 }
58
59 pub fn variance(&self) -> f64 {
60 if self.count < 2 {
61 0.0
62 } else {
63 self.m2 / self.count as f64
64 }
65 }
66
67 pub fn std_dev(&self) -> f64 {
68 self.variance().sqrt()
69 }
70
71 pub fn sample_variance(&self) -> f64 {
74 if self.count < 2 {
75 0.0
76 } else {
77 (self.m2 / (self.count - 1) as f64).max(0.0)
78 }
79 }
80
81 pub fn sample_std_dev(&self) -> f64 {
83 self.sample_variance().sqrt()
84 }
85
86 pub fn merge(&mut self, other: &WelfordAccumulator) {
87 if other.count == 0 {
88 return;
89 }
90 if self.count == 0 {
91 *self = other.clone();
92 return;
93 }
94
95 let combined_count = self.count + other.count;
96 let delta = other.mean - self.mean;
97 let new_mean = self.mean + delta * (other.count as f64 / combined_count as f64);
98 let new_m2 = self.m2
99 + other.m2
100 + delta * delta * (self.count as f64 * other.count as f64 / combined_count as f64);
101
102 self.count = combined_count;
103 self.mean = new_mean;
104 self.m2 = new_m2;
105 }
106}
107
108impl Default for WelfordAccumulator {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114#[derive(Debug, Clone)]
115pub struct StreamReservoirSampler {
116 reservoir: Vec<String>,
117 capacity: usize,
118 count: u64,
119 rng: SmallRng,
120}
121
122impl StreamReservoirSampler {
123 const DEFAULT_SEED: u64 = 0xDA7A_900D_F00D_5EED;
124
125 pub fn new(capacity: usize) -> Self {
126 let capacity = capacity.max(1);
127 Self {
128 reservoir: Vec::with_capacity(capacity.min(1024)),
129 capacity,
130 count: 0,
131 rng: SmallRng::seed_from_u64(Self::DEFAULT_SEED),
135 }
136 }
137
138 #[cfg(test)]
139 pub fn seed(capacity: usize, seed: u64) -> Self {
140 Self {
141 reservoir: Vec::with_capacity(capacity.min(1024)),
142 capacity,
143 count: 0,
144 rng: SmallRng::seed_from_u64(seed),
145 }
146 }
147
148 #[inline]
149 pub fn offer(&mut self, value: String) {
150 self.count += 1;
151 if self.reservoir.len() < self.capacity {
152 self.reservoir.push(value);
153 } else {
154 let index = self.rng.random_range(0..self.count as usize);
155 if index < self.capacity {
156 self.reservoir[index] = value;
157 }
158 }
159 }
160
161 pub fn shrink_to(&mut self, new_capacity: usize) {
162 let new_capacity = new_capacity.max(1);
163 self.capacity = new_capacity;
164 self.reservoir.truncate(new_capacity);
165 self.reservoir.shrink_to_fit();
166 }
167
168 pub fn samples(&self) -> &[String] {
169 &self.reservoir
170 }
171
172 pub fn memory_usage_bytes(&self) -> usize {
173 self.reservoir
174 .iter()
175 .map(|value| std::mem::size_of::<String>() + value.capacity())
176 .sum()
177 }
178
179 pub fn merge(&mut self, other: &StreamReservoirSampler) {
180 if other.count == 0 {
181 return;
182 }
183
184 let mut combined: Vec<String> = std::mem::take(&mut self.reservoir);
185 combined.extend(other.reservoir.iter().cloned());
186
187 let total = combined.len();
188 if total <= self.capacity {
189 self.reservoir = combined;
190 } else {
191 for index in 0..self.capacity {
192 let swap_with = self.rng.random_range(index..total);
193 combined.swap(index, swap_with);
194 }
195 combined.truncate(self.capacity);
196 self.reservoir = combined;
197 }
198
199 self.count += other.count;
200 }
201}
202
203#[derive(Debug, Clone)]
204pub struct TextLengthStats {
205 pub min_length: usize,
206 pub max_length: usize,
207 pub avg_length: f64,
208 welford: WelfordAccumulator,
209 histogram: [u64; 32],
210}
211
212impl TextLengthStats {
213 pub fn new() -> Self {
214 Self {
215 min_length: usize::MAX,
216 max_length: 0,
217 avg_length: 0.0,
218 welford: WelfordAccumulator::new(),
219 histogram: [0u64; 32],
220 }
221 }
222
223 pub fn update(&mut self, length: usize) {
224 self.min_length = self.min_length.min(length);
225 self.max_length = self.max_length.max(length);
226 self.welford.update(length as f64);
227 self.avg_length = self.welford.mean();
228
229 let bucket = if length == 0 {
230 0
231 } else {
232 (usize::BITS - length.leading_zeros()).min(31) as usize
233 };
234 self.histogram[bucket] += 1;
235 }
236
237 pub fn merge(&mut self, other: &TextLengthStats) {
238 if other.welford.count == 0 {
239 return;
240 }
241 if self.welford.count == 0 {
242 *self = other.clone();
243 return;
244 }
245
246 self.min_length = self.min_length.min(other.min_length);
247 self.max_length = self.max_length.max(other.max_length);
248 self.welford.merge(&other.welford);
249 self.avg_length = self.welford.mean();
250
251 for (left, right) in self.histogram.iter_mut().zip(other.histogram.iter()) {
252 *left += *right;
253 }
254 }
255
256 pub fn empty() -> Self {
257 Self {
258 min_length: 0,
259 max_length: 0,
260 avg_length: 0.0,
261 welford: WelfordAccumulator::new(),
262 histogram: [0u64; 32],
263 }
264 }
265}
266
267impl Default for TextLengthStats {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273#[derive(Debug, Clone)]
274pub struct StreamingStatistics {
275 pub count: usize,
276 pub null_count: usize,
277 pub min: f64,
278 pub max: f64,
279 welford: WelfordAccumulator,
280 hll: HyperLogLog,
281 sampler: StreamReservoirSampler,
282 text_length_tracker: TextLengthStats,
283 date_match_count: usize,
284}
285
286impl StreamingStatistics {
287 pub fn new() -> Self {
288 Self {
289 count: 0,
290 null_count: 0,
291 min: f64::INFINITY,
292 max: f64::NEG_INFINITY,
293 welford: WelfordAccumulator::new(),
294 hll: HyperLogLog::new(),
295 sampler: StreamReservoirSampler::new(10_000),
296 text_length_tracker: TextLengthStats::new(),
297 date_match_count: 0,
298 }
299 }
300
301 pub fn with_sample_capacity(max_sample: usize) -> Self {
302 Self {
303 sampler: StreamReservoirSampler::new(max_sample),
304 ..Self::new()
305 }
306 }
307
308 pub fn update(&mut self, value: &str) {
309 self.count += 1;
310
311 if is_null_like_token(value) {
312 self.null_count += 1;
313 return;
314 }
315
316 self.hll.insert(value);
317 self.sampler.offer(value.to_string());
318 self.text_length_tracker.update(value.len());
319 if value_matches_hint(value, SemanticHintKind::Temporal) {
320 self.date_match_count += 1;
321 }
322
323 if let Some(number) = value.parse::<f64>().ok().filter(|num| num.is_finite()) {
324 self.welford.update(number);
325 self.min = self.min.min(number);
326 self.max = self.max.max(number);
327 }
328 }
329
330 pub fn merge(&mut self, other: &StreamingStatistics) {
331 self.count += other.count;
332 self.null_count += other.null_count;
333
334 if other.min < self.min {
335 self.min = other.min;
336 }
337 if other.max > self.max {
338 self.max = other.max;
339 }
340
341 self.welford.merge(&other.welford);
342 self.hll.merge(&other.hll);
343 self.sampler.merge(&other.sampler);
344 self.text_length_tracker.merge(&other.text_length_tracker);
345 self.date_match_count += other.date_match_count;
346 }
347
348 pub fn mean(&self) -> f64 {
349 self.welford.mean()
350 }
351
352 pub fn variance(&self) -> f64 {
353 self.welford.variance()
354 }
355
356 pub fn std_dev(&self) -> f64 {
357 self.welford.std_dev()
358 }
359
360 pub fn unique_count(&self) -> usize {
361 if !self.unique_count_is_approximate() {
362 return self.sampler.samples().iter().collect::<HashSet<_>>().len();
363 }
364 self.hll.count() as usize
365 }
366
367 pub fn unique_count_is_approximate(&self) -> bool {
368 (self.sampler.samples().len() as u64) < self.sampler.count
369 }
370
371 pub fn sample_values(&self) -> &[String] {
372 self.sampler.samples()
373 }
374
375 pub fn date_match_count(&self) -> usize {
377 self.date_match_count
378 }
379
380 pub fn exact_numeric_aggregates(
387 &self,
388 ) -> Option<crate::profile_builder::ExactNumericAggregates> {
389 let count = self.welford.count();
390 if count == 0 {
391 return None;
392 }
393 Some(crate::profile_builder::ExactNumericAggregates {
394 min: self.min,
395 max: self.max,
396 mean: self.welford.mean(),
397 std_dev: self.welford.sample_std_dev(),
398 variance: self.welford.sample_variance(),
399 count: count as usize,
400 })
401 }
402
403 pub fn text_length_stats(&self) -> TextLengthStats {
404 if self.text_length_tracker.welford.count == 0 {
405 return TextLengthStats::empty();
406 }
407 self.text_length_tracker.clone()
408 }
409
410 pub fn reduce_sample_capacity(&mut self) {
411 self.sampler.shrink_to(self.sampler.capacity / 2);
412 }
413
414 pub fn memory_usage_bytes(&self) -> usize {
415 let struct_size = std::mem::size_of::<Self>();
416 let hll_size = self.hll.memory_usage_bytes();
417 let reservoir_size = self.sampler.memory_usage_bytes();
418
419 struct_size + hll_size + reservoir_size
420 }
421}
422
423impl Default for StreamingStatistics {
424 fn default() -> Self {
425 Self::new()
426 }
427}
428
429#[derive(Debug, Clone, Default)]
438pub struct RowUniquenessTracker {
439 rows_seen: usize,
440 distinct: CardinalityEstimator,
441}
442
443impl RowUniquenessTracker {
444 pub fn observe(&mut self, signature: String) {
445 self.rows_seen += 1;
446 self.distinct.insert_owned(signature);
447 }
448
449 pub fn rows_seen(&self) -> usize {
450 self.rows_seen
451 }
452
453 pub fn duplicate_rows(&self) -> usize {
455 self.rows_seen.saturating_sub(self.distinct.estimate())
456 }
457
458 pub fn is_approximate(&self) -> bool {
459 self.distinct.is_approximate()
460 }
461
462 pub fn merge(&mut self, other: &RowUniquenessTracker) {
463 self.rows_seen += other.rows_seen;
464 self.distinct.merge(&other.distinct);
465 }
466
467 pub fn memory_usage_bytes(&self) -> usize {
468 self.distinct.memory_usage_bytes()
469 }
470
471 pub fn summary(&self) -> Option<RowDuplicateSummary> {
474 if self.rows_seen == 0 {
475 return None;
476 }
477 Some(RowDuplicateSummary {
478 duplicate_rows: self.duplicate_rows(),
479 rows_checked: self.rows_seen,
480 approximate: self.is_approximate(),
481 })
482 }
483}
484
485#[derive(Debug, Clone, Default)]
492pub struct RowCompletenessTracker {
493 rows_seen: usize,
494 complete_rows: usize,
495}
496
497impl RowCompletenessTracker {
498 pub fn observe(&mut self, had_null: bool) {
500 self.rows_seen += 1;
501 if !had_null {
502 self.complete_rows += 1;
503 }
504 }
505
506 pub fn invalidate_completed_rows(&mut self) {
509 self.complete_rows = 0;
510 }
511
512 pub fn observe_aligned_columns(&mut self, columns: &[&[String]], total_rows: usize) {
519 if columns.is_empty() {
520 return;
521 }
522 for row_index in 0..total_rows {
523 let had_null = columns.iter().any(|cells| {
524 cells
525 .get(row_index)
526 .is_none_or(|value| is_null_like_token(value))
527 });
528 self.observe(had_null);
529 }
530 }
531
532 pub fn merge(&mut self, other: &RowCompletenessTracker) {
533 self.rows_seen += other.rows_seen;
534 self.complete_rows += other.complete_rows;
535 }
536
537 pub fn summary(&self) -> Option<RowCompletenessSummary> {
539 if self.rows_seen == 0 {
540 return None;
541 }
542 Some(RowCompletenessSummary {
543 complete_rows: self.complete_rows,
544 rows_checked: self.rows_seen,
545 })
546 }
547}
548
549pub struct StreamingColumnCollection {
550 columns: HashMap<String, StreamingStatistics>,
551 ordered_names: Vec<String>,
552 memory_limit_bytes: usize,
553 row_tracker: RowUniquenessTracker,
554 completeness_tracker: RowCompletenessTracker,
555 hint_bindings: ValueHintBindingAccumulator,
556}
557
558impl StreamingColumnCollection {
559 pub fn new() -> Self {
560 Self {
561 columns: HashMap::new(),
562 ordered_names: Vec::new(),
563 memory_limit_bytes: 100 * 1024 * 1024,
564 row_tracker: RowUniquenessTracker::default(),
565 completeness_tracker: RowCompletenessTracker::default(),
566 hint_bindings: ValueHintBindingAccumulator::default(),
567 }
568 }
569
570 pub fn memory_limit(limit_mb: usize) -> Self {
571 Self {
572 columns: HashMap::new(),
573 ordered_names: Vec::new(),
574 memory_limit_bytes: limit_mb * 1024 * 1024,
575 row_tracker: RowUniquenessTracker::default(),
576 completeness_tracker: RowCompletenessTracker::default(),
577 hint_bindings: ValueHintBindingAccumulator::default(),
578 }
579 }
580
581 pub fn with_semantic_hints(mut self, hints: &SemanticHints) -> Self {
583 self.hint_bindings = ValueHintBindingAccumulator::new(hints);
584 self
585 }
586
587 pub fn init_columns(&mut self, headers: &[String]) {
588 for header in headers {
589 if !self.columns.contains_key(header) {
590 self.columns
591 .insert(header.clone(), StreamingStatistics::default());
592 self.ordered_names.push(header.clone());
593 }
594 }
595 }
596
597 pub fn init_column_with_missing(&mut self, header: &str, prior_rows: usize) {
604 if self.columns.contains_key(header) {
605 return;
606 }
607
608 let stats = StreamingStatistics {
609 count: prior_rows,
610 null_count: prior_rows,
611 ..Default::default()
612 };
613 self.columns.insert(header.to_string(), stats);
614 self.ordered_names.push(header.to_string());
615 if prior_rows > 0 {
616 self.completeness_tracker.invalidate_completed_rows();
617 }
618 }
619
620 pub fn process_record<I>(&mut self, headers: &[String], values: I)
621 where
622 I: IntoIterator<Item = String>,
623 {
624 let mut row_signature = String::new();
627 let mut row_has_null = false;
628 let mut values = values.into_iter();
629
630 for header in headers {
634 let value = values.next().unwrap_or_default();
635 let _ = write!(row_signature, "{}:", value.len());
636 row_signature.push_str(&value);
637
638 if !self.columns.contains_key(header) {
639 self.ordered_names.push(header.clone());
640 }
641 let stats = self.columns.entry(header.to_string()).or_default();
642 stats.update(&value);
643 row_has_null |= is_null_like_token(&value);
646 self.hint_bindings.observe(header, &value);
647 }
648
649 if !headers.is_empty() {
650 self.row_tracker.observe(row_signature);
651 self.completeness_tracker.observe(row_has_null);
652 }
653 }
654
655 pub fn row_duplicate_summary(&self) -> Option<RowDuplicateSummary> {
657 self.row_tracker.summary()
658 }
659
660 pub fn row_completeness_summary(&self) -> Option<RowCompletenessSummary> {
662 self.completeness_tracker.summary()
663 }
664
665 pub fn semantic_hint_bindings(&self) -> Vec<SemanticHintBinding> {
667 self.hint_bindings
668 .bindings(self.ordered_names.iter().map(String::as_str))
669 }
670
671 pub fn get_column_stats(&self, column_name: &str) -> Option<&StreamingStatistics> {
672 self.columns.get(column_name)
673 }
674
675 pub fn column_names(&self) -> Vec<String> {
676 self.ordered_names.clone()
677 }
678
679 pub fn memory_usage_bytes(&self) -> usize {
680 self.columns
681 .values()
682 .map(|stats| stats.memory_usage_bytes())
683 .sum::<usize>()
684 + self.row_tracker.memory_usage_bytes()
685 }
686
687 pub fn is_memory_pressure(&self) -> bool {
688 self.memory_usage_bytes() > (self.memory_limit_bytes * 80 / 100)
689 }
690
691 pub fn reduce_memory_usage(&mut self) {
692 for stats in self.columns.values_mut() {
693 stats.reduce_sample_capacity();
694 }
695 }
696
697 pub fn column_type_fingerprint(&self) -> u64 {
702 use std::collections::hash_map::DefaultHasher;
703 use std::hash::{Hash, Hasher};
704
705 let mut hasher = DefaultHasher::new();
706 let mut names: Vec<&String> = self.columns.keys().collect();
707 names.sort();
708 for name in names {
709 let stats = &self.columns[name];
710 let data_type = infer_data_type_streaming(stats);
711 name.hash(&mut hasher);
712 std::mem::discriminant(&data_type).hash(&mut hasher);
713 }
714 hasher.finish()
715 }
716
717 pub fn merge(&mut self, other: StreamingColumnCollection) {
718 for (column_name, other_stats) in other.columns {
719 match self.columns.get_mut(&column_name) {
720 Some(existing_stats) => existing_stats.merge(&other_stats),
721 None => {
722 self.columns.insert(column_name, other_stats);
723 }
724 }
725 }
726 self.row_tracker.merge(&other.row_tracker);
727 self.hint_bindings.merge(&other.hint_bindings);
728 }
729}
730
731impl Default for StreamingColumnCollection {
732 fn default() -> Self {
733 Self::new()
734 }
735}
736
737#[cfg(test)]
738mod row_tracker_tests {
739 use super::*;
740
741 fn record(collection: &mut StreamingColumnCollection, headers: &[String], values: &[&str]) {
742 collection.process_record(headers, values.iter().map(|v| v.to_string()));
743 }
744
745 fn completeness(collection: &StreamingColumnCollection) -> (usize, usize) {
746 let summary = collection
747 .row_completeness_summary()
748 .expect("rows were observed");
749 (summary.complete_rows, summary.rows_checked)
750 }
751
752 #[test]
753 fn test_complete_rows_count_rows_not_null_cells() {
754 let headers = vec!["a".to_string(), "b".to_string()];
755 let mut collection = StreamingColumnCollection::new();
756 record(&mut collection, &headers, &["", ""]);
760 record(&mut collection, &headers, &["x", "1"]);
761 record(&mut collection, &headers, &["y", "2"]);
762 record(&mut collection, &headers, &["z", "3"]);
763
764 assert_eq!(completeness(&collection), (3, 4));
765 }
766
767 #[test]
768 fn test_null_like_tokens_make_a_row_incomplete() {
769 let headers = vec!["a".to_string(), "b".to_string()];
770 let mut collection = StreamingColumnCollection::new();
771 record(&mut collection, &headers, &["x", "NULL"]);
774 record(&mut collection, &headers, &["y", "NaN"]);
775 record(&mut collection, &headers, &["z", " "]);
776 record(&mut collection, &headers, &["w", "3"]);
777
778 assert_eq!(completeness(&collection), (1, 4));
779 }
780
781 #[test]
782 fn test_ragged_rows_are_incomplete() {
783 let headers = vec!["a".to_string(), "b".to_string()];
784 let mut collection = StreamingColumnCollection::new();
785 record(&mut collection, &headers, &["x"]);
786 record(&mut collection, &headers, &["y", "1"]);
787
788 assert_eq!(completeness(&collection), (1, 2));
789 }
790
791 #[test]
792 fn test_a_late_column_makes_earlier_rows_incomplete() {
793 let mut collection = StreamingColumnCollection::new();
794 let first = vec!["a".to_string()];
795 record(&mut collection, &first, &["x"]);
796 record(&mut collection, &first, &["y"]);
797 assert_eq!(completeness(&collection), (2, 2));
798
799 collection.init_column_with_missing("b", 2);
802 let both = vec!["a".to_string(), "b".to_string()];
803 record(&mut collection, &both, &["z", "1"]);
804
805 assert_eq!(completeness(&collection), (1, 3));
806 }
807
808 #[test]
809 fn test_no_rows_means_no_completeness_summary() {
810 let collection = StreamingColumnCollection::new();
811 assert!(collection.row_completeness_summary().is_none());
812 }
813
814 #[test]
815 fn test_aligned_columns_count_the_same_complete_rows() {
816 let a: Vec<String> = ["", "x", "y"].iter().map(|v| v.to_string()).collect();
817 let b: Vec<String> = ["", "1", "2"].iter().map(|v| v.to_string()).collect();
818 let mut tracker = RowCompletenessTracker::default();
819 tracker.observe_aligned_columns(&[a.as_slice(), b.as_slice()], 3);
820
821 let summary = tracker.summary().expect("rows were observed");
822 assert_eq!((summary.complete_rows, summary.rows_checked), (2, 3));
823 }
824
825 #[test]
826 fn test_aligned_columns_treat_a_short_column_as_missing() {
827 let a: Vec<String> = ["x", "y"].iter().map(|v| v.to_string()).collect();
828 let b: Vec<String> = ["1"].iter().map(|v| v.to_string()).collect();
829 let mut tracker = RowCompletenessTracker::default();
830 tracker.observe_aligned_columns(&[a.as_slice(), b.as_slice()], 2);
831
832 let summary = tracker.summary().expect("rows were observed");
833 assert_eq!((summary.complete_rows, summary.rows_checked), (1, 2));
834 }
835
836 #[test]
837 fn test_exact_duplicates_including_null_rows() {
838 let headers = vec!["a".to_string(), "b".to_string()];
839 let mut collection = StreamingColumnCollection::new();
840 record(&mut collection, &headers, &["x", ""]);
841 record(&mut collection, &headers, &["x", ""]);
842 record(&mut collection, &headers, &["x", "1"]);
843 record(&mut collection, &headers, &["", ""]);
844
845 let summary = collection
846 .row_duplicate_summary()
847 .expect("rows were observed");
848 assert_eq!(summary.rows_checked, 4);
849 assert_eq!(summary.duplicate_rows, 1);
852 assert!(!summary.approximate);
853 }
854
855 #[test]
856 fn test_field_boundaries_are_unambiguous() {
857 let headers = vec!["a".to_string(), "b".to_string()];
858 let mut collection = StreamingColumnCollection::new();
859 record(&mut collection, &headers, &["ab", "c"]);
860 record(&mut collection, &headers, &["a", "bc"]);
861
862 let summary = collection
863 .row_duplicate_summary()
864 .expect("rows were observed");
865 assert_eq!(
866 summary.duplicate_rows, 0,
867 "different field splits must not collide"
868 );
869 }
870
871 #[test]
872 fn test_ragged_rows_normalize_missing_trailing_fields() {
873 let headers = vec!["a".to_string(), "b".to_string()];
874 let mut collection = StreamingColumnCollection::new();
875 record(&mut collection, &headers, &["x"]);
876 record(&mut collection, &headers, &["x", ""]);
877
878 let summary = collection
879 .row_duplicate_summary()
880 .expect("rows were observed");
881 assert_eq!(summary.rows_checked, 2);
882 assert_eq!(summary.duplicate_rows, 1);
883 assert_eq!(
884 collection
885 .get_column_stats("b")
886 .expect("column b")
887 .null_count,
888 2
889 );
890 }
891
892 #[test]
893 fn test_no_rows_means_no_summary() {
894 let collection = StreamingColumnCollection::new();
895 assert!(collection.row_duplicate_summary().is_none());
896 }
897
898 #[test]
899 fn test_spills_to_approximate_beyond_distinct_threshold() {
900 let headers = vec!["n".to_string()];
901 let mut collection = StreamingColumnCollection::new();
902 let distinct = dataprof_metrics::EXACT_CARDINALITY_THRESHOLD + 500;
903 for i in 0..distinct {
904 record(&mut collection, &headers, &[&i.to_string()]);
905 }
906 for i in 0..distinct {
908 record(&mut collection, &headers, &[&i.to_string()]);
909 }
910
911 let summary = collection
912 .row_duplicate_summary()
913 .expect("rows were observed");
914 assert!(summary.approximate, "past the threshold the count is HLL");
915 assert_eq!(summary.rows_checked, distinct * 2);
916 let error = (summary.duplicate_rows as f64 - distinct as f64).abs() / distinct as f64;
917 assert!(
918 error < 0.05,
919 "estimated {} duplicates for {distinct} true, off by {error:.4}",
920 summary.duplicate_rows
921 );
922 }
923
924 #[test]
925 fn test_merge_combines_row_trackers() {
926 let headers = vec!["a".to_string()];
927 let mut left = StreamingColumnCollection::new();
928 let mut right = StreamingColumnCollection::new();
929 record(&mut left, &headers, &["x"]);
930 record(&mut left, &headers, &["y"]);
931 record(&mut right, &headers, &["x"]);
932
933 left.merge(right);
934 let summary = left.row_duplicate_summary().expect("rows were observed");
935 assert_eq!(summary.rows_checked, 3);
936 assert_eq!(summary.duplicate_rows, 1);
937 }
938}
939
940#[cfg(test)]
941mod tests {
942 use super::*;
943
944 #[test]
945 fn test_streaming_statistics() {
946 let mut stats = StreamingStatistics::new();
947
948 stats.update("10.5");
949 stats.update("20.0");
950 stats.update("15.5");
951 stats.update("");
952
953 assert_eq!(stats.count, 4);
954 assert_eq!(stats.null_count, 1);
955 assert_eq!(stats.unique_count(), 3);
956 assert!(!stats.unique_count_is_approximate());
957 assert!((stats.mean() - 15.333333333333334).abs() < 1e-10);
958 assert_eq!(stats.min, 10.5);
959 assert_eq!(stats.max, 20.0);
960 }
961
962 #[test]
963 fn test_streaming_statistics_merge() {
964 let mut stats1 = StreamingStatistics::new();
965 stats1.update("10");
966 stats1.update("20");
967
968 let mut stats2 = StreamingStatistics::new();
969 stats2.update("30");
970 stats2.update("40");
971
972 stats1.merge(&stats2);
973
974 assert_eq!(stats1.count, 4);
975 assert_eq!(stats1.unique_count(), 4);
976 assert!(!stats1.unique_count_is_approximate());
977 assert!((stats1.mean() - 25.0).abs() < 1e-10);
978 assert_eq!(stats1.min, 10.0);
979 assert_eq!(stats1.max, 40.0);
980 }
981
982 #[test]
983 fn test_column_collection() {
984 let mut collection = StreamingColumnCollection::new();
985 let headers = vec!["name".to_string(), "age".to_string()];
986
987 collection.process_record(&headers, vec!["Alice".to_string(), "25".to_string()]);
988 collection.process_record(&headers, vec!["Bob".to_string(), "30".to_string()]);
989
990 let age_stats = collection.get_column_stats("age").unwrap();
991 assert_eq!(age_stats.count, 2);
992 assert!((age_stats.mean() - 27.5).abs() < 1e-10);
993 }
994
995 #[test]
996 fn test_unique_count_becomes_approximate_only_after_reservoir_truncation() {
997 let mut stats = StreamingStatistics::with_sample_capacity(2);
998 stats.update("a");
999 stats.update("b");
1000 assert_eq!(stats.unique_count(), 2);
1001 assert!(!stats.unique_count_is_approximate());
1002
1003 stats.update("c");
1004 assert!(stats.unique_count_is_approximate());
1005 }
1006
1007 #[test]
1008 fn test_default_reservoir_sampling_is_deterministic() {
1009 let mut left = StreamReservoirSampler::new(10);
1010 let mut right = StreamReservoirSampler::new(10);
1011 for value in 0..1_000 {
1012 left.offer(value.to_string());
1013 right.offer(value.to_string());
1014 }
1015
1016 assert_eq!(left.samples(), right.samples());
1017 }
1018
1019 #[test]
1020 fn test_reservoir_zero_capacity_still_retains_a_sample() {
1021 let mut sampler = StreamReservoirSampler::new(0);
1022 sampler.offer("value".to_string());
1023
1024 assert_eq!(sampler.samples(), ["value"]);
1025 }
1026
1027 #[test]
1028 fn test_late_column_is_backfilled_as_missing() {
1029 let mut collection = StreamingColumnCollection::new();
1030 collection.init_column_with_missing("late", 3);
1031 collection.process_record(&["late".to_string()], ["value".to_string()]);
1032
1033 let stats = collection.get_column_stats("late").unwrap();
1034 assert_eq!(stats.count, 4);
1035 assert_eq!(stats.null_count, 3);
1036 assert_eq!(stats.unique_count(), 1);
1037 }
1038
1039 #[test]
1040 fn test_welford_accuracy() {
1041 let mut accumulator = WelfordAccumulator::new();
1042 for value in 1..=1000 {
1043 accumulator.update(value as f64);
1044 }
1045 let expected_mean = 500.5;
1046 let expected_variance = (1000.0 * 1000.0 - 1.0) / 12.0;
1047 assert!((accumulator.mean() - expected_mean).abs() < 1e-6);
1048 assert!((accumulator.variance() - expected_variance).abs() < 1.0);
1049 }
1050
1051 #[test]
1052 fn test_welford_merge() {
1053 let mut left = WelfordAccumulator::new();
1054 let mut right = WelfordAccumulator::new();
1055 let mut full = WelfordAccumulator::new();
1056
1057 for value in 1..=500 {
1058 left.update(value as f64);
1059 full.update(value as f64);
1060 }
1061 for value in 501..=1000 {
1062 right.update(value as f64);
1063 full.update(value as f64);
1064 }
1065
1066 left.merge(&right);
1067 assert!((left.mean() - full.mean()).abs() < 1e-10);
1068 assert!((left.variance() - full.variance()).abs() < 1e-6);
1069 }
1070
1071 #[test]
1072 fn test_hll_cardinality() {
1073 let mut counter = HyperLogLog::new();
1074 let total = 100_000;
1075 for index in 0..total {
1076 counter.insert(&format!("item_{index}"));
1077 }
1078 let estimate = counter.count();
1079 let error = (estimate as f64 - total as f64).abs() / total as f64;
1080 assert!(error < 0.05);
1081 }
1082
1083 #[test]
1084 fn test_reservoir_uniformity() {
1085 let mut sampler = StreamReservoirSampler::seed(1000, 42);
1086 let total = 100_000;
1087 for index in 0..total {
1088 sampler.offer(index.to_string());
1089 }
1090
1091 assert_eq!(sampler.samples().len(), 1000);
1092 let values: Vec<usize> = sampler
1093 .samples()
1094 .iter()
1095 .map(|value| value.parse().unwrap())
1096 .collect();
1097 let max_value = *values.iter().max().unwrap();
1098 assert!(max_value > total / 2);
1099 }
1100
1101 #[test]
1102 fn test_text_length_stats_streaming() {
1103 let mut stats = TextLengthStats::new();
1104 for &length in &[3, 5, 10, 1, 7] {
1105 stats.update(length);
1106 }
1107 assert_eq!(stats.min_length, 1);
1108 assert_eq!(stats.max_length, 10);
1109 assert!((stats.avg_length - 5.2).abs() < 1e-10);
1110 }
1111
1112 #[test]
1113 fn test_memory_usage_bounded() {
1114 let mut stats = StreamingStatistics::new();
1115 for index in 0..50_000 {
1116 stats.update(&format!("value_{index}"));
1117 }
1118 let usage = stats.memory_usage_bytes();
1119 assert!(usage < 1_000_000);
1120 }
1121}