sql_cli/
non_interactive.rs

1use anyhow::{Context, Result};
2use std::fs;
3use std::io::{self, Write};
4use std::path::Path;
5use std::time::Instant;
6use tracing::{debug, info};
7
8use crate::config::config::Config;
9use crate::data::data_view::DataView;
10use crate::data::datatable::{DataTable, DataValue};
11use crate::data::datatable_loaders::{load_csv_to_datatable, load_json_to_datatable};
12use crate::services::query_execution_service::QueryExecutionService;
13use crate::sql::parser::ast::{CTEType, TableSource, CTE};
14use crate::sql::script_parser::{ScriptParser, ScriptResult};
15
16/// Extract dependencies from a CTE by analyzing what tables it references
17fn extract_cte_dependencies(cte: &CTE) -> Vec<String> {
18    // For now, we'll just return the from_table if it exists
19    // This could be enhanced to do deeper analysis of the query AST
20    let mut deps = Vec::new();
21
22    if let CTEType::Standard(query) = &cte.cte_type {
23        if let Some(from_table) = &query.from_table {
24            deps.push(from_table.clone());
25        }
26
27        // Could also check joins, subqueries, etc.
28        for join in &query.joins {
29            match &join.table {
30                TableSource::Table(table_name) => {
31                    deps.push(table_name.clone());
32                }
33                TableSource::DerivedTable { alias, .. } => {
34                    deps.push(alias.clone());
35                }
36            }
37        }
38    }
39
40    deps
41}
42
43/// Output format for query results
44#[derive(Debug, Clone)]
45pub enum OutputFormat {
46    Csv,
47    Json,
48    Table,
49    Tsv,
50}
51
52impl OutputFormat {
53    pub fn from_str(s: &str) -> Result<Self> {
54        match s.to_lowercase().as_str() {
55            "csv" => Ok(OutputFormat::Csv),
56            "json" => Ok(OutputFormat::Json),
57            "table" => Ok(OutputFormat::Table),
58            "tsv" => Ok(OutputFormat::Tsv),
59            _ => Err(anyhow::anyhow!(
60                "Invalid output format: {}. Use csv, json, table, or tsv",
61                s
62            )),
63        }
64    }
65}
66
67/// Configuration for non-interactive query execution
68pub struct NonInteractiveConfig {
69    pub data_file: String,
70    pub query: String,
71    pub output_format: OutputFormat,
72    pub output_file: Option<String>,
73    pub case_insensitive: bool,
74    pub auto_hide_empty: bool,
75    pub limit: Option<usize>,
76    pub query_plan: bool,
77    pub show_work_units: bool,
78    pub execution_plan: bool,
79    pub cte_info: bool,
80    pub rewrite_analysis: bool,
81    pub lift_in_expressions: bool,
82    pub script_file: Option<String>, // Path to the script file for relative path resolution
83    pub debug_trace: bool,
84}
85
86/// Execute a query in non-interactive mode
87pub fn execute_non_interactive(config: NonInteractiveConfig) -> Result<()> {
88    let start_time = Instant::now();
89
90    // 1. Load the data file or create DUAL table
91    let (data_table, _is_dual) = if config.data_file.is_empty() {
92        info!("No data file provided, using DUAL table");
93        (crate::data::datatable::DataTable::dual(), true)
94    } else {
95        info!("Loading data from: {}", config.data_file);
96        let table = load_data_file(&config.data_file)?;
97        info!(
98            "Loaded {} rows with {} columns",
99            table.row_count(),
100            table.column_count()
101        );
102        (table, false)
103    };
104    let _table_name = data_table.name.clone();
105
106    // 2. Create a DataView from the table
107    let dataview = DataView::new(std::sync::Arc::new(data_table));
108
109    // 3. Execute the query
110    info!("Executing query: {}", config.query);
111
112    // If execution_plan is requested, show detailed execution information
113    if config.execution_plan {
114        println!("\n=== EXECUTION PLAN ===");
115        println!("Query: {}", config.query);
116        println!("\nExecution Steps:");
117        println!("1. PARSE - Parse SQL query");
118        println!("2. LOAD_DATA - Load data from {}", &config.data_file);
119        println!(
120            "   • Loaded {} rows, {} columns",
121            dataview.row_count(),
122            dataview.column_count()
123        );
124    }
125
126    // If show_work_units is requested, analyze and display work units
127    if config.show_work_units {
128        use crate::query_plan::{ExpressionLifter, QueryAnalyzer};
129        use crate::sql::recursive_parser::Parser;
130
131        let mut parser = Parser::new(&config.query);
132        match parser.parse() {
133            Ok(stmt) => {
134                let mut analyzer = QueryAnalyzer::new();
135                let mut lifter = ExpressionLifter::new();
136
137                // Check if the query has liftable expressions
138                let mut stmt_copy = stmt.clone();
139                let lifted = lifter.lift_expressions(&mut stmt_copy);
140
141                // Build the query plan
142                match analyzer.analyze(&stmt_copy, config.query.clone()) {
143                    Ok(plan) => {
144                        println!("\n{}", plan.explain());
145
146                        if !lifted.is_empty() {
147                            println!("\nLifted CTEs:");
148                            for cte in &lifted {
149                                println!("  - {}", cte.name);
150                            }
151                        }
152
153                        return Ok(());
154                    }
155                    Err(e) => {
156                        eprintln!("Error analyzing query: {}", e);
157                        return Err(anyhow::anyhow!("Query analysis failed: {}", e));
158                    }
159                }
160            }
161            Err(e) => {
162                eprintln!("Error parsing query: {}", e);
163                return Err(anyhow::anyhow!("Parse error: {}", e));
164            }
165        }
166    }
167
168    // If query_plan is requested, parse and display the AST
169    if config.query_plan {
170        use crate::sql::recursive_parser::Parser;
171        let mut parser = Parser::new(&config.query);
172        match parser.parse() {
173            Ok(statement) => {
174                println!("\n=== QUERY PLAN (AST) ===");
175                println!("{statement:#?}");
176                println!("=== END QUERY PLAN ===\n");
177            }
178            Err(e) => {
179                eprintln!("Failed to parse query for plan: {e}");
180            }
181        }
182    }
183
184    // If rewrite analysis is requested, analyze query for optimization opportunities
185    if config.rewrite_analysis {
186        use crate::sql::query_rewriter::{QueryRewriter, RewriteAnalysis};
187        use crate::sql::recursive_parser::Parser;
188        use serde_json::json;
189
190        let mut parser = Parser::new(&config.query);
191        match parser.parse() {
192            Ok(statement) => {
193                let mut rewriter = QueryRewriter::new();
194                let suggestions = rewriter.analyze(&statement);
195
196                let analysis = RewriteAnalysis::from_suggestions(suggestions);
197                println!("{}", serde_json::to_string_pretty(&analysis).unwrap());
198                return Ok(());
199            }
200            Err(e) => {
201                let output = json!({
202                    "success": false,
203                    "error": format!("{}", e),
204                    "suggestions": [],
205                    "can_auto_rewrite": false,
206                });
207                println!("{}", serde_json::to_string_pretty(&output).unwrap());
208                return Ok(());
209            }
210        }
211    }
212
213    // If CTE info is requested, parse and output CTE information as JSON
214    if config.cte_info {
215        use crate::sql::recursive_parser::Parser;
216        use serde_json::json;
217
218        let mut parser = Parser::new(&config.query);
219        match parser.parse() {
220            Ok(statement) => {
221                let mut cte_info = Vec::new();
222
223                // Extract CTE information
224                for (index, cte) in statement.ctes.iter().enumerate() {
225                    let cte_json = json!({
226                        "index": index,
227                        "name": cte.name,
228                        "columns": cte.column_list,
229                        // We can't easily get line numbers from the AST,
230                        // but we can provide the structure
231                        "dependencies": extract_cte_dependencies(cte),
232                    });
233                    cte_info.push(cte_json);
234                }
235
236                let output = json!({
237                    "success": true,
238                    "ctes": cte_info,
239                    "total": statement.ctes.len(),
240                    "has_final_select": !statement.columns.is_empty() || !statement.select_items.is_empty(),
241                });
242
243                println!("{}", serde_json::to_string_pretty(&output).unwrap());
244                return Ok(());
245            }
246            Err(e) => {
247                let output = json!({
248                    "success": false,
249                    "error": format!("{}", e),
250                    "ctes": [],
251                    "total": 0,
252                });
253                println!("{}", serde_json::to_string_pretty(&output).unwrap());
254                return Ok(());
255            }
256        }
257    }
258
259    let query_start = Instant::now();
260
261    // Load configuration file to get date notation and other settings
262    let app_config = Config::load().unwrap_or_else(|e| {
263        debug!("Could not load config file: {}. Using defaults.", e);
264        Config::default()
265    });
266
267    // Initialize global config for function registry
268    crate::config::global::init_config(app_config.clone());
269
270    // Use QueryExecutionService with full BehaviorConfig
271    let mut behavior_config = app_config.behavior.clone();
272    debug!(
273        "Using date notation: {}",
274        behavior_config.default_date_notation
275    );
276    // Command line args override config file settings
277    if config.case_insensitive {
278        behavior_config.case_insensitive_default = true;
279    }
280    if config.auto_hide_empty {
281        behavior_config.hide_empty_columns = true;
282    }
283
284    // Parse and potentially rewrite the query
285    let exec_start = Instant::now();
286    let result = if config.lift_in_expressions {
287        use crate::data::query_engine::QueryEngine;
288        use crate::query_plan::{CTEHoister, InOperatorLifter};
289        use crate::sql::recursive_parser::Parser;
290
291        let mut parser = Parser::new(&config.query);
292        match parser.parse() {
293            Ok(mut stmt) => {
294                // Apply expression lifting for column alias dependencies
295                use crate::query_plan::ExpressionLifter;
296                let mut expr_lifter = ExpressionLifter::new();
297                let lifted = expr_lifter.lift_expressions(&mut stmt);
298                if !lifted.is_empty() {
299                    info!(
300                        "Applied expression lifting - {} CTEs generated",
301                        lifted.len()
302                    );
303                }
304
305                // Apply CTE hoisting to handle nested CTEs
306                stmt = CTEHoister::hoist_ctes(stmt);
307
308                // Try to rewrite the query with IN lifting
309                let mut lifter = InOperatorLifter::new();
310                if lifter.rewrite_query(&mut stmt) {
311                    info!("Applied IN expression lifting - query rewritten with CTEs");
312
313                    // Execute the rewritten AST directly
314                    let engine = QueryEngine::with_case_insensitive(config.case_insensitive);
315
316                    match engine.execute_statement(dataview.source_arc(), stmt) {
317                        Ok(result_view) => {
318                            // Create a QueryExecutionResult to match the expected type
319                            Ok(
320                                crate::services::query_execution_service::QueryExecutionResult {
321                                    dataview: result_view,
322                                    stats: crate::services::query_execution_service::QueryStats {
323                                        row_count: 0, // Will be filled by result_view
324                                        column_count: 0,
325                                        execution_time: exec_start.elapsed(),
326                                        query_engine_time: exec_start.elapsed(),
327                                    },
328                                    hidden_columns: Vec::new(),
329                                    query: config.query.clone(),
330                                    execution_plan: None,
331                                    debug_trace: None,
332                                },
333                            )
334                        }
335                        Err(e) => Err(e),
336                    }
337                } else {
338                    // No lifting needed, execute normally
339                    let query_service =
340                        QueryExecutionService::with_behavior_config(behavior_config);
341                    if config.debug_trace {
342                        let debug_ctx = crate::debug_trace::DebugContext::new(
343                            crate::debug_trace::DebugLevel::Debug,
344                        );
345                        query_service.execute_with_debug(
346                            &config.query,
347                            Some(&dataview),
348                            Some(dataview.source()),
349                            Some(debug_ctx),
350                        )
351                    } else {
352                        query_service.execute(
353                            &config.query,
354                            Some(&dataview),
355                            Some(dataview.source()),
356                        )
357                    }
358                }
359            }
360            Err(_) => {
361                // Parse failed, execute normally and let it fail with proper error
362                let query_service = QueryExecutionService::with_behavior_config(behavior_config);
363                if config.debug_trace {
364                    let debug_ctx = crate::debug_trace::DebugContext::new(
365                        crate::debug_trace::DebugLevel::Debug,
366                    );
367                    query_service.execute_with_debug(
368                        &config.query,
369                        Some(&dataview),
370                        Some(dataview.source()),
371                        Some(debug_ctx),
372                    )
373                } else {
374                    query_service.execute(&config.query, Some(&dataview), Some(dataview.source()))
375                }
376            }
377        }
378    } else {
379        // Normal execution without lifting
380        let query_service = QueryExecutionService::with_behavior_config(behavior_config);
381
382        // Create debug context if debug trace is enabled
383        if config.debug_trace {
384            let debug_ctx =
385                crate::debug_trace::DebugContext::new(crate::debug_trace::DebugLevel::Debug);
386            query_service.execute_with_debug(
387                &config.query,
388                Some(&dataview),
389                Some(dataview.source()),
390                Some(debug_ctx),
391            )
392        } else {
393            query_service.execute(&config.query, Some(&dataview), Some(dataview.source()))
394        }
395    }?;
396    let exec_time = exec_start.elapsed();
397
398    let query_time = query_start.elapsed();
399    info!("Query executed in {:?}", query_time);
400    info!(
401        "Result: {} rows, {} columns",
402        result.dataview.row_count(),
403        result.dataview.column_count()
404    );
405
406    // Show execution plan details if requested
407    if config.execution_plan {
408        // Try to get detailed execution plan
409        use crate::data::query_engine::QueryEngine;
410
411        let query_engine = QueryEngine::new();
412
413        match query_engine.execute_with_plan(
414            std::sync::Arc::new(dataview.source().clone()),
415            &config.query,
416        ) {
417            Ok((_view, plan)) => {
418                // Display the detailed execution plan tree
419                print!("{}", plan.format_tree());
420            }
421            Err(e) => {
422                // Fall back to simple execution plan display
423                eprintln!("Could not generate detailed execution plan: {}", e);
424                println!(
425                    "3. QUERY_EXECUTION [{:.3}ms]",
426                    exec_time.as_secs_f64() * 1000.0
427                );
428
429                // Parse query to understand what operations are being performed
430                use crate::sql::recursive_parser::Parser;
431                let mut parser = Parser::new(&config.query);
432                if let Ok(stmt) = parser.parse() {
433                    if stmt.where_clause.is_some() {
434                        println!("   • WHERE clause filtering applied");
435                        println!("   • Rows after filter: {}", result.dataview.row_count());
436                    }
437
438                    if let Some(ref order_by) = stmt.order_by {
439                        println!("   • ORDER BY: {} column(s)", order_by.len());
440                    }
441
442                    if let Some(ref group_by) = stmt.group_by {
443                        println!("   • GROUP BY: {} column(s)", group_by.len());
444                    }
445
446                    if let Some(limit) = stmt.limit {
447                        println!("   • LIMIT: {} rows", limit);
448                    }
449
450                    if stmt.distinct {
451                        println!("   • DISTINCT applied");
452                    }
453                }
454            }
455        }
456
457        println!("\nExecution Statistics:");
458        println!(
459            "  Preparation:    {:.3}ms",
460            (exec_start - start_time).as_secs_f64() * 1000.0
461        );
462        println!(
463            "  Query time:     {:.3}ms",
464            exec_time.as_secs_f64() * 1000.0
465        );
466        println!(
467            "  Total time:     {:.3}ms",
468            query_time.as_secs_f64() * 1000.0
469        );
470        println!("  Rows returned:  {}", result.dataview.row_count());
471        println!("  Columns:        {}", result.dataview.column_count());
472        println!("\n=== END EXECUTION PLAN ===");
473        println!();
474    }
475
476    // 4. Apply limit if specified
477    let final_view = if let Some(limit) = config.limit {
478        let limited_table = limit_results(&result.dataview, limit)?;
479        DataView::new(std::sync::Arc::new(limited_table))
480    } else {
481        result.dataview
482    };
483
484    // 5. Output debug trace if enabled
485    if let Some(ref trace_output) = result.debug_trace {
486        eprintln!("{}", trace_output);
487    }
488
489    // 6. Output the results
490    let output_result = if let Some(ref path) = config.output_file {
491        let mut file = fs::File::create(path)
492            .with_context(|| format!("Failed to create output file: {path}"))?;
493        output_results(&final_view, config.output_format, &mut file)?;
494        info!("Results written to: {}", path);
495        Ok(())
496    } else {
497        output_results(&final_view, config.output_format, &mut io::stdout())?;
498        Ok(())
499    };
500
501    let total_time = start_time.elapsed();
502    debug!("Total execution time: {:?}", total_time);
503
504    // Print stats to stderr so they don't interfere with output
505    if config.output_file.is_none() {
506        eprintln!(
507            "\n# Query completed: {} rows in {:?}",
508            final_view.row_count(),
509            query_time
510        );
511    }
512
513    output_result
514}
515
516/// Execute a script file with multiple SQL statements separated by GO
517pub fn execute_script(config: NonInteractiveConfig) -> Result<()> {
518    let _start_time = Instant::now();
519
520    // Parse the script into individual statements
521    let parser = ScriptParser::new(&config.query);
522    let statements = parser.parse_and_validate()?;
523
524    info!("Found {} statements in script", statements.len());
525
526    // Determine data file to use (command-line overrides script hint)
527    let data_file = if !config.data_file.is_empty() {
528        // Command-line argument takes precedence
529        config.data_file.clone()
530    } else if let Some(hint) = parser.data_file_hint() {
531        // Use data file hint from script
532        info!("Using data file from script hint: {}", hint);
533
534        // Resolve relative paths relative to script file if provided
535        if let Some(script_path) = config.script_file.as_ref() {
536            let script_dir = std::path::Path::new(script_path)
537                .parent()
538                .unwrap_or(std::path::Path::new("."));
539            let hint_path = std::path::Path::new(hint);
540
541            if hint_path.is_relative() {
542                script_dir.join(hint_path).to_string_lossy().to_string()
543            } else {
544                hint.to_string()
545            }
546        } else {
547            hint.to_string()
548        }
549    } else {
550        String::new()
551    };
552
553    // Load the data file if provided, otherwise use DUAL
554    let (data_table, _is_dual) = if data_file.is_empty() {
555        // No data file provided, use DUAL table
556        info!("No data file provided, using DUAL table");
557        (DataTable::dual(), true)
558    } else {
559        // Check if file exists before trying to load
560        if !std::path::Path::new(&data_file).exists() {
561            anyhow::bail!(
562                "Data file not found: {}\n\
563                Please check the path is correct",
564                data_file
565            );
566        }
567
568        info!("Loading data from: {}", data_file);
569        let table = load_data_file(&data_file)?;
570        info!(
571            "Loaded {} rows with {} columns",
572            table.row_count(),
573            table.column_count()
574        );
575        (table, false)
576    };
577
578    // Track script results
579    let mut script_result = ScriptResult::new();
580    let mut output = Vec::new();
581
582    // Create Arc<DataTable> once for all statements - avoids expensive cloning
583    let arc_data_table = std::sync::Arc::new(data_table);
584
585    // Execute each statement
586    for (idx, statement) in statements.iter().enumerate() {
587        let statement_num = idx + 1;
588        let stmt_start = Instant::now();
589
590        // Print separator for table format
591        if matches!(config.output_format, OutputFormat::Table) {
592            if idx > 0 {
593                output.push(String::new()); // Empty line between queries
594            }
595            output.push(format!("-- Query {} --", statement_num));
596        }
597
598        // Create a fresh DataView for each statement (reuses the Arc)
599        let dataview = DataView::new(arc_data_table.clone());
600
601        // Execute the statement
602        let service = QueryExecutionService::new(config.case_insensitive, config.auto_hide_empty);
603        match service.execute(statement, Some(&dataview), None) {
604            Ok(result) => {
605                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
606                let final_view = result.dataview;
607
608                // Format the output based on the output format
609                let mut statement_output = Vec::new();
610                match config.output_format {
611                    OutputFormat::Csv => {
612                        output_csv(&final_view, &mut statement_output, ',')?;
613                    }
614                    OutputFormat::Json => {
615                        output_json(&final_view, &mut statement_output)?;
616                    }
617                    OutputFormat::Table => {
618                        output_table(&final_view, &mut statement_output)?;
619                        writeln!(
620                            &mut statement_output,
621                            "Query completed: {} rows in {:.2}ms",
622                            final_view.row_count(),
623                            exec_time
624                        )?;
625                    }
626                    OutputFormat::Tsv => {
627                        output_csv(&final_view, &mut statement_output, '\t')?;
628                    }
629                }
630
631                // Add to overall output
632                output.extend(
633                    String::from_utf8_lossy(&statement_output)
634                        .lines()
635                        .map(String::from),
636                );
637
638                script_result.add_success(
639                    statement_num,
640                    statement.clone(),
641                    final_view.row_count(),
642                    exec_time,
643                );
644            }
645            Err(e) => {
646                let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
647                let error_msg = format!("Query {} failed: {}", statement_num, e);
648
649                if matches!(config.output_format, OutputFormat::Table) {
650                    output.push(error_msg.clone());
651                }
652
653                script_result.add_failure(
654                    statement_num,
655                    statement.clone(),
656                    e.to_string(),
657                    exec_time,
658                );
659
660                // Continue to next statement (don't stop on error)
661            }
662        }
663    }
664
665    // Write output
666    if let Some(ref output_file) = config.output_file {
667        let mut file = fs::File::create(output_file)?;
668        for line in &output {
669            writeln!(file, "{}", line)?;
670        }
671        info!("Results written to: {}", output_file);
672    } else {
673        for line in &output {
674            println!("{}", line);
675        }
676    }
677
678    // Print summary if in table mode
679    if matches!(config.output_format, OutputFormat::Table) {
680        println!("\n=== Script Summary ===");
681        println!("Total statements: {}", script_result.total_statements);
682        println!("Successful: {}", script_result.successful_statements);
683        println!("Failed: {}", script_result.failed_statements);
684        println!(
685            "Total execution time: {:.2}ms",
686            script_result.total_execution_time_ms
687        );
688    }
689
690    if !script_result.all_successful() {
691        return Err(anyhow::anyhow!(
692            "{} of {} statements failed",
693            script_result.failed_statements,
694            script_result.total_statements
695        ));
696    }
697
698    Ok(())
699}
700
701/// Load a data file (CSV or JSON) into a `DataTable`
702fn load_data_file(path: &str) -> Result<DataTable> {
703    let path = Path::new(path);
704
705    if !path.exists() {
706        return Err(anyhow::anyhow!("File not found: {}", path.display()));
707    }
708
709    // Determine file type by extension
710    let extension = path
711        .extension()
712        .and_then(|ext| ext.to_str())
713        .map(str::to_lowercase)
714        .unwrap_or_default();
715
716    let table_name = path
717        .file_stem()
718        .and_then(|stem| stem.to_str())
719        .unwrap_or("data")
720        .to_string();
721
722    match extension.as_str() {
723        "csv" => load_csv_to_datatable(path, &table_name)
724            .with_context(|| format!("Failed to load CSV file: {}", path.display())),
725        "json" => load_json_to_datatable(path, &table_name)
726            .with_context(|| format!("Failed to load JSON file: {}", path.display())),
727        _ => Err(anyhow::anyhow!(
728            "Unsupported file type: {}. Use .csv or .json",
729            extension
730        )),
731    }
732}
733
734/// Limit the number of rows in results
735fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
736    let source = dataview.source();
737    let mut limited_table = DataTable::new(&source.name);
738
739    // Copy columns
740    for col in &source.columns {
741        limited_table.add_column(col.clone());
742    }
743
744    // Copy limited rows
745    let rows_to_copy = dataview.row_count().min(limit);
746    for i in 0..rows_to_copy {
747        if let Some(row) = dataview.get_row(i) {
748            limited_table.add_row(row.clone());
749        }
750    }
751
752    Ok(limited_table)
753}
754
755/// Output query results in the specified format
756fn output_results<W: Write>(
757    dataview: &DataView,
758    format: OutputFormat,
759    writer: &mut W,
760) -> Result<()> {
761    match format {
762        OutputFormat::Csv => output_csv(dataview, writer, ','),
763        OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
764        OutputFormat::Json => output_json(dataview, writer),
765        OutputFormat::Table => output_table(dataview, writer),
766    }
767}
768
769/// Output results as CSV/TSV
770fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
771    // Write headers
772    let columns = dataview.column_names();
773    for (i, col) in columns.iter().enumerate() {
774        if i > 0 {
775            write!(writer, "{delimiter}")?;
776        }
777        write!(writer, "{}", escape_csv_field(col, delimiter))?;
778    }
779    writeln!(writer)?;
780
781    // Write rows
782    for row_idx in 0..dataview.row_count() {
783        if let Some(row) = dataview.get_row(row_idx) {
784            for (i, value) in row.values.iter().enumerate() {
785                if i > 0 {
786                    write!(writer, "{delimiter}")?;
787                }
788                write!(
789                    writer,
790                    "{}",
791                    escape_csv_field(&format_value(value), delimiter)
792                )?;
793            }
794            writeln!(writer)?;
795        }
796    }
797
798    Ok(())
799}
800
801/// Output results as JSON
802fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
803    let columns = dataview.column_names();
804    let mut rows = Vec::new();
805
806    for row_idx in 0..dataview.row_count() {
807        if let Some(row) = dataview.get_row(row_idx) {
808            let mut json_row = serde_json::Map::new();
809            for (col_idx, value) in row.values.iter().enumerate() {
810                if col_idx < columns.len() {
811                    json_row.insert(columns[col_idx].clone(), value_to_json(value));
812                }
813            }
814            rows.push(serde_json::Value::Object(json_row));
815        }
816    }
817
818    let json = serde_json::to_string_pretty(&rows)?;
819    writeln!(writer, "{json}")?;
820
821    Ok(())
822}
823
824/// Output results as an ASCII table
825fn output_table<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
826    let columns = dataview.column_names();
827
828    // Calculate column widths
829    let mut widths = vec![0; columns.len()];
830    for (i, col) in columns.iter().enumerate() {
831        widths[i] = col.len();
832    }
833
834    // Check first 100 rows for width calculation
835    let sample_size = dataview.row_count().min(100);
836    for row_idx in 0..sample_size {
837        if let Some(row) = dataview.get_row(row_idx) {
838            for (i, value) in row.values.iter().enumerate() {
839                if i < widths.len() {
840                    let value_str = format_value(value);
841                    widths[i] = widths[i].max(value_str.len());
842                }
843            }
844        }
845    }
846
847    // Limit column widths to 50 characters
848    for width in &mut widths {
849        *width = (*width).min(50);
850    }
851
852    // Print header separator
853    write!(writer, "+")?;
854    for width in &widths {
855        write!(writer, "-{}-+", "-".repeat(*width))?;
856    }
857    writeln!(writer)?;
858
859    // Print headers
860    write!(writer, "|")?;
861    for (i, col) in columns.iter().enumerate() {
862        write!(writer, " {:^width$} |", col, width = widths[i])?;
863    }
864    writeln!(writer)?;
865
866    // Print header separator
867    write!(writer, "+")?;
868    for width in &widths {
869        write!(writer, "-{}-+", "-".repeat(*width))?;
870    }
871    writeln!(writer)?;
872
873    // Print rows
874    for row_idx in 0..dataview.row_count() {
875        if let Some(row) = dataview.get_row(row_idx) {
876            write!(writer, "|")?;
877            for (i, value) in row.values.iter().enumerate() {
878                if i < widths.len() {
879                    let value_str = format_value(value);
880                    let truncated = if value_str.len() > widths[i] {
881                        format!("{}...", &value_str[..widths[i] - 3])
882                    } else {
883                        value_str
884                    };
885                    write!(writer, " {:<width$} |", truncated, width = widths[i])?;
886                }
887            }
888            writeln!(writer)?;
889        }
890    }
891
892    // Print bottom separator
893    write!(writer, "+")?;
894    for width in &widths {
895        write!(writer, "-{}-+", "-".repeat(*width))?;
896    }
897    writeln!(writer)?;
898
899    Ok(())
900}
901
902/// Format a `DataValue` for display
903fn format_value(value: &DataValue) -> String {
904    match value {
905        DataValue::Null => String::new(),
906        DataValue::Integer(i) => i.to_string(),
907        DataValue::Float(f) => f.to_string(),
908        DataValue::String(s) => s.clone(),
909        DataValue::InternedString(s) => s.to_string(),
910        DataValue::Boolean(b) => b.to_string(),
911        DataValue::DateTime(dt) => dt.to_string(),
912    }
913}
914
915/// Convert `DataValue` to JSON
916fn value_to_json(value: &DataValue) -> serde_json::Value {
917    match value {
918        DataValue::Null => serde_json::Value::Null,
919        DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
920        DataValue::Float(f) => {
921            if let Some(n) = serde_json::Number::from_f64(*f) {
922                serde_json::Value::Number(n)
923            } else {
924                serde_json::Value::Null
925            }
926        }
927        DataValue::String(s) => serde_json::Value::String(s.clone()),
928        DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
929        DataValue::Boolean(b) => serde_json::Value::Bool(*b),
930        DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
931    }
932}
933
934/// Escape a CSV field if it contains special characters
935fn escape_csv_field(field: &str, delimiter: char) -> String {
936    if field.contains(delimiter)
937        || field.contains('"')
938        || field.contains('\n')
939        || field.contains('\r')
940    {
941        format!("\"{}\"", field.replace('"', "\"\""))
942    } else {
943        field.to_string()
944    }
945}