1#![cfg(feature = "state_machine")]
23
24use crate::config::OutputConfig;
25use crate::output::{OutputError, StreamingWriter};
26use cqlite_core::query::{QueryMetadata, QueryResult, QueryRow};
27use csv::WriterBuilder;
28use std::io::Write;
29
30use super::value_fmt::ValueFormatter;
31
32#[allow(dead_code)]
34pub struct CSVWriter;
35
36impl CSVWriter {
37 #[allow(dead_code)]
54 pub fn write(
55 result: &QueryResult,
56 config: &OutputConfig,
57 ) -> Result<String, Box<dyn std::error::Error>> {
58 let mut wtr = WriterBuilder::new().from_writer(Vec::new());
60
61 let headers: Vec<&str> = result
63 .metadata
64 .columns
65 .iter()
66 .map(|col| col.name.as_str())
67 .collect();
68 wtr.write_record(&headers)?;
69
70 let rows_to_display = if let Some(limit) = config.limit {
72 &result.rows[..result.rows.len().min(limit)]
73 } else {
74 &result.rows
75 };
76
77 for row in rows_to_display {
79 let record: Vec<String> = result
80 .metadata
81 .columns
82 .iter()
83 .map(|col| {
84 row.values
85 .get(&col.name)
86 .map(|v| {
87 let formatted = ValueFormatter::format_value(v);
88 if formatted == "null" {
90 String::new()
91 } else {
92 formatted
93 }
94 })
95 .unwrap_or_else(String::new) })
97 .collect();
98 wtr.write_record(&record)?;
99 }
100
101 let data = wtr.into_inner()?;
103 String::from_utf8(data).map_err(|e| e.into())
104 }
105}
106
107pub struct StreamingCSVWriter<W: Write> {
131 writer: csv::Writer<W>,
133 columns: Vec<String>,
135 rows_written: u64,
137}
138
139impl<W: Write> StreamingCSVWriter<W> {
140 pub fn new(output: W) -> Self {
142 Self {
143 writer: WriterBuilder::new().from_writer(output),
144 columns: Vec::new(),
145 rows_written: 0,
146 }
147 }
148
149 #[allow(dead_code)]
151 pub fn with_options(output: W, delimiter: u8, quote_style: csv::QuoteStyle) -> Self {
152 Self {
153 writer: WriterBuilder::new()
154 .delimiter(delimiter)
155 .quote_style(quote_style)
156 .from_writer(output),
157 columns: Vec::new(),
158 rows_written: 0,
159 }
160 }
161}
162
163impl<W: Write + Send> StreamingWriter for StreamingCSVWriter<W> {
164 fn write_header(&mut self, metadata: &QueryMetadata) -> Result<(), OutputError> {
165 self.columns = metadata.columns.iter().map(|c| c.name.clone()).collect();
167
168 self.writer
170 .write_record(&self.columns)
171 .map_err(|e| OutputError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
172
173 Ok(())
174 }
175
176 fn write_chunk(&mut self, rows: &[QueryRow]) -> Result<usize, OutputError> {
177 for row in rows {
178 let record: Vec<String> = self
179 .columns
180 .iter()
181 .map(|col| {
182 row.values
183 .get(col)
184 .map(|v| {
185 let formatted = ValueFormatter::format_value(v);
186 if formatted == "null" {
188 String::new()
189 } else {
190 formatted
191 }
192 })
193 .unwrap_or_default()
194 })
195 .collect();
196
197 self.writer
198 .write_record(&record)
199 .map_err(|e| OutputError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
200 }
201
202 self.rows_written += rows.len() as u64;
203 Ok(rows.len())
204 }
205
206 fn finalize(&mut self) -> Result<(), OutputError> {
207 self.writer.flush().map_err(OutputError::Io)
208 }
209
210 fn rows_written(&self) -> u64 {
211 self.rows_written
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use cqlite_core::query::{ColumnInfo, QueryMetadata, QueryRow};
219 use cqlite_core::types::DataType;
220 use cqlite_core::{RowKey, Value};
221 use std::collections::HashMap;
222
223 fn default_config() -> OutputConfig {
224 OutputConfig::default()
225 }
226
227 fn create_test_result(
229 columns: Vec<(&str, DataType)>,
230 rows_data: Vec<Vec<(&str, Value)>>,
231 ) -> QueryResult {
232 let mut metadata = QueryMetadata::default();
233 metadata.columns = columns
234 .iter()
235 .enumerate()
236 .map(|(pos, (name, data_type))| ColumnInfo {
237 name: name.to_string(),
238 data_type: data_type.clone(),
239 nullable: true,
240 position: pos,
241 table_name: None,
242 cql_type: None,
243 })
244 .collect();
245
246 let rows = rows_data
247 .into_iter()
248 .enumerate()
249 .map(|(idx, row_data)| {
250 let mut values = HashMap::new();
251 for (col_name, value) in row_data {
252 values.insert(col_name.to_string(), value);
253 }
254 QueryRow {
255 values,
256 key: RowKey::new(vec![idx as u8]),
257 metadata: Default::default(),
258 }
259 })
260 .collect();
261
262 QueryResult {
263 rows,
264 rows_affected: 0,
265 execution_time_ms: 0,
266 metadata,
267 }
268 }
269
270 #[test]
271 fn test_csv_headers_match_column_order() {
272 let result = create_test_result(
273 vec![
274 ("id", DataType::Integer),
275 ("name", DataType::Text),
276 ("age", DataType::Integer),
277 ],
278 vec![],
279 );
280
281 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
282 let lines: Vec<&str> = csv.lines().collect();
283
284 assert_eq!(lines.len(), 1); assert_eq!(lines[0], "id,name,age");
286 }
287
288 #[test]
289 fn test_csv_basic_data() {
290 let result = create_test_result(
291 vec![("id", DataType::Integer), ("name", DataType::Text)],
292 vec![
293 vec![
294 ("id", Value::Integer(1)),
295 ("name", Value::Text("Alice".to_string())),
296 ],
297 vec![
298 ("id", Value::Integer(2)),
299 ("name", Value::Text("Bob".to_string())),
300 ],
301 ],
302 );
303
304 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
305 let lines: Vec<&str> = csv.lines().collect();
306
307 assert_eq!(lines.len(), 3); assert_eq!(lines[0], "id,name");
309 assert_eq!(lines[1], "1,Alice");
310 assert_eq!(lines[2], "2,Bob");
311 }
312
313 #[test]
314 fn test_csv_null_values_become_empty() {
315 let result = create_test_result(
316 vec![("id", DataType::Integer), ("name", DataType::Text)],
317 vec![
318 vec![("id", Value::Integer(1)), ("name", Value::Null)],
319 vec![
320 ("id", Value::Null),
321 ("name", Value::Text("Bob".to_string())),
322 ],
323 ],
324 );
325
326 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
327 let lines: Vec<&str> = csv.lines().collect();
328
329 assert_eq!(lines.len(), 3);
330 assert_eq!(lines[0], "id,name");
331 assert_eq!(lines[1], "1,"); assert_eq!(lines[2], ",Bob"); }
334
335 #[test]
336 fn test_csv_missing_columns_become_empty() {
337 let result = create_test_result(
338 vec![
339 ("id", DataType::Integer),
340 ("name", DataType::Text),
341 ("email", DataType::Text),
342 ],
343 vec![
344 vec![
346 ("id", Value::Integer(1)),
347 ("name", Value::Text("Alice".to_string())),
348 ],
349 vec![
351 ("id", Value::Integer(2)),
352 ("email", Value::Text("bob@test.com".to_string())),
353 ],
354 ],
355 );
356
357 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
358 let lines: Vec<&str> = csv.lines().collect();
359
360 assert_eq!(lines.len(), 3);
361 assert_eq!(lines[0], "id,name,email");
362 assert_eq!(lines[1], "1,Alice,"); assert_eq!(lines[2], "2,,bob@test.com"); }
365
366 #[test]
367 fn test_csv_special_characters_are_escaped() {
368 let result = create_test_result(
369 vec![("id", DataType::Integer), ("description", DataType::Text)],
370 vec![
371 vec![
372 ("id", Value::Integer(1)),
373 ("description", Value::Text("Contains, comma".to_string())),
374 ],
375 vec![
376 ("id", Value::Integer(2)),
377 ("description", Value::Text("Has \"quotes\"".to_string())),
378 ],
379 vec![
380 ("id", Value::Integer(3)),
381 ("description", Value::Text("Line\nbreak".to_string())),
382 ],
383 ],
384 );
385
386 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
387
388 assert!(csv.contains("\"Contains, comma\"") || csv.contains("Contains, comma"));
390 assert!(csv.contains("\"Has \"\"quotes\"\"\"") || csv.contains("Has \"quotes\""));
391 assert!(csv.contains("Line\nbreak") || csv.contains("\"Line\nbreak\""));
392 }
393
394 #[test]
395 fn test_csv_column_order_stability() {
396 let result = create_test_result(
398 vec![
399 ("z_field", DataType::Text),
400 ("a_field", DataType::Text),
401 ("m_field", DataType::Text),
402 ],
403 vec![vec![
404 ("a_field", Value::Text("aaa".to_string())),
405 ("m_field", Value::Text("mmm".to_string())),
406 ("z_field", Value::Text("zzz".to_string())),
407 ]],
408 );
409
410 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
411 let lines: Vec<&str> = csv.lines().collect();
412
413 assert_eq!(lines[0], "z_field,a_field,m_field");
415 assert_eq!(lines[1], "zzz,aaa,mmm");
416 }
417
418 #[test]
419 fn test_csv_config_limit() {
420 let result = create_test_result(
421 vec![("id", DataType::Integer)],
422 vec![
423 vec![("id", Value::Integer(1))],
424 vec![("id", Value::Integer(2))],
425 vec![("id", Value::Integer(3))],
426 vec![("id", Value::Integer(4))],
427 vec![("id", Value::Integer(5))],
428 ],
429 );
430
431 let config = OutputConfig {
433 color_enabled: true,
434 limit: Some(2),
435 page_size: None,
436 target: crate::output::OutputTarget::Stdout,
437 overwrite: false,
438 };
439 let csv = CSVWriter::write(&result, &config).expect("CSV write failed");
440 let lines: Vec<&str> = csv.lines().collect();
441
442 assert_eq!(
444 lines.len(),
445 3,
446 "Limit should restrict output to 2 data rows"
447 );
448 assert_eq!(lines[0], "id");
449 assert_eq!(lines[1], "1");
450 assert_eq!(lines[2], "2");
451 }
452
453 #[test]
454 fn test_csv_empty_result() {
455 let result = create_test_result(
456 vec![("id", DataType::Integer), ("name", DataType::Text)],
457 vec![],
458 );
459
460 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
461 let lines: Vec<&str> = csv.lines().collect();
462
463 assert_eq!(lines.len(), 1);
465 assert_eq!(lines[0], "id,name");
466 }
467
468 #[test]
469 fn test_csv_various_data_types() {
470 let result = create_test_result(
471 vec![
472 ("bool_col", DataType::Boolean),
473 ("int_col", DataType::Integer),
474 ("text_col", DataType::Text),
475 ("blob_col", DataType::Blob),
476 ],
477 vec![vec![
478 ("bool_col", Value::Boolean(true)),
479 ("int_col", Value::Integer(42)),
480 ("text_col", Value::Text("test".to_string())),
481 ("blob_col", Value::Blob(vec![0xDE, 0xAD])),
482 ]],
483 );
484
485 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
486 let lines: Vec<&str> = csv.lines().collect();
487
488 assert_eq!(lines.len(), 2);
489 assert_eq!(lines[0], "bool_col,int_col,text_col,blob_col");
490 assert_eq!(lines[1], "true,42,test,0xdead");
491 }
492
493 #[test]
494 fn test_csv_collections() {
495 let result = create_test_result(
496 vec![
497 ("id", DataType::Integer),
498 ("list_col", DataType::List),
499 ("set_col", DataType::Set),
500 ],
501 vec![vec![
502 ("id", Value::Integer(1)),
503 (
504 "list_col",
505 Value::List(vec![Value::Integer(1), Value::Integer(2)]),
506 ),
507 (
508 "set_col",
509 Value::Set(vec![
510 Value::Text("a".to_string()),
511 Value::Text("b".to_string()),
512 ]),
513 ),
514 ]],
515 );
516
517 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
518 let lines: Vec<&str> = csv.lines().collect();
519
520 assert_eq!(lines.len(), 2);
521 assert_eq!(lines[0], "id,list_col,set_col");
522 assert!(lines[1].contains("[1, 2]"));
524 assert!(lines[1].contains("{a, b}"));
525 }
526
527 #[test]
528 fn test_csv_uuid_formatting() {
529 let uuid_bytes = [
530 0xa8, 0xf1, 0x67, 0xf0, 0xeb, 0xe7, 0x4f, 0x20, 0xa3, 0x86, 0x31, 0xff, 0x13, 0x8b,
531 0xec, 0x3b,
532 ];
533 let result = create_test_result(
534 vec![("id", DataType::Uuid)],
535 vec![vec![("id", Value::Uuid(uuid_bytes))]],
536 );
537
538 let csv = CSVWriter::write(&result, &default_config()).expect("CSV write failed");
539 let lines: Vec<&str> = csv.lines().collect();
540
541 assert_eq!(lines.len(), 2);
542 assert_eq!(lines[1], "a8f167f0-ebe7-4f20-a386-31ff138bec3b");
544 }
545}