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