1use std::io::Write;
7
8use crate::error::Result;
9use crate::models::{Column, Row, Value};
10
11use super::formatter::Formatter;
12
13pub struct CsvFormatter {
17 _row_count: usize,
19}
20
21impl CsvFormatter {
22 pub fn new() -> Self {
24 Self { _row_count: 0 }
25 }
26}
27
28impl Default for CsvFormatter {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl Formatter for CsvFormatter {
35 fn write_header(&mut self, writer: &mut dyn Write, columns: &[Column]) -> Result<()> {
36 let header: Vec<String> = columns.iter().map(|c| escape_csv(&c.name)).collect();
37 writeln!(writer, "{}", header.join(","))?;
38 Ok(())
39 }
40
41 fn write_row(&mut self, writer: &mut dyn Write, row: &Row) -> Result<()> {
42 let values: Vec<String> = row.columns.iter().map(format_value_csv).collect();
43 writeln!(writer, "{}", values.join(","))?;
44 self._row_count += 1;
45 Ok(())
46 }
47
48 fn write_footer(&mut self, _writer: &mut dyn Write) -> Result<()> {
49 Ok(())
51 }
52
53 fn supports_streaming(&self) -> bool {
54 true
55 }
56}
57
58fn format_value_csv(value: &Value) -> String {
60 match value {
61 Value::Null => String::new(),
62 Value::Bool(b) => b.to_string(),
63 Value::Int(i) => i.to_string(),
64 Value::Float(f) => f.to_string(),
65 Value::Text(s) => escape_csv(s),
66 Value::Bytes(b) => {
67 escape_csv(
69 &b.iter()
70 .map(|byte| format!("{:02x}", byte))
71 .collect::<String>(),
72 )
73 }
74 Value::Vector(v) => {
75 let json = serde_json::to_string(v).unwrap_or_default();
77 escape_csv(&json)
78 }
79 }
80}
81
82fn escape_csv(s: &str) -> String {
86 if s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r') {
87 format!("\"{}\"", s.replace('"', "\"\""))
89 } else {
90 s.to_string()
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::models::DataType;
98
99 fn test_columns() -> Vec<Column> {
100 vec![
101 Column::new("id", DataType::Int),
102 Column::new("name", DataType::Text),
103 ]
104 }
105
106 #[test]
107 fn test_csv_basic() {
108 let mut formatter = CsvFormatter::new();
109 let mut output = Vec::new();
110
111 let columns = test_columns();
112 formatter.write_header(&mut output, &columns).unwrap();
113
114 let row = Row::new(vec![Value::Int(1), Value::Text("Alice".to_string())]);
115 formatter.write_row(&mut output, &row).unwrap();
116
117 formatter.write_footer(&mut output).unwrap();
118
119 let result = String::from_utf8(output).unwrap();
120 assert_eq!(result, "id,name\n1,Alice\n");
121 }
122
123 #[test]
124 fn test_csv_escape_comma() {
125 let mut formatter = CsvFormatter::new();
126 let mut output = Vec::new();
127
128 let columns = test_columns();
129 formatter.write_header(&mut output, &columns).unwrap();
130
131 let row = Row::new(vec![Value::Int(1), Value::Text("Alice, Bob".to_string())]);
132 formatter.write_row(&mut output, &row).unwrap();
133
134 let result = String::from_utf8(output).unwrap();
135 assert!(result.contains("\"Alice, Bob\""));
136 }
137
138 #[test]
139 fn test_csv_escape_quote() {
140 let mut formatter = CsvFormatter::new();
141 let mut output = Vec::new();
142
143 let columns = test_columns();
144 formatter.write_header(&mut output, &columns).unwrap();
145
146 let row = Row::new(vec![
147 Value::Int(1),
148 Value::Text("Alice \"The Great\"".to_string()),
149 ]);
150 formatter.write_row(&mut output, &row).unwrap();
151
152 let result = String::from_utf8(output).unwrap();
153 assert!(result.contains("\"Alice \"\"The Great\"\"\""));
154 }
155
156 #[test]
157 fn test_csv_escape_newline() {
158 let mut formatter = CsvFormatter::new();
159 let mut output = Vec::new();
160
161 let columns = test_columns();
162 formatter.write_header(&mut output, &columns).unwrap();
163
164 let row = Row::new(vec![Value::Int(1), Value::Text("Line1\nLine2".to_string())]);
165 formatter.write_row(&mut output, &row).unwrap();
166
167 let result = String::from_utf8(output).unwrap();
168 assert!(result.contains("\"Line1\nLine2\""));
169 }
170
171 #[test]
172 fn test_csv_null_value() {
173 let mut formatter = CsvFormatter::new();
174 let mut output = Vec::new();
175
176 let columns = test_columns();
177 formatter.write_header(&mut output, &columns).unwrap();
178
179 let row = Row::new(vec![Value::Int(1), Value::Null]);
180 formatter.write_row(&mut output, &row).unwrap();
181
182 let result = String::from_utf8(output).unwrap();
183 assert_eq!(result, "id,name\n1,\n");
184 }
185
186 #[test]
187 fn test_csv_supports_streaming() {
188 let formatter = CsvFormatter::new();
189 assert!(formatter.supports_streaming());
190 }
191}