Skip to main content

dataprof_db/
streaming.rs

1//! Streaming utilities for processing large database result sets
2
3use crate::DataProfilerError;
4use std::collections::HashMap;
5
6/// Configuration for streaming database results
7#[derive(Debug, Clone)]
8pub struct StreamingConfig {
9    pub batch_size: usize,
10    pub max_memory_mb: usize,
11    pub progress_callback: Option<fn(u64, u64)>,
12}
13
14impl Default for StreamingConfig {
15    fn default() -> Self {
16        Self {
17            batch_size: 10000,
18            max_memory_mb: 512,
19            progress_callback: None,
20        }
21    }
22}
23
24/// Utility to merge multiple batches of column data
25pub fn merge_column_batches(
26    batches: Vec<HashMap<String, Vec<String>>>,
27) -> Result<HashMap<String, Vec<String>>, DataProfilerError> {
28    if batches.is_empty() {
29        return Ok(HashMap::new());
30    }
31
32    let mut merged: HashMap<String, Vec<String>> = HashMap::new();
33
34    for batch in batches {
35        for (column_name, column_data) in batch {
36            merged.entry(column_name).or_default().extend(column_data);
37        }
38    }
39
40    Ok(merged)
41}
42
43/// Calculate memory usage of column data (rough estimate)
44pub fn estimate_memory_usage(columns: &HashMap<String, Vec<String>>) -> usize {
45    columns
46        .iter()
47        .map(|(name, data)| name.len() + data.iter().map(|s| s.len()).sum::<usize>())
48        .sum::<usize>()
49}
50
51/// Sample large datasets to fit within memory constraints
52pub fn apply_sampling_if_needed(
53    mut columns: HashMap<String, Vec<String>>,
54    max_memory_mb: usize,
55    sampling_ratio: f64,
56) -> Result<(HashMap<String, Vec<String>>, bool), DataProfilerError> {
57    let memory_usage_bytes = estimate_memory_usage(&columns);
58    let memory_usage_mb = memory_usage_bytes / 1_048_576;
59
60    if memory_usage_mb <= max_memory_mb {
61        return Ok((columns, false));
62    }
63
64    // decode-audit: no-data — an empty column map has zero rows; the sampling
65    // below then returns an empty result rather than fabricating data.
66    let total_rows = columns.values().next().map(|v| v.len()).unwrap_or(0);
67    let target_rows = (total_rows as f64 * sampling_ratio) as usize;
68
69    if target_rows == 0 {
70        return Ok((HashMap::new(), true));
71    }
72
73    let step = total_rows / target_rows;
74    if step <= 1 {
75        return Ok((columns, false));
76    }
77
78    for column_data in columns.values_mut() {
79        let sampled: Vec<String> = column_data
80            .iter()
81            .step_by(step)
82            .take(target_rows)
83            .cloned()
84            .collect();
85        *column_data = sampled;
86    }
87
88    Ok((columns, true))
89}
90
91/// Progress tracking for streaming operations
92pub struct StreamingProgress {
93    pub total_rows: Option<u64>,
94    pub processed_rows: u64,
95    pub batches_processed: u64,
96    pub start_time: std::time::Instant,
97}
98
99impl StreamingProgress {
100    pub fn new(total_rows: Option<u64>) -> Self {
101        Self {
102            total_rows,
103            processed_rows: 0,
104            batches_processed: 0,
105            start_time: std::time::Instant::now(),
106        }
107    }
108
109    pub fn update(&mut self, batch_size: u64) {
110        self.processed_rows += batch_size;
111        self.batches_processed += 1;
112    }
113
114    pub fn percentage(&self) -> Option<f64> {
115        self.total_rows.map(|total| {
116            if total == 0 {
117                100.0
118            } else {
119                (self.processed_rows as f64 / total as f64) * 100.0
120            }
121        })
122    }
123
124    pub fn elapsed(&self) -> std::time::Duration {
125        self.start_time.elapsed()
126    }
127
128    pub fn estimated_total_time(&self) -> Option<std::time::Duration> {
129        if let Some(percentage) = self.percentage()
130            && percentage > 0.0
131        {
132            let elapsed_secs = self.elapsed().as_secs_f64();
133            let total_secs = elapsed_secs * (100.0 / percentage);
134            return Some(std::time::Duration::from_secs_f64(total_secs));
135        }
136        None
137    }
138
139    pub fn rows_per_second(&self) -> f64 {
140        let elapsed_secs = self.elapsed().as_secs_f64();
141        if elapsed_secs > 0.0 {
142            self.processed_rows as f64 / elapsed_secs
143        } else {
144            0.0
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_merge_column_batches() {
155        let batch1 = {
156            let mut map = HashMap::new();
157            map.insert("col1".to_string(), vec!["a".to_string(), "b".to_string()]);
158            map.insert("col2".to_string(), vec!["1".to_string(), "2".to_string()]);
159            map
160        };
161
162        let batch2 = {
163            let mut map = HashMap::new();
164            map.insert("col1".to_string(), vec!["c".to_string(), "d".to_string()]);
165            map.insert("col2".to_string(), vec!["3".to_string(), "4".to_string()]);
166            map
167        };
168
169        let merged = merge_column_batches(vec![batch1, batch2]).expect("Failed to merge batches");
170
171        assert_eq!(
172            merged.get("col1").expect("col1 not found"),
173            &vec!["a", "b", "c", "d"]
174        );
175        assert_eq!(
176            merged.get("col2").expect("col2 not found"),
177            &vec!["1", "2", "3", "4"]
178        );
179    }
180
181    #[test]
182    fn test_memory_estimation() {
183        let mut columns = HashMap::new();
184        columns.insert(
185            "test".to_string(),
186            vec!["hello".to_string(), "world".to_string()],
187        );
188
189        let memory = estimate_memory_usage(&columns);
190        assert_eq!(memory, 14);
191    }
192
193    #[test]
194    fn test_streaming_progress() {
195        let mut progress = StreamingProgress::new(Some(1000));
196
197        assert_eq!(progress.percentage(), Some(0.0));
198
199        progress.update(250);
200        assert_eq!(progress.percentage(), Some(25.0));
201
202        progress.update(250);
203        assert_eq!(progress.percentage(), Some(50.0));
204
205        assert_eq!(progress.batches_processed, 2);
206        assert_eq!(progress.processed_rows, 500);
207    }
208}