1use 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#[derive(Debug, Clone)]
46pub struct StreamingConfig {
47 pub chunk_size: usize,
49 pub buffer_size: usize,
51 pub auto_detect_compression: bool,
53 pub compression: Option<CompressionAlgorithm>,
55 pub max_chunks: Option<usize>,
57 pub skip_chunks: usize,
59}
60
61impl Default for StreamingConfig {
62 fn default() -> Self {
63 Self {
64 chunk_size: 64 * 1024, buffer_size: 8 * 1024, auto_detect_compression: true,
67 compression: None,
68 max_chunks: None,
69 skip_chunks: 0,
70 }
71 }
72}
73
74impl StreamingConfig {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn chunk_size(mut self, size: usize) -> Self {
82 self.chunk_size = size;
83 self
84 }
85
86 pub fn buffer_size(mut self, size: usize) -> Self {
88 self.buffer_size = size;
89 self
90 }
91
92 pub fn auto_detect_compression(mut self, enable: bool) -> Self {
94 self.auto_detect_compression = enable;
95 self
96 }
97
98 pub fn compression(mut self, algorithm: CompressionAlgorithm) -> Self {
100 self.compression = Some(algorithm);
101 self
102 }
103
104 pub fn max_chunks(mut self, max: usize) -> Self {
106 self.max_chunks = Some(max);
107 self
108 }
109
110 pub fn skip_chunks(mut self, skip: usize) -> Self {
112 self.skip_chunks = skip;
113 self
114 }
115}
116
117pub 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 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 pub fn bytes_read(&self) -> u64 {
145 self.total_bytes_read
146 }
147
148 pub fn chunks_read(&self) -> usize {
150 self.chunks_read
151 }
152
153 pub fn is_finished(&self) -> bool {
155 self.finished
156 }
157
158 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 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 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(); }
191 Err(e) => return Some(Err(e)),
192 }
193 }
194
195 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 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
227pub struct LineChunkedReader {
229 reader: BufReader<File>,
230 config: StreamingConfig,
231 lines_read: usize,
232 finished: bool,
233}
234
235impl LineChunkedReader {
236 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 pub fn lines_read(&self) -> usize {
253 self.lines_read
254 }
255
256 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 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(); }
282 Err(e) => {
283 return Some(Err(IoError::FileError(format!(
284 "Failed to skip line: {}",
285 e
286 ))))
287 }
288 }
289 }
290
291 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; for _ in 0..target_lines {
303 let mut line = String::new();
304 match self.reader.read_line(&mut line) {
305 Ok(0) => {
306 self.finished = true;
308 break;
309 }
310 Ok(_) => {
311 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
339pub struct StreamingCsvReader {
341 line_reader: LineChunkedReader,
342 header: Option<Vec<String>>,
343 delimiter: char,
344 has_header: bool,
345}
346
347impl StreamingCsvReader {
348 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 pub fn with_delimiter(mut self, delimiter: char) -> Self {
362 self.delimiter = delimiter;
363 self
364 }
365
366 pub fn with_header(mut self, hasheader: bool) -> Self {
368 self.has_header = hasheader;
369 self
370 }
371
372 pub fn header(&self) -> Option<&Vec<String>> {
374 self.header.as_ref()
375 }
376
377 fn parse_line(&self, line: &str) -> Vec<String> {
379 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 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 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(); } 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 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#[derive(Debug, Clone, Default)]
430pub struct StreamingStats {
431 pub bytes_processed: u64,
433 pub chunks_processed: usize,
435 pub lines_processed: usize,
437 pub processing_time_ms: f64,
439 pub avg_bytes_per_chunk: f64,
441 pub avg_speed_mbps: f64,
443}
444
445impl StreamingStats {
446 pub fn new() -> Self {
448 Self::default()
449 }
450
451 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 pub fn update_lines(&mut self, lines: usize) {
468 self.lines_processed += lines;
469 }
470
471 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#[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#[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); 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
559pub 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 let test_data = "0123456789".repeat(100); 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); 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 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); 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); 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 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); 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 assert_eq!(chunks.len(), 5);
641
642 let total_rows: usize = chunks.iter().map(|chunk| chunk.len()).sum();
644 assert_eq!(total_rows, 20);
645
646 for chunk in &chunks {
648 for row in chunk {
649 assert_eq!(row.len(), 3); }
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 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); assert!(stats.bytes_processed > 0);
687 assert!(stats.chunks_processed > 0);
688 }
689}
690
691pub mod relational;