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                            script_result.add_failure(
898                                statement_num,
899                                statement.to_string(),
900                                format!("Template expansion error: {}", e),
901                                stmt_start.elapsed().as_secs_f64() * 1000.0,
902                            );
903                            continue; // Skip this statement
904                        }
905                    }
906                }
907            }
908            Err(e) => {
909                script_result.add_failure(
910                    statement_num,
911                    statement.to_string(),
912                    format!("Template parse error: {}", e),
913                    stmt_start.elapsed().as_secs_f64() * 1000.0,
914                );
915                continue; // Skip this statement
916            }
917        };
918
919        // Use the expanded statement for the rest of the processing
920        let statement = expanded_statement.as_str();
921
922        // Step 2: Parse the (possibly expanded) statement
923        let mut parser = Parser::new(statement);
924        let parsed_stmt = match parser.parse() {
925            Ok(stmt) => stmt,
926            Err(e) => {
927                // If parsing fails, record error and stop (scripts stop on first error)
928                script_result.add_failure(
929                    statement_num,
930                    statement.to_string(),
931                    format!("Parse error: {}", e),
932                    stmt_start.elapsed().as_secs_f64() * 1000.0,
933                );
934                break;
935            }
936        };
937
938        // Phase 1: Check if temp table is referenced (for better error message)
939        // The executor will handle resolution, but we check here for early validation
940        if let Some(from_table) = &parsed_stmt.from_table {
941            if from_table.starts_with('#') && !context.has_temp_table(from_table) {
942                script_result.add_failure(
943                    statement_num,
944                    statement.to_string(),
945                    format!("Temporary table {} not found", from_table),
946                    stmt_start.elapsed().as_secs_f64() * 1000.0,
947                );
948                break;
949            }
950        }
951
952        // Phase 1: Remove INTO clause before execution (executor doesn't handle INTO syntax)
953        // We'll handle it ourselves after execution
954        let into_table = parsed_stmt.into_table.clone();
955        let stmt_without_into = if into_table.is_some() {
956            use crate::query_plan::IntoClauseRemover;
957            IntoClauseRemover::remove_into_clause(parsed_stmt)
958        } else {
959            parsed_stmt
960        };
961
962        // Phase 1: Execute using unified StatementExecutor
963        // This handles:
964        // - Table resolution (temp tables, base table, DUAL)
965        // - Preprocessing pipeline (alias expansion, transformers)
966        // - Direct AST execution (no re-parsing!)
967        let result = executor.execute(stmt_without_into, &mut context);
968
969        match result {
970            Ok(exec_result) => {
971                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
972                let final_view = exec_result.dataview;
973
974                // Phase 1: Check if this is an INTO statement - store result in temp table
975                if let Some(into_table) = &into_table {
976                    // Get the source table from the DataView - this contains the query result
977                    let result_table = final_view.source_arc();
978                    let row_count = result_table.row_count();
979
980                    // Phase 1: Use context.store_temp_table() instead of temp_tables.insert()
981                    match context.store_temp_table(into_table.name.clone(), result_table) {
982                        Ok(_) => {
983                            info!(
984                                "Stored {} rows in temporary table {}",
985                                row_count, into_table.name
986                            );
987
988                            // For INTO statements, output a confirmation message only for table format
989                            // CSV/TSV/JSON formats should not include this message as it pollutes machine-readable output
990                            if matches!(config.output_format, OutputFormat::Table) {
991                                let mut statement_output = Vec::new();
992                                writeln!(
993                                    &mut statement_output,
994                                    "({} rows affected) -> {}",
995                                    row_count, into_table.name
996                                )?;
997                                output.extend(
998                                    String::from_utf8_lossy(&statement_output)
999                                        .lines()
1000                                        .map(String::from),
1001                                );
1002                            }
1003
1004                            script_result.add_success(
1005                                statement_num,
1006                                statement.to_string(),
1007                                row_count,
1008                                exec_time,
1009                            );
1010                            continue; // Skip normal output formatting for INTO statements
1011                        }
1012                        Err(e) => {
1013                            script_result.add_failure(
1014                                statement_num,
1015                                statement.to_string(),
1016                                e.to_string(),
1017                                exec_time,
1018                            );
1019                            break;
1020                        }
1021                    }
1022                }
1023
1024                // Format the output based on the output format (normal query without INTO)
1025                let mut statement_output = Vec::new();
1026                match config.output_format {
1027                    OutputFormat::Csv => {
1028                        output_csv(&final_view, &mut statement_output, ',')?;
1029                    }
1030                    OutputFormat::Json => {
1031                        output_json(&final_view, &mut statement_output)?;
1032                    }
1033                    OutputFormat::JsonStructured => {
1034                        output_json_structured(&final_view, &mut statement_output, exec_time)?;
1035                    }
1036                    OutputFormat::Table => {
1037                        output_table(
1038                            &final_view,
1039                            &mut statement_output,
1040                            config.max_col_width,
1041                            config.col_sample_rows,
1042                            config.table_style,
1043                            config.styled,
1044                            config.style_file.as_deref(),
1045                        )?;
1046                        writeln!(
1047                            &mut statement_output,
1048                            "Query completed: {} rows in {:.2}ms",
1049                            final_view.row_count(),
1050                            exec_time
1051                        )?;
1052                    }
1053                    OutputFormat::Tsv => {
1054                        output_csv(&final_view, &mut statement_output, '\t')?;
1055                    }
1056                }
1057
1058                // Add to overall output
1059                output.extend(
1060                    String::from_utf8_lossy(&statement_output)
1061                        .lines()
1062                        .map(String::from),
1063                );
1064
1065                script_result.add_success(
1066                    statement_num,
1067                    statement.to_string(),
1068                    final_view.row_count(),
1069                    exec_time,
1070                );
1071            }
1072            Err(e) => {
1073                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
1074                let error_msg = format!("Query {} failed: {}", statement_num, e);
1075
1076                if matches!(config.output_format, OutputFormat::Table) {
1077                    output.push(error_msg.clone());
1078                }
1079
1080                script_result.add_failure(
1081                    statement_num,
1082                    statement.to_string(),
1083                    e.to_string(),
1084                    exec_time,
1085                );
1086
1087                // Continue to next statement (don't stop on error)
1088            }
1089        }
1090    }
1091
1092    // Write output
1093    if let Some(ref output_file) = config.output_file {
1094        let mut file = fs::File::create(output_file)?;
1095        for line in &output {
1096            writeln!(file, "{}", line)?;
1097        }
1098        info!("Results written to: {}", output_file);
1099    } else {
1100        for line in &output {
1101            println!("{}", line);
1102        }
1103    }
1104
1105    // Print summary if in table mode
1106    if matches!(config.output_format, OutputFormat::Table) {
1107        println!("\n=== Script Summary ===");
1108        println!("Total statements: {}", script_result.total_statements);
1109        println!("Successful: {}", script_result.successful_statements);
1110        println!("Failed: {}", script_result.failed_statements);
1111        println!(
1112            "Total execution time: {:.2}ms",
1113            script_result.total_execution_time_ms
1114        );
1115    }
1116
1117    if !script_result.all_successful() {
1118        return Err(anyhow::anyhow!(
1119            "{} of {} statements failed",
1120            script_result.failed_statements,
1121            script_result.total_statements
1122        ));
1123    }
1124
1125    Ok(())
1126}
1127
1128/// Load a data file (CSV or JSON) into a `DataTable`.
1129///
1130/// `delimiter_override` (typically from `--delimiter`) wins over extension
1131/// auto-detect. `None` falls back to `.tsv` → tab / `.psv` → pipe / else comma.
1132fn load_data_file(path: &str, delimiter_override: Option<u8>) -> Result<DataTable> {
1133    use crate::data::datatable_loaders::load_csv_to_datatable_with_opts;
1134    use crate::data::stream_loader::{resolve_delimiter, CsvReadOptions};
1135
1136    let path = Path::new(path);
1137
1138    if !path.exists() {
1139        return Err(anyhow::anyhow!("File not found: {}", path.display()));
1140    }
1141
1142    // Determine file type by extension
1143    let extension = path
1144        .extension()
1145        .and_then(|ext| ext.to_str())
1146        .map(str::to_lowercase)
1147        .unwrap_or_default();
1148
1149    let table_name = path
1150        .file_stem()
1151        .and_then(|stem| stem.to_str())
1152        .unwrap_or("data")
1153        .to_string();
1154
1155    // .csv/.tsv/.psv share the CSV loader. So does any other extension when
1156    // an explicit --delimiter override is supplied — the user has told us
1157    // it's a delimited file, so we trust them.
1158    let is_csv_family =
1159        matches!(extension.as_str(), "csv" | "tsv" | "psv") || delimiter_override.is_some();
1160    if is_csv_family {
1161        let path_str = path.display().to_string();
1162        let opts = CsvReadOptions {
1163            delimiter: resolve_delimiter(&path_str, delimiter_override),
1164            has_headers: true,
1165        };
1166        return load_csv_to_datatable_with_opts(path, &table_name, &opts)
1167            .with_context(|| format!("Failed to load CSV-family file: {}", path.display()));
1168    }
1169
1170    match extension.as_str() {
1171        "json" | "jsonl" | "ndjson" => load_json_to_datatable(path, &table_name)
1172            .with_context(|| format!("Failed to load JSON file: {}", path.display())),
1173        _ => Err(anyhow::anyhow!(
1174            "Unsupported file type: {}. Use .csv, .tsv, .psv, .json, .jsonl, or .ndjson \
1175             (or pass --delimiter to force CSV parsing on an unknown extension)",
1176            extension
1177        )),
1178    }
1179}
1180
1181/// Limit the number of rows in results
1182fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
1183    let source = dataview.source();
1184    let mut limited_table = DataTable::new(&source.name);
1185
1186    // Copy columns
1187    for col in &source.columns {
1188        limited_table.add_column(col.clone());
1189    }
1190
1191    // Copy limited rows
1192    let rows_to_copy = dataview.row_count().min(limit);
1193    for i in 0..rows_to_copy {
1194        if let Some(row) = dataview.get_row(i) {
1195            let _ = limited_table.add_row(row.clone());
1196        }
1197    }
1198
1199    Ok(limited_table)
1200}
1201
1202/// Output query results in the specified format
1203fn output_results<W: Write>(
1204    dataview: &DataView,
1205    format: OutputFormat,
1206    writer: &mut W,
1207    max_col_width: Option<usize>,
1208    col_sample_rows: usize,
1209    exec_time_ms: f64,
1210    table_style: TableStyle,
1211    styled: bool,
1212    style_file: Option<&str>,
1213) -> Result<()> {
1214    match format {
1215        OutputFormat::Csv => output_csv(dataview, writer, ','),
1216        OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
1217        OutputFormat::Json => output_json(dataview, writer),
1218        OutputFormat::JsonStructured => output_json_structured(dataview, writer, exec_time_ms),
1219        OutputFormat::Table => output_table(
1220            dataview,
1221            writer,
1222            max_col_width,
1223            col_sample_rows,
1224            table_style,
1225            styled,
1226            style_file,
1227        ),
1228    }
1229}
1230
1231/// Output results as CSV/TSV
1232fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
1233    // Write headers
1234    let columns = dataview.column_names();
1235    for (i, col) in columns.iter().enumerate() {
1236        if i > 0 {
1237            write!(writer, "{delimiter}")?;
1238        }
1239        write!(writer, "{}", escape_csv_field(col, delimiter))?;
1240    }
1241    writeln!(writer)?;
1242
1243    // Write rows
1244    for row_idx in 0..dataview.row_count() {
1245        if let Some(row) = dataview.get_row(row_idx) {
1246            for (i, value) in row.values.iter().enumerate() {
1247                if i > 0 {
1248                    write!(writer, "{delimiter}")?;
1249                }
1250                write!(
1251                    writer,
1252                    "{}",
1253                    escape_csv_field(&format_value(value), delimiter)
1254                )?;
1255            }
1256            writeln!(writer)?;
1257        }
1258    }
1259
1260    Ok(())
1261}
1262
1263/// Output results as JSON
1264fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
1265    let columns = dataview.column_names();
1266    let mut rows = Vec::new();
1267
1268    for row_idx in 0..dataview.row_count() {
1269        if let Some(row) = dataview.get_row(row_idx) {
1270            let mut json_row = serde_json::Map::new();
1271            for (col_idx, value) in row.values.iter().enumerate() {
1272                if col_idx < columns.len() {
1273                    json_row.insert(columns[col_idx].clone(), value_to_json(value));
1274                }
1275            }
1276            rows.push(serde_json::Value::Object(json_row));
1277        }
1278    }
1279
1280    let json = serde_json::to_string_pretty(&rows)?;
1281    writeln!(writer, "{json}")?;
1282
1283    Ok(())
1284}
1285
1286/// Output results as structured JSON with metadata for IDE/plugin integration
1287fn output_json_structured<W: Write>(
1288    dataview: &DataView,
1289    writer: &mut W,
1290    exec_time: f64,
1291) -> Result<()> {
1292    let column_names = dataview.column_names();
1293    let data_table = dataview.source();
1294
1295    // Build column metadata
1296    let mut columns = Vec::new();
1297    for (idx, name) in column_names.iter().enumerate() {
1298        let col_type = data_table
1299            .columns
1300            .get(idx)
1301            .map(|c| format!("{:?}", c.data_type))
1302            .unwrap_or_else(|| "UNKNOWN".to_string());
1303
1304        // Calculate max width for this column
1305        let mut max_width = name.len();
1306        for row_idx in 0..dataview.row_count() {
1307            if let Some(row) = dataview.get_row(row_idx) {
1308                if let Some(value) = row.values.get(idx) {
1309                    let display_width = match value {
1310                        DataValue::Null => 4, // "NULL"
1311                        DataValue::Integer(i) => i.to_string().len(),
1312                        DataValue::Float(f) => format!("{:.2}", f).len(),
1313                        DataValue::String(s) => s.len(),
1314                        DataValue::InternedString(s) => s.len(),
1315                        DataValue::Boolean(b) => {
1316                            if *b {
1317                                4
1318                            } else {
1319                                5
1320                            }
1321                        } // "true" or "false"
1322                        DataValue::DateTime(dt) => dt.len(),
1323                        DataValue::Vector(v) => {
1324                            let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1325                            format!("[{}]", components.join(",")).len()
1326                        }
1327                    };
1328                    max_width = max_width.max(display_width);
1329                }
1330            }
1331        }
1332
1333        let alignment = match data_table.columns.get(idx).map(|c| &c.data_type) {
1334            Some(crate::data::datatable::DataType::Integer) => "right",
1335            Some(crate::data::datatable::DataType::Float) => "right",
1336            _ => "left",
1337        };
1338
1339        let col_meta = serde_json::json!({
1340            "name": name,
1341            "type": col_type,
1342            "max_width": max_width,
1343            "alignment": alignment
1344        });
1345        columns.push(col_meta);
1346    }
1347
1348    // Build rows as arrays of strings
1349    let mut rows = Vec::new();
1350    for row_idx in 0..dataview.row_count() {
1351        if let Some(row) = dataview.get_row(row_idx) {
1352            let row_values: Vec<String> = row
1353                .values
1354                .iter()
1355                .map(|v| match v {
1356                    DataValue::Null => String::new(),
1357                    DataValue::Integer(i) => i.to_string(),
1358                    DataValue::Float(f) => format!("{:.2}", f),
1359                    DataValue::String(s) => s.clone(),
1360                    DataValue::InternedString(s) => s.to_string(),
1361                    DataValue::Boolean(b) => b.to_string(),
1362                    DataValue::DateTime(dt) => dt.clone(),
1363                    DataValue::Vector(v) => {
1364                        let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1365                        format!("[{}]", components.join(","))
1366                    }
1367                })
1368                .collect();
1369            rows.push(serde_json::Value::Array(
1370                row_values
1371                    .into_iter()
1372                    .map(serde_json::Value::String)
1373                    .collect(),
1374            ));
1375        }
1376    }
1377
1378    // Build complete structured output
1379    let output = serde_json::json!({
1380        "columns": columns,
1381        "rows": rows,
1382        "metadata": {
1383            "total_rows": dataview.row_count(),
1384            "query_time_ms": exec_time
1385        }
1386    });
1387
1388    let json = serde_json::to_string_pretty(&output)?;
1389    writeln!(writer, "{json}")?;
1390
1391    Ok(())
1392}
1393
1394/// Output results using the old custom ASCII table format (for Nvim compatibility)
1395fn output_table_old_style<W: Write>(
1396    dataview: &DataView,
1397    writer: &mut W,
1398    max_col_width: Option<usize>,
1399) -> Result<()> {
1400    let columns = dataview.column_names();
1401
1402    // Calculate column widths
1403    let mut widths = vec![0; columns.len()];
1404    for (i, col) in columns.iter().enumerate() {
1405        widths[i] = col.len();
1406    }
1407
1408    // Scan all rows for width calculation
1409    for row_idx in 0..dataview.row_count() {
1410        if let Some(row) = dataview.get_row(row_idx) {
1411            for (i, value) in row.values.iter().enumerate() {
1412                if i < widths.len() {
1413                    let value_str = format_value(value);
1414                    widths[i] = widths[i].max(display_width(&value_str));
1415                }
1416            }
1417        }
1418    }
1419
1420    // Apply maximum column width if specified
1421    if let Some(max_width) = max_col_width {
1422        for width in &mut widths {
1423            *width = (*width).min(max_width);
1424        }
1425    }
1426
1427    // Print top border
1428    write!(writer, "+")?;
1429    for width in &widths {
1430        write!(writer, "{}", "-".repeat(*width + 2))?;
1431        write!(writer, "+")?;
1432    }
1433    writeln!(writer)?;
1434
1435    // Print headers
1436    write!(writer, "|")?;
1437    for (i, col) in columns.iter().enumerate() {
1438        write!(writer, " {:^width$} |", col, width = widths[i])?;
1439    }
1440    writeln!(writer)?;
1441
1442    // Print header separator
1443    write!(writer, "+")?;
1444    for width in &widths {
1445        write!(writer, "{}", "-".repeat(*width + 2))?;
1446        write!(writer, "+")?;
1447    }
1448    writeln!(writer)?;
1449
1450    // Print data rows (no separators between rows)
1451    for row_idx in 0..dataview.row_count() {
1452        if let Some(row) = dataview.get_row(row_idx) {
1453            write!(writer, "|")?;
1454            for (i, value) in row.values.iter().enumerate() {
1455                if i < widths.len() {
1456                    let value_str = format_value(value);
1457                    let display_len = display_width(&value_str);
1458
1459                    // For ANSI-colored strings, manual padding is needed
1460                    // because format! uses byte length, not display width
1461                    write!(writer, " {}", value_str)?;
1462                    let padding_needed = if display_len < widths[i] {
1463                        widths[i] - display_len
1464                    } else {
1465                        0
1466                    };
1467                    write!(writer, "{} |", " ".repeat(padding_needed))?;
1468                }
1469            }
1470            writeln!(writer)?;
1471        }
1472    }
1473
1474    // Print bottom border
1475    write!(writer, "+")?;
1476    for width in &widths {
1477        write!(writer, "{}", "-".repeat(*width + 2))?;
1478        write!(writer, "+")?;
1479    }
1480    writeln!(writer)?;
1481
1482    Ok(())
1483}
1484
1485/// Output results as a table using comfy-table with styling
1486fn output_table<W: Write>(
1487    dataview: &DataView,
1488    writer: &mut W,
1489    max_col_width: Option<usize>,
1490    _col_sample_rows: usize, // Not needed with comfy-table
1491    style: TableStyle,
1492    styled: bool,
1493    style_file: Option<&str>,
1494) -> Result<()> {
1495    let mut table = Table::new();
1496
1497    // Apply the selected style preset
1498    match style {
1499        TableStyle::Default => {
1500            // Use custom old-style renderer for Nvim compatibility
1501            // This matches what the table navigation parser expects
1502            return output_table_old_style(dataview, writer, max_col_width);
1503        }
1504        TableStyle::AsciiFull => {
1505            table.load_preset(ASCII_FULL);
1506        }
1507        TableStyle::AsciiCondensed => {
1508            table.load_preset(ASCII_FULL_CONDENSED);
1509        }
1510        TableStyle::AsciiBordersOnly => {
1511            table.load_preset(ASCII_BORDERS_ONLY);
1512        }
1513        TableStyle::AsciiHorizontalOnly => {
1514            table.load_preset(ASCII_HORIZONTAL_ONLY);
1515        }
1516        TableStyle::AsciiNoBorders => {
1517            table.load_preset(ASCII_NO_BORDERS);
1518        }
1519        TableStyle::Markdown => {
1520            table.load_preset(ASCII_MARKDOWN);
1521        }
1522        TableStyle::Utf8Full => {
1523            table.load_preset(UTF8_FULL);
1524        }
1525        TableStyle::Utf8Condensed => {
1526            table.load_preset(UTF8_FULL_CONDENSED);
1527        }
1528        TableStyle::Utf8BordersOnly => {
1529            table.load_preset(UTF8_BORDERS_ONLY);
1530        }
1531        TableStyle::Utf8HorizontalOnly => {
1532            table.load_preset(UTF8_HORIZONTAL_ONLY);
1533        }
1534        TableStyle::Utf8NoBorders => {
1535            table.load_preset(UTF8_NO_BORDERS);
1536        }
1537        TableStyle::Plain => {
1538            table.load_preset(NOTHING);
1539        }
1540    }
1541
1542    // Set content arrangement (automatic width adjustment)
1543    if max_col_width.is_some() {
1544        table.set_content_arrangement(ContentArrangement::Dynamic);
1545    }
1546
1547    // Set column headers
1548    let columns = dataview.column_names();
1549
1550    // Apply color styling if requested
1551    if styled {
1552        use crate::output::styled_table::{apply_styles_to_table, StyleConfig};
1553        use std::path::PathBuf;
1554
1555        // Load style configuration
1556        let style_config = if let Some(file_path) = style_file {
1557            let path = PathBuf::from(file_path);
1558            StyleConfig::from_file(&path).ok()
1559        } else {
1560            StyleConfig::load_default()
1561        };
1562
1563        if let Some(config) = style_config {
1564            // Convert DataView rows to Vec<Vec<String>> for styling
1565            let rows: Vec<Vec<String>> = (0..dataview.row_count())
1566                .filter_map(|i| {
1567                    dataview.get_row(i).map(|row| {
1568                        row.values
1569                            .iter()
1570                            .map(|v| {
1571                                let s = format_value(v);
1572                                // Apply max width truncation if specified
1573                                if let Some(max_width) = max_col_width {
1574                                    if s.len() > max_width {
1575                                        format!("{}...", &s[..max_width.saturating_sub(3)])
1576                                    } else {
1577                                        s
1578                                    }
1579                                } else {
1580                                    s
1581                                }
1582                            })
1583                            .collect()
1584                    })
1585                })
1586                .collect();
1587
1588            if let Err(e) = apply_styles_to_table(&mut table, &columns, &rows, &config) {
1589                eprintln!("Warning: Failed to apply styles: {}", e);
1590            }
1591        }
1592    } else {
1593        // No styling - add headers and rows normally
1594        table.set_header(&columns);
1595
1596        // Add data rows
1597        for row_idx in 0..dataview.row_count() {
1598            if let Some(row) = dataview.get_row(row_idx) {
1599                let row_strings: Vec<String> = row
1600                    .values
1601                    .iter()
1602                    .map(|v| {
1603                        let s = format_value(v);
1604                        // Apply max width truncation if specified
1605                        if let Some(max_width) = max_col_width {
1606                            if s.len() > max_width {
1607                                format!("{}...", &s[..max_width.saturating_sub(3)])
1608                            } else {
1609                                s
1610                            }
1611                        } else {
1612                            s
1613                        }
1614                    })
1615                    .collect();
1616                table.add_row(row_strings);
1617            }
1618        }
1619    }
1620
1621    // Write the table to the writer
1622    writeln!(writer, "{}", table)?;
1623
1624    Ok(())
1625}
1626
1627/// Format a `DataValue` for display
1628fn format_value(value: &DataValue) -> String {
1629    match value {
1630        DataValue::Null => String::new(),
1631        DataValue::Integer(i) => i.to_string(),
1632        DataValue::Float(f) => f.to_string(),
1633        DataValue::String(s) => s.clone(),
1634        DataValue::InternedString(s) => s.to_string(),
1635        DataValue::Boolean(b) => b.to_string(),
1636        DataValue::DateTime(dt) => dt.to_string(),
1637        DataValue::Vector(v) => {
1638            let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1639            format!("[{}]", components.join(","))
1640        }
1641    }
1642}
1643
1644/// Convert `DataValue` to JSON
1645fn value_to_json(value: &DataValue) -> serde_json::Value {
1646    match value {
1647        DataValue::Null => serde_json::Value::Null,
1648        DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
1649        DataValue::Float(f) => {
1650            if let Some(n) = serde_json::Number::from_f64(*f) {
1651                serde_json::Value::Number(n)
1652            } else {
1653                serde_json::Value::Null
1654            }
1655        }
1656        DataValue::String(s) => serde_json::Value::String(s.clone()),
1657        DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
1658        DataValue::Boolean(b) => serde_json::Value::Bool(*b),
1659        DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
1660        DataValue::Vector(v) => serde_json::Value::Array(
1661            v.iter()
1662                .map(|f| {
1663                    if let Some(n) = serde_json::Number::from_f64(*f) {
1664                        serde_json::Value::Number(n)
1665                    } else {
1666                        serde_json::Value::Null
1667                    }
1668                })
1669                .collect(),
1670        ),
1671    }
1672}
1673
1674/// Escape a CSV field if it contains special characters
1675fn escape_csv_field(field: &str, delimiter: char) -> String {
1676    if field.contains(delimiter)
1677        || field.contains('"')
1678        || field.contains('\n')
1679        || field.contains('\r')
1680    {
1681        format!("\"{}\"", field.replace('"', "\"\""))
1682    } else {
1683        field.to_string()
1684    }
1685}