sql_cli/
non_interactive.rs

1use anyhow::{Context, Result};
2use std::fs;
3use std::io::{self, Write};
4use std::path::Path;
5use std::time::Instant;
6use tracing::{debug, info};
7
8use crate::data::data_view::DataView;
9use crate::data::datatable::{DataTable, DataValue};
10use crate::data::datatable_loaders::{load_csv_to_datatable, load_json_to_datatable};
11use crate::data::query_engine::QueryEngine;
12use crate::services::query_execution_service::QueryExecutionService;
13
14/// Output format for query results
15#[derive(Debug, Clone)]
16pub enum OutputFormat {
17    Csv,
18    Json,
19    Table,
20    Tsv,
21}
22
23impl OutputFormat {
24    pub fn from_str(s: &str) -> Result<Self> {
25        match s.to_lowercase().as_str() {
26            "csv" => Ok(OutputFormat::Csv),
27            "json" => Ok(OutputFormat::Json),
28            "table" => Ok(OutputFormat::Table),
29            "tsv" => Ok(OutputFormat::Tsv),
30            _ => Err(anyhow::anyhow!(
31                "Invalid output format: {}. Use csv, json, table, or tsv",
32                s
33            )),
34        }
35    }
36}
37
38/// Configuration for non-interactive query execution
39pub struct NonInteractiveConfig {
40    pub data_file: String,
41    pub query: String,
42    pub output_format: OutputFormat,
43    pub output_file: Option<String>,
44    pub case_insensitive: bool,
45    pub auto_hide_empty: bool,
46    pub limit: Option<usize>,
47}
48
49/// Execute a query in non-interactive mode
50pub fn execute_non_interactive(config: NonInteractiveConfig) -> Result<()> {
51    let start_time = Instant::now();
52
53    // 1. Load the data file
54    info!("Loading data from: {}", config.data_file);
55    let data_table = load_data_file(&config.data_file)?;
56    let table_name = data_table.name.clone();
57
58    info!(
59        "Loaded {} rows with {} columns",
60        data_table.row_count(),
61        data_table.column_count()
62    );
63
64    // 2. Create a DataView from the table
65    let dataview = DataView::new(std::sync::Arc::new(data_table));
66
67    // 3. Execute the query
68    info!("Executing query: {}", config.query);
69    let query_start = Instant::now();
70
71    // Use QueryExecutionService for consistency with interactive mode
72    let query_service = QueryExecutionService::new(config.case_insensitive, config.auto_hide_empty);
73    let result = query_service.execute(&config.query, Some(&dataview), Some(dataview.source()))?;
74
75    let query_time = query_start.elapsed();
76    info!("Query executed in {:?}", query_time);
77    info!(
78        "Result: {} rows, {} columns",
79        result.dataview.row_count(),
80        result.dataview.column_count()
81    );
82
83    // 4. Apply limit if specified
84    let final_view = if let Some(limit) = config.limit {
85        let limited_table = limit_results(&result.dataview, limit)?;
86        DataView::new(std::sync::Arc::new(limited_table))
87    } else {
88        result.dataview
89    };
90
91    // 5. Output the results
92    let output_result = match config.output_file {
93        Some(ref path) => {
94            let mut file = fs::File::create(path)
95                .with_context(|| format!("Failed to create output file: {}", path))?;
96            output_results(&final_view, config.output_format, &mut file)?;
97            info!("Results written to: {}", path);
98            Ok(())
99        }
100        None => {
101            output_results(&final_view, config.output_format, &mut io::stdout())?;
102            Ok(())
103        }
104    };
105
106    let total_time = start_time.elapsed();
107    debug!("Total execution time: {:?}", total_time);
108
109    // Print stats to stderr so they don't interfere with output
110    if config.output_file.is_none() {
111        eprintln!(
112            "\n# Query completed: {} rows in {:?}",
113            final_view.row_count(),
114            query_time
115        );
116    }
117
118    output_result
119}
120
121/// Load a data file (CSV or JSON) into a DataTable
122fn load_data_file(path: &str) -> Result<DataTable> {
123    let path = Path::new(path);
124
125    if !path.exists() {
126        return Err(anyhow::anyhow!("File not found: {}", path.display()));
127    }
128
129    // Determine file type by extension
130    let extension = path
131        .extension()
132        .and_then(|ext| ext.to_str())
133        .map(|s| s.to_lowercase())
134        .unwrap_or_default();
135
136    let table_name = path
137        .file_stem()
138        .and_then(|stem| stem.to_str())
139        .unwrap_or("data")
140        .to_string();
141
142    match extension.as_str() {
143        "csv" => load_csv_to_datatable(path, &table_name)
144            .with_context(|| format!("Failed to load CSV file: {}", path.display())),
145        "json" => load_json_to_datatable(path, &table_name)
146            .with_context(|| format!("Failed to load JSON file: {}", path.display())),
147        _ => Err(anyhow::anyhow!(
148            "Unsupported file type: {}. Use .csv or .json",
149            extension
150        )),
151    }
152}
153
154/// Limit the number of rows in results
155fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
156    let source = dataview.source();
157    let mut limited_table = DataTable::new(&source.name);
158
159    // Copy columns
160    for col in source.columns.iter() {
161        limited_table.add_column(col.clone());
162    }
163
164    // Copy limited rows
165    let rows_to_copy = dataview.row_count().min(limit);
166    for i in 0..rows_to_copy {
167        if let Some(row) = dataview.get_row(i) {
168            limited_table.add_row(row.clone());
169        }
170    }
171
172    Ok(limited_table)
173}
174
175/// Output query results in the specified format
176fn output_results<W: Write>(
177    dataview: &DataView,
178    format: OutputFormat,
179    writer: &mut W,
180) -> Result<()> {
181    match format {
182        OutputFormat::Csv => output_csv(dataview, writer, ','),
183        OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
184        OutputFormat::Json => output_json(dataview, writer),
185        OutputFormat::Table => output_table(dataview, writer),
186    }
187}
188
189/// Output results as CSV/TSV
190fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
191    // Write headers
192    let columns = dataview.column_names();
193    for (i, col) in columns.iter().enumerate() {
194        if i > 0 {
195            write!(writer, "{}", delimiter)?;
196        }
197        write!(writer, "{}", escape_csv_field(col, delimiter))?;
198    }
199    writeln!(writer)?;
200
201    // Write rows
202    for row_idx in 0..dataview.row_count() {
203        if let Some(row) = dataview.get_row(row_idx) {
204            for (i, value) in row.values.iter().enumerate() {
205                if i > 0 {
206                    write!(writer, "{}", delimiter)?;
207                }
208                write!(
209                    writer,
210                    "{}",
211                    escape_csv_field(&format_value(value), delimiter)
212                )?;
213            }
214            writeln!(writer)?;
215        }
216    }
217
218    Ok(())
219}
220
221/// Output results as JSON
222fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
223    let columns = dataview.column_names();
224    let mut rows = Vec::new();
225
226    for row_idx in 0..dataview.row_count() {
227        if let Some(row) = dataview.get_row(row_idx) {
228            let mut json_row = serde_json::Map::new();
229            for (col_idx, value) in row.values.iter().enumerate() {
230                if col_idx < columns.len() {
231                    json_row.insert(columns[col_idx].clone(), value_to_json(value));
232                }
233            }
234            rows.push(serde_json::Value::Object(json_row));
235        }
236    }
237
238    let json = serde_json::to_string_pretty(&rows)?;
239    writeln!(writer, "{}", json)?;
240
241    Ok(())
242}
243
244/// Output results as an ASCII table
245fn output_table<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
246    let columns = dataview.column_names();
247
248    // Calculate column widths
249    let mut widths = vec![0; columns.len()];
250    for (i, col) in columns.iter().enumerate() {
251        widths[i] = col.len();
252    }
253
254    // Check first 100 rows for width calculation
255    let sample_size = dataview.row_count().min(100);
256    for row_idx in 0..sample_size {
257        if let Some(row) = dataview.get_row(row_idx) {
258            for (i, value) in row.values.iter().enumerate() {
259                if i < widths.len() {
260                    let value_str = format_value(value);
261                    widths[i] = widths[i].max(value_str.len());
262                }
263            }
264        }
265    }
266
267    // Limit column widths to 50 characters
268    for width in widths.iter_mut() {
269        *width = (*width).min(50);
270    }
271
272    // Print header separator
273    write!(writer, "+")?;
274    for width in &widths {
275        write!(writer, "-{}-+", "-".repeat(*width))?;
276    }
277    writeln!(writer)?;
278
279    // Print headers
280    write!(writer, "|")?;
281    for (i, col) in columns.iter().enumerate() {
282        write!(writer, " {:^width$} |", col, width = widths[i])?;
283    }
284    writeln!(writer)?;
285
286    // Print header separator
287    write!(writer, "+")?;
288    for width in &widths {
289        write!(writer, "-{}-+", "-".repeat(*width))?;
290    }
291    writeln!(writer)?;
292
293    // Print rows
294    for row_idx in 0..dataview.row_count() {
295        if let Some(row) = dataview.get_row(row_idx) {
296            write!(writer, "|")?;
297            for (i, value) in row.values.iter().enumerate() {
298                if i < widths.len() {
299                    let value_str = format_value(value);
300                    let truncated = if value_str.len() > widths[i] {
301                        format!("{}...", &value_str[..widths[i] - 3])
302                    } else {
303                        value_str
304                    };
305                    write!(writer, " {:<width$} |", truncated, width = widths[i])?;
306                }
307            }
308            writeln!(writer)?;
309        }
310    }
311
312    // Print bottom separator
313    write!(writer, "+")?;
314    for width in &widths {
315        write!(writer, "-{}-+", "-".repeat(*width))?;
316    }
317    writeln!(writer)?;
318
319    Ok(())
320}
321
322/// Format a DataValue for display
323fn format_value(value: &DataValue) -> String {
324    match value {
325        DataValue::Null => "".to_string(),
326        DataValue::Integer(i) => i.to_string(),
327        DataValue::Float(f) => f.to_string(),
328        DataValue::String(s) => s.clone(),
329        DataValue::InternedString(s) => s.to_string(),
330        DataValue::Boolean(b) => b.to_string(),
331        DataValue::DateTime(dt) => dt.to_string(),
332    }
333}
334
335/// Convert DataValue to JSON
336fn value_to_json(value: &DataValue) -> serde_json::Value {
337    match value {
338        DataValue::Null => serde_json::Value::Null,
339        DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
340        DataValue::Float(f) => {
341            if let Some(n) = serde_json::Number::from_f64(*f) {
342                serde_json::Value::Number(n)
343            } else {
344                serde_json::Value::Null
345            }
346        }
347        DataValue::String(s) => serde_json::Value::String(s.clone()),
348        DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
349        DataValue::Boolean(b) => serde_json::Value::Bool(*b),
350        DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
351    }
352}
353
354/// Escape a CSV field if it contains special characters
355fn escape_csv_field(field: &str, delimiter: char) -> String {
356    if field.contains(delimiter)
357        || field.contains('"')
358        || field.contains('\n')
359        || field.contains('\r')
360    {
361        format!("\"{}\"", field.replace('"', "\"\""))
362    } else {
363        field.to_string()
364    }
365}