Skip to main content

scirs2_io/streaming/
mod.rs

1//! Streaming and iterator interfaces for large data handling
2//!
3//! This module provides memory-efficient streaming interfaces for processing large datasets
4//! without loading everything into memory at once. It includes iterator-based APIs for
5//! reading data in chunks, streaming decompression, and incremental processing.
6//!
7//! ## Features
8//!
9//! - **Chunked Reading**: Read large files in configurable chunks
10//! - **Streaming Decompression**: Decompress data on-the-fly while reading
11//! - **Iterator Interfaces**: Process data using Rust's iterator paradigm
12//! - **Memory Efficiency**: Minimize memory usage for large dataset processing
13//! - **Parallel Processing**: Combine with rayon for parallel chunk processing
14//! - **Format Support**: Streaming support for CSV, Matrix Market, and binary formats
15//!
16//! ## Examples
17//!
18//! ```rust,no_run
19//! use scirs2_io::streaming::{ChunkedReader, StreamingConfig};
20//! use std::path::Path;
21//!
22//! // Read a large CSV file in 1MB chunks
23//! let config = StreamingConfig::new()
24//!     .chunk_size(1024 * 1024)  // 1MB chunks
25//!     .buffer_size(8192);       // 8KB buffer
26//!
27//! let reader = ChunkedReader::new("large_data.csv", config)?;
28//!
29//! for (chunk_id, chunk_data) in reader.enumerate() {
30//!     let data = chunk_data?;
31//!     println!("Processing chunk {}: {} bytes", chunk_id, data.len());
32//!     // Process data without loading entire file
33//! }
34//! # Ok::<(), scirs2_io::error::IoError>(())
35//! ```
36
37use std::fs::File;
38use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
39use std::path::Path;
40
41use crate::compression::CompressionAlgorithm;
42use crate::error::{IoError, Result};
43
44/// Configuration for streaming operations
45#[derive(Debug, Clone)]
46pub struct StreamingConfig {
47    /// Size of each chunk in bytes (default: 64KB)
48    pub chunk_size: usize,
49    /// Buffer size for I/O operations (default: 8KB)
50    pub buffer_size: usize,
51    /// Enable automatic compression detection
52    pub auto_detect_compression: bool,
53    /// Compression algorithm (if known)
54    pub compression: Option<CompressionAlgorithm>,
55    /// Maximum number of chunks to process (None for unlimited)
56    pub max_chunks: Option<usize>,
57    /// Skip the first N chunks
58    pub skip_chunks: usize,
59}
60
61impl Default for StreamingConfig {
62    fn default() -> Self {
63        Self {
64            chunk_size: 64 * 1024, // 64KB
65            buffer_size: 8 * 1024, // 8KB
66            auto_detect_compression: true,
67            compression: None,
68            max_chunks: None,
69            skip_chunks: 0,
70        }
71    }
72}
73
74impl StreamingConfig {
75    /// Create a new streaming configuration with default values
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Set the chunk size
81    pub fn chunk_size(mut self, size: usize) -> Self {
82        self.chunk_size = size;
83        self
84    }
85
86    /// Set the buffer size
87    pub fn buffer_size(mut self, size: usize) -> Self {
88        self.buffer_size = size;
89        self
90    }
91
92    /// Enable or disable automatic compression detection
93    pub fn auto_detect_compression(mut self, enable: bool) -> Self {
94        self.auto_detect_compression = enable;
95        self
96    }
97
98    /// Set the compression algorithm
99    pub fn compression(mut self, algorithm: CompressionAlgorithm) -> Self {
100        self.compression = Some(algorithm);
101        self
102    }
103
104    /// Set maximum number of chunks to process
105    pub fn max_chunks(mut self, max: usize) -> Self {
106        self.max_chunks = Some(max);
107        self
108    }
109
110    /// Set number of chunks to skip
111    pub fn skip_chunks(mut self, skip: usize) -> Self {
112        self.skip_chunks = skip;
113        self
114    }
115}
116
117/// Iterator for reading files in chunks
118pub struct ChunkedReader {
119    reader: BufReader<File>,
120    config: StreamingConfig,
121    chunks_read: usize,
122    total_bytes_read: u64,
123    finished: bool,
124}
125
126impl ChunkedReader {
127    /// Create a new chunked reader for the specified file
128    pub fn new<P: AsRef<Path>>(path: P, config: StreamingConfig) -> Result<Self> {
129        let file = File::open(path.as_ref())
130            .map_err(|e| IoError::FileError(format!("Failed to open file: {e}")))?;
131
132        let reader = BufReader::with_capacity(config.buffer_size, file);
133
134        Ok(Self {
135            reader,
136            config,
137            chunks_read: 0,
138            total_bytes_read: 0,
139            finished: false,
140        })
141    }
142
143    /// Get the total number of bytes read so far
144    pub fn bytes_read(&self) -> u64 {
145        self.total_bytes_read
146    }
147
148    /// Get the number of chunks read so far
149    pub fn chunks_read(&self) -> usize {
150        self.chunks_read
151    }
152
153    /// Check if the reader has finished
154    pub fn is_finished(&self) -> bool {
155        self.finished
156    }
157
158    /// Skip the specified number of bytes
159    pub fn skip_bytes(&mut self, bytes: u64) -> Result<u64> {
160        let skipped = self
161            .reader
162            .seek(SeekFrom::Current(bytes as i64))
163            .map_err(|e| IoError::FileError(format!("Failed to skip bytes: {e}")))?;
164        self.total_bytes_read += bytes;
165        Ok(skipped)
166    }
167
168    /// Get the file position
169    pub fn position(&mut self) -> Result<u64> {
170        self.reader
171            .stream_position()
172            .map_err(|e| IoError::FileError(format!("Failed to get position: {e}")))
173    }
174}
175
176impl Iterator for ChunkedReader {
177    type Item = Result<Vec<u8>>;
178
179    fn next(&mut self) -> Option<Self::Item> {
180        if self.finished {
181            return None;
182        }
183
184        // Check if we should skip chunks
185        if self.chunks_read < self.config.skip_chunks {
186            match self.skip_bytes(self.config.chunk_size as u64) {
187                Ok(_) => {
188                    self.chunks_read += 1;
189                    return self.next(); // Recursive call to skip
190                }
191                Err(e) => return Some(Err(e)),
192            }
193        }
194
195        // Check max chunks limit
196        if let Some(max) = self.config.max_chunks {
197            if self.chunks_read >= max + self.config.skip_chunks {
198                self.finished = true;
199                return None;
200            }
201        }
202
203        let mut chunk = vec![0u8; self.config.chunk_size];
204        match self.reader.read(&mut chunk) {
205            Ok(0) => {
206                // End of file
207                self.finished = true;
208                None
209            }
210            Ok(bytes_read) => {
211                chunk.truncate(bytes_read);
212                self.total_bytes_read += bytes_read as u64;
213                self.chunks_read += 1;
214                Some(Ok(chunk))
215            }
216            Err(e) => {
217                self.finished = true;
218                Some(Err(IoError::FileError(format!(
219                    "Failed to read chunk: {}",
220                    e
221                ))))
222            }
223        }
224    }
225}
226
227/// Iterator for reading lines from a file in chunks
228pub struct LineChunkedReader {
229    reader: BufReader<File>,
230    config: StreamingConfig,
231    lines_read: usize,
232    finished: bool,
233}
234
235impl LineChunkedReader {
236    /// Create a new line-based chunked reader
237    pub fn new<P: AsRef<Path>>(path: P, config: StreamingConfig) -> Result<Self> {
238        let file = File::open(path.as_ref())
239            .map_err(|e| IoError::FileError(format!("Failed to open file: {e}")))?;
240
241        let reader = BufReader::with_capacity(config.buffer_size, file);
242
243        Ok(Self {
244            reader,
245            config,
246            lines_read: 0,
247            finished: false,
248        })
249    }
250
251    /// Get the number of lines read so far
252    pub fn lines_read(&self) -> usize {
253        self.lines_read
254    }
255
256    /// Check if the reader has finished
257    pub fn is_finished(&self) -> bool {
258        self.finished
259    }
260}
261
262impl Iterator for LineChunkedReader {
263    type Item = Result<Vec<String>>;
264
265    fn next(&mut self) -> Option<Self::Item> {
266        if self.finished {
267            return None;
268        }
269
270        // Check if we should skip lines
271        if self.lines_read < self.config.skip_chunks {
272            let mut line = String::new();
273            match self.reader.read_line(&mut line) {
274                Ok(0) => {
275                    self.finished = true;
276                    return None;
277                }
278                Ok(_) => {
279                    self.lines_read += 1;
280                    return self.next(); // Recursive call to skip
281                }
282                Err(e) => {
283                    return Some(Err(IoError::FileError(format!(
284                        "Failed to skip line: {}",
285                        e
286                    ))))
287                }
288            }
289        }
290
291        // Check max chunks limit
292        if let Some(max) = self.config.max_chunks {
293            if self.lines_read >= max + self.config.skip_chunks {
294                self.finished = true;
295                return None;
296            }
297        }
298
299        let mut lines = Vec::new();
300        let target_lines = self.config.chunk_size; // Treat chunk_size as number of lines
301
302        for _ in 0..target_lines {
303            let mut line = String::new();
304            match self.reader.read_line(&mut line) {
305                Ok(0) => {
306                    // End of file
307                    self.finished = true;
308                    break;
309                }
310                Ok(_) => {
311                    // Remove trailing newline
312                    if line.ends_with('\n') {
313                        line.pop();
314                        if line.ends_with('\r') {
315                            line.pop();
316                        }
317                    }
318                    lines.push(line);
319                    self.lines_read += 1;
320                }
321                Err(e) => {
322                    self.finished = true;
323                    return Some(Err(IoError::FileError(format!(
324                        "Failed to read line: {}",
325                        e
326                    ))));
327                }
328            }
329        }
330
331        if lines.is_empty() {
332            None
333        } else {
334            Some(Ok(lines))
335        }
336    }
337}
338
339/// Streaming CSV reader that processes rows in chunks
340pub struct StreamingCsvReader {
341    line_reader: LineChunkedReader,
342    header: Option<Vec<String>>,
343    delimiter: char,
344    has_header: bool,
345}
346
347impl StreamingCsvReader {
348    /// Create a new streaming CSV reader
349    pub fn new<P: AsRef<Path>>(path: P, config: StreamingConfig) -> Result<Self> {
350        let line_reader = LineChunkedReader::new(path, config)?;
351
352        Ok(Self {
353            line_reader,
354            header: None,
355            delimiter: ',',
356            has_header: false,
357        })
358    }
359
360    /// Set the delimiter character
361    pub fn with_delimiter(mut self, delimiter: char) -> Self {
362        self.delimiter = delimiter;
363        self
364    }
365
366    /// Enable header row processing
367    pub fn with_header(mut self, hasheader: bool) -> Self {
368        self.has_header = hasheader;
369        self
370    }
371
372    /// Get the header row (if available)
373    pub fn header(&self) -> Option<&Vec<String>> {
374        self.header.as_ref()
375    }
376
377    /// Parse a CSV line into fields
378    fn parse_line(&self, line: &str) -> Vec<String> {
379        // Simple CSV parsing - in production, you'd use a proper CSV parser
380        line.split(self.delimiter)
381            .map(|field| field.trim().to_string())
382            .collect()
383    }
384}
385
386impl Iterator for StreamingCsvReader {
387    type Item = Result<Vec<Vec<String>>>;
388
389    fn next(&mut self) -> Option<Self::Item> {
390        // Handle header if not yet processed
391        if self.has_header && self.header.is_none() {
392            match self.line_reader.next() {
393                Some(Ok(lines)) => {
394                    if let Some(header_line) = lines.first() {
395                        self.header = Some(self.parse_line(header_line));
396                    }
397                    // Continue to process remaining lines in this chunk
398                    let data_lines: Vec<Vec<String>> = lines
399                        .iter()
400                        .skip(1)
401                        .map(|line| self.parse_line(line))
402                        .collect();
403
404                    if data_lines.is_empty() {
405                        return self.next(); // Get next chunk
406                    } else {
407                        return Some(Ok(data_lines));
408                    }
409                }
410                Some(Err(e)) => return Some(Err(e)),
411                None => return None,
412            }
413        }
414
415        // Process regular data chunks
416        match self.line_reader.next() {
417            Some(Ok(lines)) => {
418                let data_rows: Vec<Vec<String>> =
419                    lines.iter().map(|line| self.parse_line(line)).collect();
420                Some(Ok(data_rows))
421            }
422            Some(Err(e)) => Some(Err(e)),
423            None => None,
424        }
425    }
426}
427
428/// Statistics for streaming operations
429#[derive(Debug, Clone, Default)]
430pub struct StreamingStats {
431    /// Total bytes processed
432    pub bytes_processed: u64,
433    /// Total chunks processed
434    pub chunks_processed: usize,
435    /// Total lines processed (for line-based readers)
436    pub lines_processed: usize,
437    /// Processing time in milliseconds
438    pub processing_time_ms: f64,
439    /// Average bytes per chunk
440    pub avg_bytes_per_chunk: f64,
441    /// Average processing speed in MB/s
442    pub avg_speed_mbps: f64,
443}
444
445impl StreamingStats {
446    /// Create new streaming statistics
447    pub fn new() -> Self {
448        Self::default()
449    }
450
451    /// Update statistics with chunk information
452    pub fn update_chunk(&mut self, bytes: u64, processing_time_ms: f64) {
453        self.bytes_processed += bytes;
454        self.chunks_processed += 1;
455        self.processing_time_ms += processing_time_ms;
456
457        self.avg_bytes_per_chunk = self.bytes_processed as f64 / self.chunks_processed as f64;
458
459        if self.processing_time_ms > 0.0 {
460            let total_mb = self.bytes_processed as f64 / (1024.0 * 1024.0);
461            let total_seconds = self.processing_time_ms / 1000.0;
462            self.avg_speed_mbps = total_mb / total_seconds;
463        }
464    }
465
466    /// Update statistics with line information
467    pub fn update_lines(&mut self, lines: usize) {
468        self.lines_processed += lines;
469    }
470
471    /// Get a summary string of the statistics
472    pub fn summary(&self) -> String {
473        format!(
474            "Processed {} bytes in {} chunks ({} lines), avg {:.2} MB/s",
475            self.bytes_processed, self.chunks_processed, self.lines_processed, self.avg_speed_mbps
476        )
477    }
478}
479
480/// Process a file using a streaming reader with progress tracking
481#[allow(dead_code)]
482pub fn process_file_chunked<P, F, T>(
483    path: P,
484    config: StreamingConfig,
485    mut processor: F,
486) -> Result<(T, StreamingStats)>
487where
488    P: AsRef<Path>,
489    F: FnMut(&[u8], usize) -> Result<T>,
490    T: Default,
491{
492    let reader = ChunkedReader::new(path, config)?;
493    let mut stats = StreamingStats::new();
494    let mut result = T::default();
495
496    let start_time = std::time::Instant::now();
497
498    for (chunk_id, chunk_result) in reader.enumerate() {
499        let chunk_start = std::time::Instant::now();
500
501        match chunk_result {
502            Ok(chunk_data) => {
503                result = processor(&chunk_data, chunk_id)?;
504
505                let chunk_time = chunk_start.elapsed().as_secs_f64() * 1000.0;
506                stats.update_chunk(chunk_data.len() as u64, chunk_time);
507            }
508            Err(e) => return Err(e),
509        }
510    }
511
512    stats.processing_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
513
514    Ok((result, stats))
515}
516
517/// Process a CSV file using streaming with progress tracking
518#[allow(dead_code)]
519pub fn process_csv_chunked<P, F, T>(
520    path: P,
521    config: StreamingConfig,
522    has_header: bool,
523    mut processor: F,
524) -> Result<(T, StreamingStats)>
525where
526    P: AsRef<Path>,
527    F: FnMut(&[Vec<String>], usize, Option<&Vec<String>>) -> Result<T>,
528    T: Default,
529{
530    let mut reader = StreamingCsvReader::new(path, config)?.with_header(has_header);
531    let mut stats = StreamingStats::new();
532    let mut result = T::default();
533
534    let start_time = std::time::Instant::now();
535
536    let mut chunk_id = 0;
537    while let Some(chunk_result) = reader.next() {
538        let chunk_start = std::time::Instant::now();
539
540        match chunk_result {
541            Ok(rows) => {
542                let header = reader.header();
543                result = processor(&rows, chunk_id, header)?;
544
545                let chunk_time = chunk_start.elapsed().as_secs_f64() * 1000.0;
546                stats.update_chunk(0, chunk_time); // CSV doesn't track bytes easily
547                stats.update_lines(rows.len());
548                chunk_id += 1;
549            }
550            Err(e) => return Err(e),
551        }
552    }
553
554    stats.processing_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
555
556    Ok((result, stats))
557}
558
559// ─── Submodules ─────────────────────────────────────────────────────────────
560
561pub mod cdc;
562pub mod checkpoint;
563pub mod log_parsing;
564pub mod time_series_ingestion;
565pub mod watermark;
566pub mod windows;
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571    use std::io::Write;
572    use tempfile::tempdir;
573
574    #[test]
575    fn test_chunked_reader() {
576        let temp_dir = tempdir().expect("Operation failed");
577        let file_path = temp_dir.path().join("test_data.txt");
578
579        // Create test data
580        let test_data = "0123456789".repeat(100); // 1000 bytes
581        std::fs::write(&file_path, &test_data).expect("Operation failed");
582
583        let config = StreamingConfig::new().chunk_size(100);
584        let reader = ChunkedReader::new(&file_path, config).expect("Operation failed");
585
586        let chunks: Result<Vec<_>> = reader.collect();
587        let chunks = chunks.expect("Operation failed");
588
589        assert_eq!(chunks.len(), 10); // 1000 bytes / 100 bytes per chunk
590        for chunk in &chunks {
591            assert_eq!(chunk.len(), 100);
592        }
593    }
594
595    #[test]
596    fn test_line_chunked_reader() {
597        let temp_dir = tempdir().expect("Operation failed");
598        let file_path = temp_dir.path().join("test_lines.txt");
599
600        // Create test data with lines
601        let lines: Vec<String> = (0..50).map(|i| format!("Line {i}")).collect();
602        std::fs::write(&file_path, lines.join("\n")).expect("Operation failed");
603
604        let config = StreamingConfig::new().chunk_size(10); // 10 lines per chunk
605        let reader = LineChunkedReader::new(&file_path, config).expect("Operation failed");
606
607        let chunks: Result<Vec<_>> = reader.collect();
608        let chunks = chunks.expect("Operation failed");
609
610        assert_eq!(chunks.len(), 5); // 50 lines / 10 lines per chunk
611        for chunk in &chunks {
612            assert_eq!(chunk.len(), 10);
613        }
614    }
615
616    #[test]
617    fn test_streaming_csv_reader() {
618        let temp_dir = tempdir().expect("Operation failed");
619        let file_path = temp_dir.path().join("test.csv");
620
621        // Create test CSV
622        let mut file = File::create(&file_path).expect("Operation failed");
623        writeln!(file, "name,age,city").expect("Operation failed");
624        for i in 0..20 {
625            writeln!(file, "Person{},{},City{}", i, 20 + i, i % 5).expect("Operation failed");
626        }
627
628        let config = StreamingConfig::new().chunk_size(5); // 5 lines per chunk
629        let reader = StreamingCsvReader::new(&file_path, config)
630            .expect("Operation failed")
631            .with_header(true);
632
633        let chunks: Result<Vec<_>> = reader.collect();
634        let chunks = chunks.expect("Operation failed");
635
636        // With 1 header + 20 data rows, chunk size 5:
637        // First chunk: header + 4 data rows -> returns 4 data rows
638        // Remaining chunks: 5, 5, 5, 1 data rows -> 4 more chunks
639        // Total: 5 chunks (4 + 5 + 5 + 5 + 1 = 20 data rows)
640        assert_eq!(chunks.len(), 5);
641
642        // Verify total data rows
643        let total_rows: usize = chunks.iter().map(|chunk| chunk.len()).sum();
644        assert_eq!(total_rows, 20);
645
646        // Verify all rows have 3 columns
647        for chunk in &chunks {
648            for row in chunk {
649                assert_eq!(row.len(), 3); // 3 columns
650            }
651        }
652    }
653
654    #[test]
655    fn test_streaming_config() {
656        let config = StreamingConfig::new()
657            .chunk_size(1024)
658            .buffer_size(4096)
659            .max_chunks(10)
660            .skip_chunks(2);
661
662        assert_eq!(config.chunk_size, 1024);
663        assert_eq!(config.buffer_size, 4096);
664        assert_eq!(config.max_chunks, Some(10));
665        assert_eq!(config.skip_chunks, 2);
666    }
667
668    #[test]
669    fn test_process_file_chunked() {
670        let temp_dir = tempdir().expect("Operation failed");
671        let file_path = temp_dir.path().join("test_process.txt");
672
673        // Create test data
674        let test_data = "Hello World!".repeat(100);
675        std::fs::write(&file_path, &test_data).expect("Operation failed");
676
677        let config = StreamingConfig::new().chunk_size(100);
678
679        let (total_size, stats) =
680            process_file_chunked(&file_path, config, |chunk, _chunk_id| -> Result<usize> {
681                Ok(chunk.len())
682            })
683            .expect("Operation failed");
684
685        assert_eq!(total_size, 100); // Last chunk size
686        assert!(stats.bytes_processed > 0);
687        assert!(stats.chunks_processed > 0);
688    }
689}
690
691/// Relational streaming operations on JSON record streams.
692///
693/// Stateless and stateful transformations for processing large JSONL datasets
694/// without loading everything into memory:
695/// - `StreamingFilter`: keep only matching records
696/// - `StreamingProject`: keep only specified fields
697/// - `StreamingRename`: rename fields
698/// - `StreamingDeduplicate`: remove duplicates by key (stateful)
699/// - `StreamingSort`: external merge-sort
700/// - `StreamingGroupBy`: sort-based group-by with pluggable aggregates
701/// - `StreamingPipeline`: chain multiple operators
702pub mod relational;