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