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::data::data_view::DataView;
9use crate::data::datatable::{DataTable, DataValue};
10use crate::data::datatable_loaders::{load_csv_to_datatable, load_json_to_datatable};
11use crate::services::query_execution_service::QueryExecutionService;
12
13#[derive(Debug, Clone)]
15pub enum OutputFormat {
16 Csv,
17 Json,
18 Table,
19 Tsv,
20}
21
22impl OutputFormat {
23 pub fn from_str(s: &str) -> Result<Self> {
24 match s.to_lowercase().as_str() {
25 "csv" => Ok(OutputFormat::Csv),
26 "json" => Ok(OutputFormat::Json),
27 "table" => Ok(OutputFormat::Table),
28 "tsv" => Ok(OutputFormat::Tsv),
29 _ => Err(anyhow::anyhow!(
30 "Invalid output format: {}. Use csv, json, table, or tsv",
31 s
32 )),
33 }
34 }
35}
36
37pub struct NonInteractiveConfig {
39 pub data_file: String,
40 pub query: String,
41 pub output_format: OutputFormat,
42 pub output_file: Option<String>,
43 pub case_insensitive: bool,
44 pub auto_hide_empty: bool,
45 pub limit: Option<usize>,
46 pub query_plan: bool,
47}
48
49pub fn execute_non_interactive(config: NonInteractiveConfig) -> Result<()> {
51 let start_time = Instant::now();
52
53 use crate::sql::recursive_parser::Parser;
55 let mut parser = Parser::new(&config.query);
56 let statement = parser
57 .parse()
58 .map_err(|e| anyhow::anyhow!("Parse error: {}", e))?;
59
60 let uses_dual = statement
61 .from_table
62 .as_ref()
63 .map(|t| t.to_uppercase() == "DUAL")
64 .unwrap_or(false);
65
66 let no_from_clause = statement.from_table.is_none();
67
68 let (data_table, is_dual) = if uses_dual || no_from_clause || config.data_file.is_empty() {
70 info!("Using DUAL table for expression evaluation");
71 (crate::data::datatable::DataTable::dual(), true)
72 } else {
73 info!("Loading data from: {}", config.data_file);
74 let table = load_data_file(&config.data_file)?;
75 info!(
76 "Loaded {} rows with {} columns",
77 table.row_count(),
78 table.column_count()
79 );
80 (table, false)
81 };
82 let table_name = data_table.name.clone();
83
84 let dataview = DataView::new(std::sync::Arc::new(data_table));
86
87 info!("Executing query: {}", config.query);
89
90 if config.query_plan {
92 use crate::sql::recursive_parser::Parser;
93 let mut parser = Parser::new(&config.query);
94 match parser.parse() {
95 Ok(statement) => {
96 println!("\n=== QUERY PLAN (AST) ===");
97 println!("{:#?}", statement);
98 println!("=== END QUERY PLAN ===\n");
99 }
100 Err(e) => {
101 eprintln!("Failed to parse query for plan: {}", e);
102 }
103 }
104 }
105
106 let query_start = Instant::now();
107
108 let query_service = QueryExecutionService::new(config.case_insensitive, config.auto_hide_empty);
110 let result = query_service.execute(&config.query, Some(&dataview), Some(dataview.source()))?;
111
112 let query_time = query_start.elapsed();
113 info!("Query executed in {:?}", query_time);
114 info!(
115 "Result: {} rows, {} columns",
116 result.dataview.row_count(),
117 result.dataview.column_count()
118 );
119
120 let final_view = if let Some(limit) = config.limit {
122 let limited_table = limit_results(&result.dataview, limit)?;
123 DataView::new(std::sync::Arc::new(limited_table))
124 } else {
125 result.dataview
126 };
127
128 let output_result = match config.output_file {
130 Some(ref path) => {
131 let mut file = fs::File::create(path)
132 .with_context(|| format!("Failed to create output file: {}", path))?;
133 output_results(&final_view, config.output_format, &mut file)?;
134 info!("Results written to: {}", path);
135 Ok(())
136 }
137 None => {
138 output_results(&final_view, config.output_format, &mut io::stdout())?;
139 Ok(())
140 }
141 };
142
143 let total_time = start_time.elapsed();
144 debug!("Total execution time: {:?}", total_time);
145
146 if config.output_file.is_none() {
148 eprintln!(
149 "\n# Query completed: {} rows in {:?}",
150 final_view.row_count(),
151 query_time
152 );
153 }
154
155 output_result
156}
157
158fn load_data_file(path: &str) -> Result<DataTable> {
160 let path = Path::new(path);
161
162 if !path.exists() {
163 return Err(anyhow::anyhow!("File not found: {}", path.display()));
164 }
165
166 let extension = path
168 .extension()
169 .and_then(|ext| ext.to_str())
170 .map(|s| s.to_lowercase())
171 .unwrap_or_default();
172
173 let table_name = path
174 .file_stem()
175 .and_then(|stem| stem.to_str())
176 .unwrap_or("data")
177 .to_string();
178
179 match extension.as_str() {
180 "csv" => load_csv_to_datatable(path, &table_name)
181 .with_context(|| format!("Failed to load CSV file: {}", path.display())),
182 "json" => load_json_to_datatable(path, &table_name)
183 .with_context(|| format!("Failed to load JSON file: {}", path.display())),
184 _ => Err(anyhow::anyhow!(
185 "Unsupported file type: {}. Use .csv or .json",
186 extension
187 )),
188 }
189}
190
191fn limit_results(dataview: &DataView, limit: usize) -> Result<DataTable> {
193 let source = dataview.source();
194 let mut limited_table = DataTable::new(&source.name);
195
196 for col in source.columns.iter() {
198 limited_table.add_column(col.clone());
199 }
200
201 let rows_to_copy = dataview.row_count().min(limit);
203 for i in 0..rows_to_copy {
204 if let Some(row) = dataview.get_row(i) {
205 limited_table.add_row(row.clone());
206 }
207 }
208
209 Ok(limited_table)
210}
211
212fn output_results<W: Write>(
214 dataview: &DataView,
215 format: OutputFormat,
216 writer: &mut W,
217) -> Result<()> {
218 match format {
219 OutputFormat::Csv => output_csv(dataview, writer, ','),
220 OutputFormat::Tsv => output_csv(dataview, writer, '\t'),
221 OutputFormat::Json => output_json(dataview, writer),
222 OutputFormat::Table => output_table(dataview, writer),
223 }
224}
225
226fn output_csv<W: Write>(dataview: &DataView, writer: &mut W, delimiter: char) -> Result<()> {
228 let columns = dataview.column_names();
230 for (i, col) in columns.iter().enumerate() {
231 if i > 0 {
232 write!(writer, "{}", delimiter)?;
233 }
234 write!(writer, "{}", escape_csv_field(col, delimiter))?;
235 }
236 writeln!(writer)?;
237
238 for row_idx in 0..dataview.row_count() {
240 if let Some(row) = dataview.get_row(row_idx) {
241 for (i, value) in row.values.iter().enumerate() {
242 if i > 0 {
243 write!(writer, "{}", delimiter)?;
244 }
245 write!(
246 writer,
247 "{}",
248 escape_csv_field(&format_value(value), delimiter)
249 )?;
250 }
251 writeln!(writer)?;
252 }
253 }
254
255 Ok(())
256}
257
258fn output_json<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
260 let columns = dataview.column_names();
261 let mut rows = Vec::new();
262
263 for row_idx in 0..dataview.row_count() {
264 if let Some(row) = dataview.get_row(row_idx) {
265 let mut json_row = serde_json::Map::new();
266 for (col_idx, value) in row.values.iter().enumerate() {
267 if col_idx < columns.len() {
268 json_row.insert(columns[col_idx].clone(), value_to_json(value));
269 }
270 }
271 rows.push(serde_json::Value::Object(json_row));
272 }
273 }
274
275 let json = serde_json::to_string_pretty(&rows)?;
276 writeln!(writer, "{}", json)?;
277
278 Ok(())
279}
280
281fn output_table<W: Write>(dataview: &DataView, writer: &mut W) -> Result<()> {
283 let columns = dataview.column_names();
284
285 let mut widths = vec![0; columns.len()];
287 for (i, col) in columns.iter().enumerate() {
288 widths[i] = col.len();
289 }
290
291 let sample_size = dataview.row_count().min(100);
293 for row_idx in 0..sample_size {
294 if let Some(row) = dataview.get_row(row_idx) {
295 for (i, value) in row.values.iter().enumerate() {
296 if i < widths.len() {
297 let value_str = format_value(value);
298 widths[i] = widths[i].max(value_str.len());
299 }
300 }
301 }
302 }
303
304 for width in widths.iter_mut() {
306 *width = (*width).min(50);
307 }
308
309 write!(writer, "+")?;
311 for width in &widths {
312 write!(writer, "-{}-+", "-".repeat(*width))?;
313 }
314 writeln!(writer)?;
315
316 write!(writer, "|")?;
318 for (i, col) in columns.iter().enumerate() {
319 write!(writer, " {:^width$} |", col, width = widths[i])?;
320 }
321 writeln!(writer)?;
322
323 write!(writer, "+")?;
325 for width in &widths {
326 write!(writer, "-{}-+", "-".repeat(*width))?;
327 }
328 writeln!(writer)?;
329
330 for row_idx in 0..dataview.row_count() {
332 if let Some(row) = dataview.get_row(row_idx) {
333 write!(writer, "|")?;
334 for (i, value) in row.values.iter().enumerate() {
335 if i < widths.len() {
336 let value_str = format_value(value);
337 let truncated = if value_str.len() > widths[i] {
338 format!("{}...", &value_str[..widths[i] - 3])
339 } else {
340 value_str
341 };
342 write!(writer, " {:<width$} |", truncated, width = widths[i])?;
343 }
344 }
345 writeln!(writer)?;
346 }
347 }
348
349 write!(writer, "+")?;
351 for width in &widths {
352 write!(writer, "-{}-+", "-".repeat(*width))?;
353 }
354 writeln!(writer)?;
355
356 Ok(())
357}
358
359fn format_value(value: &DataValue) -> String {
361 match value {
362 DataValue::Null => "".to_string(),
363 DataValue::Integer(i) => i.to_string(),
364 DataValue::Float(f) => f.to_string(),
365 DataValue::String(s) => s.clone(),
366 DataValue::InternedString(s) => s.to_string(),
367 DataValue::Boolean(b) => b.to_string(),
368 DataValue::DateTime(dt) => dt.to_string(),
369 }
370}
371
372fn value_to_json(value: &DataValue) -> serde_json::Value {
374 match value {
375 DataValue::Null => serde_json::Value::Null,
376 DataValue::Integer(i) => serde_json::Value::Number((*i).into()),
377 DataValue::Float(f) => {
378 if let Some(n) = serde_json::Number::from_f64(*f) {
379 serde_json::Value::Number(n)
380 } else {
381 serde_json::Value::Null
382 }
383 }
384 DataValue::String(s) => serde_json::Value::String(s.clone()),
385 DataValue::InternedString(s) => serde_json::Value::String(s.to_string()),
386 DataValue::Boolean(b) => serde_json::Value::Bool(*b),
387 DataValue::DateTime(dt) => serde_json::Value::String(dt.to_string()),
388 }
389}
390
391fn escape_csv_field(field: &str, delimiter: char) -> String {
393 if field.contains(delimiter)
394 || field.contains('"')
395 || field.contains('\n')
396 || field.contains('\r')
397 {
398 format!("\"{}\"", field.replace('"', "\"\""))
399 } else {
400 field.to_string()
401 }
402}