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