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