1use serde_json::{Map, Value};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ExportFormat {
11 Csv,
12 Json,
13 SqlInsert,
14}
15
16impl ExportFormat {
17 pub fn parse(s: &str) -> Option<Self> {
18 match s.trim().to_ascii_lowercase().as_str() {
19 "csv" => Some(Self::Csv),
20 "json" => Some(Self::Json),
21 "sqlinsert" | "sql_insert" | "insert" => Some(Self::SqlInsert),
22 _ => None,
23 }
24 }
25
26 pub fn as_str(self) -> &'static str {
27 match self {
28 Self::Csv => "csv",
29 Self::Json => "json",
30 Self::SqlInsert => "sqlinsert",
31 }
32 }
33}
34
35pub fn columns_from_rows(rows: &[Value]) -> Vec<String> {
37 rows.first()
38 .and_then(|r| r.as_object())
39 .map(|obj| obj.keys().cloned().collect())
40 .unwrap_or_default()
41}
42
43pub fn rows_to_csv(rows: &[Value], columns: &[String]) -> String {
44 let mut out = String::new();
45 out.push_str(
46 &columns
47 .iter()
48 .map(|c| csv_quote(c))
49 .collect::<Vec<_>>()
50 .join(","),
51 );
52 out.push('\n');
53 for row in rows {
54 let obj = row.as_object();
55 let line = columns
56 .iter()
57 .map(|col| {
58 let val = obj.and_then(|m| m.get(col)).unwrap_or(&Value::Null);
59 csv_cell(val)
60 })
61 .collect::<Vec<_>>()
62 .join(",");
63 out.push_str(&line);
64 out.push('\n');
65 }
66 out
67}
68
69fn csv_quote(s: &str) -> String {
70 format!("\"{}\"", s.replace('"', "\"\""))
71}
72
73fn csv_cell(val: &Value) -> String {
74 if val.is_null() {
75 return String::new();
76 }
77 let str = match val {
78 Value::String(s) => s.clone(),
79 Value::Bool(b) => b.to_string(),
80 Value::Number(n) => n.to_string(),
81 other => other.to_string(),
82 };
83 if str.contains(',') || str.contains('\n') || str.contains('"') {
84 csv_quote(&str)
85 } else {
86 str
87 }
88}
89
90pub fn rows_to_sql_insert(rows: &[Value], columns: &[String], schema: &str, table: &str) -> String {
91 let table_name = format!(
92 "\"{}\".\"{}\"",
93 schema.replace('"', "\"\""),
94 table.replace('"', "\"\"")
95 );
96 let cols = columns
97 .iter()
98 .map(|c| format!("\"{}\"", c.replace('"', "\"\"")))
99 .collect::<Vec<_>>()
100 .join(", ");
101
102 rows.iter()
103 .map(|row| {
104 let obj = row.as_object().cloned().unwrap_or_else(Map::new);
105 let values = columns
106 .iter()
107 .map(|col| sql_literal(obj.get(col).unwrap_or(&Value::Null)))
108 .collect::<Vec<_>>()
109 .join(", ");
110 format!("INSERT INTO {table_name} ({cols}) VALUES ({values});")
111 })
112 .collect::<Vec<_>>()
113 .join("\n")
114}
115
116fn sql_literal(val: &Value) -> String {
117 match val {
118 Value::Null => "NULL".into(),
119 Value::Bool(b) => {
120 if *b {
121 "TRUE".into()
122 } else {
123 "FALSE".into()
124 }
125 }
126 Value::Number(n) => n.to_string(),
127 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
128 other => format!("'{}'", other.to_string().replace('\'', "''")),
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use serde_json::json;
136
137 #[test]
138 fn csv_escapes_quotes_and_commas() {
139 let rows = [json!({ "a": "hello, world", "b": "say \"hi\"" })];
140 let cols = vec!["a".into(), "b".into()];
141 let csv = rows_to_csv(&rows, &cols);
142 assert!(csv.contains("\"hello, world\""));
143 assert!(csv.contains("\"say \"\"hi\"\"\""));
144 }
145
146 #[test]
147 fn sql_insert_null_bool_number() {
148 let rows = [json!({ "id": 1, "ok": true, "note": null })];
149 let cols = vec!["id".into(), "ok".into(), "note".into()];
150 let sql = rows_to_sql_insert(&rows, &cols, "public", "t");
151 assert_eq!(
152 sql,
153 "INSERT INTO \"public\".\"t\" (\"id\", \"ok\", \"note\") VALUES (1, TRUE, NULL);"
154 );
155 }
156
157 #[test]
158 fn format_parse() {
159 assert_eq!(ExportFormat::parse("CSV"), Some(ExportFormat::Csv));
160 assert_eq!(
161 ExportFormat::parse("sql_insert"),
162 Some(ExportFormat::SqlInsert)
163 );
164 assert!(ExportFormat::parse("xlsx").is_none());
165 }
166}