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_{timestamp}.csv");
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 {row_count} rows to CSV file: {filename}"
45        ))
46    }
47
48    /// Export data to JSON format using `DataProvider` trait
49    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        // Generate filename with timestamp
56        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
57        let filename = format!("query_results_{timestamp}.json");
58
59        // Build JSON array
60        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    /// V50: Export buffer results to CSV format using `DataTable`
84    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        // Generate filename with timestamp
94        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        // Write headers
100        let headers = datatable.column_names();
101        let header_line = headers.join(",");
102        writeln!(file, "{header_line}")?;
103
104        // Write data rows
105        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    /// Export buffer results to JSON format
119    pub fn export_to_json(buffer: &dyn BufferAPI, include_filtered: bool) -> Result<String> {
120        // V50: For now, convert DataTable back to JSON for export
121        // This will be optimized later
122        let datatable = buffer
123            .get_datatable()
124            .ok_or_else(|| anyhow!("No results to export - run a query first"))?;
125
126        // Convert DataTable to JSON values for export
127        let data = Self::datatable_to_json_values(datatable);
128
129        // Determine what data to export
130        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        // Generate filename with timestamp
139        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    /// V50: Export selected rows to CSV
161    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        // Get first valid row for headers
175        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        // Generate filename with timestamp
185        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        // Write headers
191        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        // Write selected rows
196        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    /// Helper to escape CSV fields that contain special characters
223    fn escape_csv_field(field: &str) -> String {
224        if field.contains(',') || field.contains('"') || field.contains('\n') {
225            // Escape quotes by doubling them and wrap field in quotes
226            format!("\"{}\"", field.replace('"', "\"\""))
227        } else {
228            field.to_string()
229        }
230    }
231
232    /// Get filtered data based on current filter
233    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    /// Get fuzzy filtered data based on current fuzzy filter
271    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    /// Export a single value to clipboard-friendly format
291    #[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    /// Export row as tab-separated values for clipboard
303    #[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    /// Convert JSON query results to a 2D vector of strings for display
313    #[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    /// Generate CSV text from JSON data for clipboard operations
347    #[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        // Create CSV format
354        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    /// Generate TSV (Tab-Separated Values) text from JSON data for better Windows clipboard compatibility
378    #[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        // Create TSV format with headers
385        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    /// V50: Helper to convert `DataTable` to JSON Values for export compatibility
415    #[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}