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 from the SQL
896            // Strategy: Find "INTO #table_name" and remove it
897            let into_pattern = regex::Regex::new(r"(?i)\s+INTO\s+#\w+").unwrap();
898            into_pattern.replace(statement, "").to_string()
899        } else {
900            statement.to_string()
901        };
902
903        // Execute the statement
904        let service = QueryExecutionService::new(config.case_insensitive, config.auto_hide_empty);
905        match service.execute_with_temp_tables(
906            &executable_sql,
907            Some(&dataview),
908            None,
909            Some(&temp_tables),
910        ) {
911            Ok(result) => {
912                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
913                let final_view = result.dataview;
914
915                // Check if this is an INTO statement - store result in temp table
916                if let Some(into_table) = &parsed_stmt.into_table {
917                    // Get the source table from the DataView - this contains the query result
918                    let result_table = final_view.source_arc();
919                    let row_count = result_table.row_count();
920
921                    match temp_tables.insert(into_table.name.clone(), result_table) {
922                        Ok(_) => {
923                            info!(
924                                "Stored {} rows in temporary table {}",
925                                row_count, into_table.name
926                            );
927
928                            // For INTO statements, output a simple confirmation message
929                            let mut statement_output = Vec::new();
930                            writeln!(
931                                &mut statement_output,
932                                "({} rows affected) -> {}",
933                                row_count, into_table.name
934                            )?;
935                            output.extend(
936                                String::from_utf8_lossy(&statement_output)
937                                    .lines()
938                                    .map(String::from),
939                            );
940
941                            script_result.add_success(
942                                statement_num,
943                                statement.to_string(),
944                                row_count,
945                                exec_time,
946                            );
947                            continue; // Skip normal output formatting for INTO statements
948                        }
949                        Err(e) => {
950                            script_result.add_failure(
951                                statement_num,
952                                statement.to_string(),
953                                e.to_string(),
954                                exec_time,
955                            );
956                            break;
957                        }
958                    }
959                }
960
961                // Format the output based on the output format (normal query without INTO)
962                let mut statement_output = Vec::new();
963                match config.output_format {
964                    OutputFormat::Csv => {
965                        output_csv(&final_view, &mut statement_output, ',')?;
966                    }
967                    OutputFormat::Json => {
968                        output_json(&final_view, &mut statement_output)?;
969                    }
970                    OutputFormat::JsonStructured => {
971                        output_json_structured(&final_view, &mut statement_output, exec_time)?;
972                    }
973                    OutputFormat::Table => {
974                        output_table(
975                            &final_view,
976                            &mut statement_output,
977                            config.max_col_width,
978                            config.col_sample_rows,
979                            config.table_style,
980                        )?;
981                        writeln!(
982                            &mut statement_output,
983                            "Query completed: {} rows in {:.2}ms",
984                            final_view.row_count(),
985                            exec_time
986                        )?;
987                    }
988                    OutputFormat::Tsv => {
989                        output_csv(&final_view, &mut statement_output, '\t')?;
990                    }
991                }
992
993                // Add to overall output
994                output.extend(
995                    String::from_utf8_lossy(&statement_output)
996                        .lines()
997                        .map(String::from),
998                );
999
1000                script_result.add_success(
1001                    statement_num,
1002                    statement.to_string(),
1003                    final_view.row_count(),
1004                    exec_time,
1005                );
1006            }
1007            Err(e) => {
1008                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
1009                let error_msg = format!("Query {} failed: {}", statement_num, e);
1010
1011                if matches!(config.output_format, OutputFormat::Table) {
1012                    output.push(error_msg.clone());
1013                }
1014
1015                script_result.add_failure(
1016                    statement_num,
1017                    statement.to_string(),
1018                    e.to_string(),
1019                    exec_time,
1020                );
1021
1022                // Continue to next statement (don't stop on error)
1023            }
1024        }
1025    }
1026
1027    // Write output
1028    if let Some(ref output_file) = config.output_file {
1029        let mut file = fs::File::create(output_file)?;
1030        for line in &output {
1031            writeln!(file, "{}", line)?;
1032        }
1033        info!("Results written to: {}", output_file);
1034    } else {
1035        for line in &output {
1036            println!("{}", line);
1037        }
1038    }
1039
1040    // Print summary if in table mode
1041    if matches!(config.output_format, OutputFormat::Table) {
1042        println!("\n=== Script Summary ===");
1043        println!("Total statements: {}", script_result.total_statements);
1044        println!("Successful: {}", script_result.successful_statements);
1045        println!("Failed: {}", script_result.failed_statements);
1046        println!(
1047            "Total execution time: {:.2}ms",
1048            script_result.total_execution_time_ms
1049        );
1050    }
1051
1052    if !script_result.all_successful() {
1053        return Err(anyhow::anyhow!(
1054            "{} of {} statements failed",
1055            script_result.failed_statements,
1056            script_result.total_statements
1057        ));
1058    }
1059
1060    Ok(())
1061}
1062
1063/// Load a data file (CSV or JSON) into a `DataTable`
1064fn load_data_file(path: &str) -> Result<DataTable> {
1065    let path = Path::new(path);
1066
1067    if !path.exists() {
1068        return Err(anyhow::anyhow!("File not found: {}", path.display()));
1069    }
1070
1071    // Determine file type by extension
1072    let extension = path
1073        .extension()
1074        .and_then(|ext| ext.to_str())
1075        .map(str::to_lowercase)
1076        .unwrap_or_default();
1077
1078    let table_name = path
1079        .file_stem()
1080        .and_then(|stem| stem.to_str())
1081        .unwrap_or("data")
1082        .to_string();
1083
1084    match extension.as_str() {
1085        "csv" => load_csv_to_datatable(path, &table_name)
1086            .with_context(|| format!("Failed to load CSV file: {}", path.display())),
1087        "json" => load_json_to_datatable(path, &table_name)
1088            .with_context(|| format!("Failed to load JSON file: {}", path.display())),
1089        _ => Err(anyhow::anyhow!(
1090            "Unsupported file type: {}. Use .csv or .json",
1091            extension
1092        )),
1093    }
1094}
1095
1096/// Limit the number of rows in results
1097fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
1098    let source = dataview.source();
1099    let mut limited_table = DataTable::new(&source.name);
1100
1101    // Copy columns
1102    for col in &source.columns {
1103        limited_table.add_column(col.clone());
1104    }
1105
1106    // Copy limited rows
1107    let rows_to_copy = dataview.row_count().min(limit);
1108    for i in 0..rows_to_copy {
1109        if let Some(row) = dataview.get_row(i) {
1110            let _ = limited_table.add_row(row.clone());
1111        }
1112    }
1113
1114    Ok(limited_table)
1115}
1116
1117/// Output query results in the specified format
1118fn output_results<W: Write>(
1119    dataview: &DataView,
1120    format: OutputFormat,
1121    writer: &mut W,
1122    max_col_width: Option<usize>,
1123    col_sample_rows: usize,
1124    exec_time_ms: f64,
1125    table_style: TableStyle,
1126) -> Result<()> {
1127    match format {
1128        OutputFormat::Csv => output_csv(dataview, writer, ','),
1129        OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
1130        OutputFormat::Json => output_json(dataview, writer),
1131        OutputFormat::JsonStructured => output_json_structured(dataview, writer, exec_time_ms),
1132        OutputFormat::Table => output_table(
1133            dataview,
1134            writer,
1135            max_col_width,
1136            col_sample_rows,
1137            table_style,
1138        ),
1139    }
1140}
1141
1142/// Output results as CSV/TSV
1143fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
1144    // Write headers
1145    let columns = dataview.column_names();
1146    for (i, col) in columns.iter().enumerate() {
1147        if i > 0 {
1148            write!(writer, "{delimiter}")?;
1149        }
1150        write!(writer, "{}", escape_csv_field(col, delimiter))?;
1151    }
1152    writeln!(writer)?;
1153
1154    // Write rows
1155    for row_idx in 0..dataview.row_count() {
1156        if let Some(row) = dataview.get_row(row_idx) {
1157            for (i, value) in row.values.iter().enumerate() {
1158                if i > 0 {
1159                    write!(writer, "{delimiter}")?;
1160                }
1161                write!(
1162                    writer,
1163                    "{}",
1164                    escape_csv_field(&format_value(value), delimiter)
1165                )?;
1166            }
1167            writeln!(writer)?;
1168        }
1169    }
1170
1171    Ok(())
1172}
1173
1174/// Output results as JSON
1175fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
1176    let columns = dataview.column_names();
1177    let mut rows = Vec::new();
1178
1179    for row_idx in 0..dataview.row_count() {
1180        if let Some(row) = dataview.get_row(row_idx) {
1181            let mut json_row = serde_json::Map::new();
1182            for (col_idx, value) in row.values.iter().enumerate() {
1183                if col_idx < columns.len() {
1184                    json_row.insert(columns[col_idx].clone(), value_to_json(value));
1185                }
1186            }
1187            rows.push(serde_json::Value::Object(json_row));
1188        }
1189    }
1190
1191    let json = serde_json::to_string_pretty(&rows)?;
1192    writeln!(writer, "{json}")?;
1193
1194    Ok(())
1195}
1196
1197/// Output results as structured JSON with metadata for IDE/plugin integration
1198fn output_json_structured<W: Write>(
1199    dataview: &DataView,
1200    writer: &mut W,
1201    exec_time: f64,
1202) -> Result<()> {
1203    let column_names = dataview.column_names();
1204    let data_table = dataview.source();
1205
1206    // Build column metadata
1207    let mut columns = Vec::new();
1208    for (idx, name) in column_names.iter().enumerate() {
1209        let col_type = data_table
1210            .columns
1211            .get(idx)
1212            .map(|c| format!("{:?}", c.data_type))
1213            .unwrap_or_else(|| "UNKNOWN".to_string());
1214
1215        // Calculate max width for this column
1216        let mut max_width = name.len();
1217        for row_idx in 0..dataview.row_count() {
1218            if let Some(row) = dataview.get_row(row_idx) {
1219                if let Some(value) = row.values.get(idx) {
1220                    let display_width = match value {
1221                        DataValue::Null => 4, // "NULL"
1222                        DataValue::Integer(i) => i.to_string().len(),
1223                        DataValue::Float(f) => format!("{:.2}", f).len(),
1224                        DataValue::String(s) => s.len(),
1225                        DataValue::InternedString(s) => s.len(),
1226                        DataValue::Boolean(b) => {
1227                            if *b {
1228                                4
1229                            } else {
1230                                5
1231                            }
1232                        } // "true" or "false"
1233                        DataValue::DateTime(dt) => dt.len(),
1234                    };
1235                    max_width = max_width.max(display_width);
1236                }
1237            }
1238        }
1239
1240        let alignment = match data_table.columns.get(idx).map(|c| &c.data_type) {
1241            Some(crate::data::datatable::DataType::Integer) => "right",
1242            Some(crate::data::datatable::DataType::Float) => "right",
1243            _ => "left",
1244        };
1245
1246        let col_meta = serde_json::json!({
1247            "name": name,
1248            "type": col_type,
1249            "max_width": max_width,
1250            "alignment": alignment
1251        });
1252        columns.push(col_meta);
1253    }
1254
1255    // Build rows as arrays of strings
1256    let mut rows = Vec::new();
1257    for row_idx in 0..dataview.row_count() {
1258        if let Some(row) = dataview.get_row(row_idx) {
1259            let row_values: Vec<String> = row
1260                .values
1261                .iter()
1262                .map(|v| match v {
1263                    DataValue::Null => String::new(),
1264                    DataValue::Integer(i) => i.to_string(),
1265                    DataValue::Float(f) => format!("{:.2}", f),
1266                    DataValue::String(s) => s.clone(),
1267                    DataValue::InternedString(s) => s.to_string(),
1268                    DataValue::Boolean(b) => b.to_string(),
1269                    DataValue::DateTime(dt) => dt.clone(),
1270                })
1271                .collect();
1272            rows.push(serde_json::Value::Array(
1273                row_values
1274                    .into_iter()
1275                    .map(serde_json::Value::String)
1276                    .collect(),
1277            ));
1278        }
1279    }
1280
1281    // Build complete structured output
1282    let output = serde_json::json!({
1283        "columns": columns,
1284        "rows": rows,
1285        "metadata": {
1286            "total_rows": dataview.row_count(),
1287            "query_time_ms": exec_time
1288        }
1289    });
1290
1291    let json = serde_json::to_string_pretty(&output)?;
1292    writeln!(writer, "{json}")?;
1293
1294    Ok(())
1295}
1296
1297/// Output results using the old custom ASCII table format (for Nvim compatibility)
1298fn output_table_old_style<W: Write>(
1299    dataview: &DataView,
1300    writer: &mut W,
1301    max_col_width: Option<usize>,
1302) -> Result<()> {
1303    let columns = dataview.column_names();
1304
1305    // Calculate column widths
1306    let mut widths = vec![0; columns.len()];
1307    for (i, col) in columns.iter().enumerate() {
1308        widths[i] = col.len();
1309    }
1310
1311    // Scan all rows for width calculation
1312    for row_idx in 0..dataview.row_count() {
1313        if let Some(row) = dataview.get_row(row_idx) {
1314            for (i, value) in row.values.iter().enumerate() {
1315                if i < widths.len() {
1316                    let value_str = format_value(value);
1317                    widths[i] = widths[i].max(value_str.len());
1318                }
1319            }
1320        }
1321    }
1322
1323    // Apply maximum column width if specified
1324    if let Some(max_width) = max_col_width {
1325        for width in &mut widths {
1326            *width = (*width).min(max_width);
1327        }
1328    }
1329
1330    // Print top border
1331    write!(writer, "+")?;
1332    for width in &widths {
1333        write!(writer, "{}", "-".repeat(*width + 2))?;
1334        write!(writer, "+")?;
1335    }
1336    writeln!(writer)?;
1337
1338    // Print headers
1339    write!(writer, "|")?;
1340    for (i, col) in columns.iter().enumerate() {
1341        write!(writer, " {:^width$} |", col, width = widths[i])?;
1342    }
1343    writeln!(writer)?;
1344
1345    // Print header separator
1346    write!(writer, "+")?;
1347    for width in &widths {
1348        write!(writer, "{}", "-".repeat(*width + 2))?;
1349        write!(writer, "+")?;
1350    }
1351    writeln!(writer)?;
1352
1353    // Print data rows (no separators between rows)
1354    for row_idx in 0..dataview.row_count() {
1355        if let Some(row) = dataview.get_row(row_idx) {
1356            write!(writer, "|")?;
1357            for (i, value) in row.values.iter().enumerate() {
1358                if i < widths.len() {
1359                    let value_str = format_value(value);
1360                    let truncated = if value_str.len() > widths[i] {
1361                        format!("{}...", &value_str[..widths[i].saturating_sub(3)])
1362                    } else {
1363                        value_str
1364                    };
1365                    write!(writer, " {:<width$} |", truncated, width = widths[i])?;
1366                }
1367            }
1368            writeln!(writer)?;
1369        }
1370    }
1371
1372    // Print bottom border
1373    write!(writer, "+")?;
1374    for width in &widths {
1375        write!(writer, "{}", "-".repeat(*width + 2))?;
1376        write!(writer, "+")?;
1377    }
1378    writeln!(writer)?;
1379
1380    Ok(())
1381}
1382
1383/// Output results as a table using comfy-table with styling
1384fn output_table<W: Write>(
1385    dataview: &DataView,
1386    writer: &mut W,
1387    max_col_width: Option<usize>,
1388    _col_sample_rows: usize, // Not needed with comfy-table
1389    style: TableStyle,
1390) -> Result<()> {
1391    let mut table = Table::new();
1392
1393    // Apply the selected style preset
1394    match style {
1395        TableStyle::Default => {
1396            // Use custom old-style renderer for Nvim compatibility
1397            // This matches what the table navigation parser expects
1398            return output_table_old_style(dataview, writer, max_col_width);
1399        }
1400        TableStyle::AsciiFull => {
1401            table.load_preset(ASCII_FULL);
1402        }
1403        TableStyle::AsciiCondensed => {
1404            table.load_preset(ASCII_FULL_CONDENSED);
1405        }
1406        TableStyle::AsciiBordersOnly => {
1407            table.load_preset(ASCII_BORDERS_ONLY);
1408        }
1409        TableStyle::AsciiHorizontalOnly => {
1410            table.load_preset(ASCII_HORIZONTAL_ONLY);
1411        }
1412        TableStyle::AsciiNoBorders => {
1413            table.load_preset(ASCII_NO_BORDERS);
1414        }
1415        TableStyle::Markdown => {
1416            table.load_preset(ASCII_MARKDOWN);
1417        }
1418        TableStyle::Utf8Full => {
1419            table.load_preset(UTF8_FULL);
1420        }
1421        TableStyle::Utf8Condensed => {
1422            table.load_preset(UTF8_FULL_CONDENSED);
1423        }
1424        TableStyle::Utf8BordersOnly => {
1425            table.load_preset(UTF8_BORDERS_ONLY);
1426        }
1427        TableStyle::Utf8HorizontalOnly => {
1428            table.load_preset(UTF8_HORIZONTAL_ONLY);
1429        }
1430        TableStyle::Utf8NoBorders => {
1431            table.load_preset(UTF8_NO_BORDERS);
1432        }
1433        TableStyle::Plain => {
1434            table.load_preset(NOTHING);
1435        }
1436    }
1437
1438    // Set content arrangement (automatic width adjustment)
1439    if max_col_width.is_some() {
1440        table.set_content_arrangement(ContentArrangement::Dynamic);
1441    }
1442
1443    // Set column headers
1444    let columns = dataview.column_names();
1445    table.set_header(&columns);
1446
1447    // Add data rows
1448    for row_idx in 0..dataview.row_count() {
1449        if let Some(row) = dataview.get_row(row_idx) {
1450            let row_strings: Vec<String> = row
1451                .values
1452                .iter()
1453                .map(|v| {
1454                    let s = format_value(v);
1455                    // Apply max width truncation if specified
1456                    if let Some(max_width) = max_col_width {
1457                        if s.len() > max_width {
1458                            format!("{}...", &s[..max_width.saturating_sub(3)])
1459                        } else {
1460                            s
1461                        }
1462                    } else {
1463                        s
1464                    }
1465                })
1466                .collect();
1467            table.add_row(row_strings);
1468        }
1469    }
1470
1471    // Write the table to the writer
1472    writeln!(writer, "{}", table)?;
1473
1474    Ok(())
1475}
1476
1477/// Format a `DataValue` for display
1478fn format_value(value: &DataValue) -> String {
1479    match value {
1480        DataValue::Null => String::new(),
1481        DataValue::Integer(i) => i.to_string(),
1482        DataValue::Float(f) => f.to_string(),
1483        DataValue::String(s) => s.clone(),
1484        DataValue::InternedString(s) => s.to_string(),
1485        DataValue::Boolean(b) => b.to_string(),
1486        DataValue::DateTime(dt) => dt.to_string(),
1487    }
1488}
1489
1490/// Convert `DataValue` to JSON
1491fn value_to_json(value: &DataValue) -> serde_json::Value {
1492    match value {
1493        DataValue::Null => serde_json::Value::Null,
1494        DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
1495        DataValue::Float(f) => {
1496            if let Some(n) = serde_json::Number::from_f64(*f) {
1497                serde_json::Value::Number(n)
1498            } else {
1499                serde_json::Value::Null
1500            }
1501        }
1502        DataValue::String(s) => serde_json::Value::String(s.clone()),
1503        DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
1504        DataValue::Boolean(b) => serde_json::Value::Bool(*b),
1505        DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
1506    }
1507}
1508
1509/// Escape a CSV field if it contains special characters
1510fn escape_csv_field(field: &str, delimiter: char) -> String {
1511    if field.contains(delimiter)
1512        || field.contains('"')
1513        || field.contains('\n')
1514        || field.contains('\r')
1515    {
1516        format!("\"{}\"", field.replace('"', "\"\""))
1517    } else {
1518        field.to_string()
1519    }
1520}