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