Skip to main content

dataprof_db/
streaming.rs

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