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_json_to_datatable;
14use crate::services::query_execution_service::QueryExecutionService;
17use crate::sql::parser::ast::{CTEType, TableSource, CTE};
18use crate::sql::recursive_parser::Parser;
19use crate::sql::script_parser::{ScriptParser, ScriptResult};
20use crate::utils::string_utils::display_width;
21
22fn check_temp_table_usage(query: &str) -> Result<()> {
25 use crate::sql::recursive_parser::Parser;
26
27 let mut parser = Parser::new(query);
28 match parser.parse() {
29 Ok(stmt) => {
30 if let Some(from_table) = &stmt.from_table {
32 if from_table.starts_with('#') {
33 anyhow::bail!(
34 "Temporary table '{}' cannot be used in single-query mode. \
35 Temporary tables are only available in script mode (using -f flag with GO separators).",
36 from_table
37 );
38 }
39 }
40
41 if let Some(into_table) = &stmt.into_table {
43 anyhow::bail!(
44 "INTO clause for temporary table '{}' is only supported in script mode. \
45 Use -f flag with GO separators to create temporary tables.",
46 into_table.name
47 );
48 }
49
50 Ok(())
51 }
52 Err(_) => {
53 Ok(())
55 }
56 }
57}
58
59fn extract_cte_dependencies(cte: &CTE) -> Vec<String> {
61 let mut deps = Vec::new();
64
65 if let CTEType::Standard(query) = &cte.cte_type {
66 if let Some(from_table) = &query.from_table {
67 deps.push(from_table.clone());
68 }
69
70 for join in &query.joins {
72 match &join.table {
73 TableSource::Table(table_name) => {
74 deps.push(table_name.clone());
75 }
76 TableSource::DerivedTable { alias, .. } => {
77 deps.push(alias.clone());
78 }
79 TableSource::Pivot { alias, .. } => {
80 if let Some(pivot_alias) = alias {
82 deps.push(pivot_alias.clone());
83 }
84 }
85 }
86 }
87 }
88
89 deps
90}
91
92#[derive(Debug, Clone)]
94pub enum OutputFormat {
95 Csv,
96 Json,
97 JsonStructured, Table,
99 Tsv,
100}
101
102impl OutputFormat {
103 pub fn from_str(s: &str) -> Result<Self> {
104 match s.to_lowercase().as_str() {
105 "csv" => Ok(OutputFormat::Csv),
106 "json" => Ok(OutputFormat::Json),
107 "json-structured" => Ok(OutputFormat::JsonStructured),
108 "table" => Ok(OutputFormat::Table),
109 "tsv" => Ok(OutputFormat::Tsv),
110 _ => Err(anyhow::anyhow!(
111 "Invalid output format: {}. Use csv, json, json-structured, table, or tsv",
112 s
113 )),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy)]
120pub enum TableStyle {
121 Default,
123 AsciiFull,
125 AsciiCondensed,
127 AsciiBordersOnly,
129 AsciiHorizontalOnly,
131 AsciiNoBorders,
133 Markdown,
135 Utf8Full,
137 Utf8Condensed,
139 Utf8BordersOnly,
141 Utf8HorizontalOnly,
143 Utf8NoBorders,
145 Plain,
147}
148
149impl TableStyle {
150 pub fn from_str(s: &str) -> Result<Self> {
151 match s.to_lowercase().as_str() {
152 "default" => Ok(TableStyle::Default),
153 "ascii" | "ascii-full" => Ok(TableStyle::AsciiFull),
154 "ascii-condensed" => Ok(TableStyle::AsciiCondensed),
155 "ascii-borders" | "ascii-borders-only" => Ok(TableStyle::AsciiBordersOnly),
156 "ascii-horizontal" | "ascii-horizontal-only" => Ok(TableStyle::AsciiHorizontalOnly),
157 "ascii-noborders" | "ascii-no-borders" => Ok(TableStyle::AsciiNoBorders),
158 "markdown" | "md" => Ok(TableStyle::Markdown),
159 "utf8" | "utf8-full" => Ok(TableStyle::Utf8Full),
160 "utf8-condensed" => Ok(TableStyle::Utf8Condensed),
161 "utf8-borders" | "utf8-borders-only" => Ok(TableStyle::Utf8BordersOnly),
162 "utf8-horizontal" | "utf8-horizontal-only" => Ok(TableStyle::Utf8HorizontalOnly),
163 "utf8-noborders" | "utf8-no-borders" => Ok(TableStyle::Utf8NoBorders),
164 "plain" | "none" => Ok(TableStyle::Plain),
165 _ => Err(anyhow::anyhow!(
166 "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",
167 s
168 )),
169 }
170 }
171
172 pub fn list_styles() -> &'static str {
173 "Available table styles:
174 default - Current default ASCII style
175 ascii - ASCII with full borders
176 ascii-condensed - ASCII with condensed rows
177 ascii-borders - ASCII with outer borders only
178 ascii-horizontal - ASCII with horizontal lines only
179 ascii-noborders - ASCII with no borders
180 markdown - GitHub-flavored Markdown table
181 utf8 - UTF8 box-drawing characters
182 utf8-condensed - UTF8 with condensed rows
183 utf8-borders - UTF8 with outer borders only
184 utf8-horizontal - UTF8 with horizontal lines only
185 utf8-noborders - UTF8 with no borders
186 plain - No formatting, data only"
187 }
188}
189
190pub struct NonInteractiveConfig {
192 pub data_file: String,
193 pub query: String,
194 pub output_format: OutputFormat,
195 pub output_file: Option<String>,
196 pub case_insensitive: bool,
197 pub auto_hide_empty: bool,
198 pub limit: Option<usize>,
199 pub query_plan: bool,
200 pub show_work_units: bool,
201 pub execution_plan: bool,
202 pub show_preprocessing: bool,
203 pub show_transformations: bool,
204 pub cte_info: bool,
205 pub rewrite_analysis: bool,
206 pub lift_in_expressions: bool,
207 pub script_file: Option<String>, pub debug_trace: bool,
209 pub max_col_width: Option<usize>, pub col_sample_rows: usize, pub table_style: TableStyle, pub styled: bool, pub style_file: Option<String>, pub no_where_expansion: bool, pub no_group_by_expansion: bool, pub no_having_expansion: bool, pub no_order_by_expansion: bool, pub no_qualify_to_where: bool, pub no_expression_lifter: bool, pub no_cte_hoister: bool, pub no_in_lifter: bool, pub delimiter_override: Option<u8>, }
224
225fn make_transformer_config(config: &NonInteractiveConfig) -> crate::query_plan::TransformerConfig {
227 crate::query_plan::TransformerConfig {
228 enable_pivot_expander: true, enable_expression_lifter: !config.no_expression_lifter,
230 enable_where_expansion: !config.no_where_expansion,
231 enable_group_by_expansion: !config.no_group_by_expansion,
232 enable_having_expansion: !config.no_having_expansion,
233 enable_order_by_expansion: !config.no_order_by_expansion,
234 enable_qualify_to_where: !config.no_qualify_to_where,
235 enable_ilike_to_like: true, enable_cte_hoister: !config.no_cte_hoister,
237 enable_in_lifter: !config.no_in_lifter,
238 }
239}
240
241pub fn execute_non_interactive(config: NonInteractiveConfig) -> Result<()> {
243 let start_time = Instant::now();
244
245 let data_file_to_use = if config.data_file.is_empty() {
250 let lines: Vec<&str> = config.query.lines().take(10).collect();
252 let mut hint_path: Option<String> = None;
253
254 for line in lines {
255 let trimmed = line.trim();
256 if trimmed.starts_with("-- #!") {
257 hint_path = Some(trimmed[5..].trim().to_string());
258 break;
259 }
260 }
261
262 if let Some(path) = hint_path {
263 debug!("Found data file hint: {}", path);
264 let resolved_path = if path.starts_with("../") {
266 std::path::Path::new(&path).to_path_buf()
268 } else if path.starts_with("data/") {
269 std::path::Path::new(&path).to_path_buf()
271 } else {
272 std::path::Path::new(&path).to_path_buf()
273 };
274
275 if resolved_path.exists() {
276 info!("Using data file from hint: {:?}", resolved_path);
277 resolved_path.to_string_lossy().to_string()
278 } else {
279 debug!("Data file hint path does not exist: {:?}", resolved_path);
280 String::new()
281 }
282 } else {
283 String::new()
284 }
285 } else {
286 config.data_file.clone()
287 };
288
289 let (data_table, _is_dual) = if data_file_to_use.is_empty() {
291 info!("No data file provided, using DUAL table");
292 (crate::data::datatable::DataTable::dual(), true)
293 } else {
294 info!("Loading data from: {}", data_file_to_use);
295 let table = load_data_file(&data_file_to_use, config.delimiter_override)?;
296 info!(
297 "Loaded {} rows with {} columns",
298 table.row_count(),
299 table.column_count()
300 );
301 (table, false)
302 };
303 let _table_name = data_table.name.clone();
304
305 use crate::execution::{ExecutionConfig, ExecutionContext, StatementExecutor};
307 let mut context = ExecutionContext::new(std::sync::Arc::new(data_table));
308
309 let dataview = DataView::new(context.source_table.clone());
311
312 info!("Executing query: {}", config.query);
314
315 if config.execution_plan {
317 println!("\n=== EXECUTION PLAN ===");
318 println!("Query: {}", config.query);
319 println!("\nExecution Steps:");
320 println!("1. PARSE - Parse SQL query");
321 println!("2. LOAD_DATA - Load data from {}", &config.data_file);
322 println!(
323 " • Loaded {} rows, {} columns",
324 dataview.row_count(),
325 dataview.column_count()
326 );
327 }
328
329 if config.show_work_units {
331 use crate::query_plan::{ExpressionLifter, QueryAnalyzer};
332 use crate::sql::recursive_parser::Parser;
333
334 let mut parser = Parser::new(&config.query);
335 match parser.parse() {
336 Ok(stmt) => {
337 let mut analyzer = QueryAnalyzer::new();
338 let mut lifter = ExpressionLifter::new();
339
340 let mut stmt_copy = stmt.clone();
342 let lifted = lifter.lift_expressions(&mut stmt_copy);
343
344 match analyzer.analyze(&stmt_copy, config.query.clone()) {
346 Ok(plan) => {
347 println!("\n{}", plan.explain());
348
349 if !lifted.is_empty() {
350 println!("\nLifted CTEs:");
351 for cte in &lifted {
352 println!(" - {}", cte.name);
353 }
354 }
355
356 return Ok(());
357 }
358 Err(e) => {
359 eprintln!("Error analyzing query: {}", e);
360 return Err(anyhow::anyhow!("Query analysis failed: {}", e));
361 }
362 }
363 }
364 Err(e) => {
365 eprintln!("Error parsing query: {}", e);
366 return Err(anyhow::anyhow!("Parse error: {}", e));
367 }
368 }
369 }
370
371 if config.query_plan {
373 use crate::sql::recursive_parser::Parser;
374 let mut parser = Parser::new(&config.query);
375 match parser.parse() {
376 Ok(statement) => {
377 println!("\n=== QUERY PLAN (AST) ===");
378 println!("{statement:#?}");
379 println!("=== END QUERY PLAN ===\n");
380 }
381 Err(e) => {
382 eprintln!("Failed to parse query for plan: {e}");
383 }
384 }
385 }
386
387 if config.rewrite_analysis {
389 use crate::sql::query_rewriter::{QueryRewriter, RewriteAnalysis};
390 use crate::sql::recursive_parser::Parser;
391 use serde_json::json;
392
393 let mut parser = Parser::new(&config.query);
394 match parser.parse() {
395 Ok(statement) => {
396 let mut rewriter = QueryRewriter::new();
397 let suggestions = rewriter.analyze(&statement);
398
399 let analysis = RewriteAnalysis::from_suggestions(suggestions);
400 println!("{}", serde_json::to_string_pretty(&analysis).unwrap());
401 return Ok(());
402 }
403 Err(e) => {
404 let output = json!({
405 "success": false,
406 "error": format!("{}", e),
407 "suggestions": [],
408 "can_auto_rewrite": false,
409 });
410 println!("{}", serde_json::to_string_pretty(&output).unwrap());
411 return Ok(());
412 }
413 }
414 }
415
416 if config.cte_info {
418 use crate::sql::recursive_parser::Parser;
419 use serde_json::json;
420
421 let mut parser = Parser::new(&config.query);
422 match parser.parse() {
423 Ok(statement) => {
424 let mut cte_info = Vec::new();
425
426 for (index, cte) in statement.ctes.iter().enumerate() {
428 let cte_json = json!({
429 "index": index,
430 "name": cte.name,
431 "columns": cte.column_list,
432 "dependencies": extract_cte_dependencies(cte),
435 });
436 cte_info.push(cte_json);
437 }
438
439 let output = json!({
440 "success": true,
441 "ctes": cte_info,
442 "total": statement.ctes.len(),
443 "has_final_select": !statement.columns.is_empty() || !statement.select_items.is_empty(),
444 });
445
446 println!("{}", serde_json::to_string_pretty(&output).unwrap());
447 return Ok(());
448 }
449 Err(e) => {
450 let output = json!({
451 "success": false,
452 "error": format!("{}", e),
453 "ctes": [],
454 "total": 0,
455 });
456 println!("{}", serde_json::to_string_pretty(&output).unwrap());
457 return Ok(());
458 }
459 }
460 }
461
462 let query_start = Instant::now();
463
464 let app_config = Config::load().unwrap_or_else(|e| {
466 debug!("Could not load config file: {}. Using defaults.", e);
467 Config::default()
468 });
469
470 crate::config::global::init_config(app_config.clone());
472
473 let exec_config = ExecutionConfig::from_cli_flags(
475 config.show_preprocessing,
476 config.show_transformations,
477 config.case_insensitive,
478 config.auto_hide_empty,
479 config.no_expression_lifter,
480 config.no_where_expansion,
481 config.no_group_by_expansion,
482 config.no_having_expansion,
483 config.no_order_by_expansion,
484 config.no_qualify_to_where,
485 config.no_cte_hoister,
486 config.no_in_lifter,
487 config.debug_trace,
488 );
489
490 let executor = StatementExecutor::with_config(exec_config);
492
493 let exec_start = Instant::now();
495 let result = {
496 use crate::sql::recursive_parser::Parser;
497
498 use crate::sql::template_expander::TemplateExpander;
500 let expander = TemplateExpander::new(&context.temp_tables);
501
502 let query_to_parse = match expander.parse_templates(&config.query) {
503 Ok(vars) => {
504 if vars.is_empty() {
505 config.query.clone()
506 } else {
507 match expander.expand(&config.query, &vars) {
508 Ok(expanded) => {
509 debug!(
510 "Template expansion in single query mode: {} vars expanded",
511 vars.len()
512 );
513 for var in &vars {
514 debug!(" {} -> resolved", var.placeholder);
515 }
516 expanded
517 }
518 Err(e) => {
519 return Err(anyhow::anyhow!(
520 "Template expansion failed: {}. Available tables: {}",
521 e,
522 context.temp_tables.list_tables().join(", ")
523 ));
524 }
525 }
526 }
527 }
528 Err(e) => {
529 return Err(anyhow::anyhow!("Template parsing failed: {}", e));
530 }
531 };
532
533 let mut parser = Parser::new(&query_to_parse);
534 match parser.parse() {
535 Ok(stmt) => {
536 match executor.execute(stmt, &mut context) {
542 Ok(exec_result) => {
543 Ok(
545 crate::services::query_execution_service::QueryExecutionResult {
546 dataview: exec_result.dataview,
547 stats: crate::services::query_execution_service::QueryStats {
548 row_count: exec_result.stats.row_count,
549 column_count: exec_result.stats.column_count,
550 execution_time: exec_start.elapsed(),
551 query_engine_time: exec_start.elapsed(),
552 },
553 hidden_columns: Vec::new(),
554 query: config.query.clone(),
555 execution_plan: None,
556 debug_trace: None,
557 },
558 )
559 }
560 Err(e) => Err(e),
561 }
562 }
563 Err(e) => {
564 Err(anyhow::anyhow!("Parse error: {}", e))
566 }
567 }
568 }?;
569 let exec_time = exec_start.elapsed();
570
571 let query_time = query_start.elapsed();
572 info!("Query executed in {:?}", query_time);
573 info!(
574 "Result: {} rows, {} columns",
575 result.dataview.row_count(),
576 result.dataview.column_count()
577 );
578
579 if config.execution_plan {
581 use crate::data::query_engine::QueryEngine;
583
584 let query_engine = QueryEngine::new();
585
586 match query_engine.execute_with_plan(
587 std::sync::Arc::new(dataview.source().clone()),
588 &config.query,
589 ) {
590 Ok((_view, plan)) => {
591 print!("{}", plan.format_tree());
593 }
594 Err(e) => {
595 eprintln!("Could not generate detailed execution plan: {}", e);
597 println!(
598 "3. QUERY_EXECUTION [{:.3}ms]",
599 exec_time.as_secs_f64() * 1000.0
600 );
601
602 use crate::sql::recursive_parser::Parser;
604 let mut parser = Parser::new(&config.query);
605 if let Ok(stmt) = parser.parse() {
606 if stmt.where_clause.is_some() {
607 println!(" • WHERE clause filtering applied");
608 println!(" • Rows after filter: {}", result.dataview.row_count());
609 }
610
611 if let Some(ref order_by) = stmt.order_by {
612 println!(" • ORDER BY: {} column(s)", order_by.len());
613 }
614
615 if let Some(ref group_by) = stmt.group_by {
616 println!(" • GROUP BY: {} column(s)", group_by.len());
617 }
618
619 if let Some(limit) = stmt.limit {
620 println!(" • LIMIT: {} rows", limit);
621 }
622
623 if stmt.distinct {
624 println!(" • DISTINCT applied");
625 }
626 }
627 }
628 }
629
630 println!("\nExecution Statistics:");
631 println!(
632 " Preparation: {:.3}ms",
633 (exec_start - start_time).as_secs_f64() * 1000.0
634 );
635 println!(
636 " Query time: {:.3}ms",
637 exec_time.as_secs_f64() * 1000.0
638 );
639 println!(
640 " Total time: {:.3}ms",
641 query_time.as_secs_f64() * 1000.0
642 );
643 println!(" Rows returned: {}", result.dataview.row_count());
644 println!(" Columns: {}", result.dataview.column_count());
645 println!("\n=== END EXECUTION PLAN ===");
646 println!();
647 }
648
649 let final_view = if let Some(limit) = config.limit {
651 let limited_table = limit_results(&result.dataview, limit)?;
652 DataView::new(std::sync::Arc::new(limited_table))
653 } else {
654 result.dataview
655 };
656
657 if let Some(ref trace_output) = result.debug_trace {
659 eprintln!("{}", trace_output);
660 }
661
662 let exec_time_ms = exec_time.as_secs_f64() * 1000.0;
664 let output_result = if let Some(ref path) = config.output_file {
665 let mut file = fs::File::create(path)
666 .with_context(|| format!("Failed to create output file: {path}"))?;
667 output_results(
668 &final_view,
669 config.output_format,
670 &mut file,
671 config.max_col_width,
672 config.col_sample_rows,
673 exec_time_ms,
674 config.table_style,
675 config.styled,
676 config.style_file.as_deref(),
677 )?;
678 info!("Results written to: {}", path);
679 Ok(())
680 } else {
681 output_results(
682 &final_view,
683 config.output_format,
684 &mut io::stdout(),
685 config.max_col_width,
686 config.col_sample_rows,
687 exec_time_ms,
688 config.table_style,
689 config.styled,
690 config.style_file.as_deref(),
691 )?;
692 Ok(())
693 };
694
695 let total_time = start_time.elapsed();
696 debug!("Total execution time: {:?}", total_time);
697
698 if config.output_file.is_none() {
700 eprintln!(
701 "\n# Query completed: {} rows in {:?}",
702 final_view.row_count(),
703 query_time
704 );
705 }
706
707 output_result
708}
709
710pub fn execute_script(config: NonInteractiveConfig) -> Result<()> {
712 let _start_time = Instant::now();
713
714 let parser = ScriptParser::new(&config.query);
716 let script_statements = parser.parse_script_statements();
717
718 if script_statements.is_empty() {
719 anyhow::bail!("No statements found in script");
720 }
721
722 info!("Found {} statements in script", script_statements.len());
723
724 let data_file = if !config.data_file.is_empty() {
726 config.data_file.clone()
728 } else if let Some(hint) = parser.data_file_hint() {
729 info!("Using data file from script hint: {}", hint);
731
732 if let Some(script_path) = config.script_file.as_ref() {
734 let script_dir = std::path::Path::new(script_path)
735 .parent()
736 .unwrap_or(std::path::Path::new("."));
737 let hint_path = std::path::Path::new(hint);
738
739 if hint_path.is_relative() {
740 script_dir.join(hint_path).to_string_lossy().to_string()
741 } else {
742 hint.to_string()
743 }
744 } else {
745 hint.to_string()
746 }
747 } else {
748 String::new()
749 };
750
751 let (data_table, _is_dual) = if data_file.is_empty() {
753 info!("No data file provided, using DUAL table");
755 (DataTable::dual(), true)
756 } else {
757 if !std::path::Path::new(&data_file).exists() {
759 anyhow::bail!(
760 "Data file not found: {}\n\
761 Please check the path is correct",
762 data_file
763 );
764 }
765
766 info!("Loading data from: {}", data_file);
767 let table = load_data_file(&data_file, config.delimiter_override)?;
768 info!(
769 "Loaded {} rows with {} columns",
770 table.row_count(),
771 table.column_count()
772 );
773 (table, false)
774 };
775
776 let mut script_result = ScriptResult::new();
778 let mut output = Vec::new();
779
780 use crate::execution::{ExecutionConfig, ExecutionContext, StatementExecutor};
782 let mut context = ExecutionContext::new(std::sync::Arc::new(data_table));
783
784 let exec_config = ExecutionConfig::from_cli_flags(
786 config.show_preprocessing,
787 config.show_transformations,
788 config.case_insensitive,
789 config.auto_hide_empty,
790 config.no_expression_lifter,
791 config.no_where_expansion,
792 config.no_group_by_expansion,
793 config.no_having_expansion,
794 config.no_order_by_expansion,
795 config.no_qualify_to_where,
796 config.no_cte_hoister,
797 config.no_in_lifter,
798 false, );
800
801 let executor = StatementExecutor::with_config(exec_config);
803
804 for (idx, script_stmt) in script_statements.iter().enumerate() {
806 let statement_num = idx + 1;
807 let stmt_start = Instant::now();
808
809 if script_stmt.is_exit() {
811 let exit_code = script_stmt.get_exit_code().unwrap_or(0);
812 info!("EXIT statement encountered (code: {})", exit_code);
813
814 if matches!(config.output_format, OutputFormat::Table) {
816 if idx > 0 {
817 output.push(String::new());
818 }
819 output.push(format!("-- Statement {} --", statement_num));
820 output.push(format!("Script execution stopped by EXIT {}", exit_code));
821 }
822
823 script_result.add_success(
825 statement_num,
826 format!("EXIT {}", exit_code),
827 0,
828 stmt_start.elapsed().as_secs_f64() * 1000.0,
829 );
830
831 break;
833 }
834
835 if script_stmt.should_skip() {
837 info!(
838 "Skipping statement {} due to [SKIP] directive",
839 statement_num
840 );
841
842 if matches!(config.output_format, OutputFormat::Table) {
844 if idx > 0 {
845 output.push(String::new());
846 }
847 output.push(format!("-- Statement {} [SKIPPED] --", statement_num));
848 }
849
850 script_result.add_success(
852 statement_num,
853 "[SKIPPED]".to_string(),
854 0,
855 stmt_start.elapsed().as_secs_f64() * 1000.0,
856 );
857
858 continue;
859 }
860
861 let statement = match script_stmt.get_query() {
863 Some(sql) => sql,
864 None => continue, };
866
867 if matches!(config.output_format, OutputFormat::Table) {
869 if idx > 0 {
870 output.push(String::new()); }
872 output.push(format!("-- Query {} --", statement_num));
873 }
874
875 use crate::sql::template_expander::TemplateExpander;
878 let expander = TemplateExpander::new(&context.temp_tables);
879
880 let expanded_statement = match expander.parse_templates(statement) {
881 Ok(vars) => {
882 if vars.is_empty() {
883 statement.to_string()
884 } else {
885 match expander.expand(statement, &vars) {
886 Ok(expanded) => {
887 debug!("Expanded templates in SQL: {} vars found", vars.len());
888 for var in &vars {
889 debug!(
890 " {} -> expanding from {}",
891 var.placeholder, var.table_name
892 );
893 }
894 expanded
895 }
896 Err(e) => {
897 let msg =
898 format!("Query {} template expansion error: {}", statement_num, e);
899 if matches!(config.output_format, OutputFormat::Table) {
900 output.push(msg.clone());
901 } else {
902 eprintln!("{}", msg);
903 }
904 script_result.add_failure(
905 statement_num,
906 statement.to_string(),
907 msg,
908 stmt_start.elapsed().as_secs_f64() * 1000.0,
909 );
910 continue; }
912 }
913 }
914 }
915 Err(e) => {
916 let msg = format!("Query {} template parse error: {}", statement_num, e);
917 if matches!(config.output_format, OutputFormat::Table) {
918 output.push(msg.clone());
919 } else {
920 eprintln!("{}", msg);
921 }
922 script_result.add_failure(
923 statement_num,
924 statement.to_string(),
925 msg,
926 stmt_start.elapsed().as_secs_f64() * 1000.0,
927 );
928 continue; }
930 };
931
932 let statement = expanded_statement.as_str();
934
935 let mut parser = Parser::new(statement);
937 let parsed_stmt = match parser.parse() {
938 Ok(stmt) => stmt,
939 Err(e) => {
940 let msg = format!("Query {} parse error: {}", statement_num, e);
942 if matches!(config.output_format, OutputFormat::Table) {
943 output.push(msg.clone());
944 } else {
945 eprintln!("{}", msg);
946 }
947 script_result.add_failure(
948 statement_num,
949 statement.to_string(),
950 msg,
951 stmt_start.elapsed().as_secs_f64() * 1000.0,
952 );
953 break;
954 }
955 };
956
957 if let Some(from_table) = &parsed_stmt.from_table {
960 if from_table.starts_with('#') && !context.has_temp_table(from_table) {
961 let msg = format!(
962 "Query {} failed: Temporary table {} not found",
963 statement_num, from_table
964 );
965 if matches!(config.output_format, OutputFormat::Table) {
966 output.push(msg.clone());
967 } else {
968 eprintln!("{}", msg);
969 }
970 script_result.add_failure(
971 statement_num,
972 statement.to_string(),
973 msg,
974 stmt_start.elapsed().as_secs_f64() * 1000.0,
975 );
976 break;
977 }
978 }
979
980 let into_table = parsed_stmt.into_table.clone();
983 let stmt_without_into = if into_table.is_some() {
984 use crate::query_plan::IntoClauseRemover;
985 IntoClauseRemover::remove_into_clause(parsed_stmt)
986 } else {
987 parsed_stmt
988 };
989
990 let result = executor.execute(stmt_without_into, &mut context);
996
997 match result {
998 Ok(exec_result) => {
999 let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
1000 let final_view = exec_result.dataview;
1001
1002 if let Some(into_table) = &into_table {
1004 use crate::data::query_engine::QueryEngine;
1008 let engine = QueryEngine::with_case_insensitive(config.case_insensitive);
1009 let result_table = match engine.materialize_view(final_view.clone()) {
1010 Ok(table) => std::sync::Arc::new(table),
1011 Err(e) => {
1012 let msg = format!(
1013 "Query {} failed: could not materialize {}: {}",
1014 statement_num, into_table.name, e
1015 );
1016 if matches!(config.output_format, OutputFormat::Table) {
1017 output.push(msg.clone());
1018 } else {
1019 eprintln!("{}", msg);
1020 }
1021 script_result.add_failure(
1022 statement_num,
1023 statement.to_string(),
1024 msg,
1025 exec_time,
1026 );
1027 break;
1028 }
1029 };
1030 let row_count = result_table.row_count();
1031
1032 match context.store_temp_table(into_table.name.clone(), result_table) {
1034 Ok(_) => {
1035 info!(
1036 "Stored {} rows in temporary table {}",
1037 row_count, into_table.name
1038 );
1039
1040 if matches!(config.output_format, OutputFormat::Table) {
1043 let mut statement_output = Vec::new();
1044 writeln!(
1045 &mut statement_output,
1046 "({} rows affected) -> {}",
1047 row_count, into_table.name
1048 )?;
1049 output.extend(
1050 String::from_utf8_lossy(&statement_output)
1051 .lines()
1052 .map(String::from),
1053 );
1054 }
1055
1056 script_result.add_success(
1057 statement_num,
1058 statement.to_string(),
1059 row_count,
1060 exec_time,
1061 );
1062 continue; }
1064 Err(e) => {
1065 script_result.add_failure(
1066 statement_num,
1067 statement.to_string(),
1068 e.to_string(),
1069 exec_time,
1070 );
1071 break;
1072 }
1073 }
1074 }
1075
1076 let mut statement_output = Vec::new();
1078 match config.output_format {
1079 OutputFormat::Csv => {
1080 output_csv(&final_view, &mut statement_output, ',')?;
1081 }
1082 OutputFormat::Json => {
1083 output_json(&final_view, &mut statement_output)?;
1084 }
1085 OutputFormat::JsonStructured => {
1086 output_json_structured(&final_view, &mut statement_output, exec_time)?;
1087 }
1088 OutputFormat::Table => {
1089 output_table(
1090 &final_view,
1091 &mut statement_output,
1092 config.max_col_width,
1093 config.col_sample_rows,
1094 config.table_style,
1095 config.styled,
1096 config.style_file.as_deref(),
1097 )?;
1098 writeln!(
1099 &mut statement_output,
1100 "Query completed: {} rows in {:.2}ms",
1101 final_view.row_count(),
1102 exec_time
1103 )?;
1104 }
1105 OutputFormat::Tsv => {
1106 output_csv(&final_view, &mut statement_output, '\t')?;
1107 }
1108 }
1109
1110 output.extend(
1112 String::from_utf8_lossy(&statement_output)
1113 .lines()
1114 .map(String::from),
1115 );
1116
1117 script_result.add_success(
1118 statement_num,
1119 statement.to_string(),
1120 final_view.row_count(),
1121 exec_time,
1122 );
1123 }
1124 Err(e) => {
1125 let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
1126 let error_msg = format!("Query {} failed: {:#}", statement_num, e);
1127
1128 if matches!(config.output_format, OutputFormat::Table) {
1132 output.push(error_msg.clone());
1133 } else {
1134 eprintln!("{}", error_msg);
1135 }
1136
1137 script_result.add_failure(
1138 statement_num,
1139 statement.to_string(),
1140 e.to_string(),
1141 exec_time,
1142 );
1143
1144 }
1146 }
1147 }
1148
1149 if let Some(ref output_file) = config.output_file {
1151 let mut file = fs::File::create(output_file)?;
1152 for line in &output {
1153 writeln!(file, "{}", line)?;
1154 }
1155 info!("Results written to: {}", output_file);
1156 } else {
1157 for line in &output {
1158 println!("{}", line);
1159 }
1160 }
1161
1162 if matches!(config.output_format, OutputFormat::Table) {
1164 println!("\n=== Script Summary ===");
1165 println!("Total statements: {}", script_result.total_statements);
1166 println!("Successful: {}", script_result.successful_statements);
1167 println!("Failed: {}", script_result.failed_statements);
1168 println!(
1169 "Total execution time: {:.2}ms",
1170 script_result.total_execution_time_ms
1171 );
1172 }
1173
1174 if !script_result.all_successful() {
1175 return Err(anyhow::anyhow!(
1176 "{} of {} statements failed",
1177 script_result.failed_statements,
1178 script_result.total_statements
1179 ));
1180 }
1181
1182 Ok(())
1183}
1184
1185fn load_data_file(path: &str, delimiter_override: Option<u8>) -> Result<DataTable> {
1190 use crate::data::datatable_loaders::load_csv_to_datatable_with_opts;
1191 use crate::data::stream_loader::{resolve_delimiter, CsvReadOptions};
1192
1193 let path = Path::new(path);
1194
1195 if !path.exists() {
1196 return Err(anyhow::anyhow!("File not found: {}", path.display()));
1197 }
1198
1199 let extension = path
1201 .extension()
1202 .and_then(|ext| ext.to_str())
1203 .map(str::to_lowercase)
1204 .unwrap_or_default();
1205
1206 let table_name = path
1207 .file_stem()
1208 .and_then(|stem| stem.to_str())
1209 .unwrap_or("data")
1210 .to_string();
1211
1212 let is_csv_family =
1216 matches!(extension.as_str(), "csv" | "tsv" | "psv") || delimiter_override.is_some();
1217 if is_csv_family {
1218 let path_str = path.display().to_string();
1219 let opts = CsvReadOptions {
1220 delimiter: resolve_delimiter(&path_str, delimiter_override),
1221 has_headers: true,
1222 };
1223 return load_csv_to_datatable_with_opts(path, &table_name, &opts)
1224 .with_context(|| format!("Failed to load CSV-family file: {}", path.display()));
1225 }
1226
1227 match extension.as_str() {
1228 "json" | "jsonl" | "ndjson" => load_json_to_datatable(path, &table_name)
1229 .with_context(|| format!("Failed to load JSON file: {}", path.display())),
1230 _ => Err(anyhow::anyhow!(
1231 "Unsupported file type: {}. Use .csv, .tsv, .psv, .json, .jsonl, or .ndjson \
1232 (or pass --delimiter to force CSV parsing on an unknown extension)",
1233 extension
1234 )),
1235 }
1236}
1237
1238fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
1240 use crate::data::query_engine::QueryEngine;
1246 let name = dataview.source().name.clone();
1247 let mut limited_table =
1248 QueryEngine::new().materialize_view(dataview.clone().with_max_rows(limit))?;
1249 limited_table.name = name;
1250 Ok(limited_table)
1251}
1252
1253fn output_results<W: Write>(
1255 dataview: &DataView,
1256 format: OutputFormat,
1257 writer: &mut W,
1258 max_col_width: Option<usize>,
1259 col_sample_rows: usize,
1260 exec_time_ms: f64,
1261 table_style: TableStyle,
1262 styled: bool,
1263 style_file: Option<&str>,
1264) -> Result<()> {
1265 match format {
1266 OutputFormat::Csv => output_csv(dataview, writer, ','),
1267 OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
1268 OutputFormat::Json => output_json(dataview, writer),
1269 OutputFormat::JsonStructured => output_json_structured(dataview, writer, exec_time_ms),
1270 OutputFormat::Table => output_table(
1271 dataview,
1272 writer,
1273 max_col_width,
1274 col_sample_rows,
1275 table_style,
1276 styled,
1277 style_file,
1278 ),
1279 }
1280}
1281
1282fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
1284 let columns = dataview.column_names();
1286 for (i, col) in columns.iter().enumerate() {
1287 if i > 0 {
1288 write!(writer, "{delimiter}")?;
1289 }
1290 write!(writer, "{}", escape_csv_field(col, delimiter))?;
1291 }
1292 writeln!(writer)?;
1293
1294 for row_idx in 0..dataview.row_count() {
1296 if let Some(row) = dataview.get_row(row_idx) {
1297 for (i, value) in row.values.iter().enumerate() {
1298 if i > 0 {
1299 write!(writer, "{delimiter}")?;
1300 }
1301 write!(
1302 writer,
1303 "{}",
1304 escape_csv_field(&format_value(value), delimiter)
1305 )?;
1306 }
1307 writeln!(writer)?;
1308 }
1309 }
1310
1311 Ok(())
1312}
1313
1314fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
1316 let columns = dataview.column_names();
1317 let mut rows = Vec::new();
1318
1319 for row_idx in 0..dataview.row_count() {
1320 if let Some(row) = dataview.get_row(row_idx) {
1321 let mut json_row = serde_json::Map::new();
1322 for (col_idx, value) in row.values.iter().enumerate() {
1323 if col_idx < columns.len() {
1324 json_row.insert(columns[col_idx].clone(), value_to_json(value));
1325 }
1326 }
1327 rows.push(serde_json::Value::Object(json_row));
1328 }
1329 }
1330
1331 let json = serde_json::to_string_pretty(&rows)?;
1332 writeln!(writer, "{json}")?;
1333
1334 Ok(())
1335}
1336
1337fn output_json_structured<W: Write>(
1339 dataview: &DataView,
1340 writer: &mut W,
1341 exec_time: f64,
1342) -> Result<()> {
1343 let column_names = dataview.column_names();
1344 let data_table = dataview.source();
1345
1346 let mut columns = Vec::new();
1348 for (idx, name) in column_names.iter().enumerate() {
1349 let col_type = data_table
1350 .columns
1351 .get(idx)
1352 .map(|c| format!("{:?}", c.data_type))
1353 .unwrap_or_else(|| "UNKNOWN".to_string());
1354
1355 let mut max_width = name.len();
1357 for row_idx in 0..dataview.row_count() {
1358 if let Some(row) = dataview.get_row(row_idx) {
1359 if let Some(value) = row.values.get(idx) {
1360 let display_width = match value {
1361 DataValue::Null => 4, DataValue::Integer(i) => i.to_string().len(),
1363 DataValue::Float(f) => format!("{:.2}", f).len(),
1364 DataValue::String(s) => s.len(),
1365 DataValue::InternedString(s) => s.len(),
1366 DataValue::Boolean(b) => {
1367 if *b {
1368 4
1369 } else {
1370 5
1371 }
1372 } DataValue::DateTime(dt) => dt.len(),
1374 DataValue::Vector(v) => {
1375 let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1376 format!("[{}]", components.join(",")).len()
1377 }
1378 };
1379 max_width = max_width.max(display_width);
1380 }
1381 }
1382 }
1383
1384 let alignment = match data_table.columns.get(idx).map(|c| &c.data_type) {
1385 Some(crate::data::datatable::DataType::Integer) => "right",
1386 Some(crate::data::datatable::DataType::Float) => "right",
1387 _ => "left",
1388 };
1389
1390 let col_meta = serde_json::json!({
1391 "name": name,
1392 "type": col_type,
1393 "max_width": max_width,
1394 "alignment": alignment
1395 });
1396 columns.push(col_meta);
1397 }
1398
1399 let mut rows = Vec::new();
1401 for row_idx in 0..dataview.row_count() {
1402 if let Some(row) = dataview.get_row(row_idx) {
1403 let row_values: Vec<String> = row
1404 .values
1405 .iter()
1406 .map(|v| match v {
1407 DataValue::Null => String::new(),
1408 DataValue::Integer(i) => i.to_string(),
1409 DataValue::Float(f) => format!("{:.2}", f),
1410 DataValue::String(s) => s.clone(),
1411 DataValue::InternedString(s) => s.to_string(),
1412 DataValue::Boolean(b) => b.to_string(),
1413 DataValue::DateTime(dt) => dt.clone(),
1414 DataValue::Vector(v) => {
1415 let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1416 format!("[{}]", components.join(","))
1417 }
1418 })
1419 .collect();
1420 rows.push(serde_json::Value::Array(
1421 row_values
1422 .into_iter()
1423 .map(serde_json::Value::String)
1424 .collect(),
1425 ));
1426 }
1427 }
1428
1429 let output = serde_json::json!({
1431 "columns": columns,
1432 "rows": rows,
1433 "metadata": {
1434 "total_rows": dataview.row_count(),
1435 "query_time_ms": exec_time
1436 }
1437 });
1438
1439 let json = serde_json::to_string_pretty(&output)?;
1440 writeln!(writer, "{json}")?;
1441
1442 Ok(())
1443}
1444
1445fn output_table_old_style<W: Write>(
1447 dataview: &DataView,
1448 writer: &mut W,
1449 max_col_width: Option<usize>,
1450) -> Result<()> {
1451 let columns = dataview.column_names();
1452
1453 let mut widths = vec![0; columns.len()];
1455 for (i, col) in columns.iter().enumerate() {
1456 widths[i] = col.len();
1457 }
1458
1459 for row_idx in 0..dataview.row_count() {
1461 if let Some(row) = dataview.get_row(row_idx) {
1462 for (i, value) in row.values.iter().enumerate() {
1463 if i < widths.len() {
1464 let value_str = format_value(value);
1465 widths[i] = widths[i].max(display_width(&value_str));
1466 }
1467 }
1468 }
1469 }
1470
1471 if let Some(max_width) = max_col_width {
1473 for width in &mut widths {
1474 *width = (*width).min(max_width);
1475 }
1476 }
1477
1478 write!(writer, "+")?;
1480 for width in &widths {
1481 write!(writer, "{}", "-".repeat(*width + 2))?;
1482 write!(writer, "+")?;
1483 }
1484 writeln!(writer)?;
1485
1486 write!(writer, "|")?;
1488 for (i, col) in columns.iter().enumerate() {
1489 write!(writer, " {:^width$} |", col, width = widths[i])?;
1490 }
1491 writeln!(writer)?;
1492
1493 write!(writer, "+")?;
1495 for width in &widths {
1496 write!(writer, "{}", "-".repeat(*width + 2))?;
1497 write!(writer, "+")?;
1498 }
1499 writeln!(writer)?;
1500
1501 for row_idx in 0..dataview.row_count() {
1503 if let Some(row) = dataview.get_row(row_idx) {
1504 write!(writer, "|")?;
1505 for (i, value) in row.values.iter().enumerate() {
1506 if i < widths.len() {
1507 let value_str = format_value(value);
1508 let display_len = display_width(&value_str);
1509
1510 write!(writer, " {}", value_str)?;
1513 let padding_needed = if display_len < widths[i] {
1514 widths[i] - display_len
1515 } else {
1516 0
1517 };
1518 write!(writer, "{} |", " ".repeat(padding_needed))?;
1519 }
1520 }
1521 writeln!(writer)?;
1522 }
1523 }
1524
1525 write!(writer, "+")?;
1527 for width in &widths {
1528 write!(writer, "{}", "-".repeat(*width + 2))?;
1529 write!(writer, "+")?;
1530 }
1531 writeln!(writer)?;
1532
1533 Ok(())
1534}
1535
1536fn output_table<W: Write>(
1538 dataview: &DataView,
1539 writer: &mut W,
1540 max_col_width: Option<usize>,
1541 _col_sample_rows: usize, style: TableStyle,
1543 styled: bool,
1544 style_file: Option<&str>,
1545) -> Result<()> {
1546 let mut table = Table::new();
1547
1548 match style {
1550 TableStyle::Default => {
1551 return output_table_old_style(dataview, writer, max_col_width);
1554 }
1555 TableStyle::AsciiFull => {
1556 table.load_preset(ASCII_FULL);
1557 }
1558 TableStyle::AsciiCondensed => {
1559 table.load_preset(ASCII_FULL_CONDENSED);
1560 }
1561 TableStyle::AsciiBordersOnly => {
1562 table.load_preset(ASCII_BORDERS_ONLY);
1563 }
1564 TableStyle::AsciiHorizontalOnly => {
1565 table.load_preset(ASCII_HORIZONTAL_ONLY);
1566 }
1567 TableStyle::AsciiNoBorders => {
1568 table.load_preset(ASCII_NO_BORDERS);
1569 }
1570 TableStyle::Markdown => {
1571 table.load_preset(ASCII_MARKDOWN);
1572 }
1573 TableStyle::Utf8Full => {
1574 table.load_preset(UTF8_FULL);
1575 }
1576 TableStyle::Utf8Condensed => {
1577 table.load_preset(UTF8_FULL_CONDENSED);
1578 }
1579 TableStyle::Utf8BordersOnly => {
1580 table.load_preset(UTF8_BORDERS_ONLY);
1581 }
1582 TableStyle::Utf8HorizontalOnly => {
1583 table.load_preset(UTF8_HORIZONTAL_ONLY);
1584 }
1585 TableStyle::Utf8NoBorders => {
1586 table.load_preset(UTF8_NO_BORDERS);
1587 }
1588 TableStyle::Plain => {
1589 table.load_preset(NOTHING);
1590 }
1591 }
1592
1593 if max_col_width.is_some() {
1595 table.set_content_arrangement(ContentArrangement::Dynamic);
1596 }
1597
1598 let columns = dataview.column_names();
1600
1601 if styled {
1603 use crate::output::styled_table::{apply_styles_to_table, StyleConfig};
1604 use std::path::PathBuf;
1605
1606 let style_config = if let Some(file_path) = style_file {
1608 let path = PathBuf::from(file_path);
1609 StyleConfig::from_file(&path).ok()
1610 } else {
1611 StyleConfig::load_default()
1612 };
1613
1614 if let Some(config) = style_config {
1615 let rows: Vec<Vec<String>> = (0..dataview.row_count())
1617 .filter_map(|i| {
1618 dataview.get_row(i).map(|row| {
1619 row.values
1620 .iter()
1621 .map(|v| {
1622 let s = format_value(v);
1623 if let Some(max_width) = max_col_width {
1625 if s.len() > max_width {
1626 format!("{}...", &s[..max_width.saturating_sub(3)])
1627 } else {
1628 s
1629 }
1630 } else {
1631 s
1632 }
1633 })
1634 .collect()
1635 })
1636 })
1637 .collect();
1638
1639 if let Err(e) = apply_styles_to_table(&mut table, &columns, &rows, &config) {
1640 eprintln!("Warning: Failed to apply styles: {}", e);
1641 }
1642 }
1643 } else {
1644 table.set_header(&columns);
1646
1647 for row_idx in 0..dataview.row_count() {
1649 if let Some(row) = dataview.get_row(row_idx) {
1650 let row_strings: Vec<String> = row
1651 .values
1652 .iter()
1653 .map(|v| {
1654 let s = format_value(v);
1655 if let Some(max_width) = max_col_width {
1657 if s.len() > max_width {
1658 format!("{}...", &s[..max_width.saturating_sub(3)])
1659 } else {
1660 s
1661 }
1662 } else {
1663 s
1664 }
1665 })
1666 .collect();
1667 table.add_row(row_strings);
1668 }
1669 }
1670 }
1671
1672 writeln!(writer, "{}", table)?;
1674
1675 Ok(())
1676}
1677
1678fn format_value(value: &DataValue) -> String {
1680 match value {
1681 DataValue::Null => String::new(),
1682 DataValue::Integer(i) => i.to_string(),
1683 DataValue::Float(f) => f.to_string(),
1684 DataValue::String(s) => s.clone(),
1685 DataValue::InternedString(s) => s.to_string(),
1686 DataValue::Boolean(b) => b.to_string(),
1687 DataValue::DateTime(dt) => dt.to_string(),
1688 DataValue::Vector(v) => {
1689 let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
1690 format!("[{}]", components.join(","))
1691 }
1692 }
1693}
1694
1695fn value_to_json(value: &DataValue) -> serde_json::Value {
1697 match value {
1698 DataValue::Null => serde_json::Value::Null,
1699 DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
1700 DataValue::Float(f) => {
1701 if let Some(n) = serde_json::Number::from_f64(*f) {
1702 serde_json::Value::Number(n)
1703 } else {
1704 serde_json::Value::Null
1705 }
1706 }
1707 DataValue::String(s) => serde_json::Value::String(s.clone()),
1708 DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
1709 DataValue::Boolean(b) => serde_json::Value::Bool(*b),
1710 DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
1711 DataValue::Vector(v) => serde_json::Value::Array(
1712 v.iter()
1713 .map(|f| {
1714 if let Some(n) = serde_json::Number::from_f64(*f) {
1715 serde_json::Value::Number(n)
1716 } else {
1717 serde_json::Value::Null
1718 }
1719 })
1720 .collect(),
1721 ),
1722 }
1723}
1724
1725fn escape_csv_field(field: &str, delimiter: char) -> String {
1727 if field.contains(delimiter)
1728 || field.contains('"')
1729 || field.contains('\n')
1730 || field.contains('\r')
1731 {
1732 format!("\"{}\"", field.replace('"', "\"\""))
1733 } else {
1734 field.to_string()
1735 }
1736}