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::config::config::Config;
9use crate::data::data_view::DataView;
10use crate::data::datatable::{DataTable, DataValue};
11use crate::data::datatable_loaders::{load_csv_to_datatable, load_json_to_datatable};
12use crate::services::query_execution_service::QueryExecutionService;
13use crate::sql::script_parser::{ScriptParser, ScriptResult};
14
15/// Output format for query results
16#[derive(Debug, Clone)]
17pub enum OutputFormat {
18    Csv,
19    Json,
20    Table,
21    Tsv,
22}
23
24impl OutputFormat {
25    pub fn from_str(s: &str) -> Result<Self> {
26        match s.to_lowercase().as_str() {
27            "csv" => Ok(OutputFormat::Csv),
28            "json" => Ok(OutputFormat::Json),
29            "table" => Ok(OutputFormat::Table),
30            "tsv" => Ok(OutputFormat::Tsv),
31            _ => Err(anyhow::anyhow!(
32                "Invalid output format: {}. Use csv, json, table, or tsv",
33                s
34            )),
35        }
36    }
37}
38
39/// Configuration for non-interactive query execution
40pub struct NonInteractiveConfig {
41    pub data_file: String,
42    pub query: String,
43    pub output_format: OutputFormat,
44    pub output_file: Option<String>,
45    pub case_insensitive: bool,
46    pub auto_hide_empty: bool,
47    pub limit: Option<usize>,
48    pub query_plan: bool,
49}
50
51/// Execute a query in non-interactive mode
52pub fn execute_non_interactive(config: NonInteractiveConfig) -> Result<()> {
53    let start_time = Instant::now();
54
55    // Check if query uses DUAL or has no FROM clause
56    use crate::sql::recursive_parser::Parser;
57    let mut parser = Parser::new(&config.query);
58    let statement = parser
59        .parse()
60        .map_err(|e| anyhow::anyhow!("Parse error: {}", e))?;
61
62    let uses_dual = statement
63        .from_table
64        .as_ref()
65        .is_some_and(|t| t.to_uppercase() == "DUAL");
66
67    let no_from_clause = statement.from_table.is_none();
68
69    // 1. Load the data file or create DUAL table
70    let (data_table, is_dual) = if uses_dual || no_from_clause || config.data_file.is_empty() {
71        info!("Using DUAL table for expression evaluation");
72        (crate::data::datatable::DataTable::dual(), true)
73    } else {
74        info!("Loading data from: {}", config.data_file);
75        let table = load_data_file(&config.data_file)?;
76        info!(
77            "Loaded {} rows with {} columns",
78            table.row_count(),
79            table.column_count()
80        );
81        (table, false)
82    };
83    let table_name = data_table.name.clone();
84
85    // 2. Create a DataView from the table
86    let dataview = DataView::new(std::sync::Arc::new(data_table));
87
88    // 3. Execute the query
89    info!("Executing query: {}", config.query);
90
91    // If query_plan is requested, parse and display the AST
92    if config.query_plan {
93        use crate::sql::recursive_parser::Parser;
94        let mut parser = Parser::new(&config.query);
95        match parser.parse() {
96            Ok(statement) => {
97                println!("\n=== QUERY PLAN (AST) ===");
98                println!("{statement:#?}");
99                println!("=== END QUERY PLAN ===\n");
100            }
101            Err(e) => {
102                eprintln!("Failed to parse query for plan: {e}");
103            }
104        }
105    }
106
107    let query_start = Instant::now();
108
109    // Load configuration file to get date notation and other settings
110    let app_config = Config::load().unwrap_or_else(|e| {
111        debug!("Could not load config file: {}. Using defaults.", e);
112        Config::default()
113    });
114
115    // Initialize global config for function registry
116    crate::config::global::init_config(app_config.clone());
117
118    // Use QueryExecutionService with full BehaviorConfig
119    let mut behavior_config = app_config.behavior.clone();
120    debug!(
121        "Using date notation: {}",
122        behavior_config.default_date_notation
123    );
124    // Command line args override config file settings
125    if config.case_insensitive {
126        behavior_config.case_insensitive_default = true;
127    }
128    if config.auto_hide_empty {
129        behavior_config.hide_empty_columns = true;
130    }
131
132    let query_service = QueryExecutionService::with_behavior_config(behavior_config);
133    let result = query_service.execute(&config.query, Some(&dataview), Some(dataview.source()))?;
134
135    let query_time = query_start.elapsed();
136    info!("Query executed in {:?}", query_time);
137    info!(
138        "Result: {} rows, {} columns",
139        result.dataview.row_count(),
140        result.dataview.column_count()
141    );
142
143    // 4. Apply limit if specified
144    let final_view = if let Some(limit) = config.limit {
145        let limited_table = limit_results(&result.dataview, limit)?;
146        DataView::new(std::sync::Arc::new(limited_table))
147    } else {
148        result.dataview
149    };
150
151    // 5. Output the results
152    let output_result = if let Some(ref path) = config.output_file {
153        let mut file = fs::File::create(path)
154            .with_context(|| format!("Failed to create output file: {path}"))?;
155        output_results(&final_view, config.output_format, &mut file)?;
156        info!("Results written to: {}", path);
157        Ok(())
158    } else {
159        output_results(&final_view, config.output_format, &mut io::stdout())?;
160        Ok(())
161    };
162
163    let total_time = start_time.elapsed();
164    debug!("Total execution time: {:?}", total_time);
165
166    // Print stats to stderr so they don't interfere with output
167    if config.output_file.is_none() {
168        eprintln!(
169            "\n# Query completed: {} rows in {:?}",
170            final_view.row_count(),
171            query_time
172        );
173    }
174
175    output_result
176}
177
178/// Execute a script file with multiple SQL statements separated by GO
179pub fn execute_script(config: NonInteractiveConfig) -> Result<()> {
180    let start_time = Instant::now();
181
182    // Parse the script into individual statements
183    let parser = ScriptParser::new(&config.query);
184    let statements = parser.parse_and_validate()?;
185
186    info!("Found {} statements in script", statements.len());
187
188    // Load the data file once (or use DUAL)
189    let (data_table, is_dual) = if config.data_file.is_empty() {
190        info!("Using DUAL table for script execution");
191        (DataTable::dual(), true)
192    } else {
193        info!("Loading data from: {}", config.data_file);
194        let table = load_data_file(&config.data_file)?;
195        info!(
196            "Loaded {} rows with {} columns",
197            table.row_count(),
198            table.column_count()
199        );
200        (table, false)
201    };
202
203    // Track script results
204    let mut script_result = ScriptResult::new();
205    let mut output = Vec::new();
206
207    // Execute each statement
208    for (idx, statement) in statements.iter().enumerate() {
209        let statement_num = idx + 1;
210        let stmt_start = Instant::now();
211
212        // Print separator for table format
213        if matches!(config.output_format, OutputFormat::Table) {
214            if idx > 0 {
215                output.push(String::new()); // Empty line between queries
216            }
217            output.push(format!("-- Query {} --", statement_num));
218        }
219
220        // Create a fresh DataView for each statement
221        let dataview = DataView::new(std::sync::Arc::new(data_table.clone()));
222
223        // Execute the statement
224        let service = QueryExecutionService::new(config.case_insensitive, config.auto_hide_empty);
225        match service.execute(statement, Some(&dataview), None) {
226            Ok(result) => {
227                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
228                let final_view = result.dataview;
229
230                // Format the output based on the output format
231                let mut statement_output = Vec::new();
232                match config.output_format {
233                    OutputFormat::Csv => {
234                        output_csv(&final_view, &mut statement_output, ',')?;
235                    }
236                    OutputFormat::Json => {
237                        output_json(&final_view, &mut statement_output)?;
238                    }
239                    OutputFormat::Table => {
240                        output_table(&final_view, &mut statement_output)?;
241                        writeln!(
242                            &mut statement_output,
243                            "Query completed: {} rows in {:.2}ms",
244                            final_view.row_count(),
245                            exec_time
246                        )?;
247                    }
248                    OutputFormat::Tsv => {
249                        output_csv(&final_view, &mut statement_output, '\t')?;
250                    }
251                }
252
253                // Add to overall output
254                output.extend(
255                    String::from_utf8_lossy(&statement_output)
256                        .lines()
257                        .map(String::from),
258                );
259
260                script_result.add_success(
261                    statement_num,
262                    statement.clone(),
263                    final_view.row_count(),
264                    exec_time,
265                );
266            }
267            Err(e) => {
268                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
269                let error_msg = format!("Query {} failed: {}", statement_num, e);
270
271                if matches!(config.output_format, OutputFormat::Table) {
272                    output.push(error_msg.clone());
273                }
274
275                script_result.add_failure(
276                    statement_num,
277                    statement.clone(),
278                    e.to_string(),
279                    exec_time,
280                );
281
282                // Continue to next statement (don't stop on error)
283            }
284        }
285    }
286
287    // Write output
288    if let Some(ref output_file) = config.output_file {
289        let mut file = fs::File::create(output_file)?;
290        for line in &output {
291            writeln!(file, "{}", line)?;
292        }
293        info!("Results written to: {}", output_file);
294    } else {
295        for line in &output {
296            println!("{}", line);
297        }
298    }
299
300    // Print summary if in table mode
301    if matches!(config.output_format, OutputFormat::Table) {
302        println!("\n=== Script Summary ===");
303        println!("Total statements: {}", script_result.total_statements);
304        println!("Successful: {}", script_result.successful_statements);
305        println!("Failed: {}", script_result.failed_statements);
306        println!(
307            "Total execution time: {:.2}ms",
308            script_result.total_execution_time_ms
309        );
310    }
311
312    if !script_result.all_successful() {
313        return Err(anyhow::anyhow!(
314            "{} of {} statements failed",
315            script_result.failed_statements,
316            script_result.total_statements
317        ));
318    }
319
320    Ok(())
321}
322
323/// Load a data file (CSV or JSON) into a `DataTable`
324fn load_data_file(path: &str) -> Result<DataTable> {
325    let path = Path::new(path);
326
327    if !path.exists() {
328        return Err(anyhow::anyhow!("File not found: {}", path.display()));
329    }
330
331    // Determine file type by extension
332    let extension = path
333        .extension()
334        .and_then(|ext| ext.to_str())
335        .map(str::to_lowercase)
336        .unwrap_or_default();
337
338    let table_name = path
339        .file_stem()
340        .and_then(|stem| stem.to_str())
341        .unwrap_or("data")
342        .to_string();
343
344    match extension.as_str() {
345        "csv" => load_csv_to_datatable(path, &table_name)
346            .with_context(|| format!("Failed to load CSV file: {}", path.display())),
347        "json" => load_json_to_datatable(path, &table_name)
348            .with_context(|| format!("Failed to load JSON file: {}", path.display())),
349        _ => Err(anyhow::anyhow!(
350            "Unsupported file type: {}. Use .csv or .json",
351            extension
352        )),
353    }
354}
355
356/// Limit the number of rows in results
357fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
358    let source = dataview.source();
359    let mut limited_table = DataTable::new(&source.name);
360
361    // Copy columns
362    for col in &source.columns {
363        limited_table.add_column(col.clone());
364    }
365
366    // Copy limited rows
367    let rows_to_copy = dataview.row_count().min(limit);
368    for i in 0..rows_to_copy {
369        if let Some(row) = dataview.get_row(i) {
370            limited_table.add_row(row.clone());
371        }
372    }
373
374    Ok(limited_table)
375}
376
377/// Output query results in the specified format
378fn output_results<W: Write>(
379    dataview: &DataView,
380    format: OutputFormat,
381    writer: &mut W,
382) -> Result<()> {
383    match format {
384        OutputFormat::Csv => output_csv(dataview, writer, ','),
385        OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
386        OutputFormat::Json => output_json(dataview, writer),
387        OutputFormat::Table => output_table(dataview, writer),
388    }
389}
390
391/// Output results as CSV/TSV
392fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
393    // Write headers
394    let columns = dataview.column_names();
395    for (i, col) in columns.iter().enumerate() {
396        if i > 0 {
397            write!(writer, "{delimiter}")?;
398        }
399        write!(writer, "{}", escape_csv_field(col, delimiter))?;
400    }
401    writeln!(writer)?;
402
403    // Write rows
404    for row_idx in 0..dataview.row_count() {
405        if let Some(row) = dataview.get_row(row_idx) {
406            for (i, value) in row.values.iter().enumerate() {
407                if i > 0 {
408                    write!(writer, "{delimiter}")?;
409                }
410                write!(
411                    writer,
412                    "{}",
413                    escape_csv_field(&format_value(value), delimiter)
414                )?;
415            }
416            writeln!(writer)?;
417        }
418    }
419
420    Ok(())
421}
422
423/// Output results as JSON
424fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
425    let columns = dataview.column_names();
426    let mut rows = Vec::new();
427
428    for row_idx in 0..dataview.row_count() {
429        if let Some(row) = dataview.get_row(row_idx) {
430            let mut json_row = serde_json::Map::new();
431            for (col_idx, value) in row.values.iter().enumerate() {
432                if col_idx < columns.len() {
433                    json_row.insert(columns[col_idx].clone(), value_to_json(value));
434                }
435            }
436            rows.push(serde_json::Value::Object(json_row));
437        }
438    }
439
440    let json = serde_json::to_string_pretty(&rows)?;
441    writeln!(writer, "{json}")?;
442
443    Ok(())
444}
445
446/// Output results as an ASCII table
447fn output_table<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
448    let columns = dataview.column_names();
449
450    // Calculate column widths
451    let mut widths = vec![0; columns.len()];
452    for (i, col) in columns.iter().enumerate() {
453        widths[i] = col.len();
454    }
455
456    // Check first 100 rows for width calculation
457    let sample_size = dataview.row_count().min(100);
458    for row_idx in 0..sample_size {
459        if let Some(row) = dataview.get_row(row_idx) {
460            for (i, value) in row.values.iter().enumerate() {
461                if i < widths.len() {
462                    let value_str = format_value(value);
463                    widths[i] = widths[i].max(value_str.len());
464                }
465            }
466        }
467    }
468
469    // Limit column widths to 50 characters
470    for width in &mut widths {
471        *width = (*width).min(50);
472    }
473
474    // Print header separator
475    write!(writer, "+")?;
476    for width in &widths {
477        write!(writer, "-{}-+", "-".repeat(*width))?;
478    }
479    writeln!(writer)?;
480
481    // Print headers
482    write!(writer, "|")?;
483    for (i, col) in columns.iter().enumerate() {
484        write!(writer, " {:^width$} |", col, width = widths[i])?;
485    }
486    writeln!(writer)?;
487
488    // Print header separator
489    write!(writer, "+")?;
490    for width in &widths {
491        write!(writer, "-{}-+", "-".repeat(*width))?;
492    }
493    writeln!(writer)?;
494
495    // Print rows
496    for row_idx in 0..dataview.row_count() {
497        if let Some(row) = dataview.get_row(row_idx) {
498            write!(writer, "|")?;
499            for (i, value) in row.values.iter().enumerate() {
500                if i < widths.len() {
501                    let value_str = format_value(value);
502                    let truncated = if value_str.len() > widths[i] {
503                        format!("{}...", &value_str[..widths[i] - 3])
504                    } else {
505                        value_str
506                    };
507                    write!(writer, " {:<width$} |", truncated, width = widths[i])?;
508                }
509            }
510            writeln!(writer)?;
511        }
512    }
513
514    // Print bottom separator
515    write!(writer, "+")?;
516    for width in &widths {
517        write!(writer, "-{}-+", "-".repeat(*width))?;
518    }
519    writeln!(writer)?;
520
521    Ok(())
522}
523
524/// Format a `DataValue` for display
525fn format_value(value: &DataValue) -> String {
526    match value {
527        DataValue::Null => String::new(),
528        DataValue::Integer(i) => i.to_string(),
529        DataValue::Float(f) => f.to_string(),
530        DataValue::String(s) => s.clone(),
531        DataValue::InternedString(s) => s.to_string(),
532        DataValue::Boolean(b) => b.to_string(),
533        DataValue::DateTime(dt) => dt.to_string(),
534    }
535}
536
537/// Convert `DataValue` to JSON
538fn value_to_json(value: &DataValue) -> serde_json::Value {
539    match value {
540        DataValue::Null => serde_json::Value::Null,
541        DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
542        DataValue::Float(f) => {
543            if let Some(n) = serde_json::Number::from_f64(*f) {
544                serde_json::Value::Number(n)
545            } else {
546                serde_json::Value::Null
547            }
548        }
549        DataValue::String(s) => serde_json::Value::String(s.clone()),
550        DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
551        DataValue::Boolean(b) => serde_json::Value::Bool(*b),
552        DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
553    }
554}
555
556/// Escape a CSV field if it contains special characters
557fn escape_csv_field(field: &str, delimiter: char) -> String {
558    if field.contains(delimiter)
559        || field.contains('"')
560        || field.contains('\n')
561        || field.contains('\r')
562    {
563        format!("\"{}\"", field.replace('"', "\"\""))
564    } else {
565        field.to_string()
566    }
567}