1use std::future::Future;
18use std::io;
19use std::path::Path;
20
21use bzip2::read::BzDecoder;
22use flate2::read::GzDecoder;
23use thiserror::Error;
24use tokio::fs::File;
25use tokio::io::{AsyncWrite, AsyncWriteExt, BufWriter};
26use tokio::sync::mpsc;
27
28use crate::query::export::{Compression, ExportQuery, ExportSource, RowSeparator};
29use crate::transport::protocol::TransportProtocol;
30use crate::transport::HttpTransportClient;
31
32const DEFAULT_PIPE_BUFFER_SIZE: usize = 16;
34
35#[derive(Error, Debug)]
37pub enum ExportError {
38 #[error("I/O error: {0}")]
40 IoError(#[from] io::Error),
41
42 #[error("Transport error: {0}")]
44 TransportError(#[from] crate::error::TransportError),
45
46 #[error("HTTP transport error: {message}")]
48 HttpTransportError { message: String },
49
50 #[error("SQL execution error: {message}")]
52 SqlExecutionError { message: String },
53
54 #[error("CSV parsing error at row {row}: {message}")]
56 CsvParseError { row: usize, message: String },
57
58 #[error("Decompression error: {0}")]
60 DecompressionError(String),
61
62 #[error("Channel error: {0}")]
64 ChannelError(String),
65
66 #[error("Export timed out after {timeout_ms}ms")]
68 Timeout { timeout_ms: u64 },
69
70 #[error("Export was cancelled")]
72 Cancelled,
73}
74
75#[derive(Debug, Clone)]
77pub struct CsvExportOptions {
78 pub column_separator: char,
80
81 pub column_delimiter: char,
83
84 pub row_separator: RowSeparator,
86
87 pub encoding: String,
89
90 pub null_value: Option<String>,
92
93 pub compression: Compression,
95
96 pub with_column_names: bool,
98
99 pub use_tls: bool,
101
102 pub timeout_ms: u64,
104
105 pub host: String,
108
109 pub port: u16,
112}
113
114impl Default for CsvExportOptions {
115 fn default() -> Self {
116 Self {
117 column_separator: ',',
118 column_delimiter: '"',
119 row_separator: RowSeparator::LF,
120 encoding: "UTF-8".to_string(),
121 null_value: None,
122 compression: Compression::None,
123 with_column_names: false,
124 use_tls: false,
125 timeout_ms: 300_000, host: String::new(),
127 port: 0,
128 }
129 }
130}
131
132impl CsvExportOptions {
133 #[must_use]
135 pub fn new() -> Self {
136 Self::default()
137 }
138
139 #[must_use]
140 pub fn column_separator(mut self, sep: char) -> Self {
141 self.column_separator = sep;
142 self
143 }
144
145 #[must_use]
146 pub fn column_delimiter(mut self, delim: char) -> Self {
147 self.column_delimiter = delim;
148 self
149 }
150
151 #[must_use]
152 pub fn row_separator(mut self, sep: RowSeparator) -> Self {
153 self.row_separator = sep;
154 self
155 }
156
157 #[must_use]
158 pub fn encoding(mut self, enc: &str) -> Self {
159 self.encoding = enc.to_string();
160 self
161 }
162
163 #[must_use]
164 pub fn null_value(mut self, val: &str) -> Self {
165 self.null_value = Some(val.to_string());
166 self
167 }
168
169 #[must_use]
170 pub fn compression(mut self, compression: Compression) -> Self {
171 self.compression = compression;
172 self
173 }
174
175 #[must_use]
176 pub fn with_column_names(mut self, include: bool) -> Self {
177 self.with_column_names = include;
178 self
179 }
180
181 #[must_use]
182 pub fn use_tls(mut self, use_tls: bool) -> Self {
183 self.use_tls = use_tls;
184 self
185 }
186
187 #[must_use]
188 pub fn timeout_ms(mut self, timeout: u64) -> Self {
189 self.timeout_ms = timeout;
190 self
191 }
192
193 #[must_use]
197 pub fn exasol_host(mut self, host: impl Into<String>) -> Self {
198 self.host = host.into();
199 self
200 }
201
202 #[must_use]
206 pub fn exasol_port(mut self, port: u16) -> Self {
207 self.port = port;
208 self
209 }
210}
211
212pub struct DataPipeReceiver {
214 rx: mpsc::Receiver<Vec<u8>>,
215}
216
217impl DataPipeReceiver {
218 pub async fn recv(&mut self) -> Option<Vec<u8>> {
222 self.rx.recv().await
223 }
224}
225
226pub async fn export_to_file<T: TransportProtocol + ?Sized>(
246 ws_transport: &mut T,
247 source: ExportSource,
248 file_path: &Path,
249 options: CsvExportOptions,
250) -> Result<u64, ExportError> {
251 let file = File::create(file_path).await?;
253 let writer = BufWriter::new(file);
254
255 export_to_stream(ws_transport, source, writer, options).await
256}
257
258pub async fn export_to_stream<T: TransportProtocol + ?Sized, W: AsyncWrite + Unpin>(
275 ws_transport: &mut T,
276 source: ExportSource,
277 mut writer: W,
278 options: CsvExportOptions,
279) -> Result<u64, ExportError> {
280 let compression = options.compression;
281
282 export_to_callback(ws_transport, source, options, |mut receiver| async move {
284 let mut row_count = 0u64;
285 let mut buffer = Vec::new();
286
287 while let Some(chunk) = receiver.recv().await {
289 buffer.extend_from_slice(&chunk);
290 }
291
292 let data = decompress(buffer, compression)?;
294
295 writer.write_all(&data).await?;
297 writer.flush().await?;
298
299 for byte in &data {
301 if *byte == b'\n' {
302 row_count += 1;
303 }
304 }
305
306 Ok(row_count)
307 })
308 .await
309}
310
311pub async fn export_to_list<T: TransportProtocol + ?Sized>(
332 ws_transport: &mut T,
333 source: ExportSource,
334 options: CsvExportOptions,
335) -> Result<Vec<Vec<String>>, ExportError> {
336 let separator = options.column_separator;
337 let delimiter = options.column_delimiter;
338 let compression = options.compression;
339
340 export_to_callback(ws_transport, source, options, |mut receiver| async move {
341 let mut buffer = Vec::new();
342
343 while let Some(chunk) = receiver.recv().await {
345 buffer.extend_from_slice(&chunk);
346 }
347
348 let data = decompress(buffer, compression)?;
350
351 let csv_string = String::from_utf8(data).map_err(|e| ExportError::CsvParseError {
353 row: 0,
354 message: format!("Invalid UTF-8: {}", e),
355 })?;
356
357 parse_csv(&csv_string, separator, delimiter)
358 })
359 .await
360}
361
362pub async fn export_to_callback<T, F, Fut, R>(
382 ws_transport: &mut T,
383 source: ExportSource,
384 options: CsvExportOptions,
385 callback: F,
386) -> Result<R, ExportError>
387where
388 T: TransportProtocol + ?Sized,
389 F: FnOnce(DataPipeReceiver) -> Fut,
390 Fut: Future<Output = Result<R, ExportError>>,
391{
392 let mut client = HttpTransportClient::connect(&options.host, options.port, options.use_tls)
394 .await
395 .map_err(|e| ExportError::HttpTransportError {
396 message: format!("Failed to connect to Exasol: {e}"),
397 })?;
398
399 let internal_addr = client.internal_address().to_string();
401 let fingerprint = client.public_key_fingerprint().map(|s| s.to_string());
402
403 let mut query_builder = match source {
405 ExportSource::Table {
406 ref schema,
407 ref name,
408 ref columns,
409 } => {
410 let mut builder = ExportQuery::from_table(name);
411 if let Some(s) = schema {
412 builder = builder.schema(s);
413 }
414 if !columns.is_empty() {
415 builder = builder.columns(columns.iter().map(|s| s.as_str()).collect());
416 }
417 builder
418 }
419 ExportSource::Query { ref sql } => ExportQuery::from_query(sql),
420 };
421
422 query_builder = query_builder
424 .at_address(&internal_addr)
425 .column_separator(options.column_separator)
426 .column_delimiter(options.column_delimiter)
427 .row_separator(options.row_separator)
428 .encoding(&options.encoding)
429 .with_column_names(options.with_column_names)
430 .compressed(options.compression);
431
432 if let Some(ref null_val) = options.null_value {
433 query_builder = query_builder.null_value(null_val);
434 }
435
436 if let Some(ref fp) = fingerprint {
437 query_builder = query_builder.with_public_key(fp);
438 }
439
440 let export_sql = query_builder.build();
441
442 let (tx, rx) = mpsc::channel::<Vec<u8>>(DEFAULT_PIPE_BUFFER_SIZE);
444 let receiver = DataPipeReceiver { rx };
445
446 let http_task =
452 tokio::spawn(async move {
453 let (_request, body) = client.handle_export_request().await.map_err(|e| {
455 ExportError::HttpTransportError {
456 message: format!("Failed to handle export request: {e}"),
457 }
458 })?;
459
460 if !body.is_empty() && tx.send(body).await.is_err() {
462 return Err(ExportError::ChannelError("Receiver dropped".to_string()));
463 }
464
465 let _ = client.shutdown().await;
467
468 Ok::<(), ExportError>(())
469 });
470
471 let sql_task = async {
474 ws_transport
475 .execute_query(&export_sql)
476 .await
477 .map_err(|e| ExportError::SqlExecutionError {
478 message: e.to_string(),
479 })
480 };
481
482 let callback_task = callback(receiver);
484
485 let timeout = tokio::time::Duration::from_millis(options.timeout_ms);
487
488 let result = tokio::time::timeout(timeout, async {
489 let sql_result = sql_task.await;
491
492 let (http_result, callback_result) = tokio::join!(http_task, callback_task);
494
495 sql_result?;
497 http_result.map_err(|e| ExportError::HttpTransportError {
498 message: format!("HTTP task panicked: {}", e),
499 })??;
500
501 callback_result
502 })
503 .await
504 .map_err(|_| ExportError::Timeout {
505 timeout_ms: options.timeout_ms,
506 })?;
507
508 result
509}
510
511fn decompress(buffer: Vec<u8>, compression: Compression) -> Result<Vec<u8>, ExportError> {
515 match compression {
516 Compression::Gzip => {
517 let decoder = GzDecoder::new(buffer.as_slice());
518 let mut decompressed = Vec::new();
519 std::io::Read::read_to_end(&mut std::io::BufReader::new(decoder), &mut decompressed)
520 .map_err(|e| ExportError::DecompressionError(e.to_string()))?;
521 Ok(decompressed)
522 }
523 Compression::Bzip2 => {
524 let decoder = BzDecoder::new(buffer.as_slice());
525 let mut decompressed = Vec::new();
526 std::io::Read::read_to_end(&mut std::io::BufReader::new(decoder), &mut decompressed)
527 .map_err(|e| ExportError::DecompressionError(e.to_string()))?;
528 Ok(decompressed)
529 }
530 Compression::None => Ok(buffer),
531 }
532}
533
534fn parse_csv(
536 data: &str,
537 separator: char,
538 delimiter: char,
539) -> Result<Vec<Vec<String>>, ExportError> {
540 let mut rows = Vec::new();
541 let mut current_row = Vec::new();
542 let mut current_field = String::new();
543 let mut in_quotes = false;
544 let mut row_num = 0;
545
546 let chars: Vec<char> = data.chars().collect();
547 let mut i = 0;
548
549 while i < chars.len() {
550 let c = chars[i];
551
552 if in_quotes {
553 if c == delimiter {
554 if i + 1 < chars.len() && chars[i + 1] == delimiter {
556 current_field.push(delimiter);
557 i += 2;
558 continue;
559 }
560 in_quotes = false;
562 } else {
563 current_field.push(c);
564 }
565 } else if c == delimiter {
566 in_quotes = true;
568 } else if c == separator {
569 current_row.push(current_field);
571 current_field = String::new();
572 } else if c == '\n' {
573 current_row.push(current_field);
575 current_field = String::new();
576 rows.push(current_row);
577 current_row = Vec::new();
578 row_num += 1;
579 } else if c == '\r' {
580 if i + 1 < chars.len() && chars[i + 1] == '\n' {
582 } else {
584 current_row.push(current_field);
586 current_field = String::new();
587 rows.push(current_row);
588 current_row = Vec::new();
589 row_num += 1;
590 }
591 } else {
592 current_field.push(c);
593 }
594 i += 1;
595 }
596
597 if !current_field.is_empty() || !current_row.is_empty() {
599 current_row.push(current_field);
600 rows.push(current_row);
601 }
602
603 if in_quotes {
605 return Err(ExportError::CsvParseError {
606 row: row_num,
607 message: "Unclosed quote at end of data".to_string(),
608 });
609 }
610
611 Ok(rows)
612}
613
614pub(crate) fn parse_csv_row(line: &str, separator: char, delimiter: char) -> (Vec<String>, bool) {
625 let mut fields = Vec::new();
626 let mut current_field = String::new();
627 let mut in_quotes = false;
628 let mut chars = line.chars().peekable();
629
630 while let Some(c) = chars.next() {
631 if in_quotes {
632 if c == delimiter {
633 if chars.peek() == Some(&delimiter) {
635 current_field.push(delimiter);
636 chars.next();
637 } else {
638 in_quotes = false;
639 }
640 } else {
641 current_field.push(c);
642 }
643 } else if c == delimiter {
644 in_quotes = true;
645 } else if c == separator {
646 fields.push(current_field);
647 current_field = String::new();
648 } else {
649 current_field.push(c);
650 }
651 }
652
653 fields.push(current_field);
655
656 (fields, in_quotes)
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
666 fn test_csv_export_options_default() {
667 let options = CsvExportOptions::default();
668
669 assert_eq!(options.column_separator, ',');
670 assert_eq!(options.column_delimiter, '"');
671 assert_eq!(options.row_separator, RowSeparator::LF);
672 assert_eq!(options.encoding, "UTF-8");
673 assert!(options.null_value.is_none());
674 assert_eq!(options.compression, Compression::None);
675 assert!(!options.with_column_names);
676 assert!(!options.use_tls);
677 assert_eq!(options.timeout_ms, 300_000);
678 assert_eq!(options.host, "");
679 assert_eq!(options.port, 0);
680 }
681
682 #[test]
683 fn test_csv_export_options_builder() {
684 let options = CsvExportOptions::new()
685 .column_separator(';')
686 .column_delimiter('\'')
687 .row_separator(RowSeparator::CRLF)
688 .encoding("ISO-8859-1")
689 .null_value("NULL")
690 .compression(Compression::Gzip)
691 .with_column_names(true)
692 .use_tls(false)
693 .timeout_ms(60_000)
694 .exasol_host("exasol.example.com")
695 .exasol_port(8563);
696
697 assert_eq!(options.column_separator, ';');
698 assert_eq!(options.column_delimiter, '\'');
699 assert_eq!(options.row_separator, RowSeparator::CRLF);
700 assert_eq!(options.encoding, "ISO-8859-1");
701 assert_eq!(options.null_value, Some("NULL".to_string()));
702 assert_eq!(options.compression, Compression::Gzip);
703 assert!(options.with_column_names);
704 assert!(!options.use_tls);
705 assert_eq!(options.timeout_ms, 60_000);
706 assert_eq!(options.host, "exasol.example.com");
707 assert_eq!(options.port, 8563);
708 }
709
710 #[test]
713 fn test_parse_csv_simple() {
714 let data = "a,b,c\n1,2,3\n";
715 let result = parse_csv(data, ',', '"').unwrap();
716
717 assert_eq!(result.len(), 2);
718 assert_eq!(result[0], vec!["a", "b", "c"]);
719 assert_eq!(result[1], vec!["1", "2", "3"]);
720 }
721
722 #[test]
723 fn test_parse_csv_with_quotes() {
724 let data = "\"hello\",\"world\"\n\"foo\",\"bar\"\n";
725 let result = parse_csv(data, ',', '"').unwrap();
726
727 assert_eq!(result.len(), 2);
728 assert_eq!(result[0], vec!["hello", "world"]);
729 assert_eq!(result[1], vec!["foo", "bar"]);
730 }
731
732 #[test]
733 fn test_parse_csv_with_escaped_quotes() {
734 let data = "\"hello \"\"world\"\"\",normal\n";
735 let result = parse_csv(data, ',', '"').unwrap();
736
737 assert_eq!(result.len(), 1);
738 assert_eq!(result[0], vec!["hello \"world\"", "normal"]);
739 }
740
741 #[test]
742 fn test_parse_csv_with_separator_in_quotes() {
743 let data = "\"a,b,c\",\"d,e\"\n";
744 let result = parse_csv(data, ',', '"').unwrap();
745
746 assert_eq!(result.len(), 1);
747 assert_eq!(result[0], vec!["a,b,c", "d,e"]);
748 }
749
750 #[test]
751 fn test_parse_csv_with_newline_in_quotes() {
752 let data = "\"line1\nline2\",normal\n";
753 let result = parse_csv(data, ',', '"').unwrap();
754
755 assert_eq!(result.len(), 1);
756 assert_eq!(result[0], vec!["line1\nline2", "normal"]);
757 }
758
759 #[test]
760 fn test_parse_csv_with_crlf() {
761 let data = "a,b\r\nc,d\r\n";
762 let result = parse_csv(data, ',', '"').unwrap();
763
764 assert_eq!(result.len(), 2);
765 assert_eq!(result[0], vec!["a", "b"]);
766 assert_eq!(result[1], vec!["c", "d"]);
767 }
768
769 #[test]
770 fn test_parse_csv_with_semicolon_separator() {
771 let data = "a;b;c\n1;2;3\n";
772 let result = parse_csv(data, ';', '"').unwrap();
773
774 assert_eq!(result.len(), 2);
775 assert_eq!(result[0], vec!["a", "b", "c"]);
776 assert_eq!(result[1], vec!["1", "2", "3"]);
777 }
778
779 #[test]
780 fn test_parse_csv_empty_fields() {
781 let data = "a,,c\n,b,\n";
782 let result = parse_csv(data, ',', '"').unwrap();
783
784 assert_eq!(result.len(), 2);
785 assert_eq!(result[0], vec!["a", "", "c"]);
786 assert_eq!(result[1], vec!["", "b", ""]);
787 }
788
789 #[test]
790 fn test_parse_csv_unclosed_quote_error() {
791 let data = "\"unclosed,quote\n";
792 let result = parse_csv(data, ',', '"');
793
794 assert!(result.is_err());
795 if let Err(ExportError::CsvParseError { row, message }) = result {
796 assert_eq!(row, 0);
797 assert!(message.contains("Unclosed quote"));
798 } else {
799 panic!("Expected CsvParseError");
800 }
801 }
802
803 #[test]
804 fn test_parse_csv_empty_data() {
805 let data = "";
806 let result = parse_csv(data, ',', '"').unwrap();
807
808 assert!(result.is_empty());
809 }
810
811 #[test]
812 fn test_parse_csv_single_field() {
813 let data = "single\n";
814 let result = parse_csv(data, ',', '"').unwrap();
815
816 assert_eq!(result.len(), 1);
817 assert_eq!(result[0], vec!["single"]);
818 }
819
820 #[test]
821 fn test_parse_csv_no_trailing_newline() {
822 let data = "a,b,c";
823 let result = parse_csv(data, ',', '"').unwrap();
824
825 assert_eq!(result.len(), 1);
826 assert_eq!(result[0], vec!["a", "b", "c"]);
827 }
828
829 #[test]
832 fn test_export_error_display() {
833 let err = ExportError::IoError(io::Error::new(io::ErrorKind::NotFound, "file not found"));
834 assert!(err.to_string().contains("I/O error"));
835
836 let err = ExportError::HttpTransportError {
837 message: "connection refused".to_string(),
838 };
839 assert!(err.to_string().contains("HTTP transport error"));
840 assert!(err.to_string().contains("connection refused"));
841
842 let err = ExportError::CsvParseError {
843 row: 5,
844 message: "invalid data".to_string(),
845 };
846 assert!(err.to_string().contains("row 5"));
847 assert!(err.to_string().contains("invalid data"));
848
849 let err = ExportError::Timeout { timeout_ms: 5000 };
850 assert!(err.to_string().contains("5000ms"));
851 }
852
853 #[test]
854 fn test_export_error_from_io_error() {
855 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
856 let export_err: ExportError = io_err.into();
857
858 assert!(matches!(export_err, ExportError::IoError(_)));
859 }
860
861 #[tokio::test]
864 async fn test_data_pipe_receiver_recv() {
865 let (tx, rx) = mpsc::channel::<Vec<u8>>(16);
866 let mut receiver = DataPipeReceiver { rx };
867
868 tx.send(vec![1, 2, 3]).await.unwrap();
869 tx.send(vec![4, 5, 6]).await.unwrap();
870 drop(tx);
871
872 let chunk1 = receiver.recv().await;
873 assert_eq!(chunk1, Some(vec![1, 2, 3]));
874
875 let chunk2 = receiver.recv().await;
876 assert_eq!(chunk2, Some(vec![4, 5, 6]));
877
878 let chunk3 = receiver.recv().await;
879 assert!(chunk3.is_none());
880 }
881
882 #[tokio::test]
883 async fn test_data_pipe_receiver_empty() {
884 let (tx, rx) = mpsc::channel::<Vec<u8>>(16);
885 let mut receiver = DataPipeReceiver { rx };
886
887 drop(tx);
888
889 let chunk = receiver.recv().await;
890 assert!(chunk.is_none());
891 }
892
893 #[test]
896 fn test_parse_csv_with_column_headers() {
897 let data = "id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com\n";
898 let result = parse_csv(data, ',', '"').unwrap();
899
900 assert_eq!(result.len(), 3);
901 assert_eq!(result[0], vec!["id", "name", "email"]);
903 assert_eq!(result[1], vec!["1", "Alice", "alice@example.com"]);
905 assert_eq!(result[2], vec!["2", "Bob", "bob@example.com"]);
906 }
907
908 #[test]
909 fn test_parse_csv_with_quoted_headers() {
910 let data = "\"Column A\",\"Column B\",\"Column C\"\n1,2,3\n";
911 let result = parse_csv(data, ',', '"').unwrap();
912
913 assert_eq!(result.len(), 2);
914 assert_eq!(result[0], vec!["Column A", "Column B", "Column C"]);
915 assert_eq!(result[1], vec!["1", "2", "3"]);
916 }
917
918 #[test]
921 fn test_gzip_decompression() {
922 use flate2::write::GzEncoder;
923 use std::io::Write;
924
925 let original = b"hello,world\n1,2\n";
927 let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::default());
928 encoder.write_all(original).unwrap();
929 let compressed = encoder.finish().unwrap();
930
931 let decoder = GzDecoder::new(compressed.as_slice());
933 let mut decompressed = Vec::new();
934 std::io::Read::read_to_end(&mut std::io::BufReader::new(decoder), &mut decompressed)
935 .unwrap();
936
937 assert_eq!(decompressed, original);
938
939 let csv_string = String::from_utf8(decompressed).unwrap();
941 let rows = parse_csv(&csv_string, ',', '"').unwrap();
942 assert_eq!(rows.len(), 2);
943 assert_eq!(rows[0], vec!["hello", "world"]);
944 assert_eq!(rows[1], vec!["1", "2"]);
945 }
946
947 #[test]
948 fn test_bzip2_decompression() {
949 use bzip2::write::BzEncoder;
950 use std::io::Write;
951
952 let original = b"foo,bar\na,b\n";
954 let mut encoder = BzEncoder::new(Vec::new(), bzip2::Compression::default());
955 encoder.write_all(original).unwrap();
956 let compressed = encoder.finish().unwrap();
957
958 let decoder = BzDecoder::new(compressed.as_slice());
960 let mut decompressed = Vec::new();
961 std::io::Read::read_to_end(&mut std::io::BufReader::new(decoder), &mut decompressed)
962 .unwrap();
963
964 assert_eq!(decompressed, original);
965
966 let csv_string = String::from_utf8(decompressed).unwrap();
968 let rows = parse_csv(&csv_string, ',', '"').unwrap();
969 assert_eq!(rows.len(), 2);
970 assert_eq!(rows[0], vec!["foo", "bar"]);
971 assert_eq!(rows[1], vec!["a", "b"]);
972 }
973
974 #[test]
977 fn test_csv_export_options_with_column_names() {
978 let options = CsvExportOptions::new().with_column_names(true);
979 assert!(options.with_column_names);
980
981 let options = CsvExportOptions::new().with_column_names(false);
982 assert!(!options.with_column_names);
983 }
984
985 #[test]
988 fn test_csv_export_options_compression_gzip() {
989 let options = CsvExportOptions::new().compression(Compression::Gzip);
990 assert_eq!(options.compression, Compression::Gzip);
991 }
992
993 #[test]
994 fn test_csv_export_options_compression_bzip2() {
995 let options = CsvExportOptions::new().compression(Compression::Bzip2);
996 assert_eq!(options.compression, Compression::Bzip2);
997 }
998
999 #[test]
1000 fn test_csv_export_options_compression_none() {
1001 let options = CsvExportOptions::new().compression(Compression::None);
1002 assert_eq!(options.compression, Compression::None);
1003 }
1004
1005 #[test]
1008 fn test_export_error_decompression() {
1009 let err = ExportError::DecompressionError("invalid gzip header".to_string());
1010 assert!(err.to_string().contains("Decompression error"));
1011 assert!(err.to_string().contains("invalid gzip header"));
1012 }
1013
1014 #[test]
1015 fn test_export_error_channel() {
1016 let err = ExportError::ChannelError("receiver dropped".to_string());
1017 assert!(err.to_string().contains("Channel error"));
1018 assert!(err.to_string().contains("receiver dropped"));
1019 }
1020
1021 #[test]
1022 fn test_export_error_cancelled() {
1023 let err = ExportError::Cancelled;
1024 assert!(err.to_string().contains("cancelled"));
1025 }
1026
1027 #[tokio::test]
1030 async fn test_data_pipe_receiver_multiple_chunks() {
1031 let (tx, rx) = mpsc::channel::<Vec<u8>>(DEFAULT_PIPE_BUFFER_SIZE);
1032 let mut receiver = DataPipeReceiver { rx };
1033
1034 let chunk1 = b"id,name\n".to_vec();
1036 let chunk2 = b"1,Alice\n".to_vec();
1037 let chunk3 = b"2,Bob\n".to_vec();
1038
1039 tx.send(chunk1.clone()).await.unwrap();
1040 tx.send(chunk2.clone()).await.unwrap();
1041 tx.send(chunk3.clone()).await.unwrap();
1042 drop(tx);
1043
1044 let mut buffer = Vec::new();
1046 while let Some(chunk) = receiver.recv().await {
1047 buffer.extend_from_slice(&chunk);
1048 }
1049
1050 let csv_string = String::from_utf8(buffer).unwrap();
1051 let rows = parse_csv(&csv_string, ',', '"').unwrap();
1052
1053 assert_eq!(rows.len(), 3);
1054 assert_eq!(rows[0], vec!["id", "name"]);
1055 assert_eq!(rows[1], vec!["1", "Alice"]);
1056 assert_eq!(rows[2], vec!["2", "Bob"]);
1057 }
1058}