Skip to main content

sql_cli/
non_interactive.rs

1use anyhow::{Context, Result};
2use comfy_table::presets::*;
3use comfy_table::{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_json_to_datatable;
14// Phase 1: TempTableRegistry no longer used directly - it's in ExecutionContext
15// use crate::data::temp_table_registry::TempTableRegistry;
16use crate::services::query_execution_service::QueryExecutionService;
17use crate::sql::parser::ast::{CTEType, TableSource, CTE};
18use crate::sql::recursive_parser::Parser;
19use crate::sql::script_parser::{ScriptParser, ScriptResult};
20use crate::utils::string_utils::display_width;
21
22/// Check if a query references temporary tables (starting with #)
23/// Temporary tables are only valid in script mode
24fn check_temp_table_usage(query: &str) -> Result<()> {
25    use crate::sql::recursive_parser::Parser;
26
27    let mut parser = Parser::new(query);
28    match parser.parse() {
29        Ok(stmt) => {
30            // Check FROM clause
31            if let Some(from_table) = &stmt.from_table {
32                if from_table.starts_with('#') {
33                    anyhow::bail!(
34                        "Temporary table '{}' cannot be used in single-query mode. \
35                        Temporary tables are only available in script mode (using -f flag with GO separators).",
36                        from_table
37                    );
38                }
39            }
40
41            // Check INTO clause
42            if let Some(into_table) = &stmt.into_table {
43                anyhow::bail!(
44                    "INTO clause for temporary table '{}' is only supported in script mode. \
45                    Use -f flag with GO separators to create temporary tables.",
46                    into_table.name
47                );
48            }
49
50            Ok(())
51        }
52        Err(_) => {
53            // If parse fails, let it fail later in the actual execution
54            Ok(())
55        }
56    }
57}
58
59/// Extract dependencies from a CTE by analyzing what tables it references
60fn extract_cte_dependencies(cte: &CTE) -> Vec<String> {
61    // For now, we'll just return the from_table if it exists
62    // This could be enhanced to do deeper analysis of the query AST
63    let mut deps = Vec::new();
64
65    if let CTEType::Standard(query) = &cte.cte_type {
66        if let Some(from_table) = &query.from_table {
67            deps.push(from_table.clone());
68        }
69
70        // Could also check joins, subqueries, etc.
71        for join in &query.joins {
72            match &join.table {
73                TableSource::Table(table_name) => {
74                    deps.push(table_name.clone());
75                }
76                TableSource::DerivedTable { alias, .. } => {
77                    deps.push(alias.clone());
78                }
79                TableSource::Pivot { alias, .. } => {
80                    // Use the pivot alias if available
81                    if let Some(pivot_alias) = alias {
82                        deps.push(pivot_alias.clone());
83                    }
84                }
85            }
86        }
87    }
88
89    deps
90}
91
92/// Output format for query results
93#[derive(Debug, Clone)]
94pub enum OutputFormat {
95    Csv,
96    Json,
97    JsonStructured, // Structured JSON with metadata for IDE/plugin integration
98    Table,
99    Tsv,
100}
101
102impl OutputFormat {
103    pub fn from_str(s: &str) -> Result<Self> {
104        match s.to_lowercase().as_str() {
105            "csv" => Ok(OutputFormat::Csv),
106            "json" => Ok(OutputFormat::Json),
107            "json-structured" => Ok(OutputFormat::JsonStructured),
108            "table" => Ok(OutputFormat::Table),
109            "tsv" => Ok(OutputFormat::Tsv),
110            _ => Err(anyhow::anyhow!(
111                "Invalid output format: {}. Use csv, json, json-structured, table, or tsv",
112                s
113            )),
114        }
115    }
116}
117
118/// Table styling presets for table output format
119#[derive(Debug, Clone, Copy)]
120pub enum TableStyle {
121    /// Current default ASCII style with borders
122    Default,
123    /// ASCII table with full borders
124    AsciiFull,
125    /// ASCII table with condensed rows
126    AsciiCondensed,
127    /// ASCII table with only outer borders
128    AsciiBordersOnly,
129    /// ASCII table with horizontal lines only
130    AsciiHorizontalOnly,
131    /// ASCII table with no borders
132    AsciiNoBorders,
133    /// Markdown-style table
134    Markdown,
135    /// UTF8 table with box-drawing characters
136    Utf8Full,
137    /// UTF8 table with condensed rows
138    Utf8Condensed,
139    /// UTF8 table with only outer borders
140    Utf8BordersOnly,
141    /// UTF8 table with horizontal lines only
142    Utf8HorizontalOnly,
143    /// UTF8 table with no borders
144    Utf8NoBorders,
145    /// No table formatting, just data
146    Plain,
147}
148
149impl TableStyle {
150    pub fn from_str(s: &str) -> Result<Self> {
151        match s.to_lowercase().as_str() {
152            "default" => Ok(TableStyle::Default),
153            "ascii" | "ascii-full" => Ok(TableStyle::AsciiFull),
154            "ascii-condensed" => Ok(TableStyle::AsciiCondensed),
155            "ascii-borders" | "ascii-borders-only" => Ok(TableStyle::AsciiBordersOnly),
156            "ascii-horizontal" | "ascii-horizontal-only" => Ok(TableStyle::AsciiHorizontalOnly),
157            "ascii-noborders" | "ascii-no-borders" => Ok(TableStyle::AsciiNoBorders),
158            "markdown" | "md" => Ok(TableStyle::Markdown),
159            "utf8" | "utf8-full" => Ok(TableStyle::Utf8Full),
160            "utf8-condensed" => Ok(TableStyle::Utf8Condensed),
161            "utf8-borders" | "utf8-borders-only" => Ok(TableStyle::Utf8BordersOnly),
162            "utf8-horizontal" | "utf8-horizontal-only" => Ok(TableStyle::Utf8HorizontalOnly),
163            "utf8-noborders" | "utf8-no-borders" => Ok(TableStyle::Utf8NoBorders),
164            "plain" | "none" => Ok(TableStyle::Plain),
165            _ => Err(anyhow::anyhow!(
166                "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",
167                s
168            )),
169        }
170    }
171
172    pub fn list_styles() -> &'static str {
173        "Available table styles:
174  default            - Current default ASCII style
175  ascii              - ASCII with full borders
176  ascii-condensed    - ASCII with condensed rows
177  ascii-borders      - ASCII with outer borders only
178  ascii-horizontal   - ASCII with horizontal lines only
179  ascii-noborders    - ASCII with no borders
180  markdown           - GitHub-flavored Markdown table
181  utf8               - UTF8 box-drawing characters
182  utf8-condensed     - UTF8 with condensed rows
183  utf8-borders       - UTF8 with outer borders only
184  utf8-horizontal    - UTF8 with horizontal lines only
185  utf8-noborders     - UTF8 with no borders
186  plain              - No formatting, data only"
187    }
188}
189
190/// Configuration for non-interactive query execution
191pub struct NonInteractiveConfig {
192    pub data_file: String,
193    pub query: String,
194    pub output_format: OutputFormat,
195    pub output_file: Option<String>,
196    pub case_insensitive: bool,
197    pub auto_hide_empty: bool,
198    pub limit: Option<usize>,
199    pub query_plan: bool,
200    pub show_work_units: bool,
201    pub execution_plan: bool,
202    pub show_preprocessing: bool,
203    pub show_transformations: bool,
204    pub cte_info: bool,
205    pub rewrite_analysis: bool,
206    pub lift_in_expressions: bool,
207    pub script_file: Option<String>, // Path to the script file for relative path resolution
208    pub debug_trace: bool,
209    pub max_col_width: Option<usize>, // Maximum column width for table output (None = unlimited)
210    pub col_sample_rows: usize,       // Number of rows to sample for column width (0 = all rows)
211    pub table_style: TableStyle,      // Table styling preset (only affects table output format)
212    pub styled: bool,                 // Whether to apply color styling rules
213    pub style_file: Option<String>,   // Path to YAML style configuration file
214    pub no_where_expansion: bool,     // Disable WHERE clause alias expansion
215    pub no_group_by_expansion: bool,  // Disable GROUP BY clause alias expansion
216    pub no_having_expansion: bool,    // Disable HAVING clause auto-aliasing
217    pub no_order_by_expansion: bool,  // Disable ORDER BY aggregate expansion
218    pub no_qualify_to_where: bool,    // Disable QUALIFY to WHERE transformation
219    pub no_expression_lifter: bool,   // Disable expression lifting transformer
220    pub no_cte_hoister: bool,         // Disable CTE hoisting transformer
221    pub no_in_lifter: bool,           // Disable IN operator lifting transformer
222    pub delimiter_override: Option<u8>, // Explicit --delimiter flag; overrides extension auto-detect
223}
224
225/// Convert NonInteractiveConfig flags to TransformerConfig
226fn make_transformer_config(config: &NonInteractiveConfig) -> crate::query_plan::TransformerConfig {
227    crate::query_plan::TransformerConfig {
228        enable_pivot_expander: true, // Always enabled
229        enable_expression_lifter: !config.no_expression_lifter,
230        enable_where_expansion: !config.no_where_expansion,
231        enable_group_by_expansion: !config.no_group_by_expansion,
232        enable_having_expansion: !config.no_having_expansion,
233        enable_order_by_expansion: !config.no_order_by_expansion,
234        enable_qualify_to_where: !config.no_qualify_to_where,
235        enable_ilike_to_like: true, // Always enabled
236        enable_cte_hoister: !config.no_cte_hoister,
237        enable_in_lifter: !config.no_in_lifter,
238    }
239}
240
241/// Execute a query in non-interactive mode
242pub fn execute_non_interactive(config: NonInteractiveConfig) -> Result<()> {
243    let start_time = Instant::now();
244
245    // Phase 2: REMOVED temp table blocking - temp tables now work in single query mode!
246    // check_temp_table_usage(&config.query)?;
247
248    // Phase 3.3: Check for data file hint in query if no data file specified
249    let data_file_to_use = if config.data_file.is_empty() {
250        // Look for -- #! hint in first 10 lines
251        let lines: Vec<&str> = config.query.lines().take(10).collect();
252        let mut hint_path: Option<String> = None;
253
254        for line in lines {
255            let trimmed = line.trim();
256            if trimmed.starts_with("-- #!") {
257                hint_path = Some(trimmed[5..].trim().to_string());
258                break;
259            }
260        }
261
262        if let Some(path) = hint_path {
263            debug!("Found data file hint: {}", path);
264            // Resolve relative paths
265            let resolved_path = if path.starts_with("../") {
266                // Relative to current directory
267                std::path::Path::new(&path).to_path_buf()
268            } else if path.starts_with("data/") {
269                // Relative to project root (assume current dir)
270                std::path::Path::new(&path).to_path_buf()
271            } else {
272                std::path::Path::new(&path).to_path_buf()
273            };
274
275            if resolved_path.exists() {
276                info!("Using data file from hint: {:?}", resolved_path);
277                resolved_path.to_string_lossy().to_string()
278            } else {
279                debug!("Data file hint path does not exist: {:?}", resolved_path);
280                String::new()
281            }
282        } else {
283            String::new()
284        }
285    } else {
286        config.data_file.clone()
287    };
288
289    // Phase 2: Load data file and create unified execution context
290    let (data_table, _is_dual) = if data_file_to_use.is_empty() {
291        info!("No data file provided, using DUAL table");
292        (crate::data::datatable::DataTable::dual(), true)
293    } else {
294        info!("Loading data from: {}", data_file_to_use);
295        let table = load_data_file(&data_file_to_use, config.delimiter_override)?;
296        info!(
297            "Loaded {} rows with {} columns",
298            table.row_count(),
299            table.column_count()
300        );
301        (table, false)
302    };
303    let _table_name = data_table.name.clone();
304
305    // Phase 2: Create unified execution context (replaces standalone DataView)
306    use crate::execution::{ExecutionConfig, ExecutionContext, StatementExecutor};
307    let mut context = ExecutionContext::new(std::sync::Arc::new(data_table));
308
309    // Keep DataView for backward compatibility with special flags
310    let dataview = DataView::new(context.source_table.clone());
311
312    // 3. Execute the query
313    info!("Executing query: {}", config.query);
314
315    // If execution_plan is requested, show detailed execution information
316    if config.execution_plan {
317        println!("\n=== EXECUTION PLAN ===");
318        println!("Query: {}", config.query);
319        println!("\nExecution Steps:");
320        println!("1. PARSE - Parse SQL query");
321        println!("2. LOAD_DATA - Load data from {}", &config.data_file);
322        println!(
323            "   • Loaded {} rows, {} columns",
324            dataview.row_count(),
325            dataview.column_count()
326        );
327    }
328
329    // If show_work_units is requested, analyze and display work units
330    if config.show_work_units {
331        use crate::query_plan::{ExpressionLifter, QueryAnalyzer};
332        use crate::sql::recursive_parser::Parser;
333
334        let mut parser = Parser::new(&config.query);
335        match parser.parse() {
336            Ok(stmt) => {
337                let mut analyzer = QueryAnalyzer::new();
338                let mut lifter = ExpressionLifter::new();
339
340                // Check if the query has liftable expressions
341                let mut stmt_copy = stmt.clone();
342                let lifted = lifter.lift_expressions(&mut stmt_copy);
343
344                // Build the query plan
345                match analyzer.analyze(&stmt_copy, config.query.clone()) {
346                    Ok(plan) => {
347                        println!("\n{}", plan.explain());
348
349                        if !lifted.is_empty() {
350                            println!("\nLifted CTEs:");
351                            for cte in &lifted {
352                                println!("  - {}", cte.name);
353                            }
354                        }
355
356                        return Ok(());
357                    }
358                    Err(e) => {
359                        eprintln!("Error analyzing query: {}", e);
360                        return Err(anyhow::anyhow!("Query analysis failed: {}", e));
361                    }
362                }
363            }
364            Err(e) => {
365                eprintln!("Error parsing query: {}", e);
366                return Err(anyhow::anyhow!("Parse error: {}", e));
367            }
368        }
369    }
370
371    // If query_plan is requested, parse and display the AST
372    if config.query_plan {
373        use crate::sql::recursive_parser::Parser;
374        let mut parser = Parser::new(&config.query);
375        match parser.parse() {
376            Ok(statement) => {
377                println!("\n=== QUERY PLAN (AST) ===");
378                println!("{statement:#?}");
379                println!("=== END QUERY PLAN ===\n");
380            }
381            Err(e) => {
382                eprintln!("Failed to parse query for plan: {e}");
383            }
384        }
385    }
386
387    // If rewrite analysis is requested, analyze query for optimization opportunities
388    if config.rewrite_analysis {
389        use crate::sql::query_rewriter::{QueryRewriter, RewriteAnalysis};
390        use crate::sql::recursive_parser::Parser;
391        use serde_json::json;
392
393        let mut parser = Parser::new(&config.query);
394        match parser.parse() {
395            Ok(statement) => {
396                let mut rewriter = QueryRewriter::new();
397                let suggestions = rewriter.analyze(&statement);
398
399                let analysis = RewriteAnalysis::from_suggestions(suggestions);
400                println!("{}", serde_json::to_string_pretty(&analysis).unwrap());
401                return Ok(());
402            }
403            Err(e) => {
404                let output = json!({
405                    "success": false,
406                    "error": format!("{}", e),
407                    "suggestions": [],
408                    "can_auto_rewrite": false,
409                });
410                println!("{}", serde_json::to_string_pretty(&output).unwrap());
411                return Ok(());
412            }
413        }
414    }
415
416    // If CTE info is requested, parse and output CTE information as JSON
417    if config.cte_info {
418        use crate::sql::recursive_parser::Parser;
419        use serde_json::json;
420
421        let mut parser = Parser::new(&config.query);
422        match parser.parse() {
423            Ok(statement) => {
424                let mut cte_info = Vec::new();
425
426                // Extract CTE information
427                for (index, cte) in statement.ctes.iter().enumerate() {
428                    let cte_json = json!({
429                        "index": index,
430                        "name": cte.name,
431                        "columns": cte.column_list,
432                        // We can't easily get line numbers from the AST,
433                        // but we can provide the structure
434                        "dependencies": extract_cte_dependencies(cte),
435                    });
436                    cte_info.push(cte_json);
437                }
438
439                let output = json!({
440                    "success": true,
441                    "ctes": cte_info,
442                    "total": statement.ctes.len(),
443                    "has_final_select": !statement.columns.is_empty() || !statement.select_items.is_empty(),
444                });
445
446                println!("{}", serde_json::to_string_pretty(&output).unwrap());
447                return Ok(());
448            }
449            Err(e) => {
450                let output = json!({
451                    "success": false,
452                    "error": format!("{}", e),
453                    "ctes": [],
454                    "total": 0,
455                });
456                println!("{}", serde_json::to_string_pretty(&output).unwrap());
457                return Ok(());
458            }
459        }
460    }
461
462    let query_start = Instant::now();
463
464    // Load configuration file to get date notation and other settings
465    let app_config = Config::load().unwrap_or_else(|e| {
466        debug!("Could not load config file: {}. Using defaults.", e);
467        Config::default()
468    });
469
470    // Initialize global config for function registry
471    crate::config::global::init_config(app_config.clone());
472
473    // Phase 2: Create unified execution config from CLI flags
474    let exec_config = ExecutionConfig::from_cli_flags(
475        config.show_preprocessing,
476        config.show_transformations,
477        config.case_insensitive,
478        config.auto_hide_empty,
479        config.no_expression_lifter,
480        config.no_where_expansion,
481        config.no_group_by_expansion,
482        config.no_having_expansion,
483        config.no_order_by_expansion,
484        config.no_qualify_to_where,
485        config.no_cte_hoister,
486        config.no_in_lifter,
487        config.debug_trace,
488    );
489
490    // Phase 2: Create unified statement executor
491    let executor = StatementExecutor::with_config(exec_config);
492
493    // Phase 2: Parse and execute using unified infrastructure
494    let exec_start = Instant::now();
495    let result = {
496        use crate::sql::recursive_parser::Parser;
497
498        // Phase 3.1: Expand templates BEFORE parsing (same as script mode)
499        use crate::sql::template_expander::TemplateExpander;
500        let expander = TemplateExpander::new(&context.temp_tables);
501
502        let query_to_parse = match expander.parse_templates(&config.query) {
503            Ok(vars) => {
504                if vars.is_empty() {
505                    config.query.clone()
506                } else {
507                    match expander.expand(&config.query, &vars) {
508                        Ok(expanded) => {
509                            debug!(
510                                "Template expansion in single query mode: {} vars expanded",
511                                vars.len()
512                            );
513                            for var in &vars {
514                                debug!("  {} -> resolved", var.placeholder);
515                            }
516                            expanded
517                        }
518                        Err(e) => {
519                            return Err(anyhow::anyhow!(
520                                "Template expansion failed: {}. Available tables: {}",
521                                e,
522                                context.temp_tables.list_tables().join(", ")
523                            ));
524                        }
525                    }
526                }
527            }
528            Err(e) => {
529                return Err(anyhow::anyhow!("Template parsing failed: {}", e));
530            }
531        };
532
533        let mut parser = Parser::new(&query_to_parse);
534        match parser.parse() {
535            Ok(stmt) => {
536                // Phase 2: Execute using unified StatementExecutor
537                // This handles:
538                // - Table resolution (base, temp tables, DUAL)
539                // - Preprocessing pipeline (all transformers)
540                // - Direct AST execution (no re-parsing!)
541                match executor.execute(stmt, &mut context) {
542                    Ok(exec_result) => {
543                        // Convert to QueryExecutionResult for compatibility
544                        Ok(
545                            crate::services::query_execution_service::QueryExecutionResult {
546                                dataview: exec_result.dataview,
547                                stats: crate::services::query_execution_service::QueryStats {
548                                    row_count: exec_result.stats.row_count,
549                                    column_count: exec_result.stats.column_count,
550                                    execution_time: exec_start.elapsed(),
551                                    query_engine_time: exec_start.elapsed(),
552                                },
553                                hidden_columns: Vec::new(),
554                                query: config.query.clone(),
555                                execution_plan: None,
556                                debug_trace: None,
557                            },
558                        )
559                    }
560                    Err(e) => Err(e),
561                }
562            }
563            Err(e) => {
564                // Parse failed, return error
565                Err(anyhow::anyhow!("Parse error: {}", e))
566            }
567        }
568    }?;
569    let exec_time = exec_start.elapsed();
570
571    let query_time = query_start.elapsed();
572    info!("Query executed in {:?}", query_time);
573    info!(
574        "Result: {} rows, {} columns",
575        result.dataview.row_count(),
576        result.dataview.column_count()
577    );
578
579    // Show execution plan details if requested
580    if config.execution_plan {
581        // Try to get detailed execution plan
582        use crate::data::query_engine::QueryEngine;
583
584        let query_engine = QueryEngine::new();
585
586        match query_engine.execute_with_plan(
587            std::sync::Arc::new(dataview.source().clone()),
588            &config.query,
589        ) {
590            Ok((_view, plan)) => {
591                // Display the detailed execution plan tree
592                print!("{}", plan.format_tree());
593            }
594            Err(e) => {
595                // Fall back to simple execution plan display
596                eprintln!("Could not generate detailed execution plan: {}", e);
597                println!(
598                    "3. QUERY_EXECUTION [{:.3}ms]",
599                    exec_time.as_secs_f64() * 1000.0
600                );
601
602                // Parse query to understand what operations are being performed
603                use crate::sql::recursive_parser::Parser;
604                let mut parser = Parser::new(&config.query);
605                if let Ok(stmt) = parser.parse() {
606                    if stmt.where_clause.is_some() {
607                        println!("   • WHERE clause filtering applied");
608                        println!("   • Rows after filter: {}", result.dataview.row_count());
609                    }
610
611                    if let Some(ref order_by) = stmt.order_by {
612                        println!("   • ORDER BY: {} column(s)", order_by.len());
613                    }
614
615                    if let Some(ref group_by) = stmt.group_by {
616                        println!("   • GROUP BY: {} column(s)", group_by.len());
617                    }
618
619                    if let Some(limit) = stmt.limit {
620                        println!("   • LIMIT: {} rows", limit);
621                    }
622
623                    if stmt.distinct {
624                        println!("   • DISTINCT applied");
625                    }
626                }
627            }
628        }
629
630        println!("\nExecution Statistics:");
631        println!(
632            "  Preparation:    {:.3}ms",
633            (exec_start - start_time).as_secs_f64() * 1000.0
634        );
635        println!(
636            "  Query time:     {:.3}ms",
637            exec_time.as_secs_f64() * 1000.0
638        );
639        println!(
640            "  Total time:     {:.3}ms",
641            query_time.as_secs_f64() * 1000.0
642        );
643        println!("  Rows returned:  {}", result.dataview.row_count());
644        println!("  Columns:        {}", result.dataview.column_count());
645        println!("\n=== END EXECUTION PLAN ===");
646        println!();
647    }
648
649    // 4. Apply limit if specified
650    let final_view = if let Some(limit) = config.limit {
651        let limited_table = limit_results(&result.dataview, limit)?;
652        DataView::new(std::sync::Arc::new(limited_table))
653    } else {
654        result.dataview
655    };
656
657    // 5. Output debug trace if enabled
658    if let Some(ref trace_output) = result.debug_trace {
659        eprintln!("{}", trace_output);
660    }
661
662    // 6. Output the results
663    let exec_time_ms = exec_time.as_secs_f64() * 1000.0;
664    let output_result = if let Some(ref path) = config.output_file {
665        let mut file = fs::File::create(path)
666            .with_context(|| format!("Failed to create output file: {path}"))?;
667        output_results(
668            &final_view,
669            config.output_format,
670            &mut file,
671            config.max_col_width,
672            config.col_sample_rows,
673            exec_time_ms,
674            config.table_style,
675            config.styled,
676            config.style_file.as_deref(),
677        )?;
678        info!("Results written to: {}", path);
679        Ok(())
680    } else {
681        output_results(
682            &final_view,
683            config.output_format,
684            &mut io::stdout(),
685            config.max_col_width,
686            config.col_sample_rows,
687            exec_time_ms,
688            config.table_style,
689            config.styled,
690            config.style_file.as_deref(),
691        )?;
692        Ok(())
693    };
694
695    let total_time = start_time.elapsed();
696    debug!("Total execution time: {:?}", total_time);
697
698    // Print stats to stderr so they don't interfere with output
699    if config.output_file.is_none() {
700        eprintln!(
701            "\n# Query completed: {} rows in {:?}",
702            final_view.row_count(),
703            query_time
704        );
705    }
706
707    output_result
708}
709
710/// Execute a script file with multiple SQL statements separated by GO
711pub fn execute_script(config: NonInteractiveConfig) -> Result<()> {
712    let _start_time = Instant::now();
713
714    // Parse the script into individual statements with directives
715    let parser = ScriptParser::new(&config.query);
716    let script_statements = parser.parse_script_statements();
717
718    if script_statements.is_empty() {
719        anyhow::bail!("No statements found in script");
720    }
721
722    info!("Found {} statements in script", script_statements.len());
723
724    // Determine data file to use (command-line overrides script hint)
725    let data_file = if !config.data_file.is_empty() {
726        // Command-line argument takes precedence
727        config.data_file.clone()
728    } else if let Some(hint) = parser.data_file_hint() {
729        // Use data file hint from script
730        info!("Using data file from script hint: {}", hint);
731
732        // Resolve relative paths relative to script file if provided
733        if let Some(script_path) = config.script_file.as_ref() {
734            let script_dir = std::path::Path::new(script_path)
735                .parent()
736                .unwrap_or(std::path::Path::new("."));
737            let hint_path = std::path::Path::new(hint);
738
739            if hint_path.is_relative() {
740                script_dir.join(hint_path).to_string_lossy().to_string()
741            } else {
742                hint.to_string()
743            }
744        } else {
745            hint.to_string()
746        }
747    } else {
748        String::new()
749    };
750
751    // Load the data file if provided, otherwise use DUAL
752    let (data_table, _is_dual) = if data_file.is_empty() {
753        // No data file provided, use DUAL table
754        info!("No data file provided, using DUAL table");
755        (DataTable::dual(), true)
756    } else {
757        // Check if file exists before trying to load
758        if !std::path::Path::new(&data_file).exists() {
759            anyhow::bail!(
760                "Data file not found: {}\n\
761                Please check the path is correct",
762                data_file
763            );
764        }
765
766        info!("Loading data from: {}", data_file);
767        let table = load_data_file(&data_file, config.delimiter_override)?;
768        info!(
769            "Loaded {} rows with {} columns",
770            table.row_count(),
771            table.column_count()
772        );
773        (table, false)
774    };
775
776    // Track script results
777    let mut script_result = ScriptResult::new();
778    let mut output = Vec::new();
779
780    // Phase 1: Create unified execution context (replaces TempTableRegistry)
781    use crate::execution::{ExecutionConfig, ExecutionContext, StatementExecutor};
782    let mut context = ExecutionContext::new(std::sync::Arc::new(data_table));
783
784    // Phase 1: Create unified execution config from CLI flags
785    let exec_config = ExecutionConfig::from_cli_flags(
786        config.show_preprocessing,
787        config.show_transformations,
788        config.case_insensitive,
789        config.auto_hide_empty,
790        config.no_expression_lifter,
791        config.no_where_expansion,
792        config.no_group_by_expansion,
793        config.no_having_expansion,
794        config.no_order_by_expansion,
795        config.no_qualify_to_where,
796        config.no_cte_hoister,
797        config.no_in_lifter,
798        false, // debug_trace - not used in script mode
799    );
800
801    // Phase 1: Create unified statement executor
802    let executor = StatementExecutor::with_config(exec_config);
803
804    // Execute each statement
805    for (idx, script_stmt) in script_statements.iter().enumerate() {
806        let statement_num = idx + 1;
807        let stmt_start = Instant::now();
808
809        // Check if this is an EXIT statement
810        if script_stmt.is_exit() {
811            let exit_code = script_stmt.get_exit_code().unwrap_or(0);
812            info!("EXIT statement encountered (code: {})", exit_code);
813
814            // Print message for table format
815            if matches!(config.output_format, OutputFormat::Table) {
816                if idx > 0 {
817                    output.push(String::new());
818                }
819                output.push(format!("-- Statement {} --", statement_num));
820                output.push(format!("Script execution stopped by EXIT {}", exit_code));
821            }
822
823            // Mark as success in results
824            script_result.add_success(
825                statement_num,
826                format!("EXIT {}", exit_code),
827                0,
828                stmt_start.elapsed().as_secs_f64() * 1000.0,
829            );
830
831            // Stop execution
832            break;
833        }
834
835        // Check if this statement should be skipped
836        if script_stmt.should_skip() {
837            info!(
838                "Skipping statement {} due to [SKIP] directive",
839                statement_num
840            );
841
842            // Print message for table format
843            if matches!(config.output_format, OutputFormat::Table) {
844                if idx > 0 {
845                    output.push(String::new());
846                }
847                output.push(format!("-- Statement {} [SKIPPED] --", statement_num));
848            }
849
850            // Mark as success but with 0 rows
851            script_result.add_success(
852                statement_num,
853                "[SKIPPED]".to_string(),
854                0,
855                stmt_start.elapsed().as_secs_f64() * 1000.0,
856            );
857
858            continue;
859        }
860
861        // Get the SQL query
862        let statement = match script_stmt.get_query() {
863            Some(sql) => sql,
864            None => continue, // Should not happen
865        };
866
867        // Print separator for table format
868        if matches!(config.output_format, OutputFormat::Table) {
869            if idx > 0 {
870                output.push(String::new()); // Empty line between queries
871            }
872            output.push(format!("-- Query {} --", statement_num));
873        }
874
875        // Step 1: Expand templates in the SQL string FIRST (before parsing)
876        // Phase 1: Use context.temp_tables instead of temp_tables directly
877        use crate::sql::template_expander::TemplateExpander;
878        let expander = TemplateExpander::new(&context.temp_tables);
879
880        let expanded_statement = match expander.parse_templates(statement) {
881            Ok(vars) => {
882                if vars.is_empty() {
883                    statement.to_string()
884                } else {
885                    match expander.expand(statement, &vars) {
886                        Ok(expanded) => {
887                            debug!("Expanded templates in SQL: {} vars found", vars.len());
888                            for var in &vars {
889                                debug!(
890                                    "  {} -> expanding from {}",
891                                    var.placeholder, var.table_name
892                                );
893                            }
894                            expanded
895                        }
896                        Err(e) => {
897                            let msg =
898                                format!("Query {} template expansion error: {}", statement_num, e);
899                            if matches!(config.output_format, OutputFormat::Table) {
900                                output.push(msg.clone());
901                            } else {
902                                eprintln!("{}", msg);
903                            }
904                            script_result.add_failure(
905                                statement_num,
906                                statement.to_string(),
907                                msg,
908                                stmt_start.elapsed().as_secs_f64() * 1000.0,
909                            );
910                            continue; // Skip this statement
911                        }
912                    }
913                }
914            }
915            Err(e) => {
916                let msg = format!("Query {} template parse error: {}", statement_num, e);
917                if matches!(config.output_format, OutputFormat::Table) {
918                    output.push(msg.clone());
919                } else {
920                    eprintln!("{}", msg);
921                }
922                script_result.add_failure(
923                    statement_num,
924                    statement.to_string(),
925                    msg,
926                    stmt_start.elapsed().as_secs_f64() * 1000.0,
927                );
928                continue; // Skip this statement
929            }
930        };
931
932        // Use the expanded statement for the rest of the processing
933        let statement = expanded_statement.as_str();
934
935        // Step 2: Parse the (possibly expanded) statement
936        let mut parser = Parser::new(statement);
937        let parsed_stmt = match parser.parse() {
938            Ok(stmt) => stmt,
939            Err(e) => {
940                // If parsing fails, record error and stop (scripts stop on first error)
941                let msg = format!("Query {} parse error: {}", statement_num, e);
942                if matches!(config.output_format, OutputFormat::Table) {
943                    output.push(msg.clone());
944                } else {
945                    eprintln!("{}", msg);
946                }
947                script_result.add_failure(
948                    statement_num,
949                    statement.to_string(),
950                    msg,
951                    stmt_start.elapsed().as_secs_f64() * 1000.0,
952                );
953                break;
954            }
955        };
956
957        // Phase 1: Check if temp table is referenced (for better error message)
958        // The executor will handle resolution, but we check here for early validation
959        if let Some(from_table) = &parsed_stmt.from_table {
960            if from_table.starts_with('#') && !context.has_temp_table(from_table) {
961                let msg = format!(
962                    "Query {} failed: Temporary table {} not found",
963                    statement_num, from_table
964                );
965                if matches!(config.output_format, OutputFormat::Table) {
966                    output.push(msg.clone());
967                } else {
968                    eprintln!("{}", msg);
969                }
970                script_result.add_failure(
971                    statement_num,
972                    statement.to_string(),
973                    msg,
974                    stmt_start.elapsed().as_secs_f64() * 1000.0,
975                );
976                break;
977            }
978        }
979
980        // Phase 1: Remove INTO clause before execution (executor doesn't handle INTO syntax)
981        // We'll handle it ourselves after execution
982        let into_table = parsed_stmt.into_table.clone();
983        let stmt_without_into = if into_table.is_some() {
984            use crate::query_plan::IntoClauseRemover;
985            IntoClauseRemover::remove_into_clause(parsed_stmt)
986        } else {
987            parsed_stmt
988        };
989
990        // Phase 1: Execute using unified StatementExecutor
991        // This handles:
992        // - Table resolution (temp tables, base table, DUAL)
993        // - Preprocessing pipeline (alias expansion, transformers)
994        // - Direct AST execution (no re-parsing!)
995        let result = executor.execute(stmt_without_into, &mut context);
996
997        match result {
998            Ok(exec_result) => {
999                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
1000                let final_view = exec_result.dataview;
1001
1002                // Phase 1: Check if this is an INTO statement - store result in temp table
1003                if let Some(into_table) = &into_table {
1004                    // Get the source table from the DataView - this contains the query result
1005                    let result_table = final_view.source_arc();
1006                    let row_count = result_table.row_count();
1007
1008                    // Phase 1: Use context.store_temp_table() instead of temp_tables.insert()
1009                    match context.store_temp_table(into_table.name.clone(), result_table) {
1010                        Ok(_) => {
1011                            info!(
1012                                "Stored {} rows in temporary table {}",
1013                                row_count, into_table.name
1014                            );
1015
1016                            // For INTO statements, output a confirmation message only for table format
1017                            // CSV/TSV/JSON formats should not include this message as it pollutes machine-readable output
1018                            if matches!(config.output_format, OutputFormat::Table) {
1019                                let mut statement_output = Vec::new();
1020                                writeln!(
1021                                    &mut statement_output,
1022                                    "({} rows affected) -> {}",
1023                                    row_count, into_table.name
1024                                )?;
1025                                output.extend(
1026                                    String::from_utf8_lossy(&statement_output)
1027                                        .lines()
1028                                        .map(String::from),
1029                                );
1030                            }
1031
1032                            script_result.add_success(
1033                                statement_num,
1034                                statement.to_string(),
1035                                row_count,
1036                                exec_time,
1037                            );
1038                            continue; // Skip normal output formatting for INTO statements
1039                        }
1040                        Err(e) => {
1041                            script_result.add_failure(
1042                                statement_num,
1043                                statement.to_string(),
1044                                e.to_string(),
1045                                exec_time,
1046                            );
1047                            break;
1048                        }
1049                    }
1050                }
1051
1052                // Format the output based on the output format (normal query without INTO)
1053                let mut statement_output = Vec::new();
1054                match config.output_format {
1055                    OutputFormat::Csv => {
1056                        output_csv(&final_view, &mut statement_output, ',')?;
1057                    }
1058                    OutputFormat::Json => {
1059                        output_json(&final_view, &mut statement_output)?;
1060                    }
1061                    OutputFormat::JsonStructured => {
1062                        output_json_structured(&final_view, &mut statement_output, exec_time)?;
1063                    }
1064                    OutputFormat::Table => {
1065                        output_table(
1066                            &final_view,
1067                            &mut statement_output,
1068                            config.max_col_width,
1069                            config.col_sample_rows,
1070                            config.table_style,
1071                            config.styled,
1072                            config.style_file.as_deref(),
1073                        )?;
1074                        writeln!(
1075                            &mut statement_output,
1076                            "Query completed: {} rows in {:.2}ms",
1077                            final_view.row_count(),
1078                            exec_time
1079                        )?;
1080                    }
1081                    OutputFormat::Tsv => {
1082                        output_csv(&final_view, &mut statement_output, '\t')?;
1083                    }
1084                }
1085
1086                // Add to overall output
1087                output.extend(
1088                    String::from_utf8_lossy(&statement_output)
1089                        .lines()
1090                        .map(String::from),
1091                );
1092
1093                script_result.add_success(
1094                    statement_num,
1095                    statement.to_string(),
1096                    final_view.row_count(),
1097                    exec_time,
1098                );
1099            }
1100            Err(e) => {
1101                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
1102                let error_msg = format!("Query {} failed: {:#}", statement_num, e);
1103
1104                // Table mode embeds the error in the output stream (alongside the
1105                // result tables). CSV/JSON/TSV go to stderr instead so the parseable
1106                // output on stdout stays clean.
1107                if matches!(config.output_format, OutputFormat::Table) {
1108                    output.push(error_msg.clone());
1109                } else {
1110                    eprintln!("{}", error_msg);
1111                }
1112
1113                script_result.add_failure(
1114                    statement_num,
1115                    statement.to_string(),
1116                    e.to_string(),
1117                    exec_time,
1118                );
1119
1120                // Continue to next statement (don't stop on error)
1121            }
1122        }
1123    }
1124
1125    // Write output
1126    if let Some(ref output_file) = config.output_file {
1127        let mut file = fs::File::create(output_file)?;
1128        for line in &output {
1129            writeln!(file, "{}", line)?;
1130        }
1131        info!("Results written to: {}", output_file);
1132    } else {
1133        for line in &output {
1134            println!("{}", line);
1135        }
1136    }
1137
1138    // Print summary if in table mode
1139    if matches!(config.output_format, OutputFormat::Table) {
1140        println!("\n=== Script Summary ===");
1141        println!("Total statements: {}", script_result.total_statements);
1142        println!("Successful: {}", script_result.successful_statements);
1143        println!("Failed: {}", script_result.failed_statements);
1144        println!(
1145            "Total execution time: {:.2}ms",
1146            script_result.total_execution_time_ms
1147        );
1148    }
1149
1150    if !script_result.all_successful() {
1151        return Err(anyhow::anyhow!(
1152            "{} of {} statements failed",
1153            script_result.failed_statements,
1154            script_result.total_statements
1155        ));
1156    }
1157
1158    Ok(())
1159}
1160
1161/// Load a data file (CSV or JSON) into a `DataTable`.
1162///
1163/// `delimiter_override` (typically from `--delimiter`) wins over extension
1164/// auto-detect. `None` falls back to `.tsv` → tab / `.psv` → pipe / else comma.
1165fn load_data_file(path: &str, delimiter_override: Option<u8>) -> Result<DataTable> {
1166    use crate::data::datatable_loaders::load_csv_to_datatable_with_opts;
1167    use crate::data::stream_loader::{resolve_delimiter, CsvReadOptions};
1168
1169    let path = Path::new(path);
1170
1171    if !path.exists() {
1172        return Err(anyhow::anyhow!("File not found: {}", path.display()));
1173    }
1174
1175    // Determine file type by extension
1176    let extension = path
1177        .extension()
1178        .and_then(|ext| ext.to_str())
1179        .map(str::to_lowercase)
1180        .unwrap_or_default();
1181
1182    let table_name = path
1183        .file_stem()
1184        .and_then(|stem| stem.to_str())
1185        .unwrap_or("data")
1186        .to_string();
1187
1188    // .csv/.tsv/.psv share the CSV loader. So does any other extension when
1189    // an explicit --delimiter override is supplied — the user has told us
1190    // it's a delimited file, so we trust them.
1191    let is_csv_family =
1192        matches!(extension.as_str(), "csv" | "tsv" | "psv") || delimiter_override.is_some();
1193    if is_csv_family {
1194        let path_str = path.display().to_string();
1195        let opts = CsvReadOptions {
1196            delimiter: resolve_delimiter(&path_str, delimiter_override),
1197            has_headers: true,
1198        };
1199        return load_csv_to_datatable_with_opts(path, &table_name, &opts)
1200            .with_context(|| format!("Failed to load CSV-family file: {}", path.display()));
1201    }
1202
1203    match extension.as_str() {
1204        "json" | "jsonl" | "ndjson" => load_json_to_datatable(path, &table_name)
1205            .with_context(|| format!("Failed to load JSON file: {}", path.display())),
1206        _ => Err(anyhow::anyhow!(
1207            "Unsupported file type: {}. Use .csv, .tsv, .psv, .json, .jsonl, or .ndjson \
1208             (or pass --delimiter to force CSV parsing on an unknown extension)",
1209            extension
1210        )),
1211    }
1212}
1213
1214/// Limit the number of rows in results
1215fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
1216    let source = dataview.source();
1217    let mut limited_table = DataTable::new(&source.name);
1218
1219    // Copy columns
1220    for col in &source.columns {
1221        limited_table.add_column(col.clone());
1222    }
1223
1224    // Copy limited rows
1225    let rows_to_copy = dataview.row_count().min(limit);
1226    for i in 0..rows_to_copy {
1227        if let Some(row) = dataview.get_row(i) {
1228            let _ = limited_table.add_row(row.clone());
1229        }
1230    }
1231
1232    Ok(limited_table)
1233}
1234
1235/// Output query results in the specified format
1236fn output_results<W: Write>(
1237    dataview: &DataView,
1238    format: OutputFormat,
1239    writer: &mut W,
1240    max_col_width: Option<usize>,
1241    col_sample_rows: usize,
1242    exec_time_ms: f64,
1243    table_style: TableStyle,
1244    styled: bool,
1245    style_file: Option<&str>,
1246) -> Result<()> {
1247    match format {
1248        OutputFormat::Csv => output_csv(dataview, writer, ','),
1249        OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
1250        OutputFormat::Json => output_json(dataview, writer),
1251        OutputFormat::JsonStructured => output_json_structured(dataview, writer, exec_time_ms),
1252        OutputFormat::Table => output_table(
1253            dataview,
1254            writer,
1255            max_col_width,
1256            col_sample_rows,
1257            table_style,
1258            styled,
1259            style_file,
1260        ),
1261    }
1262}
1263
1264/// Output results as CSV/TSV
1265fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
1266    // Write headers
1267    let columns = dataview.column_names();
1268    for (i, col) in columns.iter().enumerate() {
1269        if i > 0 {
1270            write!(writer, "{delimiter}")?;
1271        }
1272        write!(writer, "{}", escape_csv_field(col, delimiter))?;
1273    }
1274    writeln!(writer)?;
1275
1276    // Write rows
1277    for row_idx in 0..dataview.row_count() {
1278        if let Some(row) = dataview.get_row(row_idx) {
1279            for (i, value) in row.values.iter().enumerate() {
1280                if i > 0 {
1281                    write!(writer, "{delimiter}")?;
1282                }
1283                write!(
1284                    writer,
1285                    "{}",
1286                    escape_csv_field(&format_value(value), delimiter)
1287                )?;
1288            }
1289            writeln!(writer)?;
1290        }
1291    }
1292
1293    Ok(())
1294}
1295
1296/// Output results as JSON
1297fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
1298    let columns = dataview.column_names();
1299    let mut rows = Vec::new();
1300
1301    for row_idx in 0..dataview.row_count() {
1302        if let Some(row) = dataview.get_row(row_idx) {
1303            let mut json_row = serde_json::Map::new();
1304            for (col_idx, value) in row.values.iter().enumerate() {
1305                if col_idx < columns.len() {
1306                    json_row.insert(columns[col_idx].clone(), value_to_json(value));
1307                }
1308            }
1309            rows.push(serde_json::Value::Object(json_row));
1310        }
1311    }
1312
1313    let json = serde_json::to_string_pretty(&rows)?;
1314    writeln!(writer, "{json}")?;
1315
1316    Ok(())
1317}
1318
1319/// Output results as structured JSON with metadata for IDE/plugin integration
1320fn output_json_structured<W: Write>(
1321    dataview: &DataView,
1322    writer: &mut W,
1323    exec_time: f64,
1324) -> Result<()> {
1325    let column_names = dataview.column_names();
1326    let data_table = dataview.source();
1327
1328    // Build column metadata
1329    let mut columns = Vec::new();
1330    for (idx, name) in column_names.iter().enumerate() {
1331        let col_type = data_table
1332            .columns
1333            .get(idx)
1334            .map(|c| format!("{:?}", c.data_type))
1335            .unwrap_or_else(|| "UNKNOWN".to_string());
1336
1337        // Calculate max width for this column
1338        let mut max_width = name.len();
1339        for row_idx in 0..dataview.row_count() {
1340            if let Some(row) = dataview.get_row(row_idx) {
1341                if let Some(value) = row.values.get(idx) {
1342                    let display_width = match value {
1343                        DataValue::Null => 4, // "NULL"
1344                        DataValue::Integer(i) => i.to_string().len(),
1345                        DataValue::Float(f) => format!("{:.2}", f).len(),
1346                        DataValue::String(s) => s.len(),
1347                        DataValue::InternedString(s) => s.len(),
1348                        DataValue::Boolean(b) => {
1349                            if *b {
1350                                4
1351                            } else {
1352                                5
1353                            }
1354                        } // "true" or "false"
1355                        DataValue::DateTime(dt) => dt.len(),
1356                        DataValue::Vector(v) => {
1357                            let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1358                            format!("[{}]", components.join(",")).len()
1359                        }
1360                    };
1361                    max_width = max_width.max(display_width);
1362                }
1363            }
1364        }
1365
1366        let alignment = match data_table.columns.get(idx).map(|c| &c.data_type) {
1367            Some(crate::data::datatable::DataType::Integer) => "right",
1368            Some(crate::data::datatable::DataType::Float) => "right",
1369            _ => "left",
1370        };
1371
1372        let col_meta = serde_json::json!({
1373            "name": name,
1374            "type": col_type,
1375            "max_width": max_width,
1376            "alignment": alignment
1377        });
1378        columns.push(col_meta);
1379    }
1380
1381    // Build rows as arrays of strings
1382    let mut rows = Vec::new();
1383    for row_idx in 0..dataview.row_count() {
1384        if let Some(row) = dataview.get_row(row_idx) {
1385            let row_values: Vec<String> = row
1386                .values
1387                .iter()
1388                .map(|v| match v {
1389                    DataValue::Null => String::new(),
1390                    DataValue::Integer(i) => i.to_string(),
1391                    DataValue::Float(f) => format!("{:.2}", f),
1392                    DataValue::String(s) => s.clone(),
1393                    DataValue::InternedString(s) => s.to_string(),
1394                    DataValue::Boolean(b) => b.to_string(),
1395                    DataValue::DateTime(dt) => dt.clone(),
1396                    DataValue::Vector(v) => {
1397                        let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1398                        format!("[{}]", components.join(","))
1399                    }
1400                })
1401                .collect();
1402            rows.push(serde_json::Value::Array(
1403                row_values
1404                    .into_iter()
1405                    .map(serde_json::Value::String)
1406                    .collect(),
1407            ));
1408        }
1409    }
1410
1411    // Build complete structured output
1412    let output = serde_json::json!({
1413        "columns": columns,
1414        "rows": rows,
1415        "metadata": {
1416            "total_rows": dataview.row_count(),
1417            "query_time_ms": exec_time
1418        }
1419    });
1420
1421    let json = serde_json::to_string_pretty(&output)?;
1422    writeln!(writer, "{json}")?;
1423
1424    Ok(())
1425}
1426
1427/// Output results using the old custom ASCII table format (for Nvim compatibility)
1428fn output_table_old_style<W: Write>(
1429    dataview: &DataView,
1430    writer: &mut W,
1431    max_col_width: Option<usize>,
1432) -> Result<()> {
1433    let columns = dataview.column_names();
1434
1435    // Calculate column widths
1436    let mut widths = vec![0; columns.len()];
1437    for (i, col) in columns.iter().enumerate() {
1438        widths[i] = col.len();
1439    }
1440
1441    // Scan all rows for width calculation
1442    for row_idx in 0..dataview.row_count() {
1443        if let Some(row) = dataview.get_row(row_idx) {
1444            for (i, value) in row.values.iter().enumerate() {
1445                if i < widths.len() {
1446                    let value_str = format_value(value);
1447                    widths[i] = widths[i].max(display_width(&value_str));
1448                }
1449            }
1450        }
1451    }
1452
1453    // Apply maximum column width if specified
1454    if let Some(max_width) = max_col_width {
1455        for width in &mut widths {
1456            *width = (*width).min(max_width);
1457        }
1458    }
1459
1460    // Print top border
1461    write!(writer, "+")?;
1462    for width in &widths {
1463        write!(writer, "{}", "-".repeat(*width + 2))?;
1464        write!(writer, "+")?;
1465    }
1466    writeln!(writer)?;
1467
1468    // Print headers
1469    write!(writer, "|")?;
1470    for (i, col) in columns.iter().enumerate() {
1471        write!(writer, " {:^width$} |", col, width = widths[i])?;
1472    }
1473    writeln!(writer)?;
1474
1475    // Print header separator
1476    write!(writer, "+")?;
1477    for width in &widths {
1478        write!(writer, "{}", "-".repeat(*width + 2))?;
1479        write!(writer, "+")?;
1480    }
1481    writeln!(writer)?;
1482
1483    // Print data rows (no separators between rows)
1484    for row_idx in 0..dataview.row_count() {
1485        if let Some(row) = dataview.get_row(row_idx) {
1486            write!(writer, "|")?;
1487            for (i, value) in row.values.iter().enumerate() {
1488                if i < widths.len() {
1489                    let value_str = format_value(value);
1490                    let display_len = display_width(&value_str);
1491
1492                    // For ANSI-colored strings, manual padding is needed
1493                    // because format! uses byte length, not display width
1494                    write!(writer, " {}", value_str)?;
1495                    let padding_needed = if display_len < widths[i] {
1496                        widths[i] - display_len
1497                    } else {
1498                        0
1499                    };
1500                    write!(writer, "{} |", " ".repeat(padding_needed))?;
1501                }
1502            }
1503            writeln!(writer)?;
1504        }
1505    }
1506
1507    // Print bottom border
1508    write!(writer, "+")?;
1509    for width in &widths {
1510        write!(writer, "{}", "-".repeat(*width + 2))?;
1511        write!(writer, "+")?;
1512    }
1513    writeln!(writer)?;
1514
1515    Ok(())
1516}
1517
1518/// Output results as a table using comfy-table with styling
1519fn output_table<W: Write>(
1520    dataview: &DataView,
1521    writer: &mut W,
1522    max_col_width: Option<usize>,
1523    _col_sample_rows: usize, // Not needed with comfy-table
1524    style: TableStyle,
1525    styled: bool,
1526    style_file: Option<&str>,
1527) -> Result<()> {
1528    let mut table = Table::new();
1529
1530    // Apply the selected style preset
1531    match style {
1532        TableStyle::Default => {
1533            // Use custom old-style renderer for Nvim compatibility
1534            // This matches what the table navigation parser expects
1535            return output_table_old_style(dataview, writer, max_col_width);
1536        }
1537        TableStyle::AsciiFull => {
1538            table.load_preset(ASCII_FULL);
1539        }
1540        TableStyle::AsciiCondensed => {
1541            table.load_preset(ASCII_FULL_CONDENSED);
1542        }
1543        TableStyle::AsciiBordersOnly => {
1544            table.load_preset(ASCII_BORDERS_ONLY);
1545        }
1546        TableStyle::AsciiHorizontalOnly => {
1547            table.load_preset(ASCII_HORIZONTAL_ONLY);
1548        }
1549        TableStyle::AsciiNoBorders => {
1550            table.load_preset(ASCII_NO_BORDERS);
1551        }
1552        TableStyle::Markdown => {
1553            table.load_preset(ASCII_MARKDOWN);
1554        }
1555        TableStyle::Utf8Full => {
1556            table.load_preset(UTF8_FULL);
1557        }
1558        TableStyle::Utf8Condensed => {
1559            table.load_preset(UTF8_FULL_CONDENSED);
1560        }
1561        TableStyle::Utf8BordersOnly => {
1562            table.load_preset(UTF8_BORDERS_ONLY);
1563        }
1564        TableStyle::Utf8HorizontalOnly => {
1565            table.load_preset(UTF8_HORIZONTAL_ONLY);
1566        }
1567        TableStyle::Utf8NoBorders => {
1568            table.load_preset(UTF8_NO_BORDERS);
1569        }
1570        TableStyle::Plain => {
1571            table.load_preset(NOTHING);
1572        }
1573    }
1574
1575    // Set content arrangement (automatic width adjustment)
1576    if max_col_width.is_some() {
1577        table.set_content_arrangement(ContentArrangement::Dynamic);
1578    }
1579
1580    // Set column headers
1581    let columns = dataview.column_names();
1582
1583    // Apply color styling if requested
1584    if styled {
1585        use crate::output::styled_table::{apply_styles_to_table, StyleConfig};
1586        use std::path::PathBuf;
1587
1588        // Load style configuration
1589        let style_config = if let Some(file_path) = style_file {
1590            let path = PathBuf::from(file_path);
1591            StyleConfig::from_file(&path).ok()
1592        } else {
1593            StyleConfig::load_default()
1594        };
1595
1596        if let Some(config) = style_config {
1597            // Convert DataView rows to Vec<Vec<String>> for styling
1598            let rows: Vec<Vec<String>> = (0..dataview.row_count())
1599                .filter_map(|i| {
1600                    dataview.get_row(i).map(|row| {
1601                        row.values
1602                            .iter()
1603                            .map(|v| {
1604                                let s = format_value(v);
1605                                // Apply max width truncation if specified
1606                                if let Some(max_width) = max_col_width {
1607                                    if s.len() > max_width {
1608                                        format!("{}...", &s[..max_width.saturating_sub(3)])
1609                                    } else {
1610                                        s
1611                                    }
1612                                } else {
1613                                    s
1614                                }
1615                            })
1616                            .collect()
1617                    })
1618                })
1619                .collect();
1620
1621            if let Err(e) = apply_styles_to_table(&mut table, &columns, &rows, &config) {
1622                eprintln!("Warning: Failed to apply styles: {}", e);
1623            }
1624        }
1625    } else {
1626        // No styling - add headers and rows normally
1627        table.set_header(&columns);
1628
1629        // Add data rows
1630        for row_idx in 0..dataview.row_count() {
1631            if let Some(row) = dataview.get_row(row_idx) {
1632                let row_strings: Vec<String> = row
1633                    .values
1634                    .iter()
1635                    .map(|v| {
1636                        let s = format_value(v);
1637                        // Apply max width truncation if specified
1638                        if let Some(max_width) = max_col_width {
1639                            if s.len() > max_width {
1640                                format!("{}...", &s[..max_width.saturating_sub(3)])
1641                            } else {
1642                                s
1643                            }
1644                        } else {
1645                            s
1646                        }
1647                    })
1648                    .collect();
1649                table.add_row(row_strings);
1650            }
1651        }
1652    }
1653
1654    // Write the table to the writer
1655    writeln!(writer, "{}", table)?;
1656
1657    Ok(())
1658}
1659
1660/// Format a `DataValue` for display
1661fn format_value(value: &DataValue) -> String {
1662    match value {
1663        DataValue::Null => String::new(),
1664        DataValue::Integer(i) => i.to_string(),
1665        DataValue::Float(f) => f.to_string(),
1666        DataValue::String(s) => s.clone(),
1667        DataValue::InternedString(s) => s.to_string(),
1668        DataValue::Boolean(b) => b.to_string(),
1669        DataValue::DateTime(dt) => dt.to_string(),
1670        DataValue::Vector(v) => {
1671            let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1672            format!("[{}]", components.join(","))
1673        }
1674    }
1675}
1676
1677/// Convert `DataValue` to JSON
1678fn value_to_json(value: &DataValue) -> serde_json::Value {
1679    match value {
1680        DataValue::Null => serde_json::Value::Null,
1681        DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
1682        DataValue::Float(f) => {
1683            if let Some(n) = serde_json::Number::from_f64(*f) {
1684                serde_json::Value::Number(n)
1685            } else {
1686                serde_json::Value::Null
1687            }
1688        }
1689        DataValue::String(s) => serde_json::Value::String(s.clone()),
1690        DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
1691        DataValue::Boolean(b) => serde_json::Value::Bool(*b),
1692        DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
1693        DataValue::Vector(v) => serde_json::Value::Array(
1694            v.iter()
1695                .map(|f| {
1696                    if let Some(n) = serde_json::Number::from_f64(*f) {
1697                        serde_json::Value::Number(n)
1698                    } else {
1699                        serde_json::Value::Null
1700                    }
1701                })
1702                .collect(),
1703        ),
1704    }
1705}
1706
1707/// Escape a CSV field if it contains special characters
1708fn escape_csv_field(field: &str, delimiter: char) -> String {
1709    if field.contains(delimiter)
1710        || field.contains('"')
1711        || field.contains('\n')
1712        || field.contains('\r')
1713    {
1714        format!("\"{}\"", field.replace('"', "\"\""))
1715    } else {
1716        field.to_string()
1717    }
1718}