1use super::QueryResult;
4use std::io::Write;
5
6#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub enum OutputFormat {
9 #[default]
11 Table,
12 Json,
14 JsonLines,
16 Csv,
18 Tsv,
20}
21
22impl std::str::FromStr for OutputFormat {
23 type Err = String;
24
25 fn from_str(s: &str) -> Result<Self, Self::Err> {
26 match s.to_lowercase().as_str() {
27 "table" => Ok(OutputFormat::Table),
28 "json" => Ok(OutputFormat::Json),
29 "jsonl" | "jsonlines" | "ndjson" => Ok(OutputFormat::JsonLines),
30 "csv" => Ok(OutputFormat::Csv),
31 "tsv" => Ok(OutputFormat::Tsv),
32 _ => Err(format!(
33 "Unknown format: {}. Valid: table, json, jsonl, csv, tsv",
34 s
35 )),
36 }
37 }
38}
39
40impl std::fmt::Display for OutputFormat {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 OutputFormat::Table => write!(f, "table"),
44 OutputFormat::Json => write!(f, "json"),
45 OutputFormat::JsonLines => write!(f, "jsonl"),
46 OutputFormat::Csv => write!(f, "csv"),
47 OutputFormat::Tsv => write!(f, "tsv"),
48 }
49 }
50}
51
52pub struct QueryResultFormatter;
54
55impl QueryResultFormatter {
56 pub fn format(result: &QueryResult, format: OutputFormat) -> String {
58 match format {
59 OutputFormat::Table => Self::format_table(result),
60 OutputFormat::Json => Self::format_json(result),
61 OutputFormat::JsonLines => Self::format_jsonl(result),
62 OutputFormat::Csv => Self::format_csv(result),
63 OutputFormat::Tsv => Self::format_tsv(result),
64 }
65 }
66
67 pub fn write<W: Write>(
69 result: &QueryResult,
70 format: OutputFormat,
71 writer: &mut W,
72 ) -> std::io::Result<()> {
73 let output = Self::format(result, format);
74 writer.write_all(output.as_bytes())
75 }
76
77 fn format_table(result: &QueryResult) -> String {
79 if result.columns.is_empty() {
80 return String::new();
81 }
82
83 let mut widths: Vec<usize> = result.columns.iter().map(|c| c.len()).collect();
85
86 for row in &result.rows {
87 for (i, val) in row.iter().enumerate() {
88 if i < widths.len() {
89 widths[i] = widths[i].max(val.len());
90 }
91 }
92 }
93
94 let max_width = 50;
96 widths.iter_mut().for_each(|w| *w = (*w).min(max_width));
97
98 let mut output = String::new();
99
100 output.push('┌');
102 for (i, width) in widths.iter().enumerate() {
103 output.push_str(&"─".repeat(*width + 2));
104 if i < widths.len() - 1 {
105 output.push('┬');
106 }
107 }
108 output.push_str("┐\n");
109
110 output.push('│');
112 for (i, col) in result.columns.iter().enumerate() {
113 let truncated = Self::truncate(col, widths[i]);
114 output.push_str(&format!(" {:width$} │", truncated, width = widths[i]));
115 }
116 output.push('\n');
117
118 output.push('├');
120 for (i, width) in widths.iter().enumerate() {
121 output.push_str(&"─".repeat(*width + 2));
122 if i < widths.len() - 1 {
123 output.push('┼');
124 }
125 }
126 output.push_str("┤\n");
127
128 for row in &result.rows {
130 output.push('│');
131 for (i, val) in row.iter().enumerate() {
132 if i < widths.len() {
133 let truncated = Self::truncate(val, widths[i]);
134 output.push_str(&format!(" {:width$} │", truncated, width = widths[i]));
135 }
136 }
137 output.push('\n');
138 }
139
140 output.push('└');
142 for (i, width) in widths.iter().enumerate() {
143 output.push_str(&"─".repeat(*width + 2));
144 if i < widths.len() - 1 {
145 output.push('┴');
146 }
147 }
148 output.push_str("┘\n");
149
150 output.push_str(&format!(
152 "{} row{}\n",
153 result.rows.len(),
154 if result.rows.len() == 1 { "" } else { "s" }
155 ));
156
157 output
158 }
159
160 fn truncate(s: &str, max_len: usize) -> String {
162 if s.len() <= max_len {
163 s.to_string()
164 } else {
165 format!("{}…", &s[..max_len - 1])
166 }
167 }
168
169 fn format_json(result: &QueryResult) -> String {
171 let rows: Vec<serde_json::Value> = result
172 .rows
173 .iter()
174 .map(|row| {
175 let obj: serde_json::Map<String, serde_json::Value> = result
176 .columns
177 .iter()
178 .zip(row.iter())
179 .map(|(col, val)| (col.clone(), Self::json_value(val)))
180 .collect();
181 serde_json::Value::Object(obj)
182 })
183 .collect();
184
185 serde_json::to_string_pretty(&rows).unwrap_or_else(|_| "[]".to_string())
186 }
187
188 fn format_jsonl(result: &QueryResult) -> String {
190 result
191 .rows
192 .iter()
193 .map(|row| {
194 let obj: serde_json::Map<String, serde_json::Value> = result
195 .columns
196 .iter()
197 .zip(row.iter())
198 .map(|(col, val)| (col.clone(), Self::json_value(val)))
199 .collect();
200 serde_json::to_string(&serde_json::Value::Object(obj))
201 .unwrap_or_else(|_| "{}".to_string())
202 })
203 .collect::<Vec<_>>()
204 .join("\n")
205 }
206
207 fn json_value(val: &str) -> serde_json::Value {
209 if val == "NULL" {
210 return serde_json::Value::Null;
211 }
212
213 if let Ok(n) = val.parse::<i64>() {
215 return serde_json::Value::Number(n.into());
216 }
217 if let Ok(n) = val.parse::<f64>() {
218 if let Some(num) = serde_json::Number::from_f64(n) {
219 return serde_json::Value::Number(num);
220 }
221 }
222
223 if val.eq_ignore_ascii_case("true") {
225 return serde_json::Value::Bool(true);
226 }
227 if val.eq_ignore_ascii_case("false") {
228 return serde_json::Value::Bool(false);
229 }
230
231 serde_json::Value::String(val.to_string())
233 }
234
235 fn format_csv(result: &QueryResult) -> String {
237 let mut output = String::new();
238
239 output.push_str(&Self::csv_row(&result.columns));
241 output.push('\n');
242
243 for row in &result.rows {
245 output.push_str(&Self::csv_row(row));
246 output.push('\n');
247 }
248
249 output
250 }
251
252 fn csv_row(values: &[String]) -> String {
254 values
255 .iter()
256 .map(|v| Self::csv_escape(v))
257 .collect::<Vec<_>>()
258 .join(",")
259 }
260
261 fn csv_escape(val: &str) -> String {
263 if val.contains(',') || val.contains('"') || val.contains('\n') || val.contains('\r') {
264 format!("\"{}\"", val.replace('"', "\"\""))
265 } else {
266 val.to_string()
267 }
268 }
269
270 fn format_tsv(result: &QueryResult) -> String {
272 let mut output = String::new();
273
274 output.push_str(&result.columns.join("\t"));
276 output.push('\n');
277
278 for row in &result.rows {
280 let escaped: Vec<String> = row
281 .iter()
282 .map(|v| v.replace('\t', "\\t").replace('\n', "\\n"))
283 .collect();
284 output.push_str(&escaped.join("\t"));
285 output.push('\n');
286 }
287
288 output
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 fn sample_result() -> QueryResult {
297 QueryResult {
298 columns: vec!["id".to_string(), "name".to_string(), "age".to_string()],
299 column_types: vec![
300 "INTEGER".to_string(),
301 "VARCHAR".to_string(),
302 "INTEGER".to_string(),
303 ],
304 rows: vec![
305 vec!["1".to_string(), "Alice".to_string(), "30".to_string()],
306 vec!["2".to_string(), "Bob".to_string(), "25".to_string()],
307 ],
308 execution_time_secs: 0.001,
309 }
310 }
311
312 #[test]
313 fn test_format_table() {
314 let result = sample_result();
315 let output = QueryResultFormatter::format(&result, OutputFormat::Table);
316 assert!(output.contains("Alice"));
317 assert!(output.contains("Bob"));
318 assert!(output.contains("2 rows"));
319 }
320
321 #[test]
322 fn test_format_json() {
323 let result = sample_result();
324 let output = QueryResultFormatter::format(&result, OutputFormat::Json);
325 let parsed: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();
326 assert_eq!(parsed.len(), 2);
327 assert_eq!(parsed[0]["name"], "Alice");
328 assert_eq!(parsed[0]["age"], 30);
329 }
330
331 #[test]
332 fn test_format_csv() {
333 let result = sample_result();
334 let output = QueryResultFormatter::format(&result, OutputFormat::Csv);
335 assert!(output.starts_with("id,name,age\n"));
336 assert!(output.contains("1,Alice,30"));
337 }
338
339 #[test]
340 fn test_csv_escape() {
341 assert_eq!(QueryResultFormatter::csv_escape("hello"), "hello");
342 assert_eq!(
343 QueryResultFormatter::csv_escape("hello,world"),
344 "\"hello,world\""
345 );
346 assert_eq!(
347 QueryResultFormatter::csv_escape("say \"hi\""),
348 "\"say \"\"hi\"\"\""
349 );
350 }
351
352 #[test]
353 fn test_format_tsv() {
354 let result = sample_result();
355 let output = QueryResultFormatter::format(&result, OutputFormat::Tsv);
356 assert!(output.starts_with("id\tname\tage\n"));
357 assert!(output.contains("1\tAlice\t30"));
358 }
359
360 #[test]
361 fn test_json_value_conversion() {
362 assert_eq!(
363 QueryResultFormatter::json_value("NULL"),
364 serde_json::Value::Null
365 );
366 assert_eq!(
367 QueryResultFormatter::json_value("42"),
368 serde_json::json!(42)
369 );
370 assert_eq!(
371 QueryResultFormatter::json_value("3.14"),
372 serde_json::json!(3.14)
373 );
374 assert_eq!(
375 QueryResultFormatter::json_value("true"),
376 serde_json::json!(true)
377 );
378 assert_eq!(
379 QueryResultFormatter::json_value("hello"),
380 serde_json::json!("hello")
381 );
382 }
383}