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