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;
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    // Check if query uses DUAL or has no FROM clause
55    use crate::sql::recursive_parser::Parser;
56    let mut parser = Parser::new(&config.query);
57    let statement = parser
58        .parse()
59        .map_err(|e| anyhow::anyhow!("Parse error: {}", e))?;
60
61    let uses_dual = statement
62        .from_table
63        .as_ref()
64        .map(|t| t.to_uppercase() == "DUAL")
65        .unwrap_or(false);
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    // Use QueryExecutionService with full BehaviorConfig
116    let mut behavior_config = app_config.behavior.clone();
117    debug!(
118        "Using date notation: {}",
119        behavior_config.default_date_notation
120    );
121    // Command line args override config file settings
122    if config.case_insensitive {
123        behavior_config.case_insensitive_default = true;
124    }
125    if config.auto_hide_empty {
126        behavior_config.hide_empty_columns = true;
127    }
128
129    let query_service = QueryExecutionService::with_behavior_config(behavior_config);
130    let result = query_service.execute(&config.query, Some(&dataview), Some(dataview.source()))?;
131
132    let query_time = query_start.elapsed();
133    info!("Query executed in {:?}", query_time);
134    info!(
135        "Result: {} rows, {} columns",
136        result.dataview.row_count(),
137        result.dataview.column_count()
138    );
139
140    // 4. Apply limit if specified
141    let final_view = if let Some(limit) = config.limit {
142        let limited_table = limit_results(&result.dataview, limit)?;
143        DataView::new(std::sync::Arc::new(limited_table))
144    } else {
145        result.dataview
146    };
147
148    // 5. Output the results
149    let output_result = match config.output_file {
150        Some(ref path) => {
151            let mut file = fs::File::create(path)
152                .with_context(|| format!("Failed to create output file: {}", path))?;
153            output_results(&final_view, config.output_format, &mut file)?;
154            info!("Results written to: {}", path);
155            Ok(())
156        }
157        None => {
158            output_results(&final_view, config.output_format, &mut io::stdout())?;
159            Ok(())
160        }
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/// Load a data file (CSV or JSON) into a DataTable
179fn load_data_file(path: &str) -> Result<DataTable> {
180    let path = Path::new(path);
181
182    if !path.exists() {
183        return Err(anyhow::anyhow!("File not found: {}", path.display()));
184    }
185
186    // Determine file type by extension
187    let extension = path
188        .extension()
189        .and_then(|ext| ext.to_str())
190        .map(|s| s.to_lowercase())
191        .unwrap_or_default();
192
193    let table_name = path
194        .file_stem()
195        .and_then(|stem| stem.to_str())
196        .unwrap_or("data")
197        .to_string();
198
199    match extension.as_str() {
200        "csv" => load_csv_to_datatable(path, &table_name)
201            .with_context(|| format!("Failed to load CSV file: {}", path.display())),
202        "json" => load_json_to_datatable(path, &table_name)
203            .with_context(|| format!("Failed to load JSON file: {}", path.display())),
204        _ => Err(anyhow::anyhow!(
205            "Unsupported file type: {}. Use .csv or .json",
206            extension
207        )),
208    }
209}
210
211/// Limit the number of rows in results
212fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
213    let source = dataview.source();
214    let mut limited_table = DataTable::new(&source.name);
215
216    // Copy columns
217    for col in source.columns.iter() {
218        limited_table.add_column(col.clone());
219    }
220
221    // Copy limited rows
222    let rows_to_copy = dataview.row_count().min(limit);
223    for i in 0..rows_to_copy {
224        if let Some(row) = dataview.get_row(i) {
225            limited_table.add_row(row.clone());
226        }
227    }
228
229    Ok(limited_table)
230}
231
232/// Output query results in the specified format
233fn output_results<W: Write>(
234    dataview: &DataView,
235    format: OutputFormat,
236    writer: &mut W,
237) -> Result<()> {
238    match format {
239        OutputFormat::Csv => output_csv(dataview, writer, ','),
240        OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
241        OutputFormat::Json => output_json(dataview, writer),
242        OutputFormat::Table => output_table(dataview, writer),
243    }
244}
245
246/// Output results as CSV/TSV
247fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
248    // Write headers
249    let columns = dataview.column_names();
250    for (i, col) in columns.iter().enumerate() {
251        if i > 0 {
252            write!(writer, "{}", delimiter)?;
253        }
254        write!(writer, "{}", escape_csv_field(col, delimiter))?;
255    }
256    writeln!(writer)?;
257
258    // Write rows
259    for row_idx in 0..dataview.row_count() {
260        if let Some(row) = dataview.get_row(row_idx) {
261            for (i, value) in row.values.iter().enumerate() {
262                if i > 0 {
263                    write!(writer, "{}", delimiter)?;
264                }
265                write!(
266                    writer,
267                    "{}",
268                    escape_csv_field(&format_value(value), delimiter)
269                )?;
270            }
271            writeln!(writer)?;
272        }
273    }
274
275    Ok(())
276}
277
278/// Output results as JSON
279fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
280    let columns = dataview.column_names();
281    let mut rows = Vec::new();
282
283    for row_idx in 0..dataview.row_count() {
284        if let Some(row) = dataview.get_row(row_idx) {
285            let mut json_row = serde_json::Map::new();
286            for (col_idx, value) in row.values.iter().enumerate() {
287                if col_idx < columns.len() {
288                    json_row.insert(columns[col_idx].clone(), value_to_json(value));
289                }
290            }
291            rows.push(serde_json::Value::Object(json_row));
292        }
293    }
294
295    let json = serde_json::to_string_pretty(&rows)?;
296    writeln!(writer, "{}", json)?;
297
298    Ok(())
299}
300
301/// Output results as an ASCII table
302fn output_table<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
303    let columns = dataview.column_names();
304
305    // Calculate column widths
306    let mut widths = vec![0; columns.len()];
307    for (i, col) in columns.iter().enumerate() {
308        widths[i] = col.len();
309    }
310
311    // Check first 100 rows for width calculation
312    let sample_size = dataview.row_count().min(100);
313    for row_idx in 0..sample_size {
314        if let Some(row) = dataview.get_row(row_idx) {
315            for (i, value) in row.values.iter().enumerate() {
316                if i < widths.len() {
317                    let value_str = format_value(value);
318                    widths[i] = widths[i].max(value_str.len());
319                }
320            }
321        }
322    }
323
324    // Limit column widths to 50 characters
325    for width in widths.iter_mut() {
326        *width = (*width).min(50);
327    }
328
329    // Print header separator
330    write!(writer, "+")?;
331    for width in &widths {
332        write!(writer, "-{}-+", "-".repeat(*width))?;
333    }
334    writeln!(writer)?;
335
336    // Print headers
337    write!(writer, "|")?;
338    for (i, col) in columns.iter().enumerate() {
339        write!(writer, " {:^width$} |", col, width = widths[i])?;
340    }
341    writeln!(writer)?;
342
343    // Print header separator
344    write!(writer, "+")?;
345    for width in &widths {
346        write!(writer, "-{}-+", "-".repeat(*width))?;
347    }
348    writeln!(writer)?;
349
350    // Print rows
351    for row_idx in 0..dataview.row_count() {
352        if let Some(row) = dataview.get_row(row_idx) {
353            write!(writer, "|")?;
354            for (i, value) in row.values.iter().enumerate() {
355                if i < widths.len() {
356                    let value_str = format_value(value);
357                    let truncated = if value_str.len() > widths[i] {
358                        format!("{}...", &value_str[..widths[i] - 3])
359                    } else {
360                        value_str
361                    };
362                    write!(writer, " {:<width$} |", truncated, width = widths[i])?;
363                }
364            }
365            writeln!(writer)?;
366        }
367    }
368
369    // Print bottom separator
370    write!(writer, "+")?;
371    for width in &widths {
372        write!(writer, "-{}-+", "-".repeat(*width))?;
373    }
374    writeln!(writer)?;
375
376    Ok(())
377}
378
379/// Format a DataValue for display
380fn format_value(value: &DataValue) -> String {
381    match value {
382        DataValue::Null => "".to_string(),
383        DataValue::Integer(i) => i.to_string(),
384        DataValue::Float(f) => f.to_string(),
385        DataValue::String(s) => s.clone(),
386        DataValue::InternedString(s) => s.to_string(),
387        DataValue::Boolean(b) => b.to_string(),
388        DataValue::DateTime(dt) => dt.to_string(),
389    }
390}
391
392/// Convert DataValue to JSON
393fn value_to_json(value: &DataValue) -> serde_json::Value {
394    match value {
395        DataValue::Null => serde_json::Value::Null,
396        DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
397        DataValue::Float(f) => {
398            if let Some(n) = serde_json::Number::from_f64(*f) {
399                serde_json::Value::Number(n)
400            } else {
401                serde_json::Value::Null
402            }
403        }
404        DataValue::String(s) => serde_json::Value::String(s.clone()),
405        DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
406        DataValue::Boolean(b) => serde_json::Value::Bool(*b),
407        DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
408    }
409}
410
411/// Escape a CSV field if it contains special characters
412fn escape_csv_field(field: &str, delimiter: char) -> String {
413    if field.contains(delimiter)
414        || field.contains('"')
415        || field.contains('\n')
416        || field.contains('\r')
417    {
418        format!("\"{}\"", field.replace('"', "\"\""))
419    } else {
420        field.to_string()
421    }
422}