use anyhow::{Context, Result};
use comfy_table::presets::*;
use comfy_table::{ContentArrangement, Table};
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::time::Instant;
use tracing::{debug, info};
use crate::config::config::Config;
use crate::data::data_view::DataView;
use crate::data::datatable::{DataTable, DataValue};
use crate::data::datatable_loaders::{load_csv_to_datatable, load_json_to_datatable};
use crate::services::query_execution_service::QueryExecutionService;
use crate::sql::parser::ast::{CTEType, TableSource, CTE};
use crate::sql::recursive_parser::Parser;
use crate::sql::script_parser::{ScriptParser, ScriptResult};
use crate::utils::string_utils::display_width;
fn check_temp_table_usage(query: &str) -> Result<()> {
use crate::sql::recursive_parser::Parser;
let mut parser = Parser::new(query);
match parser.parse() {
Ok(stmt) => {
if let Some(from_table) = &stmt.from_table {
if from_table.starts_with('#') {
anyhow::bail!(
"Temporary table '{}' cannot be used in single-query mode. \
Temporary tables are only available in script mode (using -f flag with GO separators).",
from_table
);
}
}
if let Some(into_table) = &stmt.into_table {
anyhow::bail!(
"INTO clause for temporary table '{}' is only supported in script mode. \
Use -f flag with GO separators to create temporary tables.",
into_table.name
);
}
Ok(())
}
Err(_) => {
Ok(())
}
}
}
fn extract_cte_dependencies(cte: &CTE) -> Vec<String> {
let mut deps = Vec::new();
if let CTEType::Standard(query) = &cte.cte_type {
if let Some(from_table) = &query.from_table {
deps.push(from_table.clone());
}
for join in &query.joins {
match &join.table {
TableSource::Table(table_name) => {
deps.push(table_name.clone());
}
TableSource::DerivedTable { alias, .. } => {
deps.push(alias.clone());
}
TableSource::Pivot { alias, .. } => {
if let Some(pivot_alias) = alias {
deps.push(pivot_alias.clone());
}
}
}
}
}
deps
}
#[derive(Debug, Clone)]
pub enum OutputFormat {
Csv,
Json,
JsonStructured, Table,
Tsv,
}
impl OutputFormat {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"csv" => Ok(OutputFormat::Csv),
"json" => Ok(OutputFormat::Json),
"json-structured" => Ok(OutputFormat::JsonStructured),
"table" => Ok(OutputFormat::Table),
"tsv" => Ok(OutputFormat::Tsv),
_ => Err(anyhow::anyhow!(
"Invalid output format: {}. Use csv, json, json-structured, table, or tsv",
s
)),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum TableStyle {
Default,
AsciiFull,
AsciiCondensed,
AsciiBordersOnly,
AsciiHorizontalOnly,
AsciiNoBorders,
Markdown,
Utf8Full,
Utf8Condensed,
Utf8BordersOnly,
Utf8HorizontalOnly,
Utf8NoBorders,
Plain,
}
impl TableStyle {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"default" => Ok(TableStyle::Default),
"ascii" | "ascii-full" => Ok(TableStyle::AsciiFull),
"ascii-condensed" => Ok(TableStyle::AsciiCondensed),
"ascii-borders" | "ascii-borders-only" => Ok(TableStyle::AsciiBordersOnly),
"ascii-horizontal" | "ascii-horizontal-only" => Ok(TableStyle::AsciiHorizontalOnly),
"ascii-noborders" | "ascii-no-borders" => Ok(TableStyle::AsciiNoBorders),
"markdown" | "md" => Ok(TableStyle::Markdown),
"utf8" | "utf8-full" => Ok(TableStyle::Utf8Full),
"utf8-condensed" => Ok(TableStyle::Utf8Condensed),
"utf8-borders" | "utf8-borders-only" => Ok(TableStyle::Utf8BordersOnly),
"utf8-horizontal" | "utf8-horizontal-only" => Ok(TableStyle::Utf8HorizontalOnly),
"utf8-noborders" | "utf8-no-borders" => Ok(TableStyle::Utf8NoBorders),
"plain" | "none" => Ok(TableStyle::Plain),
_ => Err(anyhow::anyhow!(
"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",
s
)),
}
}
pub fn list_styles() -> &'static str {
"Available table styles:
default - Current default ASCII style
ascii - ASCII with full borders
ascii-condensed - ASCII with condensed rows
ascii-borders - ASCII with outer borders only
ascii-horizontal - ASCII with horizontal lines only
ascii-noborders - ASCII with no borders
markdown - GitHub-flavored Markdown table
utf8 - UTF8 box-drawing characters
utf8-condensed - UTF8 with condensed rows
utf8-borders - UTF8 with outer borders only
utf8-horizontal - UTF8 with horizontal lines only
utf8-noborders - UTF8 with no borders
plain - No formatting, data only"
}
}
pub struct NonInteractiveConfig {
pub data_file: String,
pub query: String,
pub output_format: OutputFormat,
pub output_file: Option<String>,
pub case_insensitive: bool,
pub auto_hide_empty: bool,
pub limit: Option<usize>,
pub query_plan: bool,
pub show_work_units: bool,
pub execution_plan: bool,
pub show_preprocessing: bool,
pub show_transformations: bool,
pub cte_info: bool,
pub rewrite_analysis: bool,
pub lift_in_expressions: bool,
pub script_file: Option<String>, pub debug_trace: bool,
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, }
fn make_transformer_config(config: &NonInteractiveConfig) -> crate::query_plan::TransformerConfig {
crate::query_plan::TransformerConfig {
enable_pivot_expander: true, enable_expression_lifter: !config.no_expression_lifter,
enable_where_expansion: !config.no_where_expansion,
enable_group_by_expansion: !config.no_group_by_expansion,
enable_having_expansion: !config.no_having_expansion,
enable_order_by_expansion: !config.no_order_by_expansion,
enable_qualify_to_where: !config.no_qualify_to_where,
enable_ilike_to_like: true, enable_cte_hoister: !config.no_cte_hoister,
enable_in_lifter: !config.no_in_lifter,
}
}
pub fn execute_non_interactive(config: NonInteractiveConfig) -> Result<()> {
let start_time = Instant::now();
let data_file_to_use = if config.data_file.is_empty() {
let lines: Vec<&str> = config.query.lines().take(10).collect();
let mut hint_path: Option<String> = None;
for line in lines {
let trimmed = line.trim();
if trimmed.starts_with("-- #!") {
hint_path = Some(trimmed[5..].trim().to_string());
break;
}
}
if let Some(path) = hint_path {
debug!("Found data file hint: {}", path);
let resolved_path = if path.starts_with("../") {
std::path::Path::new(&path).to_path_buf()
} else if path.starts_with("data/") {
std::path::Path::new(&path).to_path_buf()
} else {
std::path::Path::new(&path).to_path_buf()
};
if resolved_path.exists() {
info!("Using data file from hint: {:?}", resolved_path);
resolved_path.to_string_lossy().to_string()
} else {
debug!("Data file hint path does not exist: {:?}", resolved_path);
String::new()
}
} else {
String::new()
}
} else {
config.data_file.clone()
};
let (data_table, _is_dual) = if data_file_to_use.is_empty() {
info!("No data file provided, using DUAL table");
(crate::data::datatable::DataTable::dual(), true)
} else {
info!("Loading data from: {}", data_file_to_use);
let table = load_data_file(&data_file_to_use)?;
info!(
"Loaded {} rows with {} columns",
table.row_count(),
table.column_count()
);
(table, false)
};
let _table_name = data_table.name.clone();
use crate::execution::{ExecutionConfig, ExecutionContext, StatementExecutor};
let mut context = ExecutionContext::new(std::sync::Arc::new(data_table));
let dataview = DataView::new(context.source_table.clone());
info!("Executing query: {}", config.query);
if config.execution_plan {
println!("\n=== EXECUTION PLAN ===");
println!("Query: {}", config.query);
println!("\nExecution Steps:");
println!("1. PARSE - Parse SQL query");
println!("2. LOAD_DATA - Load data from {}", &config.data_file);
println!(
" • Loaded {} rows, {} columns",
dataview.row_count(),
dataview.column_count()
);
}
if config.show_work_units {
use crate::query_plan::{ExpressionLifter, QueryAnalyzer};
use crate::sql::recursive_parser::Parser;
let mut parser = Parser::new(&config.query);
match parser.parse() {
Ok(stmt) => {
let mut analyzer = QueryAnalyzer::new();
let mut lifter = ExpressionLifter::new();
let mut stmt_copy = stmt.clone();
let lifted = lifter.lift_expressions(&mut stmt_copy);
match analyzer.analyze(&stmt_copy, config.query.clone()) {
Ok(plan) => {
println!("\n{}", plan.explain());
if !lifted.is_empty() {
println!("\nLifted CTEs:");
for cte in &lifted {
println!(" - {}", cte.name);
}
}
return Ok(());
}
Err(e) => {
eprintln!("Error analyzing query: {}", e);
return Err(anyhow::anyhow!("Query analysis failed: {}", e));
}
}
}
Err(e) => {
eprintln!("Error parsing query: {}", e);
return Err(anyhow::anyhow!("Parse error: {}", e));
}
}
}
if config.query_plan {
use crate::sql::recursive_parser::Parser;
let mut parser = Parser::new(&config.query);
match parser.parse() {
Ok(statement) => {
println!("\n=== QUERY PLAN (AST) ===");
println!("{statement:#?}");
println!("=== END QUERY PLAN ===\n");
}
Err(e) => {
eprintln!("Failed to parse query for plan: {e}");
}
}
}
if config.rewrite_analysis {
use crate::sql::query_rewriter::{QueryRewriter, RewriteAnalysis};
use crate::sql::recursive_parser::Parser;
use serde_json::json;
let mut parser = Parser::new(&config.query);
match parser.parse() {
Ok(statement) => {
let mut rewriter = QueryRewriter::new();
let suggestions = rewriter.analyze(&statement);
let analysis = RewriteAnalysis::from_suggestions(suggestions);
println!("{}", serde_json::to_string_pretty(&analysis).unwrap());
return Ok(());
}
Err(e) => {
let output = json!({
"success": false,
"error": format!("{}", e),
"suggestions": [],
"can_auto_rewrite": false,
});
println!("{}", serde_json::to_string_pretty(&output).unwrap());
return Ok(());
}
}
}
if config.cte_info {
use crate::sql::recursive_parser::Parser;
use serde_json::json;
let mut parser = Parser::new(&config.query);
match parser.parse() {
Ok(statement) => {
let mut cte_info = Vec::new();
for (index, cte) in statement.ctes.iter().enumerate() {
let cte_json = json!({
"index": index,
"name": cte.name,
"columns": cte.column_list,
"dependencies": extract_cte_dependencies(cte),
});
cte_info.push(cte_json);
}
let output = json!({
"success": true,
"ctes": cte_info,
"total": statement.ctes.len(),
"has_final_select": !statement.columns.is_empty() || !statement.select_items.is_empty(),
});
println!("{}", serde_json::to_string_pretty(&output).unwrap());
return Ok(());
}
Err(e) => {
let output = json!({
"success": false,
"error": format!("{}", e),
"ctes": [],
"total": 0,
});
println!("{}", serde_json::to_string_pretty(&output).unwrap());
return Ok(());
}
}
}
let query_start = Instant::now();
let app_config = Config::load().unwrap_or_else(|e| {
debug!("Could not load config file: {}. Using defaults.", e);
Config::default()
});
crate::config::global::init_config(app_config.clone());
let exec_config = ExecutionConfig::from_cli_flags(
config.show_preprocessing,
config.show_transformations,
config.case_insensitive,
config.auto_hide_empty,
config.no_expression_lifter,
config.no_where_expansion,
config.no_group_by_expansion,
config.no_having_expansion,
config.no_order_by_expansion,
config.no_qualify_to_where,
config.no_cte_hoister,
config.no_in_lifter,
config.debug_trace,
);
let executor = StatementExecutor::with_config(exec_config);
let exec_start = Instant::now();
let result = {
use crate::sql::recursive_parser::Parser;
use crate::sql::template_expander::TemplateExpander;
let expander = TemplateExpander::new(&context.temp_tables);
let query_to_parse = match expander.parse_templates(&config.query) {
Ok(vars) => {
if vars.is_empty() {
config.query.clone()
} else {
match expander.expand(&config.query, &vars) {
Ok(expanded) => {
debug!(
"Template expansion in single query mode: {} vars expanded",
vars.len()
);
for var in &vars {
debug!(" {} -> resolved", var.placeholder);
}
expanded
}
Err(e) => {
return Err(anyhow::anyhow!(
"Template expansion failed: {}. Available tables: {}",
e,
context.temp_tables.list_tables().join(", ")
));
}
}
}
}
Err(e) => {
return Err(anyhow::anyhow!("Template parsing failed: {}", e));
}
};
let mut parser = Parser::new(&query_to_parse);
match parser.parse() {
Ok(stmt) => {
match executor.execute(stmt, &mut context) {
Ok(exec_result) => {
Ok(
crate::services::query_execution_service::QueryExecutionResult {
dataview: exec_result.dataview,
stats: crate::services::query_execution_service::QueryStats {
row_count: exec_result.stats.row_count,
column_count: exec_result.stats.column_count,
execution_time: exec_start.elapsed(),
query_engine_time: exec_start.elapsed(),
},
hidden_columns: Vec::new(),
query: config.query.clone(),
execution_plan: None,
debug_trace: None,
},
)
}
Err(e) => Err(e),
}
}
Err(e) => {
Err(anyhow::anyhow!("Parse error: {}", e))
}
}
}?;
let exec_time = exec_start.elapsed();
let query_time = query_start.elapsed();
info!("Query executed in {:?}", query_time);
info!(
"Result: {} rows, {} columns",
result.dataview.row_count(),
result.dataview.column_count()
);
if config.execution_plan {
use crate::data::query_engine::QueryEngine;
let query_engine = QueryEngine::new();
match query_engine.execute_with_plan(
std::sync::Arc::new(dataview.source().clone()),
&config.query,
) {
Ok((_view, plan)) => {
print!("{}", plan.format_tree());
}
Err(e) => {
eprintln!("Could not generate detailed execution plan: {}", e);
println!(
"3. QUERY_EXECUTION [{:.3}ms]",
exec_time.as_secs_f64() * 1000.0
);
use crate::sql::recursive_parser::Parser;
let mut parser = Parser::new(&config.query);
if let Ok(stmt) = parser.parse() {
if stmt.where_clause.is_some() {
println!(" • WHERE clause filtering applied");
println!(" • Rows after filter: {}", result.dataview.row_count());
}
if let Some(ref order_by) = stmt.order_by {
println!(" • ORDER BY: {} column(s)", order_by.len());
}
if let Some(ref group_by) = stmt.group_by {
println!(" • GROUP BY: {} column(s)", group_by.len());
}
if let Some(limit) = stmt.limit {
println!(" • LIMIT: {} rows", limit);
}
if stmt.distinct {
println!(" • DISTINCT applied");
}
}
}
}
println!("\nExecution Statistics:");
println!(
" Preparation: {:.3}ms",
(exec_start - start_time).as_secs_f64() * 1000.0
);
println!(
" Query time: {:.3}ms",
exec_time.as_secs_f64() * 1000.0
);
println!(
" Total time: {:.3}ms",
query_time.as_secs_f64() * 1000.0
);
println!(" Rows returned: {}", result.dataview.row_count());
println!(" Columns: {}", result.dataview.column_count());
println!("\n=== END EXECUTION PLAN ===");
println!();
}
let final_view = if let Some(limit) = config.limit {
let limited_table = limit_results(&result.dataview, limit)?;
DataView::new(std::sync::Arc::new(limited_table))
} else {
result.dataview
};
if let Some(ref trace_output) = result.debug_trace {
eprintln!("{}", trace_output);
}
let exec_time_ms = exec_time.as_secs_f64() * 1000.0;
let output_result = if let Some(ref path) = config.output_file {
let mut file = fs::File::create(path)
.with_context(|| format!("Failed to create output file: {path}"))?;
output_results(
&final_view,
config.output_format,
&mut file,
config.max_col_width,
config.col_sample_rows,
exec_time_ms,
config.table_style,
config.styled,
config.style_file.as_deref(),
)?;
info!("Results written to: {}", path);
Ok(())
} else {
output_results(
&final_view,
config.output_format,
&mut io::stdout(),
config.max_col_width,
config.col_sample_rows,
exec_time_ms,
config.table_style,
config.styled,
config.style_file.as_deref(),
)?;
Ok(())
};
let total_time = start_time.elapsed();
debug!("Total execution time: {:?}", total_time);
if config.output_file.is_none() {
eprintln!(
"\n# Query completed: {} rows in {:?}",
final_view.row_count(),
query_time
);
}
output_result
}
pub fn execute_script(config: NonInteractiveConfig) -> Result<()> {
let _start_time = Instant::now();
let parser = ScriptParser::new(&config.query);
let script_statements = parser.parse_script_statements();
if script_statements.is_empty() {
anyhow::bail!("No statements found in script");
}
info!("Found {} statements in script", script_statements.len());
let data_file = if !config.data_file.is_empty() {
config.data_file.clone()
} else if let Some(hint) = parser.data_file_hint() {
info!("Using data file from script hint: {}", hint);
if let Some(script_path) = config.script_file.as_ref() {
let script_dir = std::path::Path::new(script_path)
.parent()
.unwrap_or(std::path::Path::new("."));
let hint_path = std::path::Path::new(hint);
if hint_path.is_relative() {
script_dir.join(hint_path).to_string_lossy().to_string()
} else {
hint.to_string()
}
} else {
hint.to_string()
}
} else {
String::new()
};
let (data_table, _is_dual) = if data_file.is_empty() {
info!("No data file provided, using DUAL table");
(DataTable::dual(), true)
} else {
if !std::path::Path::new(&data_file).exists() {
anyhow::bail!(
"Data file not found: {}\n\
Please check the path is correct",
data_file
);
}
info!("Loading data from: {}", data_file);
let table = load_data_file(&data_file)?;
info!(
"Loaded {} rows with {} columns",
table.row_count(),
table.column_count()
);
(table, false)
};
let mut script_result = ScriptResult::new();
let mut output = Vec::new();
use crate::execution::{ExecutionConfig, ExecutionContext, StatementExecutor};
let mut context = ExecutionContext::new(std::sync::Arc::new(data_table));
let exec_config = ExecutionConfig::from_cli_flags(
config.show_preprocessing,
config.show_transformations,
config.case_insensitive,
config.auto_hide_empty,
config.no_expression_lifter,
config.no_where_expansion,
config.no_group_by_expansion,
config.no_having_expansion,
config.no_order_by_expansion,
config.no_qualify_to_where,
config.no_cte_hoister,
config.no_in_lifter,
false, );
let executor = StatementExecutor::with_config(exec_config);
for (idx, script_stmt) in script_statements.iter().enumerate() {
let statement_num = idx + 1;
let stmt_start = Instant::now();
if script_stmt.is_exit() {
let exit_code = script_stmt.get_exit_code().unwrap_or(0);
info!("EXIT statement encountered (code: {})", exit_code);
if matches!(config.output_format, OutputFormat::Table) {
if idx > 0 {
output.push(String::new());
}
output.push(format!("-- Statement {} --", statement_num));
output.push(format!("Script execution stopped by EXIT {}", exit_code));
}
script_result.add_success(
statement_num,
format!("EXIT {}", exit_code),
0,
stmt_start.elapsed().as_secs_f64() * 1000.0,
);
break;
}
if script_stmt.should_skip() {
info!(
"Skipping statement {} due to [SKIP] directive",
statement_num
);
if matches!(config.output_format, OutputFormat::Table) {
if idx > 0 {
output.push(String::new());
}
output.push(format!("-- Statement {} [SKIPPED] --", statement_num));
}
script_result.add_success(
statement_num,
"[SKIPPED]".to_string(),
0,
stmt_start.elapsed().as_secs_f64() * 1000.0,
);
continue;
}
let statement = match script_stmt.get_query() {
Some(sql) => sql,
None => continue, };
if matches!(config.output_format, OutputFormat::Table) {
if idx > 0 {
output.push(String::new()); }
output.push(format!("-- Query {} --", statement_num));
}
use crate::sql::template_expander::TemplateExpander;
let expander = TemplateExpander::new(&context.temp_tables);
let expanded_statement = match expander.parse_templates(statement) {
Ok(vars) => {
if vars.is_empty() {
statement.to_string()
} else {
match expander.expand(statement, &vars) {
Ok(expanded) => {
debug!("Expanded templates in SQL: {} vars found", vars.len());
for var in &vars {
debug!(
" {} -> expanding from {}",
var.placeholder, var.table_name
);
}
expanded
}
Err(e) => {
script_result.add_failure(
statement_num,
statement.to_string(),
format!("Template expansion error: {}", e),
stmt_start.elapsed().as_secs_f64() * 1000.0,
);
continue; }
}
}
}
Err(e) => {
script_result.add_failure(
statement_num,
statement.to_string(),
format!("Template parse error: {}", e),
stmt_start.elapsed().as_secs_f64() * 1000.0,
);
continue; }
};
let statement = expanded_statement.as_str();
let mut parser = Parser::new(statement);
let parsed_stmt = match parser.parse() {
Ok(stmt) => stmt,
Err(e) => {
script_result.add_failure(
statement_num,
statement.to_string(),
format!("Parse error: {}", e),
stmt_start.elapsed().as_secs_f64() * 1000.0,
);
break;
}
};
if let Some(from_table) = &parsed_stmt.from_table {
if from_table.starts_with('#') && !context.has_temp_table(from_table) {
script_result.add_failure(
statement_num,
statement.to_string(),
format!("Temporary table {} not found", from_table),
stmt_start.elapsed().as_secs_f64() * 1000.0,
);
break;
}
}
let into_table = parsed_stmt.into_table.clone();
let stmt_without_into = if into_table.is_some() {
use crate::query_plan::IntoClauseRemover;
IntoClauseRemover::remove_into_clause(parsed_stmt)
} else {
parsed_stmt
};
let result = executor.execute(stmt_without_into, &mut context);
match result {
Ok(exec_result) => {
let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
let final_view = exec_result.dataview;
if let Some(into_table) = &into_table {
let result_table = final_view.source_arc();
let row_count = result_table.row_count();
match context.store_temp_table(into_table.name.clone(), result_table) {
Ok(_) => {
info!(
"Stored {} rows in temporary table {}",
row_count, into_table.name
);
if matches!(config.output_format, OutputFormat::Table) {
let mut statement_output = Vec::new();
writeln!(
&mut statement_output,
"({} rows affected) -> {}",
row_count, into_table.name
)?;
output.extend(
String::from_utf8_lossy(&statement_output)
.lines()
.map(String::from),
);
}
script_result.add_success(
statement_num,
statement.to_string(),
row_count,
exec_time,
);
continue; }
Err(e) => {
script_result.add_failure(
statement_num,
statement.to_string(),
e.to_string(),
exec_time,
);
break;
}
}
}
let mut statement_output = Vec::new();
match config.output_format {
OutputFormat::Csv => {
output_csv(&final_view, &mut statement_output, ',')?;
}
OutputFormat::Json => {
output_json(&final_view, &mut statement_output)?;
}
OutputFormat::JsonStructured => {
output_json_structured(&final_view, &mut statement_output, exec_time)?;
}
OutputFormat::Table => {
output_table(
&final_view,
&mut statement_output,
config.max_col_width,
config.col_sample_rows,
config.table_style,
config.styled,
config.style_file.as_deref(),
)?;
writeln!(
&mut statement_output,
"Query completed: {} rows in {:.2}ms",
final_view.row_count(),
exec_time
)?;
}
OutputFormat::Tsv => {
output_csv(&final_view, &mut statement_output, '\t')?;
}
}
output.extend(
String::from_utf8_lossy(&statement_output)
.lines()
.map(String::from),
);
script_result.add_success(
statement_num,
statement.to_string(),
final_view.row_count(),
exec_time,
);
}
Err(e) => {
let exec_time = stmt_start.elapsed().as_secs_f64() * 1000.0;
let error_msg = format!("Query {} failed: {}", statement_num, e);
if matches!(config.output_format, OutputFormat::Table) {
output.push(error_msg.clone());
}
script_result.add_failure(
statement_num,
statement.to_string(),
e.to_string(),
exec_time,
);
}
}
}
if let Some(ref output_file) = config.output_file {
let mut file = fs::File::create(output_file)?;
for line in &output {
writeln!(file, "{}", line)?;
}
info!("Results written to: {}", output_file);
} else {
for line in &output {
println!("{}", line);
}
}
if matches!(config.output_format, OutputFormat::Table) {
println!("\n=== Script Summary ===");
println!("Total statements: {}", script_result.total_statements);
println!("Successful: {}", script_result.successful_statements);
println!("Failed: {}", script_result.failed_statements);
println!(
"Total execution time: {:.2}ms",
script_result.total_execution_time_ms
);
}
if !script_result.all_successful() {
return Err(anyhow::anyhow!(
"{} of {} statements failed",
script_result.failed_statements,
script_result.total_statements
));
}
Ok(())
}
fn load_data_file(path: &str) -> Result<DataTable> {
let path = Path::new(path);
if !path.exists() {
return Err(anyhow::anyhow!("File not found: {}", path.display()));
}
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.map(str::to_lowercase)
.unwrap_or_default();
let table_name = path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("data")
.to_string();
match extension.as_str() {
"csv" => load_csv_to_datatable(path, &table_name)
.with_context(|| format!("Failed to load CSV file: {}", path.display())),
"json" => load_json_to_datatable(path, &table_name)
.with_context(|| format!("Failed to load JSON file: {}", path.display())),
_ => Err(anyhow::anyhow!(
"Unsupported file type: {}. Use .csv or .json",
extension
)),
}
}
fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
let source = dataview.source();
let mut limited_table = DataTable::new(&source.name);
for col in &source.columns {
limited_table.add_column(col.clone());
}
let rows_to_copy = dataview.row_count().min(limit);
for i in 0..rows_to_copy {
if let Some(row) = dataview.get_row(i) {
let _ = limited_table.add_row(row.clone());
}
}
Ok(limited_table)
}
fn output_results<W: Write>(
dataview: &DataView,
format: OutputFormat,
writer: &mut W,
max_col_width: Option<usize>,
col_sample_rows: usize,
exec_time_ms: f64,
table_style: TableStyle,
styled: bool,
style_file: Option<&str>,
) -> Result<()> {
match format {
OutputFormat::Csv => output_csv(dataview, writer, ','),
OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
OutputFormat::Json => output_json(dataview, writer),
OutputFormat::JsonStructured => output_json_structured(dataview, writer, exec_time_ms),
OutputFormat::Table => output_table(
dataview,
writer,
max_col_width,
col_sample_rows,
table_style,
styled,
style_file,
),
}
}
fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
let columns = dataview.column_names();
for (i, col) in columns.iter().enumerate() {
if i > 0 {
write!(writer, "{delimiter}")?;
}
write!(writer, "{}", escape_csv_field(col, delimiter))?;
}
writeln!(writer)?;
for row_idx in 0..dataview.row_count() {
if let Some(row) = dataview.get_row(row_idx) {
for (i, value) in row.values.iter().enumerate() {
if i > 0 {
write!(writer, "{delimiter}")?;
}
write!(
writer,
"{}",
escape_csv_field(&format_value(value), delimiter)
)?;
}
writeln!(writer)?;
}
}
Ok(())
}
fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
let columns = dataview.column_names();
let mut rows = Vec::new();
for row_idx in 0..dataview.row_count() {
if let Some(row) = dataview.get_row(row_idx) {
let mut json_row = serde_json::Map::new();
for (col_idx, value) in row.values.iter().enumerate() {
if col_idx < columns.len() {
json_row.insert(columns[col_idx].clone(), value_to_json(value));
}
}
rows.push(serde_json::Value::Object(json_row));
}
}
let json = serde_json::to_string_pretty(&rows)?;
writeln!(writer, "{json}")?;
Ok(())
}
fn output_json_structured<W: Write>(
dataview: &DataView,
writer: &mut W,
exec_time: f64,
) -> Result<()> {
let column_names = dataview.column_names();
let data_table = dataview.source();
let mut columns = Vec::new();
for (idx, name) in column_names.iter().enumerate() {
let col_type = data_table
.columns
.get(idx)
.map(|c| format!("{:?}", c.data_type))
.unwrap_or_else(|| "UNKNOWN".to_string());
let mut max_width = name.len();
for row_idx in 0..dataview.row_count() {
if let Some(row) = dataview.get_row(row_idx) {
if let Some(value) = row.values.get(idx) {
let display_width = match value {
DataValue::Null => 4, DataValue::Integer(i) => i.to_string().len(),
DataValue::Float(f) => format!("{:.2}", f).len(),
DataValue::String(s) => s.len(),
DataValue::InternedString(s) => s.len(),
DataValue::Boolean(b) => {
if *b {
4
} else {
5
}
} DataValue::DateTime(dt) => dt.len(),
DataValue::Vector(v) => {
let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
format!("[{}]", components.join(",")).len()
}
};
max_width = max_width.max(display_width);
}
}
}
let alignment = match data_table.columns.get(idx).map(|c| &c.data_type) {
Some(crate::data::datatable::DataType::Integer) => "right",
Some(crate::data::datatable::DataType::Float) => "right",
_ => "left",
};
let col_meta = serde_json::json!({
"name": name,
"type": col_type,
"max_width": max_width,
"alignment": alignment
});
columns.push(col_meta);
}
let mut rows = Vec::new();
for row_idx in 0..dataview.row_count() {
if let Some(row) = dataview.get_row(row_idx) {
let row_values: Vec<String> = row
.values
.iter()
.map(|v| match v {
DataValue::Null => String::new(),
DataValue::Integer(i) => i.to_string(),
DataValue::Float(f) => format!("{:.2}", f),
DataValue::String(s) => s.clone(),
DataValue::InternedString(s) => s.to_string(),
DataValue::Boolean(b) => b.to_string(),
DataValue::DateTime(dt) => dt.clone(),
DataValue::Vector(v) => {
let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
format!("[{}]", components.join(","))
}
})
.collect();
rows.push(serde_json::Value::Array(
row_values
.into_iter()
.map(serde_json::Value::String)
.collect(),
));
}
}
let output = serde_json::json!({
"columns": columns,
"rows": rows,
"metadata": {
"total_rows": dataview.row_count(),
"query_time_ms": exec_time
}
});
let json = serde_json::to_string_pretty(&output)?;
writeln!(writer, "{json}")?;
Ok(())
}
fn output_table_old_style<W: Write>(
dataview: &DataView,
writer: &mut W,
max_col_width: Option<usize>,
) -> Result<()> {
let columns = dataview.column_names();
let mut widths = vec![0; columns.len()];
for (i, col) in columns.iter().enumerate() {
widths[i] = col.len();
}
for row_idx in 0..dataview.row_count() {
if let Some(row) = dataview.get_row(row_idx) {
for (i, value) in row.values.iter().enumerate() {
if i < widths.len() {
let value_str = format_value(value);
widths[i] = widths[i].max(display_width(&value_str));
}
}
}
}
if let Some(max_width) = max_col_width {
for width in &mut widths {
*width = (*width).min(max_width);
}
}
write!(writer, "+")?;
for width in &widths {
write!(writer, "{}", "-".repeat(*width + 2))?;
write!(writer, "+")?;
}
writeln!(writer)?;
write!(writer, "|")?;
for (i, col) in columns.iter().enumerate() {
write!(writer, " {:^width$} |", col, width = widths[i])?;
}
writeln!(writer)?;
write!(writer, "+")?;
for width in &widths {
write!(writer, "{}", "-".repeat(*width + 2))?;
write!(writer, "+")?;
}
writeln!(writer)?;
for row_idx in 0..dataview.row_count() {
if let Some(row) = dataview.get_row(row_idx) {
write!(writer, "|")?;
for (i, value) in row.values.iter().enumerate() {
if i < widths.len() {
let value_str = format_value(value);
let display_len = display_width(&value_str);
write!(writer, " {}", value_str)?;
let padding_needed = if display_len < widths[i] {
widths[i] - display_len
} else {
0
};
write!(writer, "{} |", " ".repeat(padding_needed))?;
}
}
writeln!(writer)?;
}
}
write!(writer, "+")?;
for width in &widths {
write!(writer, "{}", "-".repeat(*width + 2))?;
write!(writer, "+")?;
}
writeln!(writer)?;
Ok(())
}
fn output_table<W: Write>(
dataview: &DataView,
writer: &mut W,
max_col_width: Option<usize>,
_col_sample_rows: usize, style: TableStyle,
styled: bool,
style_file: Option<&str>,
) -> Result<()> {
let mut table = Table::new();
match style {
TableStyle::Default => {
return output_table_old_style(dataview, writer, max_col_width);
}
TableStyle::AsciiFull => {
table.load_preset(ASCII_FULL);
}
TableStyle::AsciiCondensed => {
table.load_preset(ASCII_FULL_CONDENSED);
}
TableStyle::AsciiBordersOnly => {
table.load_preset(ASCII_BORDERS_ONLY);
}
TableStyle::AsciiHorizontalOnly => {
table.load_preset(ASCII_HORIZONTAL_ONLY);
}
TableStyle::AsciiNoBorders => {
table.load_preset(ASCII_NO_BORDERS);
}
TableStyle::Markdown => {
table.load_preset(ASCII_MARKDOWN);
}
TableStyle::Utf8Full => {
table.load_preset(UTF8_FULL);
}
TableStyle::Utf8Condensed => {
table.load_preset(UTF8_FULL_CONDENSED);
}
TableStyle::Utf8BordersOnly => {
table.load_preset(UTF8_BORDERS_ONLY);
}
TableStyle::Utf8HorizontalOnly => {
table.load_preset(UTF8_HORIZONTAL_ONLY);
}
TableStyle::Utf8NoBorders => {
table.load_preset(UTF8_NO_BORDERS);
}
TableStyle::Plain => {
table.load_preset(NOTHING);
}
}
if max_col_width.is_some() {
table.set_content_arrangement(ContentArrangement::Dynamic);
}
let columns = dataview.column_names();
if styled {
use crate::output::styled_table::{apply_styles_to_table, StyleConfig};
use std::path::PathBuf;
let style_config = if let Some(file_path) = style_file {
let path = PathBuf::from(file_path);
StyleConfig::from_file(&path).ok()
} else {
StyleConfig::load_default()
};
if let Some(config) = style_config {
let rows: Vec<Vec<String>> = (0..dataview.row_count())
.filter_map(|i| {
dataview.get_row(i).map(|row| {
row.values
.iter()
.map(|v| {
let s = format_value(v);
if let Some(max_width) = max_col_width {
if s.len() > max_width {
format!("{}...", &s[..max_width.saturating_sub(3)])
} else {
s
}
} else {
s
}
})
.collect()
})
})
.collect();
if let Err(e) = apply_styles_to_table(&mut table, &columns, &rows, &config) {
eprintln!("Warning: Failed to apply styles: {}", e);
}
}
} else {
table.set_header(&columns);
for row_idx in 0..dataview.row_count() {
if let Some(row) = dataview.get_row(row_idx) {
let row_strings: Vec<String> = row
.values
.iter()
.map(|v| {
let s = format_value(v);
if let Some(max_width) = max_col_width {
if s.len() > max_width {
format!("{}...", &s[..max_width.saturating_sub(3)])
} else {
s
}
} else {
s
}
})
.collect();
table.add_row(row_strings);
}
}
}
writeln!(writer, "{}", table)?;
Ok(())
}
fn format_value(value: &DataValue) -> String {
match value {
DataValue::Null => String::new(),
DataValue::Integer(i) => i.to_string(),
DataValue::Float(f) => f.to_string(),
DataValue::String(s) => s.clone(),
DataValue::InternedString(s) => s.to_string(),
DataValue::Boolean(b) => b.to_string(),
DataValue::DateTime(dt) => dt.to_string(),
DataValue::Vector(v) => {
let components: Vec<String> = v.iter().map(|f| f.to_string()).collect();
format!("[{}]", components.join(","))
}
}
}
fn value_to_json(value: &DataValue) -> serde_json::Value {
match value {
DataValue::Null => serde_json::Value::Null,
DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
DataValue::Float(f) => {
if let Some(n) = serde_json::Number::from_f64(*f) {
serde_json::Value::Number(n)
} else {
serde_json::Value::Null
}
}
DataValue::String(s) => serde_json::Value::String(s.clone()),
DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
DataValue::Boolean(b) => serde_json::Value::Bool(*b),
DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
DataValue::Vector(v) => serde_json::Value::Array(
v.iter()
.map(|f| {
if let Some(n) = serde_json::Number::from_f64(*f) {
serde_json::Value::Number(n)
} else {
serde_json::Value::Null
}
})
.collect(),
),
}
}
fn escape_csv_field(field: &str, delimiter: char) -> String {
if field.contains(delimiter)
|| field.contains('"')
|| field.contains('\n')
|| field.contains('\r')
{
format!("\"{}\"", field.replace('"', "\"\""))
} else {
field.to_string()
}
}