sql_cli/
non_interactive.rs

1use anyhow::{Context, Result};
2use comfy_table::presets::*;
3use comfy_table::{Cell, ContentArrangement, Table};
4use std::fs;
5use std::io::{self, Write};
6use std::path::Path;
7use std::time::Instant;
8use tracing::{debug, info};
9
10use crate::config::config::Config;
11use crate::data::data_view::DataView;
12use crate::data::datatable::{DataTable, DataValue};
13use crate::data::datatable_loaders::{load_csv_to_datatable, load_json_to_datatable};
14use crate::services::query_execution_service::QueryExecutionService;
15use crate::sql::parser::ast::{CTEType, TableSource, CTE};
16use crate::sql::script_parser::{ScriptParser, ScriptResult};
17
18/// Extract dependencies from a CTE by analyzing what tables it references
19fn extract_cte_dependencies(cte: &CTE) -> Vec<String> {
20    // For now, we'll just return the from_table if it exists
21    // This could be enhanced to do deeper analysis of the query AST
22    let mut deps = Vec::new();
23
24    if let CTEType::Standard(query) = &cte.cte_type {
25        if let Some(from_table) = &query.from_table {
26            deps.push(from_table.clone());
27        }
28
29        // Could also check joins, subqueries, etc.
30        for join in &query.joins {
31            match &join.table {
32                TableSource::Table(table_name) => {
33                    deps.push(table_name.clone());
34                }
35                TableSource::DerivedTable { alias, .. } => {
36                    deps.push(alias.clone());
37                }
38            }
39        }
40    }
41
42    deps
43}
44
45/// Output format for query results
46#[derive(Debug, Clone)]
47pub enum OutputFormat {
48    Csv,
49    Json,
50    JsonStructured, // Structured JSON with metadata for IDE/plugin integration
51    Table,
52    Tsv,
53}
54
55impl OutputFormat {
56    pub fn from_str(s: &str) -> Result<Self> {
57        match s.to_lowercase().as_str() {
58            "csv" => Ok(OutputFormat::Csv),
59            "json" => Ok(OutputFormat::Json),
60            "json-structured" => Ok(OutputFormat::JsonStructured),
61            "table" => Ok(OutputFormat::Table),
62            "tsv" => Ok(OutputFormat::Tsv),
63            _ => Err(anyhow::anyhow!(
64                "Invalid output format: {}. Use csv, json, json-structured, table, or tsv",
65                s
66            )),
67        }
68    }
69}
70
71/// Table styling presets for table output format
72#[derive(Debug, Clone, Copy)]
73pub enum TableStyle {
74    /// Current default ASCII style with borders
75    Default,
76    /// ASCII table with full borders
77    AsciiFull,
78    /// ASCII table with condensed rows
79    AsciiCondensed,
80    /// ASCII table with only outer borders
81    AsciiBordersOnly,
82    /// ASCII table with horizontal lines only
83    AsciiHorizontalOnly,
84    /// ASCII table with no borders
85    AsciiNoBorders,
86    /// Markdown-style table
87    Markdown,
88    /// UTF8 table with box-drawing characters
89    Utf8Full,
90    /// UTF8 table with condensed rows
91    Utf8Condensed,
92    /// UTF8 table with only outer borders
93    Utf8BordersOnly,
94    /// UTF8 table with horizontal lines only
95    Utf8HorizontalOnly,
96    /// UTF8 table with no borders
97    Utf8NoBorders,
98    /// No table formatting, just data
99    Plain,
100}
101
102impl TableStyle {
103    pub fn from_str(s: &str) -> Result<Self> {
104        match s.to_lowercase().as_str() {
105            "default" => Ok(TableStyle::Default),
106            "ascii" | "ascii-full" => Ok(TableStyle::AsciiFull),
107            "ascii-condensed" => Ok(TableStyle::AsciiCondensed),
108            "ascii-borders" | "ascii-borders-only" => Ok(TableStyle::AsciiBordersOnly),
109            "ascii-horizontal" | "ascii-horizontal-only" => Ok(TableStyle::AsciiHorizontalOnly),
110            "ascii-noborders" | "ascii-no-borders" => Ok(TableStyle::AsciiNoBorders),
111            "markdown" | "md" => Ok(TableStyle::Markdown),
112            "utf8" | "utf8-full" => Ok(TableStyle::Utf8Full),
113            "utf8-condensed" => Ok(TableStyle::Utf8Condensed),
114            "utf8-borders" | "utf8-borders-only" => Ok(TableStyle::Utf8BordersOnly),
115            "utf8-horizontal" | "utf8-horizontal-only" => Ok(TableStyle::Utf8HorizontalOnly),
116            "utf8-noborders" | "utf8-no-borders" => Ok(TableStyle::Utf8NoBorders),
117            "plain" | "none" => Ok(TableStyle::Plain),
118            _ => Err(anyhow::anyhow!(
119                "Invalid table style: {}. Use: default, ascii, ascii-condensed, ascii-borders, ascii-horizontal, ascii-noborders, markdown, utf8, utf8-condensed, utf8-borders, utf8-horizontal, utf8-noborders, plain",
120                s
121            )),
122        }
123    }
124
125    pub fn list_styles() -> &'static str {
126        "Available table styles:
127  default            - Current default ASCII style
128  ascii              - ASCII with full borders
129  ascii-condensed    - ASCII with condensed rows
130  ascii-borders      - ASCII with outer borders only
131  ascii-horizontal   - ASCII with horizontal lines only
132  ascii-noborders    - ASCII with no borders
133  markdown           - GitHub-flavored Markdown table
134  utf8               - UTF8 box-drawing characters
135  utf8-condensed     - UTF8 with condensed rows
136  utf8-borders       - UTF8 with outer borders only
137  utf8-horizontal    - UTF8 with horizontal lines only
138  utf8-noborders     - UTF8 with no borders
139  plain              - No formatting, data only"
140    }
141}
142
143/// Configuration for non-interactive query execution
144pub struct NonInteractiveConfig {
145    pub data_file: String,
146    pub query: String,
147    pub output_format: OutputFormat,
148    pub output_file: Option<String>,
149    pub case_insensitive: bool,
150    pub auto_hide_empty: bool,
151    pub limit: Option<usize>,
152    pub query_plan: bool,
153    pub show_work_units: bool,
154    pub execution_plan: bool,
155    pub cte_info: bool,
156    pub rewrite_analysis: bool,
157    pub lift_in_expressions: bool,
158    pub script_file: Option<String>, // Path to the script file for relative path resolution
159    pub debug_trace: bool,
160    pub max_col_width: Option<usize>, // Maximum column width for table output (None = unlimited)
161    pub col_sample_rows: usize,       // Number of rows to sample for column width (0 = all rows)
162    pub table_style: TableStyle,      // Table styling preset (only affects table output format)
163}
164
165/// Execute a query in non-interactive mode
166pub fn execute_non_interactive(config: NonInteractiveConfig) -> Result<()> {
167    let start_time = Instant::now();
168
169    // 1. Load the data file or create DUAL table
170    let (data_table, _is_dual) = if config.data_file.is_empty() {
171        info!("No data file provided, using DUAL table");
172        (crate::data::datatable::DataTable::dual(), true)
173    } else {
174        info!("Loading data from: {}", config.data_file);
175        let table = load_data_file(&config.data_file)?;
176        info!(
177            "Loaded {} rows with {} columns",
178            table.row_count(),
179            table.column_count()
180        );
181        (table, false)
182    };
183    let _table_name = data_table.name.clone();
184
185    // 2. Create a DataView from the table
186    let dataview = DataView::new(std::sync::Arc::new(data_table));
187
188    // 3. Execute the query
189    info!("Executing query: {}", config.query);
190
191    // If execution_plan is requested, show detailed execution information
192    if config.execution_plan {
193        println!("\n=== EXECUTION PLAN ===");
194        println!("Query: {}", config.query);
195        println!("\nExecution Steps:");
196        println!("1. PARSE - Parse SQL query");
197        println!("2. LOAD_DATA - Load data from {}", &config.data_file);
198        println!(
199            "   • Loaded {} rows, {} columns",
200            dataview.row_count(),
201            dataview.column_count()
202        );
203    }
204
205    // If show_work_units is requested, analyze and display work units
206    if config.show_work_units {
207        use crate::query_plan::{ExpressionLifter, QueryAnalyzer};
208        use crate::sql::recursive_parser::Parser;
209
210        let mut parser = Parser::new(&config.query);
211        match parser.parse() {
212            Ok(stmt) => {
213                let mut analyzer = QueryAnalyzer::new();
214                let mut lifter = ExpressionLifter::new();
215
216                // Check if the query has liftable expressions
217                let mut stmt_copy = stmt.clone();
218                let lifted = lifter.lift_expressions(&mut stmt_copy);
219
220                // Build the query plan
221                match analyzer.analyze(&stmt_copy, config.query.clone()) {
222                    Ok(plan) => {
223                        println!("\n{}", plan.explain());
224
225                        if !lifted.is_empty() {
226                            println!("\nLifted CTEs:");
227                            for cte in &lifted {
228                                println!("  - {}", cte.name);
229                            }
230                        }
231
232                        return Ok(());
233                    }
234                    Err(e) => {
235                        eprintln!("Error analyzing query: {}", e);
236                        return Err(anyhow::anyhow!("Query analysis failed: {}", e));
237                    }
238                }
239            }
240            Err(e) => {
241                eprintln!("Error parsing query: {}", e);
242                return Err(anyhow::anyhow!("Parse error: {}", e));
243            }
244        }
245    }
246
247    // If query_plan is requested, parse and display the AST
248    if config.query_plan {
249        use crate::sql::recursive_parser::Parser;
250        let mut parser = Parser::new(&config.query);
251        match parser.parse() {
252            Ok(statement) => {
253                println!("\n=== QUERY PLAN (AST) ===");
254                println!("{statement:#?}");
255                println!("=== END QUERY PLAN ===\n");
256            }
257            Err(e) => {
258                eprintln!("Failed to parse query for plan: {e}");
259            }
260        }
261    }
262
263    // If rewrite analysis is requested, analyze query for optimization opportunities
264    if config.rewrite_analysis {
265        use crate::sql::query_rewriter::{QueryRewriter, RewriteAnalysis};
266        use crate::sql::recursive_parser::Parser;
267        use serde_json::json;
268
269        let mut parser = Parser::new(&config.query);
270        match parser.parse() {
271            Ok(statement) => {
272                let mut rewriter = QueryRewriter::new();
273                let suggestions = rewriter.analyze(&statement);
274
275                let analysis = RewriteAnalysis::from_suggestions(suggestions);
276                println!("{}", serde_json::to_string_pretty(&analysis).unwrap());
277                return Ok(());
278            }
279            Err(e) => {
280                let output = json!({
281                    "success": false,
282                    "error": format!("{}", e),
283                    "suggestions": [],
284                    "can_auto_rewrite": false,
285                });
286                println!("{}", serde_json::to_string_pretty(&output).unwrap());
287                return Ok(());
288            }
289        }
290    }
291
292    // If CTE info is requested, parse and output CTE information as JSON
293    if config.cte_info {
294        use crate::sql::recursive_parser::Parser;
295        use serde_json::json;
296
297        let mut parser = Parser::new(&config.query);
298        match parser.parse() {
299            Ok(statement) => {
300                let mut cte_info = Vec::new();
301
302                // Extract CTE information
303                for (index, cte) in statement.ctes.iter().enumerate() {
304                    let cte_json = json!({
305                        "index": index,
306                        "name": cte.name,
307                        "columns": cte.column_list,
308                        // We can't easily get line numbers from the AST,
309                        // but we can provide the structure
310                        "dependencies": extract_cte_dependencies(cte),
311                    });
312                    cte_info.push(cte_json);
313                }
314
315                let output = json!({
316                    "success": true,
317                    "ctes": cte_info,
318                    "total": statement.ctes.len(),
319                    "has_final_select": !statement.columns.is_empty() || !statement.select_items.is_empty(),
320                });
321
322                println!("{}", serde_json::to_string_pretty(&output).unwrap());
323                return Ok(());
324            }
325            Err(e) => {
326                let output = json!({
327                    "success": false,
328                    "error": format!("{}", e),
329                    "ctes": [],
330                    "total": 0,
331                });
332                println!("{}", serde_json::to_string_pretty(&output).unwrap());
333                return Ok(());
334            }
335        }
336    }
337
338    let query_start = Instant::now();
339
340    // Load configuration file to get date notation and other settings
341    let app_config = Config::load().unwrap_or_else(|e| {
342        debug!("Could not load config file: {}. Using defaults.", e);
343        Config::default()
344    });
345
346    // Initialize global config for function registry
347    crate::config::global::init_config(app_config.clone());
348
349    // Use QueryExecutionService with full BehaviorConfig
350    let mut behavior_config = app_config.behavior.clone();
351    debug!(
352        "Using date notation: {}",
353        behavior_config.default_date_notation
354    );
355    // Command line args override config file settings
356    if config.case_insensitive {
357        behavior_config.case_insensitive_default = true;
358    }
359    if config.auto_hide_empty {
360        behavior_config.hide_empty_columns = true;
361    }
362
363    // Parse and potentially rewrite the query
364    let exec_start = Instant::now();
365    let result = if config.lift_in_expressions {
366        use crate::data::query_engine::QueryEngine;
367        use crate::query_plan::{CTEHoister, InOperatorLifter};
368        use crate::sql::recursive_parser::Parser;
369
370        let mut parser = Parser::new(&config.query);
371        match parser.parse() {
372            Ok(mut stmt) => {
373                // Apply expression lifting for column alias dependencies
374                use crate::query_plan::ExpressionLifter;
375                let mut expr_lifter = ExpressionLifter::new();
376                let lifted = expr_lifter.lift_expressions(&mut stmt);
377                if !lifted.is_empty() {
378                    info!(
379                        "Applied expression lifting - {} CTEs generated",
380                        lifted.len()
381                    );
382                }
383
384                // Apply CTE hoisting to handle nested CTEs
385                stmt = CTEHoister::hoist_ctes(stmt);
386
387                // Try to rewrite the query with IN lifting
388                let mut lifter = InOperatorLifter::new();
389                if lifter.rewrite_query(&mut stmt) {
390                    info!("Applied IN expression lifting - query rewritten with CTEs");
391
392                    // Execute the rewritten AST directly
393                    let engine = QueryEngine::with_case_insensitive(config.case_insensitive);
394
395                    match engine.execute_statement(dataview.source_arc(), stmt) {
396                        Ok(result_view) => {
397                            // Create a QueryExecutionResult to match the expected type
398                            Ok(
399                                crate::services::query_execution_service::QueryExecutionResult {
400                                    dataview: result_view,
401                                    stats: crate::services::query_execution_service::QueryStats {
402                                        row_count: 0, // Will be filled by result_view
403                                        column_count: 0,
404                                        execution_time: exec_start.elapsed(),
405                                        query_engine_time: exec_start.elapsed(),
406                                    },
407                                    hidden_columns: Vec::new(),
408                                    query: config.query.clone(),
409                                    execution_plan: None,
410                                    debug_trace: None,
411                                },
412                            )
413                        }
414                        Err(e) => Err(e),
415                    }
416                } else {
417                    // No lifting needed, execute normally
418                    let query_service =
419                        QueryExecutionService::with_behavior_config(behavior_config);
420                    if config.debug_trace {
421                        let debug_ctx = crate::debug_trace::DebugContext::new(
422                            crate::debug_trace::DebugLevel::Debug,
423                        );
424                        query_service.execute_with_debug(
425                            &config.query,
426                            Some(&dataview),
427                            Some(dataview.source()),
428                            Some(debug_ctx),
429                        )
430                    } else {
431                        query_service.execute(
432                            &config.query,
433                            Some(&dataview),
434                            Some(dataview.source()),
435                        )
436                    }
437                }
438            }
439            Err(_) => {
440                // Parse failed, execute normally and let it fail with proper error
441                let query_service = QueryExecutionService::with_behavior_config(behavior_config);
442                if config.debug_trace {
443                    let debug_ctx = crate::debug_trace::DebugContext::new(
444                        crate::debug_trace::DebugLevel::Debug,
445                    );
446                    query_service.execute_with_debug(
447                        &config.query,
448                        Some(&dataview),
449                        Some(dataview.source()),
450                        Some(debug_ctx),
451                    )
452                } else {
453                    query_service.execute(&config.query, Some(&dataview), Some(dataview.source()))
454                }
455            }
456        }
457    } else {
458        // Normal execution without lifting
459        let query_service = QueryExecutionService::with_behavior_config(behavior_config);
460
461        // Create debug context if debug trace is enabled
462        if config.debug_trace {
463            let debug_ctx =
464                crate::debug_trace::DebugContext::new(crate::debug_trace::DebugLevel::Debug);
465            query_service.execute_with_debug(
466                &config.query,
467                Some(&dataview),
468                Some(dataview.source()),
469                Some(debug_ctx),
470            )
471        } else {
472            query_service.execute(&config.query, Some(&dataview), Some(dataview.source()))
473        }
474    }?;
475    let exec_time = exec_start.elapsed();
476
477    let query_time = query_start.elapsed();
478    info!("Query executed in {:?}", query_time);
479    info!(
480        "Result: {} rows, {} columns",
481        result.dataview.row_count(),
482        result.dataview.column_count()
483    );
484
485    // Show execution plan details if requested
486    if config.execution_plan {
487        // Try to get detailed execution plan
488        use crate::data::query_engine::QueryEngine;
489
490        let query_engine = QueryEngine::new();
491
492        match query_engine.execute_with_plan(
493            std::sync::Arc::new(dataview.source().clone()),
494            &config.query,
495        ) {
496            Ok((_view, plan)) => {
497                // Display the detailed execution plan tree
498                print!("{}", plan.format_tree());
499            }
500            Err(e) => {
501                // Fall back to simple execution plan display
502                eprintln!("Could not generate detailed execution plan: {}", e);
503                println!(
504                    "3. QUERY_EXECUTION [{:.3}ms]",
505                    exec_time.as_secs_f64() * 1000.0
506                );
507
508                // Parse query to understand what operations are being performed
509                use crate::sql::recursive_parser::Parser;
510                let mut parser = Parser::new(&config.query);
511                if let Ok(stmt) = parser.parse() {
512                    if stmt.where_clause.is_some() {
513                        println!("   • WHERE clause filtering applied");
514                        println!("   • Rows after filter: {}", result.dataview.row_count());
515                    }
516
517                    if let Some(ref order_by) = stmt.order_by {
518                        println!("   • ORDER BY: {} column(s)", order_by.len());
519                    }
520
521                    if let Some(ref group_by) = stmt.group_by {
522                        println!("   • GROUP BY: {} column(s)", group_by.len());
523                    }
524
525                    if let Some(limit) = stmt.limit {
526                        println!("   • LIMIT: {} rows", limit);
527                    }
528
529                    if stmt.distinct {
530                        println!("   • DISTINCT applied");
531                    }
532                }
533            }
534        }
535
536        println!("\nExecution Statistics:");
537        println!(
538            "  Preparation:    {:.3}ms",
539            (exec_start - start_time).as_secs_f64() * 1000.0
540        );
541        println!(
542            "  Query time:     {:.3}ms",
543            exec_time.as_secs_f64() * 1000.0
544        );
545        println!(
546            "  Total time:     {:.3}ms",
547            query_time.as_secs_f64() * 1000.0
548        );
549        println!("  Rows returned:  {}", result.dataview.row_count());
550        println!("  Columns:        {}", result.dataview.column_count());
551        println!("\n=== END EXECUTION PLAN ===");
552        println!();
553    }
554
555    // 4. Apply limit if specified
556    let final_view = if let Some(limit) = config.limit {
557        let limited_table = limit_results(&result.dataview, limit)?;
558        DataView::new(std::sync::Arc::new(limited_table))
559    } else {
560        result.dataview
561    };
562
563    // 5. Output debug trace if enabled
564    if let Some(ref trace_output) = result.debug_trace {
565        eprintln!("{}", trace_output);
566    }
567
568    // 6. Output the results
569    let exec_time_ms = exec_time.as_secs_f64() * 1000.0;
570    let output_result = if let Some(ref path) = config.output_file {
571        let mut file = fs::File::create(path)
572            .with_context(|| format!("Failed to create output file: {path}"))?;
573        output_results(
574            &final_view,
575            config.output_format,
576            &mut file,
577            config.max_col_width,
578            config.col_sample_rows,
579            exec_time_ms,
580            config.table_style,
581        )?;
582        info!("Results written to: {}", path);
583        Ok(())
584    } else {
585        output_results(
586            &final_view,
587            config.output_format,
588            &mut io::stdout(),
589            config.max_col_width,
590            config.col_sample_rows,
591            exec_time_ms,
592            config.table_style,
593        )?;
594        Ok(())
595    };
596
597    let total_time = start_time.elapsed();
598    debug!("Total execution time: {:?}", total_time);
599
600    // Print stats to stderr so they don't interfere with output
601    if config.output_file.is_none() {
602        eprintln!(
603            "\n# Query completed: {} rows in {:?}",
604            final_view.row_count(),
605            query_time
606        );
607    }
608
609    output_result
610}
611
612/// Execute a script file with multiple SQL statements separated by GO
613pub fn execute_script(config: NonInteractiveConfig) -> Result<()> {
614    let _start_time = Instant::now();
615
616    // Parse the script into individual statements
617    let parser = ScriptParser::new(&config.query);
618    let statements = parser.parse_and_validate()?;
619
620    info!("Found {} statements in script", statements.len());
621
622    // Determine data file to use (command-line overrides script hint)
623    let data_file = if !config.data_file.is_empty() {
624        // Command-line argument takes precedence
625        config.data_file.clone()
626    } else if let Some(hint) = parser.data_file_hint() {
627        // Use data file hint from script
628        info!("Using data file from script hint: {}", hint);
629
630        // Resolve relative paths relative to script file if provided
631        if let Some(script_path) = config.script_file.as_ref() {
632            let script_dir = std::path::Path::new(script_path)
633                .parent()
634                .unwrap_or(std::path::Path::new("."));
635            let hint_path = std::path::Path::new(hint);
636
637            if hint_path.is_relative() {
638                script_dir.join(hint_path).to_string_lossy().to_string()
639            } else {
640                hint.to_string()
641            }
642        } else {
643            hint.to_string()
644        }
645    } else {
646        String::new()
647    };
648
649    // Load the data file if provided, otherwise use DUAL
650    let (data_table, _is_dual) = if data_file.is_empty() {
651        // No data file provided, use DUAL table
652        info!("No data file provided, using DUAL table");
653        (DataTable::dual(), true)
654    } else {
655        // Check if file exists before trying to load
656        if !std::path::Path::new(&data_file).exists() {
657            anyhow::bail!(
658                "Data file not found: {}\n\
659                Please check the path is correct",
660                data_file
661            );
662        }
663
664        info!("Loading data from: {}", data_file);
665        let table = load_data_file(&data_file)?;
666        info!(
667            "Loaded {} rows with {} columns",
668            table.row_count(),
669            table.column_count()
670        );
671        (table, false)
672    };
673
674    // Track script results
675    let mut script_result = ScriptResult::new();
676    let mut output = Vec::new();
677
678    // Create Arc<DataTable> once for all statements - avoids expensive cloning
679    let arc_data_table = std::sync::Arc::new(data_table);
680
681    // Execute each statement
682    for (idx, statement) in statements.iter().enumerate() {
683        let statement_num = idx + 1;
684        let stmt_start = Instant::now();
685
686        // Print separator for table format
687        if matches!(config.output_format, OutputFormat::Table) {
688            if idx > 0 {
689                output.push(String::new()); // Empty line between queries
690            }
691            output.push(format!("-- Query {} --", statement_num));
692        }
693
694        // Create a fresh DataView for each statement (reuses the Arc)
695        let dataview = DataView::new(arc_data_table.clone());
696
697        // Execute the statement
698        let service = QueryExecutionService::new(config.case_insensitive, config.auto_hide_empty);
699        match service.execute(statement, Some(&dataview), None) {
700            Ok(result) => {
701                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
702                let final_view = result.dataview;
703
704                // Format the output based on the output format
705                let mut statement_output = Vec::new();
706                match config.output_format {
707                    OutputFormat::Csv => {
708                        output_csv(&final_view, &mut statement_output, ',')?;
709                    }
710                    OutputFormat::Json => {
711                        output_json(&final_view, &mut statement_output)?;
712                    }
713                    OutputFormat::JsonStructured => {
714                        output_json_structured(&final_view, &mut statement_output, exec_time)?;
715                    }
716                    OutputFormat::Table => {
717                        output_table(
718                            &final_view,
719                            &mut statement_output,
720                            config.max_col_width,
721                            config.col_sample_rows,
722                            config.table_style,
723                        )?;
724                        writeln!(
725                            &mut statement_output,
726                            "Query completed: {} rows in {:.2}ms",
727                            final_view.row_count(),
728                            exec_time
729                        )?;
730                    }
731                    OutputFormat::Tsv => {
732                        output_csv(&final_view, &mut statement_output, '\t')?;
733                    }
734                }
735
736                // Add to overall output
737                output.extend(
738                    String::from_utf8_lossy(&statement_output)
739                        .lines()
740                        .map(String::from),
741                );
742
743                script_result.add_success(
744                    statement_num,
745                    statement.clone(),
746                    final_view.row_count(),
747                    exec_time,
748                );
749            }
750            Err(e) => {
751                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
752                let error_msg = format!("Query {} failed: {}", statement_num, e);
753
754                if matches!(config.output_format, OutputFormat::Table) {
755                    output.push(error_msg.clone());
756                }
757
758                script_result.add_failure(
759                    statement_num,
760                    statement.clone(),
761                    e.to_string(),
762                    exec_time,
763                );
764
765                // Continue to next statement (don't stop on error)
766            }
767        }
768    }
769
770    // Write output
771    if let Some(ref output_file) = config.output_file {
772        let mut file = fs::File::create(output_file)?;
773        for line in &output {
774            writeln!(file, "{}", line)?;
775        }
776        info!("Results written to: {}", output_file);
777    } else {
778        for line in &output {
779            println!("{}", line);
780        }
781    }
782
783    // Print summary if in table mode
784    if matches!(config.output_format, OutputFormat::Table) {
785        println!("\n=== Script Summary ===");
786        println!("Total statements: {}", script_result.total_statements);
787        println!("Successful: {}", script_result.successful_statements);
788        println!("Failed: {}", script_result.failed_statements);
789        println!(
790            "Total execution time: {:.2}ms",
791            script_result.total_execution_time_ms
792        );
793    }
794
795    if !script_result.all_successful() {
796        return Err(anyhow::anyhow!(
797            "{} of {} statements failed",
798            script_result.failed_statements,
799            script_result.total_statements
800        ));
801    }
802
803    Ok(())
804}
805
806/// Load a data file (CSV or JSON) into a `DataTable`
807fn load_data_file(path: &str) -> Result<DataTable> {
808    let path = Path::new(path);
809
810    if !path.exists() {
811        return Err(anyhow::anyhow!("File not found: {}", path.display()));
812    }
813
814    // Determine file type by extension
815    let extension = path
816        .extension()
817        .and_then(|ext| ext.to_str())
818        .map(str::to_lowercase)
819        .unwrap_or_default();
820
821    let table_name = path
822        .file_stem()
823        .and_then(|stem| stem.to_str())
824        .unwrap_or("data")
825        .to_string();
826
827    match extension.as_str() {
828        "csv" => load_csv_to_datatable(path, &table_name)
829            .with_context(|| format!("Failed to load CSV file: {}", path.display())),
830        "json" => load_json_to_datatable(path, &table_name)
831            .with_context(|| format!("Failed to load JSON file: {}", path.display())),
832        _ => Err(anyhow::anyhow!(
833            "Unsupported file type: {}. Use .csv or .json",
834            extension
835        )),
836    }
837}
838
839/// Limit the number of rows in results
840fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
841    let source = dataview.source();
842    let mut limited_table = DataTable::new(&source.name);
843
844    // Copy columns
845    for col in &source.columns {
846        limited_table.add_column(col.clone());
847    }
848
849    // Copy limited rows
850    let rows_to_copy = dataview.row_count().min(limit);
851    for i in 0..rows_to_copy {
852        if let Some(row) = dataview.get_row(i) {
853            limited_table.add_row(row.clone());
854        }
855    }
856
857    Ok(limited_table)
858}
859
860/// Output query results in the specified format
861fn output_results<W: Write>(
862    dataview: &DataView,
863    format: OutputFormat,
864    writer: &mut W,
865    max_col_width: Option<usize>,
866    col_sample_rows: usize,
867    exec_time_ms: f64,
868    table_style: TableStyle,
869) -> Result<()> {
870    match format {
871        OutputFormat::Csv => output_csv(dataview, writer, ','),
872        OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
873        OutputFormat::Json => output_json(dataview, writer),
874        OutputFormat::JsonStructured => output_json_structured(dataview, writer, exec_time_ms),
875        OutputFormat::Table => output_table(
876            dataview,
877            writer,
878            max_col_width,
879            col_sample_rows,
880            table_style,
881        ),
882    }
883}
884
885/// Output results as CSV/TSV
886fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
887    // Write headers
888    let columns = dataview.column_names();
889    for (i, col) in columns.iter().enumerate() {
890        if i > 0 {
891            write!(writer, "{delimiter}")?;
892        }
893        write!(writer, "{}", escape_csv_field(col, delimiter))?;
894    }
895    writeln!(writer)?;
896
897    // Write rows
898    for row_idx in 0..dataview.row_count() {
899        if let Some(row) = dataview.get_row(row_idx) {
900            for (i, value) in row.values.iter().enumerate() {
901                if i > 0 {
902                    write!(writer, "{delimiter}")?;
903                }
904                write!(
905                    writer,
906                    "{}",
907                    escape_csv_field(&format_value(value), delimiter)
908                )?;
909            }
910            writeln!(writer)?;
911        }
912    }
913
914    Ok(())
915}
916
917/// Output results as JSON
918fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
919    let columns = dataview.column_names();
920    let mut rows = Vec::new();
921
922    for row_idx in 0..dataview.row_count() {
923        if let Some(row) = dataview.get_row(row_idx) {
924            let mut json_row = serde_json::Map::new();
925            for (col_idx, value) in row.values.iter().enumerate() {
926                if col_idx < columns.len() {
927                    json_row.insert(columns[col_idx].clone(), value_to_json(value));
928                }
929            }
930            rows.push(serde_json::Value::Object(json_row));
931        }
932    }
933
934    let json = serde_json::to_string_pretty(&rows)?;
935    writeln!(writer, "{json}")?;
936
937    Ok(())
938}
939
940/// Output results as structured JSON with metadata for IDE/plugin integration
941fn output_json_structured<W: Write>(
942    dataview: &DataView,
943    writer: &mut W,
944    exec_time: f64,
945) -> Result<()> {
946    let column_names = dataview.column_names();
947    let data_table = dataview.source();
948
949    // Build column metadata
950    let mut columns = Vec::new();
951    for (idx, name) in column_names.iter().enumerate() {
952        let col_type = data_table
953            .columns
954            .get(idx)
955            .map(|c| format!("{:?}", c.data_type))
956            .unwrap_or_else(|| "UNKNOWN".to_string());
957
958        // Calculate max width for this column
959        let mut max_width = name.len();
960        for row_idx in 0..dataview.row_count() {
961            if let Some(row) = dataview.get_row(row_idx) {
962                if let Some(value) = row.values.get(idx) {
963                    let display_width = match value {
964                        DataValue::Null => 4, // "NULL"
965                        DataValue::Integer(i) => i.to_string().len(),
966                        DataValue::Float(f) => format!("{:.2}", f).len(),
967                        DataValue::String(s) => s.len(),
968                        DataValue::InternedString(s) => s.len(),
969                        DataValue::Boolean(b) => {
970                            if *b {
971                                4
972                            } else {
973                                5
974                            }
975                        } // "true" or "false"
976                        DataValue::DateTime(dt) => dt.len(),
977                    };
978                    max_width = max_width.max(display_width);
979                }
980            }
981        }
982
983        let alignment = match data_table.columns.get(idx).map(|c| &c.data_type) {
984            Some(crate::data::datatable::DataType::Integer) => "right",
985            Some(crate::data::datatable::DataType::Float) => "right",
986            _ => "left",
987        };
988
989        let col_meta = serde_json::json!({
990            "name": name,
991            "type": col_type,
992            "max_width": max_width,
993            "alignment": alignment
994        });
995        columns.push(col_meta);
996    }
997
998    // Build rows as arrays of strings
999    let mut rows = Vec::new();
1000    for row_idx in 0..dataview.row_count() {
1001        if let Some(row) = dataview.get_row(row_idx) {
1002            let row_values: Vec<String> = row
1003                .values
1004                .iter()
1005                .map(|v| match v {
1006                    DataValue::Null => String::new(),
1007                    DataValue::Integer(i) => i.to_string(),
1008                    DataValue::Float(f) => format!("{:.2}", f),
1009                    DataValue::String(s) => s.clone(),
1010                    DataValue::InternedString(s) => s.to_string(),
1011                    DataValue::Boolean(b) => b.to_string(),
1012                    DataValue::DateTime(dt) => dt.clone(),
1013                })
1014                .collect();
1015            rows.push(serde_json::Value::Array(
1016                row_values
1017                    .into_iter()
1018                    .map(serde_json::Value::String)
1019                    .collect(),
1020            ));
1021        }
1022    }
1023
1024    // Build complete structured output
1025    let output = serde_json::json!({
1026        "columns": columns,
1027        "rows": rows,
1028        "metadata": {
1029            "total_rows": dataview.row_count(),
1030            "query_time_ms": exec_time
1031        }
1032    });
1033
1034    let json = serde_json::to_string_pretty(&output)?;
1035    writeln!(writer, "{json}")?;
1036
1037    Ok(())
1038}
1039
1040/// Output results using the old custom ASCII table format (for Nvim compatibility)
1041fn output_table_old_style<W: Write>(
1042    dataview: &DataView,
1043    writer: &mut W,
1044    max_col_width: Option<usize>,
1045) -> Result<()> {
1046    let columns = dataview.column_names();
1047
1048    // Calculate column widths
1049    let mut widths = vec![0; columns.len()];
1050    for (i, col) in columns.iter().enumerate() {
1051        widths[i] = col.len();
1052    }
1053
1054    // Scan all rows for width calculation
1055    for row_idx in 0..dataview.row_count() {
1056        if let Some(row) = dataview.get_row(row_idx) {
1057            for (i, value) in row.values.iter().enumerate() {
1058                if i < widths.len() {
1059                    let value_str = format_value(value);
1060                    widths[i] = widths[i].max(value_str.len());
1061                }
1062            }
1063        }
1064    }
1065
1066    // Apply maximum column width if specified
1067    if let Some(max_width) = max_col_width {
1068        for width in &mut widths {
1069            *width = (*width).min(max_width);
1070        }
1071    }
1072
1073    // Print top border
1074    write!(writer, "+")?;
1075    for width in &widths {
1076        write!(writer, "{}", "-".repeat(*width + 2))?;
1077        write!(writer, "+")?;
1078    }
1079    writeln!(writer)?;
1080
1081    // Print headers
1082    write!(writer, "|")?;
1083    for (i, col) in columns.iter().enumerate() {
1084        write!(writer, " {:^width$} |", col, width = widths[i])?;
1085    }
1086    writeln!(writer)?;
1087
1088    // Print header separator
1089    write!(writer, "+")?;
1090    for width in &widths {
1091        write!(writer, "{}", "-".repeat(*width + 2))?;
1092        write!(writer, "+")?;
1093    }
1094    writeln!(writer)?;
1095
1096    // Print data rows (no separators between rows)
1097    for row_idx in 0..dataview.row_count() {
1098        if let Some(row) = dataview.get_row(row_idx) {
1099            write!(writer, "|")?;
1100            for (i, value) in row.values.iter().enumerate() {
1101                if i < widths.len() {
1102                    let value_str = format_value(value);
1103                    let truncated = if value_str.len() > widths[i] {
1104                        format!("{}...", &value_str[..widths[i].saturating_sub(3)])
1105                    } else {
1106                        value_str
1107                    };
1108                    write!(writer, " {:<width$} |", truncated, width = widths[i])?;
1109                }
1110            }
1111            writeln!(writer)?;
1112        }
1113    }
1114
1115    // Print bottom border
1116    write!(writer, "+")?;
1117    for width in &widths {
1118        write!(writer, "{}", "-".repeat(*width + 2))?;
1119        write!(writer, "+")?;
1120    }
1121    writeln!(writer)?;
1122
1123    Ok(())
1124}
1125
1126/// Output results as a table using comfy-table with styling
1127fn output_table<W: Write>(
1128    dataview: &DataView,
1129    writer: &mut W,
1130    max_col_width: Option<usize>,
1131    _col_sample_rows: usize, // Not needed with comfy-table
1132    style: TableStyle,
1133) -> Result<()> {
1134    let mut table = Table::new();
1135
1136    // Apply the selected style preset
1137    match style {
1138        TableStyle::Default => {
1139            // Use custom old-style renderer for Nvim compatibility
1140            // This matches what the table navigation parser expects
1141            return output_table_old_style(dataview, writer, max_col_width);
1142        }
1143        TableStyle::AsciiFull => {
1144            table.load_preset(ASCII_FULL);
1145        }
1146        TableStyle::AsciiCondensed => {
1147            table.load_preset(ASCII_FULL_CONDENSED);
1148        }
1149        TableStyle::AsciiBordersOnly => {
1150            table.load_preset(ASCII_BORDERS_ONLY);
1151        }
1152        TableStyle::AsciiHorizontalOnly => {
1153            table.load_preset(ASCII_HORIZONTAL_ONLY);
1154        }
1155        TableStyle::AsciiNoBorders => {
1156            table.load_preset(ASCII_NO_BORDERS);
1157        }
1158        TableStyle::Markdown => {
1159            table.load_preset(ASCII_MARKDOWN);
1160        }
1161        TableStyle::Utf8Full => {
1162            table.load_preset(UTF8_FULL);
1163        }
1164        TableStyle::Utf8Condensed => {
1165            table.load_preset(UTF8_FULL_CONDENSED);
1166        }
1167        TableStyle::Utf8BordersOnly => {
1168            table.load_preset(UTF8_BORDERS_ONLY);
1169        }
1170        TableStyle::Utf8HorizontalOnly => {
1171            table.load_preset(UTF8_HORIZONTAL_ONLY);
1172        }
1173        TableStyle::Utf8NoBorders => {
1174            table.load_preset(UTF8_NO_BORDERS);
1175        }
1176        TableStyle::Plain => {
1177            table.load_preset(NOTHING);
1178        }
1179    }
1180
1181    // Set content arrangement (automatic width adjustment)
1182    if max_col_width.is_some() {
1183        table.set_content_arrangement(ContentArrangement::Dynamic);
1184    }
1185
1186    // Set column headers
1187    let columns = dataview.column_names();
1188    table.set_header(&columns);
1189
1190    // Add data rows
1191    for row_idx in 0..dataview.row_count() {
1192        if let Some(row) = dataview.get_row(row_idx) {
1193            let row_strings: Vec<String> = row
1194                .values
1195                .iter()
1196                .map(|v| {
1197                    let s = format_value(v);
1198                    // Apply max width truncation if specified
1199                    if let Some(max_width) = max_col_width {
1200                        if s.len() > max_width {
1201                            format!("{}...", &s[..max_width.saturating_sub(3)])
1202                        } else {
1203                            s
1204                        }
1205                    } else {
1206                        s
1207                    }
1208                })
1209                .collect();
1210            table.add_row(row_strings);
1211        }
1212    }
1213
1214    // Write the table to the writer
1215    writeln!(writer, "{}", table)?;
1216
1217    Ok(())
1218}
1219
1220/// Format a `DataValue` for display
1221fn format_value(value: &DataValue) -> String {
1222    match value {
1223        DataValue::Null => String::new(),
1224        DataValue::Integer(i) => i.to_string(),
1225        DataValue::Float(f) => f.to_string(),
1226        DataValue::String(s) => s.clone(),
1227        DataValue::InternedString(s) => s.to_string(),
1228        DataValue::Boolean(b) => b.to_string(),
1229        DataValue::DateTime(dt) => dt.to_string(),
1230    }
1231}
1232
1233/// Convert `DataValue` to JSON
1234fn value_to_json(value: &DataValue) -> serde_json::Value {
1235    match value {
1236        DataValue::Null => serde_json::Value::Null,
1237        DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
1238        DataValue::Float(f) => {
1239            if let Some(n) = serde_json::Number::from_f64(*f) {
1240                serde_json::Value::Number(n)
1241            } else {
1242                serde_json::Value::Null
1243            }
1244        }
1245        DataValue::String(s) => serde_json::Value::String(s.clone()),
1246        DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
1247        DataValue::Boolean(b) => serde_json::Value::Bool(*b),
1248        DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
1249    }
1250}
1251
1252/// Escape a CSV field if it contains special characters
1253fn escape_csv_field(field: &str, delimiter: char) -> String {
1254    if field.contains(delimiter)
1255        || field.contains('"')
1256        || field.contains('\n')
1257        || field.contains('\r')
1258    {
1259        format!("\"{}\"", field.replace('"', "\"\""))
1260    } else {
1261        field.to_string()
1262    }
1263}