sql_cli/data/
data_exporter.rs1use crate::buffer::BufferAPI;
2use crate::data::data_provider::DataProvider;
3use anyhow::{anyhow, Result};
4use chrono::Local;
5use serde_json::Value;
6use std::fs::File;
7use std::io::Write;
8
9pub struct DataExporter;
11
12impl DataExporter {
13 pub fn export_provider_to_csv(provider: &dyn DataProvider) -> Result<String> {
15 let row_count = provider.get_row_count();
16 if row_count == 0 {
17 return Err(anyhow!("No data to export"));
18 }
19
20 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
22 let filename = format!("query_results_{timestamp}.csv");
23
24 let mut file = File::create(&filename)?;
25
26 let headers = provider.get_column_names();
28 let header_line = headers.join(",");
29 writeln!(file, "{header_line}")?;
30
31 for i in 0..row_count {
33 if let Some(row) = provider.get_row(i) {
34 let escaped_row: Vec<String> = row
35 .iter()
36 .map(|field| Self::escape_csv_field(field))
37 .collect();
38 let row_line = escaped_row.join(",");
39 writeln!(file, "{row_line}")?;
40 }
41 }
42
43 Ok(format!(
44 "✓ Exported {row_count} rows to CSV file: {filename}"
45 ))
46 }
47
48 pub fn export_provider_to_json(provider: &dyn DataProvider) -> Result<String> {
50 let row_count = provider.get_row_count();
51 if row_count == 0 {
52 return Err(anyhow!("No data to export"));
53 }
54
55 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
57 let filename = format!("query_results_{timestamp}.json");
58
59 let headers = provider.get_column_names();
61 let mut json_array = Vec::new();
62
63 for i in 0..row_count {
64 if let Some(row) = provider.get_row(i) {
65 let mut json_obj = serde_json::Map::new();
66 for (j, value) in row.iter().enumerate() {
67 if j < headers.len() {
68 json_obj.insert(headers[j].clone(), Value::String(value.clone()));
69 }
70 }
71 json_array.push(Value::Object(json_obj));
72 }
73 }
74
75 let file = File::create(&filename)?;
76 serde_json::to_writer_pretty(file, &json_array)?;
77
78 Ok(format!(
79 "✓ Exported {row_count} rows to JSON file: {filename}"
80 ))
81 }
82
83 pub fn export_to_csv(buffer: &dyn BufferAPI) -> Result<String> {
85 let datatable = buffer
86 .get_datatable()
87 .ok_or_else(|| anyhow!("No results to export - run a query first"))?;
88
89 if datatable.row_count() == 0 {
90 return Err(anyhow!("No data to export"));
91 }
92
93 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
95 let filename = format!("query_results_{timestamp}.csv");
96
97 let mut file = File::create(&filename)?;
98
99 let headers = datatable.column_names();
101 let header_line = headers.join(",");
102 writeln!(file, "{header_line}")?;
103
104 let mut row_count = 0;
106 for row_data in datatable.to_string_table() {
107 let row: Vec<String> = row_data.iter().map(|s| Self::escape_csv_field(s)).collect();
108 let row_line = row.join(",");
109 writeln!(file, "{row_line}")?;
110 row_count += 1;
111 }
112
113 Ok(format!(
114 "✓ Exported {row_count} rows to CSV file: {filename}"
115 ))
116 }
117
118 pub fn export_to_json(buffer: &dyn BufferAPI, include_filtered: bool) -> Result<String> {
120 let datatable = buffer
123 .get_datatable()
124 .ok_or_else(|| anyhow!("No results to export - run a query first"))?;
125
126 let data = Self::datatable_to_json_values(datatable);
128
129 let data_to_export = if include_filtered && buffer.is_filter_active() {
131 Self::get_filtered_data(buffer)?
132 } else if include_filtered && buffer.is_fuzzy_filter_active() {
133 Self::get_fuzzy_filtered_data(buffer)?
134 } else {
135 data.clone()
136 };
137
138 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
140 let filename = format!("query_results_{timestamp}.json");
141
142 let file = File::create(&filename)?;
143 serde_json::to_writer_pretty(file, &data_to_export)?;
144
145 let filter_info =
146 if include_filtered && (buffer.is_filter_active() || buffer.is_fuzzy_filter_active()) {
147 " (filtered)"
148 } else {
149 ""
150 };
151
152 Ok(format!(
153 "✓ Exported{} {} rows to JSON file: {}",
154 filter_info,
155 data_to_export.len(),
156 filename
157 ))
158 }
159
160 pub fn export_selected_to_csv(
162 buffer: &dyn BufferAPI,
163 selected_rows: &[usize],
164 ) -> Result<String> {
165 let datatable = buffer
166 .get_datatable()
167 .ok_or_else(|| anyhow!("No results to export"))?;
168 let data = Self::datatable_to_json_values(datatable);
169
170 if selected_rows.is_empty() {
171 return Err(anyhow!("No rows selected"));
172 }
173
174 let first_row_idx = selected_rows[0];
176 let first_row = data
177 .get(first_row_idx)
178 .ok_or_else(|| anyhow!("Invalid row index"))?;
179
180 let obj = first_row
181 .as_object()
182 .ok_or_else(|| anyhow!("Invalid data format"))?;
183
184 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
186 let filename = format!("selected_rows_{timestamp}.csv");
187
188 let mut file = File::create(&filename)?;
189
190 let headers: Vec<&str> = obj.keys().map(std::string::String::as_str).collect();
192 let header_line = headers.join(",");
193 writeln!(file, "{header_line}")?;
194
195 let mut row_count = 0;
197 for &row_idx in selected_rows {
198 if let Some(item) = data.get(row_idx) {
199 if let Some(obj) = item.as_object() {
200 let row: Vec<String> = headers
201 .iter()
202 .map(|&header| match obj.get(header) {
203 Some(Value::String(s)) => Self::escape_csv_field(s),
204 Some(Value::Number(n)) => n.to_string(),
205 Some(Value::Bool(b)) => b.to_string(),
206 Some(Value::Null) => String::new(),
207 Some(other) => Self::escape_csv_field(&other.to_string()),
208 None => String::new(),
209 })
210 .collect();
211
212 let row_line = row.join(",");
213 writeln!(file, "{row_line}")?;
214 row_count += 1;
215 }
216 }
217 }
218
219 Ok(format!("Exported {row_count} selected rows to {filename}"))
220 }
221
222 fn escape_csv_field(field: &str) -> String {
224 if field.contains(',') || field.contains('"') || field.contains('\n') {
225 format!("\"{}\"", field.replace('"', "\"\""))
227 } else {
228 field.to_string()
229 }
230 }
231
232 fn get_filtered_data(buffer: &dyn BufferAPI) -> Result<Vec<Value>> {
234 let datatable = buffer
235 .get_datatable()
236 .ok_or_else(|| anyhow!("No results available"))?;
237 let data = Self::datatable_to_json_values(datatable);
238
239 let filter_pattern = buffer.get_filter_pattern();
240 if filter_pattern.is_empty() {
241 return Ok(data.clone());
242 }
243
244 let regex = regex::Regex::new(&filter_pattern)
245 .map_err(|e| anyhow!("Invalid filter pattern: {}", e))?;
246
247 let filtered: Vec<Value> = data
248 .iter()
249 .filter(|item| {
250 if let Some(obj) = item.as_object() {
251 obj.values().any(|v| {
252 let text = match v {
253 Value::String(s) => s.clone(),
254 Value::Number(n) => n.to_string(),
255 Value::Bool(b) => b.to_string(),
256 _ => String::new(),
257 };
258 regex.is_match(&text)
259 })
260 } else {
261 false
262 }
263 })
264 .cloned()
265 .collect();
266
267 Ok(filtered)
268 }
269
270 fn get_fuzzy_filtered_data(buffer: &dyn BufferAPI) -> Result<Vec<Value>> {
272 let datatable = buffer
273 .get_datatable()
274 .ok_or_else(|| anyhow!("No results available"))?;
275 let data = Self::datatable_to_json_values(datatable);
276
277 let indices = buffer.get_fuzzy_filter_indices();
278 if indices.is_empty() {
279 return Ok(data.clone());
280 }
281
282 let filtered: Vec<Value> = indices
283 .iter()
284 .filter_map(|&idx| data.get(idx).cloned())
285 .collect();
286
287 Ok(filtered)
288 }
289
290 #[must_use]
292 pub fn format_for_clipboard(value: &Value, _header: &str) -> String {
293 match value {
294 Value::String(s) => s.clone(),
295 Value::Number(n) => n.to_string(),
296 Value::Bool(b) => b.to_string(),
297 Value::Null => "NULL".to_string(),
298 other => other.to_string(),
299 }
300 }
301
302 #[must_use]
304 pub fn format_row_for_clipboard(row: &serde_json::Map<String, Value>) -> String {
305 let values: Vec<String> = row
306 .values()
307 .map(|v| Self::format_for_clipboard(v, ""))
308 .collect();
309 values.join("\t")
310 }
311
312 #[must_use]
314 pub fn convert_json_to_strings(data: &[Value]) -> Vec<Vec<String>> {
315 if let Some(first_row) = data.first() {
316 if let Some(obj) = first_row.as_object() {
317 let headers: Vec<&str> = obj.keys().map(std::string::String::as_str).collect();
318
319 data.iter()
320 .map(|item| {
321 if let Some(obj) = item.as_object() {
322 headers
323 .iter()
324 .map(|&header| match obj.get(header) {
325 Some(Value::String(s)) => s.clone(),
326 Some(Value::Number(n)) => n.to_string(),
327 Some(Value::Bool(b)) => b.to_string(),
328 Some(Value::Null) => String::new(),
329 Some(other) => other.to_string(),
330 None => String::new(),
331 })
332 .collect()
333 } else {
334 vec![]
335 }
336 })
337 .collect()
338 } else {
339 vec![]
340 }
341 } else {
342 vec![]
343 }
344 }
345
346 #[must_use]
348 pub fn generate_csv_text(data: &[Value]) -> Option<String> {
349 let first_row = data.first()?;
350 let obj = first_row.as_object()?;
351 let headers: Vec<&str> = obj.keys().map(std::string::String::as_str).collect();
352
353 let mut csv_text = headers.join(",") + "\n";
355
356 for row in data {
357 if let Some(obj) = row.as_object() {
358 let values: Vec<String> = headers
359 .iter()
360 .map(|&header| match obj.get(header) {
361 Some(Value::String(s)) => Self::escape_csv_field(s),
362 Some(Value::Number(n)) => n.to_string(),
363 Some(Value::Bool(b)) => b.to_string(),
364 Some(Value::Null) => String::new(),
365 Some(other) => Self::escape_csv_field(&other.to_string()),
366 None => String::new(),
367 })
368 .collect();
369 csv_text.push_str(&values.join(","));
370 csv_text.push('\n');
371 }
372 }
373
374 Some(csv_text)
375 }
376
377 #[must_use]
379 pub fn generate_tsv_text(data: &[Value]) -> Option<String> {
380 let first_row = data.first()?;
381 let obj = first_row.as_object()?;
382 let headers: Vec<&str> = obj.keys().map(std::string::String::as_str).collect();
383
384 let mut tsv_text = headers.join("\t") + "\r\n";
386
387 for row in data {
388 if let Some(obj) = row.as_object() {
389 let values: Vec<String> = headers
390 .iter()
391 .map(|&header| match obj.get(header) {
392 Some(Value::String(s)) => {
393 s.replace('\t', " ").replace('\n', " ").replace('\r', "")
394 }
395 Some(Value::Number(n)) => n.to_string(),
396 Some(Value::Bool(b)) => b.to_string(),
397 Some(Value::Null) => String::new(),
398 Some(other) => other
399 .to_string()
400 .replace('\t', " ")
401 .replace('\n', " ")
402 .replace('\r', ""),
403 None => String::new(),
404 })
405 .collect();
406 tsv_text.push_str(&values.join("\t"));
407 tsv_text.push_str("\r\n");
408 }
409 }
410
411 Some(tsv_text)
412 }
413
414 #[must_use]
416 pub fn datatable_to_json_values(datatable: &crate::data::datatable::DataTable) -> Vec<Value> {
417 use serde_json::json;
418
419 let headers = datatable.column_names();
420 let mut result = Vec::new();
421
422 for row_data in datatable.to_string_table() {
423 let mut obj = serde_json::Map::new();
424 for (i, header) in headers.iter().enumerate() {
425 if let Some(value) = row_data.get(i) {
426 obj.insert(header.clone(), json!(value));
427 }
428 }
429 result.push(Value::Object(obj));
430 }
431
432 result
433 }
434}