sql_cli/data/
data_exporter.rs

1use 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
9/// Handles exporting data from buffers to various formats
10pub struct DataExporter;
11
12impl DataExporter {
13    /// Export data to CSV format using DataProvider trait
14    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        // Generate filename with timestamp
21        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
22        let filename = format!("query_results_{}.csv", timestamp);
23
24        let mut file = File::create(&filename)?;
25
26        // Write headers
27        let headers = provider.get_column_names();
28        let header_line = headers.join(",");
29        writeln!(file, "{}", header_line)?;
30
31        // Write data rows
32        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 {} rows to CSV file: {}",
45            row_count, filename
46        ))
47    }
48
49    /// Export data to JSON format using DataProvider trait
50    pub fn export_provider_to_json(provider: &dyn DataProvider) -> Result<String> {
51        let row_count = provider.get_row_count();
52        if row_count == 0 {
53            return Err(anyhow!("No data to export"));
54        }
55
56        // Generate filename with timestamp
57        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
58        let filename = format!("query_results_{}.json", timestamp);
59
60        // Build JSON array
61        let headers = provider.get_column_names();
62        let mut json_array = Vec::new();
63
64        for i in 0..row_count {
65            if let Some(row) = provider.get_row(i) {
66                let mut json_obj = serde_json::Map::new();
67                for (j, value) in row.iter().enumerate() {
68                    if j < headers.len() {
69                        json_obj.insert(headers[j].clone(), Value::String(value.clone()));
70                    }
71                }
72                json_array.push(Value::Object(json_obj));
73            }
74        }
75
76        let file = File::create(&filename)?;
77        serde_json::to_writer_pretty(file, &json_array)?;
78
79        Ok(format!(
80            "✓ Exported {} rows to JSON file: {}",
81            row_count, filename
82        ))
83    }
84
85    /// V50: Export buffer results to CSV format using DataTable
86    pub fn export_to_csv(buffer: &dyn BufferAPI) -> Result<String> {
87        let datatable = buffer
88            .get_datatable()
89            .ok_or_else(|| anyhow!("No results to export - run a query first"))?;
90
91        if datatable.row_count() == 0 {
92            return Err(anyhow!("No data to export"));
93        }
94
95        // Generate filename with timestamp
96        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
97        let filename = format!("query_results_{}.csv", timestamp);
98
99        let mut file = File::create(&filename)?;
100
101        // Write headers
102        let headers = datatable.column_names();
103        let header_line = headers.join(",");
104        writeln!(file, "{}", header_line)?;
105
106        // Write data rows
107        let mut row_count = 0;
108        for row_data in datatable.to_string_table() {
109            let row: Vec<String> = row_data.iter().map(|s| Self::escape_csv_field(s)).collect();
110            let row_line = row.join(",");
111            writeln!(file, "{}", row_line)?;
112            row_count += 1;
113        }
114
115        Ok(format!(
116            "✓ Exported {} rows to CSV file: {}",
117            row_count, filename
118        ))
119    }
120
121    /// Export buffer results to JSON format
122    pub fn export_to_json(buffer: &dyn BufferAPI, include_filtered: bool) -> Result<String> {
123        // V50: For now, convert DataTable back to JSON for export
124        // This will be optimized later
125        let datatable = buffer
126            .get_datatable()
127            .ok_or_else(|| anyhow!("No results to export - run a query first"))?;
128
129        // Convert DataTable to JSON values for export
130        let data = Self::datatable_to_json_values(datatable);
131
132        // Determine what data to export
133        let data_to_export = if include_filtered && buffer.is_filter_active() {
134            Self::get_filtered_data(buffer)?
135        } else if include_filtered && buffer.is_fuzzy_filter_active() {
136            Self::get_fuzzy_filtered_data(buffer)?
137        } else {
138            data.clone()
139        };
140
141        // Generate filename with timestamp
142        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
143        let filename = format!("query_results_{}.json", timestamp);
144
145        let file = File::create(&filename)?;
146        serde_json::to_writer_pretty(file, &data_to_export)?;
147
148        let filter_info =
149            if include_filtered && (buffer.is_filter_active() || buffer.is_fuzzy_filter_active()) {
150                " (filtered)"
151            } else {
152                ""
153            };
154
155        Ok(format!(
156            "✓ Exported{} {} rows to JSON file: {}",
157            filter_info,
158            data_to_export.len(),
159            filename
160        ))
161    }
162
163    /// V50: Export selected rows to CSV
164    pub fn export_selected_to_csv(
165        buffer: &dyn BufferAPI,
166        selected_rows: &[usize],
167    ) -> Result<String> {
168        let datatable = buffer
169            .get_datatable()
170            .ok_or_else(|| anyhow!("No results to export"))?;
171        let data = Self::datatable_to_json_values(datatable);
172
173        if selected_rows.is_empty() {
174            return Err(anyhow!("No rows selected"));
175        }
176
177        // Get first valid row for headers
178        let first_row_idx = selected_rows[0];
179        let first_row = data
180            .get(first_row_idx)
181            .ok_or_else(|| anyhow!("Invalid row index"))?;
182
183        let obj = first_row
184            .as_object()
185            .ok_or_else(|| anyhow!("Invalid data format"))?;
186
187        // Generate filename with timestamp
188        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
189        let filename = format!("selected_rows_{}.csv", timestamp);
190
191        let mut file = File::create(&filename)?;
192
193        // Write headers
194        let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
195        let header_line = headers.join(",");
196        writeln!(file, "{}", header_line)?;
197
198        // Write selected rows
199        let mut row_count = 0;
200        for &row_idx in selected_rows {
201            if let Some(item) = data.get(row_idx) {
202                if let Some(obj) = item.as_object() {
203                    let row: Vec<String> = headers
204                        .iter()
205                        .map(|&header| match obj.get(header) {
206                            Some(Value::String(s)) => Self::escape_csv_field(s),
207                            Some(Value::Number(n)) => n.to_string(),
208                            Some(Value::Bool(b)) => b.to_string(),
209                            Some(Value::Null) => String::new(),
210                            Some(other) => Self::escape_csv_field(&other.to_string()),
211                            None => String::new(),
212                        })
213                        .collect();
214
215                    let row_line = row.join(",");
216                    writeln!(file, "{}", row_line)?;
217                    row_count += 1;
218                }
219            }
220        }
221
222        Ok(format!(
223            "Exported {} selected rows to {}",
224            row_count, filename
225        ))
226    }
227
228    /// Helper to escape CSV fields that contain special characters
229    fn escape_csv_field(field: &str) -> String {
230        if field.contains(',') || field.contains('"') || field.contains('\n') {
231            // Escape quotes by doubling them and wrap field in quotes
232            format!("\"{}\"", field.replace('"', "\"\""))
233        } else {
234            field.to_string()
235        }
236    }
237
238    /// Get filtered data based on current filter
239    fn get_filtered_data(buffer: &dyn BufferAPI) -> Result<Vec<Value>> {
240        let datatable = buffer
241            .get_datatable()
242            .ok_or_else(|| anyhow!("No results available"))?;
243        let data = Self::datatable_to_json_values(datatable);
244
245        let filter_pattern = buffer.get_filter_pattern();
246        if filter_pattern.is_empty() {
247            return Ok(data.clone());
248        }
249
250        let regex = regex::Regex::new(&filter_pattern)
251            .map_err(|e| anyhow!("Invalid filter pattern: {}", e))?;
252
253        let filtered: Vec<Value> = data
254            .iter()
255            .filter(|item| {
256                if let Some(obj) = item.as_object() {
257                    obj.values().any(|v| {
258                        let text = match v {
259                            Value::String(s) => s.clone(),
260                            Value::Number(n) => n.to_string(),
261                            Value::Bool(b) => b.to_string(),
262                            _ => String::new(),
263                        };
264                        regex.is_match(&text)
265                    })
266                } else {
267                    false
268                }
269            })
270            .cloned()
271            .collect();
272
273        Ok(filtered)
274    }
275
276    /// Get fuzzy filtered data based on current fuzzy filter
277    fn get_fuzzy_filtered_data(buffer: &dyn BufferAPI) -> Result<Vec<Value>> {
278        let datatable = buffer
279            .get_datatable()
280            .ok_or_else(|| anyhow!("No results available"))?;
281        let data = Self::datatable_to_json_values(datatable);
282
283        let indices = buffer.get_fuzzy_filter_indices();
284        if indices.is_empty() {
285            return Ok(data.clone());
286        }
287
288        let filtered: Vec<Value> = indices
289            .iter()
290            .filter_map(|&idx| data.get(idx).cloned())
291            .collect();
292
293        Ok(filtered)
294    }
295
296    /// Export a single value to clipboard-friendly format
297    pub fn format_for_clipboard(value: &Value, _header: &str) -> String {
298        match value {
299            Value::String(s) => s.clone(),
300            Value::Number(n) => n.to_string(),
301            Value::Bool(b) => b.to_string(),
302            Value::Null => "NULL".to_string(),
303            other => other.to_string(),
304        }
305    }
306
307    /// Export row as tab-separated values for clipboard
308    pub fn format_row_for_clipboard(row: &serde_json::Map<String, Value>) -> String {
309        let values: Vec<String> = row
310            .values()
311            .map(|v| Self::format_for_clipboard(v, ""))
312            .collect();
313        values.join("\t")
314    }
315
316    /// Convert JSON query results to a 2D vector of strings for display
317    pub fn convert_json_to_strings(data: &[Value]) -> Vec<Vec<String>> {
318        if let Some(first_row) = data.first() {
319            if let Some(obj) = first_row.as_object() {
320                let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
321
322                data.iter()
323                    .map(|item| {
324                        if let Some(obj) = item.as_object() {
325                            headers
326                                .iter()
327                                .map(|&header| match obj.get(header) {
328                                    Some(Value::String(s)) => s.clone(),
329                                    Some(Value::Number(n)) => n.to_string(),
330                                    Some(Value::Bool(b)) => b.to_string(),
331                                    Some(Value::Null) => "".to_string(),
332                                    Some(other) => other.to_string(),
333                                    None => "".to_string(),
334                                })
335                                .collect()
336                        } else {
337                            vec![]
338                        }
339                    })
340                    .collect()
341            } else {
342                vec![]
343            }
344        } else {
345            vec![]
346        }
347    }
348
349    /// Generate CSV text from JSON data for clipboard operations
350    pub fn generate_csv_text(data: &[Value]) -> Option<String> {
351        let first_row = data.first()?;
352        let obj = first_row.as_object()?;
353        let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
354
355        // Create CSV format
356        let mut csv_text = headers.join(",") + "\n";
357
358        for row in data {
359            if let Some(obj) = row.as_object() {
360                let values: Vec<String> = headers
361                    .iter()
362                    .map(|&header| match obj.get(header) {
363                        Some(Value::String(s)) => Self::escape_csv_field(s),
364                        Some(Value::Number(n)) => n.to_string(),
365                        Some(Value::Bool(b)) => b.to_string(),
366                        Some(Value::Null) => String::new(),
367                        Some(other) => Self::escape_csv_field(&other.to_string()),
368                        None => String::new(),
369                    })
370                    .collect();
371                csv_text.push_str(&values.join(","));
372                csv_text.push('\n');
373            }
374        }
375
376        Some(csv_text)
377    }
378
379    /// Generate TSV (Tab-Separated Values) text from JSON data for better Windows clipboard compatibility
380    pub fn generate_tsv_text(data: &[Value]) -> Option<String> {
381        let first_row = data.first()?;
382        let obj = first_row.as_object()?;
383        let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
384
385        // Create TSV format with headers
386        let mut tsv_text = headers.join("\t") + "\r\n";
387
388        for row in data {
389            if let Some(obj) = row.as_object() {
390                let values: Vec<String> = headers
391                    .iter()
392                    .map(|&header| match obj.get(header) {
393                        Some(Value::String(s)) => {
394                            s.replace('\t', "    ").replace('\n', " ").replace('\r', "")
395                        }
396                        Some(Value::Number(n)) => n.to_string(),
397                        Some(Value::Bool(b)) => b.to_string(),
398                        Some(Value::Null) => String::new(),
399                        Some(other) => other
400                            .to_string()
401                            .replace('\t', "    ")
402                            .replace('\n', " ")
403                            .replace('\r', ""),
404                        None => String::new(),
405                    })
406                    .collect();
407                tsv_text.push_str(&values.join("\t"));
408                tsv_text.push_str("\r\n");
409            }
410        }
411
412        Some(tsv_text)
413    }
414
415    /// V50: Helper to convert DataTable to JSON Values for export compatibility
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}