1use std::{
2 collections::HashSet,
3 fmt::Write as _,
4 fs,
5};
6use std::path::Path;
7
8use anyhow::{Context, Result, anyhow, bail};
9use calamine::{Data, Reader, open_workbook_auto};
10use roxmltree::{Document, Node};
11use serde_json::Value as JsonValue;
12use rusqlite::{
13 Connection, params_from_iter,
14 types::{Value, ValueRef},
15};
16use rust_xlsxwriter::Workbook;
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum QueryValue {
20 Null,
21 Integer(i64),
22 Real(f64),
23 Text(String),
24}
25
26#[derive(Debug, Clone, PartialEq)]
27pub struct QueryResult {
28 pub columns: Vec<String>,
29 pub rows: Vec<Vec<QueryValue>>,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct QueryParam {
34 pub name: String,
35 pub value: QueryValue,
36}
37
38#[derive(Debug, Clone, Copy)]
39pub struct WorkbookInput<'a> {
40 pub path: &'a Path,
41 pub sheet_name: Option<&'a str>,
42}
43
44pub fn run_query(
45 workbook_path: &Path,
46 sheet_name: Option<&str>,
47 query: &str,
48 has_headers: bool,
49) -> Result<QueryResult> {
50 run_query_with_params(workbook_path, sheet_name, query, &[], has_headers)
51}
52
53pub fn run_query_with_params(
54 workbook_path: &Path,
55 sheet_name: Option<&str>,
56 query: &str,
57 params: &[QueryParam],
58 has_headers: bool,
59) -> Result<QueryResult> {
60 run_query_with_params_multi(&[workbook_path], sheet_name, query, params, has_headers)
61}
62
63pub fn run_query_with_params_multi(
64 workbook_paths: &[&Path],
65 sheet_name: Option<&str>,
66 query: &str,
67 params: &[QueryParam],
68 has_headers: bool,
69) -> Result<QueryResult> {
70 let workbook_inputs = workbook_paths
71 .iter()
72 .map(|path| WorkbookInput {
73 path,
74 sheet_name,
75 })
76 .collect::<Vec<_>>();
77
78 run_query_with_params_multi_inputs(&workbook_inputs, query, params, has_headers)
79}
80
81pub fn run_query_with_params_multi_inputs(
82 workbook_inputs: &[WorkbookInput<'_>],
83 query: &str,
84 params: &[QueryParam],
85 has_headers: bool,
86) -> Result<QueryResult> {
87 if workbook_inputs.is_empty() {
88 bail!("at least one workbook input is required");
89 }
90
91 let connection =
92 Connection::open_in_memory().context("failed to create in-memory SQLite database")?;
93 let mut registered_sheet_views = HashSet::new();
94
95 for (index, workbook_input) in workbook_inputs.iter().enumerate() {
96 let sheet = load_input(
97 workbook_input.path,
98 workbook_input.sheet_name,
99 has_headers,
100 )?;
101 let table_name = if index == 0 {
102 "sheet".to_owned()
103 } else {
104 format!("sheet{}", index + 1)
105 };
106
107 register_sheet(
108 &connection,
109 &sheet,
110 &table_name,
111 &mut registered_sheet_views,
112 )?;
113 }
114
115 execute_query(&connection, query, params)
116}
117
118pub fn render_text(result: &QueryResult) -> String {
119 if result.columns.is_empty() {
120 return String::new();
121 }
122
123 let mut column_widths = result.columns.iter().map(|column| column.len()).collect::<Vec<_>>();
124 let mut rendered_rows = Vec::with_capacity(result.rows.len());
125
126 for row in &result.rows {
127 let mut rendered_row = Vec::with_capacity(result.columns.len());
128 for index in 0..result.columns.len() {
129 let rendered_value = row
130 .get(index)
131 .map(display_value)
132 .unwrap_or_default();
133 column_widths[index] = column_widths[index].max(rendered_value.len());
134 rendered_row.push(rendered_value);
135 }
136 rendered_rows.push(rendered_row);
137 }
138
139 let mut output = String::new();
140 write_aligned_row(&mut output, &result.columns, &column_widths);
141 output.push('\n');
142 output.push_str(
143 &column_widths
144 .iter()
145 .map(|width| "-".repeat(*width))
146 .collect::<Vec<_>>()
147 .join("-+-"),
148 );
149
150 for row in rendered_rows {
151 output.push('\n');
152 write_aligned_row(&mut output, &row, &column_widths);
153 }
154
155 output
156}
157
158fn write_aligned_row(output: &mut String, values: &[String], column_widths: &[usize]) {
159 for (index, value) in values.iter().enumerate() {
160 if index > 0 {
161 output.push_str(" | ");
162 }
163
164 let _ = write!(output, "{value:<width$}", width = column_widths[index]);
165 }
166}
167
168pub fn render_csv(result: &QueryResult) -> String {
169 let mut output = String::new();
170 output.push_str(
171 &result
172 .columns
173 .iter()
174 .map(|column| escape_csv_field(column))
175 .collect::<Vec<_>>()
176 .join(","),
177 );
178
179 for row in &result.rows {
180 output.push('\n');
181 output.push_str(
182 &row.iter()
183 .map(display_value)
184 .map(|value| escape_csv_field(&value))
185 .collect::<Vec<_>>()
186 .join(","),
187 );
188 }
189
190 output
191}
192
193pub fn render_jsonl(result: &QueryResult) -> String {
194 let mut output = String::new();
195
196 for (row_index, row) in result.rows.iter().enumerate() {
197 if row_index > 0 {
198 output.push('\n');
199 }
200 output.push('{');
201 for (column_index, column) in result.columns.iter().enumerate() {
202 if column_index > 0 {
203 output.push(',');
204 }
205 output.push_str(&escape_json_string(column));
206 output.push(':');
207 output.push_str(&to_json_value(
208 row.get(column_index).unwrap_or(&QueryValue::Null),
209 ));
210 }
211 output.push('}');
212 }
213
214 output
215}
216
217pub fn render_markdown(result: &QueryResult) -> String {
218 let mut output = String::new();
219
220 output.push('|');
221 for column in &result.columns {
222 output.push(' ');
223 output.push_str(&escape_markdown_cell(column));
224 output.push(' ');
225 output.push('|');
226 }
227 output.push('\n');
228
229 output.push('|');
230 for _ in &result.columns {
231 output.push_str(" --- |");
232 }
233
234 for row in &result.rows {
235 output.push('\n');
236 output.push('|');
237 for column_index in 0..result.columns.len() {
238 let value = row.get(column_index).unwrap_or(&QueryValue::Null);
239 output.push(' ');
240 output.push_str(&escape_markdown_cell(&display_value(value)));
241 output.push(' ');
242 output.push('|');
243 }
244 }
245
246 output
247}
248
249pub fn render_xml(result: &QueryResult) -> String {
250 let mut output = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<data>\n");
251
252 for row in &result.rows {
253 output.push_str(" <row>\n");
254 for (column_index, column) in result.columns.iter().enumerate() {
255 let value = row.get(column_index).unwrap_or(&QueryValue::Null);
256 let xml_column = escape_xml_tag(column);
257 let xml_value = escape_xml_text(&display_value(value));
258 output.push_str(&format!(" <{}>{}</{}>
259", xml_column, xml_value, xml_column));
260 }
261 output.push_str(" </row>\n");
262 }
263
264 output.push_str("</data>");
265 output
266}
267
268pub fn write_xlsx(result: &QueryResult, output_path: &Path) -> Result<()> {
269 let mut workbook = Workbook::new();
270 let worksheet = workbook.add_worksheet();
271
272 for (column, header) in result.columns.iter().enumerate() {
273 worksheet.write_string(0, column as u16, header)?;
274 }
275
276 for (row_index, row) in result.rows.iter().enumerate() {
277 for (column_index, value) in row.iter().enumerate() {
278 let excel_row = (row_index + 1) as u32;
279 let excel_column = column_index as u16;
280
281 match value {
282 QueryValue::Null => {}
283 QueryValue::Integer(number) => {
284 worksheet.write_number(excel_row, excel_column, *number as f64)?;
285 }
286 QueryValue::Real(number) => {
287 worksheet.write_number(excel_row, excel_column, *number)?;
288 }
289 QueryValue::Text(text) => {
290 worksheet.write_string(excel_row, excel_column, text)?;
291 }
292 }
293 }
294 }
295
296 workbook
297 .save(output_path)
298 .with_context(|| format!("failed to write {}", output_path.display()))
299}
300
301#[derive(Debug)]
302struct SheetData {
303 original_name: String,
304 columns: Vec<String>,
305 rows: Vec<Vec<QueryValue>>,
306}
307
308fn load_input(
309 input_path: &Path,
310 requested_sheet: Option<&str>,
311 has_headers: bool,
312) -> Result<SheetData> {
313 if input_path
314 .extension()
315 .map(|extension| extension.to_string_lossy())
316 .is_some_and(|extension| extension.eq_ignore_ascii_case("xml"))
317 {
318 return load_xml_sheet(input_path, requested_sheet);
319 }
320
321 if input_path
322 .extension()
323 .map(|extension| extension.to_string_lossy())
324 .is_some_and(|extension| extension.eq_ignore_ascii_case("csv"))
325 {
326 return load_csv_sheet(input_path, requested_sheet, has_headers);
327 }
328
329 if input_path
330 .extension()
331 .map(|extension| extension.to_string_lossy())
332 .is_some_and(|extension| extension.eq_ignore_ascii_case("jsonl"))
333 {
334 return load_jsonl_sheet(input_path, requested_sheet);
335 }
336
337 if input_path
338 .extension()
339 .map(|extension| extension.to_string_lossy())
340 .is_some_and(|extension| extension.eq_ignore_ascii_case("json"))
341 {
342 return load_json_sheet(input_path, requested_sheet);
343 }
344
345 if input_path
346 .extension()
347 .map(|extension| extension.to_string_lossy())
348 .is_some_and(|extension| {
349 extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("markdown")
350 })
351 {
352 return load_markdown_sheet(input_path, requested_sheet, has_headers);
353 }
354
355 load_xlsx_sheet(input_path, requested_sheet, has_headers)
356}
357
358fn load_csv_sheet(
359 csv_path: &Path,
360 requested_sheet: Option<&str>,
361 has_headers: bool,
362) -> Result<SheetData> {
363 if requested_sheet.is_some() {
364 bail!("CSV input does not support sheet selection");
365 }
366
367 let mut reader = csv::ReaderBuilder::new()
368 .has_headers(has_headers)
369 .from_path(csv_path)
370 .with_context(|| format!("failed to open {}", csv_path.display()))?;
371
372 let mut records = Vec::new();
373 for record in reader.records() {
374 let record = record.with_context(|| {
375 format!("failed to read CSV record from {}", csv_path.display())
376 })?;
377 records.push(record.iter().map(str::to_owned).collect::<Vec<_>>());
378 }
379
380 let columns = if has_headers {
381 let headers = reader
382 .headers()
383 .with_context(|| format!("failed to read headers from {}", csv_path.display()))?
384 .iter()
385 .map(str::to_owned)
386 .collect::<Vec<_>>();
387 normalize_text_headers(&headers)
388 } else {
389 let width = records.iter().map(Vec::len).max().unwrap_or(0);
390 (1..=width).map(|index| format!("column{index}")).collect()
391 };
392
393 if columns.is_empty() {
394 bail!("CSV input {} is empty", csv_path.display());
395 }
396
397 let rows = records
398 .into_iter()
399 .map(|record| {
400 (0..columns.len())
401 .map(|index| {
402 let value = record.get(index).map(String::as_str).unwrap_or("");
403 parse_scalar_value(value)
404 })
405 .collect::<Vec<_>>()
406 })
407 .filter(|row| row.iter().any(|value| !matches!(value, QueryValue::Null)))
408 .collect::<Vec<_>>();
409
410 Ok(SheetData {
411 original_name: "csv".to_owned(),
412 columns,
413 rows,
414 })
415}
416
417fn load_jsonl_sheet(jsonl_path: &Path, requested_sheet: Option<&str>) -> Result<SheetData> {
418 if requested_sheet.is_some() {
419 bail!("JSONL input does not support sheet selection");
420 }
421
422 let content = fs::read_to_string(jsonl_path)
423 .with_context(|| format!("failed to read {}", jsonl_path.display()))?;
424
425 let mut rows_maps = Vec::<Vec<(String, QueryValue)>>::new();
426 for (line_index, raw_line) in content.lines().enumerate() {
427 let line = raw_line.trim();
428 if line.is_empty() {
429 continue;
430 }
431
432 let json_value = serde_json::from_str::<JsonValue>(line).with_context(|| {
433 format!(
434 "failed to parse JSONL line {} in {}",
435 line_index + 1,
436 jsonl_path.display()
437 )
438 })?;
439
440 let JsonValue::Object(object) = json_value else {
441 bail!(
442 "JSONL line {} in {} is not an object",
443 line_index + 1,
444 jsonl_path.display()
445 );
446 };
447
448 rows_maps.push(
449 object
450 .into_iter()
451 .map(|(key, value)| (key, json_to_query_value(value)))
452 .collect(),
453 );
454 }
455
456 if rows_maps.is_empty() {
457 bail!("JSONL input {} is empty", jsonl_path.display());
458 }
459
460 Ok(rows_maps_to_sheet_data(rows_maps, "jsonl".to_owned()))
461}
462
463fn load_json_sheet(json_path: &Path, requested_sheet: Option<&str>) -> Result<SheetData> {
464 let content = fs::read_to_string(json_path)
465 .with_context(|| format!("failed to read {}", json_path.display()))?;
466 let root = serde_json::from_str::<JsonValue>(&content)
467 .with_context(|| format!("failed to parse JSON {}", json_path.display()))?;
468
469 let scope = if let Some(sheet_key) = requested_sheet {
470 let JsonValue::Object(mut object) = root else {
471 bail!("JSON sheet selection requires a top-level object");
472 };
473
474 object
475 .remove(sheet_key)
476 .ok_or_else(|| anyhow!("JSON key '{sheet_key}' not found"))?
477 } else {
478 root
479 };
480
481 let rows_maps = json_scope_to_rows(scope);
482 if rows_maps.is_empty() {
483 bail!("JSON input {} is empty", json_path.display());
484 }
485
486 Ok(rows_maps_to_sheet_data(
487 rows_maps,
488 requested_sheet.unwrap_or("json").to_owned(),
489 ))
490}
491
492fn load_markdown_sheet(
493 markdown_path: &Path,
494 requested_sheet: Option<&str>,
495 has_headers: bool,
496) -> Result<SheetData> {
497 let content = fs::read_to_string(markdown_path)
498 .with_context(|| format!("failed to read {}", markdown_path.display()))?;
499 let tables = parse_markdown_tables(&content);
500 if tables.is_empty() {
501 bail!("Markdown input {} does not contain any table", markdown_path.display());
502 }
503
504 let table_index = if let Some(key) = requested_sheet {
505 let index = key
506 .trim()
507 .parse::<usize>()
508 .map_err(|_| anyhow!("Markdown KEY must be a positive integer (1-based table index)"))?;
509 if index == 0 {
510 bail!("Markdown KEY must be a positive integer (1-based table index)");
511 }
512 index
513 } else {
514 1
515 };
516
517 let Some(table) = tables.into_iter().nth(table_index - 1) else {
518 bail!(
519 "Markdown table {} not found in {}",
520 table_index,
521 markdown_path.display()
522 );
523 };
524
525 let mut rows = table.rows;
526 let columns = if has_headers {
527 normalize_text_headers(&table.headers)
528 } else {
529 rows.insert(0, table.headers);
530 let width = rows.iter().map(Vec::len).max().unwrap_or(0);
531 (1..=width).map(|index| format!("column{index}")).collect()
532 };
533
534 if columns.is_empty() {
535 bail!("Markdown table {} is empty", table_index);
536 }
537
538 let data_rows = rows
539 .into_iter()
540 .map(|row| {
541 (0..columns.len())
542 .map(|index| {
543 let value = row.get(index).map(String::as_str).unwrap_or("");
544 parse_scalar_value(value)
545 })
546 .collect::<Vec<_>>()
547 })
548 .filter(|row| row.iter().any(|value| !matches!(value, QueryValue::Null)))
549 .collect::<Vec<_>>();
550
551 Ok(SheetData {
552 original_name: format!("table{table_index}"),
553 columns,
554 rows: data_rows,
555 })
556}
557
558fn load_xlsx_sheet(
559 workbook_path: &Path,
560 requested_sheet: Option<&str>,
561 has_headers: bool,
562) -> Result<SheetData> {
563 let mut workbook = open_workbook_auto(workbook_path)
564 .with_context(|| format!("failed to open {}", workbook_path.display()))?;
565
566 let sheet_name = match requested_sheet {
567 Some(name) => name.to_owned(),
568 None => workbook
569 .sheet_names()
570 .first()
571 .cloned()
572 .ok_or_else(|| anyhow!("workbook does not contain any sheets"))?,
573 };
574
575 let range = workbook
576 .worksheet_range(&sheet_name)
577 .with_context(|| format!("failed to read sheet {sheet_name}"))?;
578
579 if range.width() == 0 {
580 bail!("sheet {sheet_name} is empty");
581 }
582
583 let mut rows = range.rows();
584 let columns = if has_headers {
585 let header_row = rows
586 .next()
587 .ok_or_else(|| anyhow!("sheet {sheet_name} is empty"))?;
588 normalize_headers(header_row, range.width())
589 } else {
590 (1..=range.width())
591 .map(|index| format!("column{index}"))
592 .collect()
593 };
594
595 let data_rows = rows
596 .map(|row| {
597 (0..columns.len())
598 .map(|index| convert_cell(row.get(index).unwrap_or(&Data::Empty)))
599 .collect::<Vec<_>>()
600 })
601 .filter(|row| row.iter().any(|value| !matches!(value, QueryValue::Null)))
602 .collect();
603
604 Ok(SheetData {
605 original_name: sheet_name,
606 columns,
607 rows: data_rows,
608 })
609}
610
611fn load_xml_sheet(xml_path: &Path, requested_sheet: Option<&str>) -> Result<SheetData> {
612 let xml_content = fs::read_to_string(xml_path)
613 .with_context(|| format!("failed to read {}", xml_path.display()))?;
614 let document = Document::parse(&xml_content)
615 .with_context(|| format!("failed to parse XML {}", xml_path.display()))?;
616
617 let root = document.root_element();
618 let scope = if let Some(sheet_tag) = requested_sheet {
619 root.descendants()
620 .find(|node| node.is_element() && node.tag_name().name() == sheet_tag)
621 .ok_or_else(|| anyhow!("XML tag '{sheet_tag}' not found"))?
622 } else {
623 root
624 };
625
626 let mut records = collect_xml_records(scope);
627 if records.is_empty() {
628 let fallback = xml_row_from_children(scope);
629 if fallback.is_empty() {
630 let scope_name = requested_sheet.unwrap_or(scope.tag_name().name());
631 bail!("XML scope '{scope_name}' does not contain tabular data");
632 }
633 records.push(fallback);
634 }
635
636 let mut columns = Vec::new();
637 for row in &records {
638 for (column, _) in row {
639 if !columns.iter().any(|existing| existing == column) {
640 columns.push(column.clone());
641 }
642 }
643 }
644
645 let rows = records
646 .into_iter()
647 .map(|row| {
648 columns
649 .iter()
650 .map(|column| {
651 row.iter()
652 .find(|(key, _)| key == column)
653 .map(|(_, value)| value.clone())
654 .unwrap_or(QueryValue::Null)
655 })
656 .collect::<Vec<_>>()
657 })
658 .collect::<Vec<_>>();
659
660 let original_name = requested_sheet
661 .map(str::to_owned)
662 .unwrap_or_else(|| scope.tag_name().name().to_owned());
663
664 Ok(SheetData {
665 original_name,
666 columns,
667 rows,
668 })
669}
670
671fn collect_xml_records(scope: Node<'_, '_>) -> Vec<Vec<(String, QueryValue)>> {
672 let direct_row_nodes = scope
673 .children()
674 .filter(|node| node.is_element() && node.tag_name().name().eq_ignore_ascii_case("row"))
675 .collect::<Vec<_>>();
676 if !direct_row_nodes.is_empty() {
677 return direct_row_nodes
678 .into_iter()
679 .map(xml_row_from_children)
680 .filter(|row| !row.is_empty())
681 .collect();
682 }
683
684 scope
685 .descendants()
686 .filter(|node| node.is_element() && *node != scope)
687 .filter(|node| is_xml_record_candidate(*node))
688 .map(xml_row_from_children)
689 .filter(|row| !row.is_empty())
690 .collect()
691}
692
693fn is_xml_record_candidate(node: Node<'_, '_>) -> bool {
694 let children = node
695 .children()
696 .filter(|child| child.is_element())
697 .collect::<Vec<_>>();
698
699 !children.is_empty()
700 && children
701 .iter()
702 .all(|child| !child.children().any(|inner| inner.is_element()))
703}
704
705fn xml_row_from_children(node: Node<'_, '_>) -> Vec<(String, QueryValue)> {
706 node.children()
707 .filter(|child| child.is_element())
708 .map(|child| {
709 let column = child.tag_name().name().to_owned();
710 let value = xml_text_to_query_value(&xml_text_content(child));
711 (column, value)
712 })
713 .collect()
714}
715
716fn xml_text_content(node: Node<'_, '_>) -> String {
717 node.text().unwrap_or_default().trim().to_owned()
718}
719
720fn xml_text_to_query_value(raw: &str) -> QueryValue {
721 parse_scalar_value(raw)
722}
723
724fn parse_scalar_value(raw: &str) -> QueryValue {
725 if raw.is_empty() {
726 return QueryValue::Null;
727 }
728
729 let trimmed = raw.trim();
730 if trimmed.is_empty() {
731 return QueryValue::Null;
732 }
733
734 if trimmed.eq_ignore_ascii_case("true") {
735 return QueryValue::Integer(1);
736 }
737
738 if trimmed.eq_ignore_ascii_case("false") {
739 return QueryValue::Integer(0);
740 }
741
742 if let Ok(value) = trimmed.parse::<i64>() {
743 return QueryValue::Integer(value);
744 }
745
746 if let Ok(value) = trimmed.parse::<f64>() {
747 return QueryValue::Real(value);
748 }
749
750 QueryValue::Text(raw.to_owned())
751}
752
753fn normalize_text_headers(headers: &[String]) -> Vec<String> {
754 let mut seen = std::collections::HashMap::new();
755
756 headers
757 .iter()
758 .enumerate()
759 .map(|(index, header)| {
760 let base = if header.trim().is_empty() {
761 format!("column{}", index + 1)
762 } else {
763 header.clone()
764 };
765
766 let count = seen
767 .entry(base.clone())
768 .and_modify(|count| *count += 1)
769 .or_insert(1usize);
770
771 if *count == 1 {
772 base
773 } else {
774 format!("{base}_{count}")
775 }
776 })
777 .collect()
778}
779
780fn rows_maps_to_sheet_data(rows_maps: Vec<Vec<(String, QueryValue)>>, original_name: String) -> SheetData {
781 let mut columns = Vec::<String>::new();
782 for row in &rows_maps {
783 for (column, _) in row {
784 if !columns.iter().any(|existing| existing == column) {
785 columns.push(column.clone());
786 }
787 }
788 }
789
790 let rows = rows_maps
791 .into_iter()
792 .map(|row| {
793 columns
794 .iter()
795 .map(|column| {
796 row.iter()
797 .find(|(key, _)| key == column)
798 .map(|(_, value)| value.clone())
799 .unwrap_or(QueryValue::Null)
800 })
801 .collect::<Vec<_>>()
802 })
803 .collect::<Vec<_>>();
804
805 SheetData {
806 original_name,
807 columns,
808 rows,
809 }
810}
811
812#[derive(Debug)]
813struct MarkdownTable {
814 headers: Vec<String>,
815 rows: Vec<Vec<String>>,
816}
817
818fn parse_markdown_tables(content: &str) -> Vec<MarkdownTable> {
819 let lines = content.lines().collect::<Vec<_>>();
820 let mut tables = Vec::new();
821 let mut index = 0usize;
822
823 while index + 1 < lines.len() {
824 if !looks_like_markdown_row(lines[index]) || !is_markdown_separator(lines[index + 1]) {
825 index += 1;
826 continue;
827 }
828
829 let headers = parse_markdown_row(lines[index]);
830 if headers.is_empty() {
831 index += 1;
832 continue;
833 }
834
835 let mut rows = Vec::new();
836 index += 2;
837 while index < lines.len() && looks_like_markdown_row(lines[index]) {
838 if is_markdown_separator(lines[index]) {
839 break;
840 }
841 let row = parse_markdown_row(lines[index]);
842 if row.iter().any(|value| !value.trim().is_empty()) {
843 rows.push(row);
844 }
845 index += 1;
846 }
847
848 tables.push(MarkdownTable { headers, rows });
849 }
850
851 tables
852}
853
854fn looks_like_markdown_row(line: &str) -> bool {
855 line.contains('|')
856}
857
858fn is_markdown_separator(line: &str) -> bool {
859 let parts = split_markdown_cells(line);
860 if parts.is_empty() {
861 return false;
862 }
863
864 parts.iter().all(|part| {
865 let cell = part.trim();
866 if cell.is_empty() {
867 return false;
868 }
869
870 let inner = cell.trim_matches(':');
871 !inner.is_empty() && inner.chars().all(|character| character == '-')
872 })
873}
874
875fn parse_markdown_row(line: &str) -> Vec<String> {
876 split_markdown_cells(line)
877 .into_iter()
878 .map(|cell| cell.replace("\\|", "|").trim().to_owned())
879 .collect()
880}
881
882fn split_markdown_cells(line: &str) -> Vec<String> {
883 let trimmed = line.trim();
884 if trimmed.is_empty() {
885 return Vec::new();
886 }
887
888 let without_prefix = trimmed.strip_prefix('|').unwrap_or(trimmed);
889 let content = without_prefix
890 .strip_suffix('|')
891 .unwrap_or(without_prefix);
892
893 content.split('|').map(str::to_owned).collect()
894}
895
896fn json_scope_to_rows(scope: JsonValue) -> Vec<Vec<(String, QueryValue)>> {
897 match scope {
898 JsonValue::Array(items) => items
899 .into_iter()
900 .map(json_item_to_row)
901 .collect::<Vec<_>>(),
902 JsonValue::Object(object) => vec![object
903 .into_iter()
904 .map(|(key, value)| (key, json_to_query_value(value)))
905 .collect()],
906 scalar => vec![vec![("value".to_owned(), json_to_query_value(scalar))]],
907 }
908}
909
910fn json_item_to_row(item: JsonValue) -> Vec<(String, QueryValue)> {
911 match item {
912 JsonValue::Object(object) => object
913 .into_iter()
914 .map(|(key, value)| (key, json_to_query_value(value)))
915 .collect(),
916 scalar => vec![("value".to_owned(), json_to_query_value(scalar))],
917 }
918}
919
920fn json_to_query_value(value: JsonValue) -> QueryValue {
921 match value {
922 JsonValue::Null => QueryValue::Null,
923 JsonValue::Bool(flag) => QueryValue::Integer(i64::from(flag)),
924 JsonValue::Number(number) => {
925 if let Some(integer) = number.as_i64() {
926 QueryValue::Integer(integer)
927 } else if let Some(real) = number.as_f64() {
928 QueryValue::Real(real)
929 } else {
930 QueryValue::Text(number.to_string())
931 }
932 }
933 JsonValue::String(text) => parse_scalar_value(&text),
934 JsonValue::Array(_) | JsonValue::Object(_) => QueryValue::Text(value.to_string()),
935 }
936}
937
938fn normalize_headers(header_row: &[Data], width: usize) -> Vec<String> {
939 let mut seen = std::collections::HashMap::new();
940
941 (0..width)
942 .map(|index| {
943 let base = header_row
944 .get(index)
945 .map(cell_to_string)
946 .filter(|value| !value.trim().is_empty())
947 .unwrap_or_else(|| format!("column{}", index + 1));
948
949 let count = seen
950 .entry(base.clone())
951 .and_modify(|count| *count += 1)
952 .or_insert(1usize);
953
954 if *count == 1 {
955 base
956 } else {
957 format!("{base}_{count}")
958 }
959 })
960 .collect()
961}
962
963fn convert_cell(cell: &Data) -> QueryValue {
964 match cell {
965 Data::Empty => QueryValue::Null,
966 Data::Int(value) => QueryValue::Integer(*value),
967 Data::Float(value) => QueryValue::Real(*value),
968 Data::String(value) => QueryValue::Text(value.clone()),
969 Data::Bool(value) => QueryValue::Integer(i64::from(*value)),
970 Data::DateTime(value) => QueryValue::Text(value.to_string()),
971 Data::DateTimeIso(value) => QueryValue::Text(value.clone()),
972 Data::DurationIso(value) => QueryValue::Text(value.clone()),
973 Data::Error(value) => QueryValue::Text(value.to_string()),
974 }
975}
976
977fn cell_to_string(cell: &Data) -> String {
978 match convert_cell(cell) {
979 QueryValue::Null => String::new(),
980 QueryValue::Integer(value) => value.to_string(),
981 QueryValue::Real(value) => value.to_string(),
982 QueryValue::Text(value) => value,
983 }
984}
985
986fn register_sheet(
987 connection: &Connection,
988 sheet: &SheetData,
989 table_name: &str,
990 registered_views: &mut HashSet<String>,
991) -> Result<()> {
992 let columns = sheet
993 .columns
994 .iter()
995 .map(|column| quote_identifier(column))
996 .collect::<Vec<_>>()
997 .join(", ");
998
999 connection
1000 .execute(
1001 &format!("CREATE TABLE {} ({columns})", quote_identifier(table_name)),
1002 [],
1003 )
1004 .context("failed to create sheet table")?;
1005
1006 let sheet1_key = normalize_view_key("sheet1");
1007 if table_name == "sheet" && !registered_views.contains(&sheet1_key) {
1008 connection
1009 .execute(
1010 &format!(
1011 "CREATE VIEW {} AS SELECT * FROM {}",
1012 quote_identifier("sheet1"),
1013 quote_identifier("sheet")
1014 ),
1015 [],
1016 )
1017 .context("failed to register alias view sheet1 for first input")?;
1018 registered_views.insert(sheet1_key);
1019 }
1020
1021 let sanitized_sheet_name = sanitize_table_name(&sheet.original_name);
1022 let sanitized_sheet_key = normalize_view_key(&sanitized_sheet_name);
1023 if sanitized_sheet_name != table_name && !registered_views.contains(&sanitized_sheet_key) {
1024 connection
1025 .execute(
1026 &format!(
1027 "CREATE VIEW {} AS SELECT * FROM {}",
1028 quote_identifier(&sanitized_sheet_name),
1029 quote_identifier(table_name)
1030 ),
1031 [],
1032 )
1033 .with_context(|| {
1034 format!("failed to register view for sheet {}", sheet.original_name)
1035 })?;
1036 registered_views.insert(sanitized_sheet_key);
1037 }
1038
1039 if sheet.rows.is_empty() {
1040 return Ok(());
1041 }
1042
1043 let placeholders = vec!["?"; sheet.columns.len()].join(", ");
1044 let insert_sql = format!(
1045 "INSERT INTO {} VALUES ({placeholders})",
1046 quote_identifier(table_name)
1047 );
1048 let mut statement = connection
1049 .prepare(&insert_sql)
1050 .context("failed to prepare insert statement")?;
1051
1052 for row in &sheet.rows {
1053 let values = row.iter().map(to_sql_value).collect::<Vec<_>>();
1054 statement
1055 .execute(params_from_iter(values))
1056 .context("failed to insert sheet row")?;
1057 }
1058
1059 Ok(())
1060}
1061
1062fn execute_query(connection: &Connection, query: &str, params: &[QueryParam]) -> Result<QueryResult> {
1063 let mut statement = connection
1064 .prepare(query)
1065 .with_context(|| format!("failed to prepare query: {query}"))?;
1066
1067 if statement.column_count() == 0 {
1068 bail!("query must return rows");
1069 }
1070
1071 let columns = statement
1072 .column_names()
1073 .into_iter()
1074 .map(str::to_owned)
1075 .collect::<Vec<_>>();
1076
1077 bind_query_params(&mut statement, params)?;
1078
1079 let column_count = statement.column_count();
1080 let mut rows = statement.raw_query();
1081 let mut result_rows = Vec::new();
1082
1083 while let Some(row) = rows.next().context("failed to fetch query row")? {
1084 let mut values = Vec::with_capacity(column_count);
1085
1086 for index in 0..column_count {
1087 values.push(match row.get_ref(index)? {
1088 ValueRef::Null => QueryValue::Null,
1089 ValueRef::Integer(value) => QueryValue::Integer(value),
1090 ValueRef::Real(value) => QueryValue::Real(value),
1091 ValueRef::Text(value) => {
1092 QueryValue::Text(String::from_utf8_lossy(value).into_owned())
1093 }
1094 ValueRef::Blob(value) => QueryValue::Text(format_blob(value)),
1095 });
1096 }
1097
1098 result_rows.push(values);
1099 }
1100
1101 Ok(QueryResult {
1102 columns,
1103 rows: result_rows,
1104 })
1105}
1106
1107fn bind_query_params(statement: &mut rusqlite::Statement<'_>, params: &[QueryParam]) -> Result<()> {
1108 for param in params {
1109 let mut bound = false;
1110
1111 for prefix in [":", "@", "$"] {
1112 let parameter_name = format!("{prefix}{}", param.name);
1113 if let Some(index) = statement
1114 .parameter_index(¶meter_name)
1115 .with_context(|| format!("failed to inspect parameter {parameter_name}"))?
1116 {
1117 statement
1118 .raw_bind_parameter(index, to_sql_value(¶m.value))
1119 .with_context(|| format!("failed to bind parameter {parameter_name}"))?;
1120 bound = true;
1121 break;
1122 }
1123 }
1124
1125 if !bound {
1126 bail!("query does not contain parameter :{}", param.name);
1127 }
1128 }
1129
1130 Ok(())
1131}
1132
1133fn to_sql_value(value: &QueryValue) -> Value {
1134 match value {
1135 QueryValue::Null => Value::Null,
1136 QueryValue::Integer(value) => Value::Integer(*value),
1137 QueryValue::Real(value) => Value::Real(*value),
1138 QueryValue::Text(value) => Value::Text(value.clone()),
1139 }
1140}
1141
1142fn display_value(value: &QueryValue) -> String {
1143 match value {
1144 QueryValue::Null => String::new(),
1145 QueryValue::Integer(value) => value.to_string(),
1146 QueryValue::Real(value) => value.to_string(),
1147 QueryValue::Text(value) => value.clone(),
1148 }
1149}
1150
1151fn escape_csv_field(value: &str) -> String {
1152 if value.contains([',', '"', '\n', '\r']) {
1153 format!("\"{}\"", value.replace('"', "\"\""))
1154 } else {
1155 value.to_owned()
1156 }
1157}
1158
1159fn escape_json_string(value: &str) -> String {
1160 let mut escaped = String::from("\"");
1161 for character in value.chars() {
1162 match character {
1163 '"' => escaped.push_str("\\\""),
1164 '\\' => escaped.push_str("\\\\"),
1165 '\n' => escaped.push_str("\\n"),
1166 '\r' => escaped.push_str("\\r"),
1167 '\t' => escaped.push_str("\\t"),
1168 '\u{08}' => escaped.push_str("\\b"),
1169 '\u{0C}' => escaped.push_str("\\f"),
1170 c if c <= '\u{1F}' => {
1171 let _ = write!(&mut escaped, "\\u{:04x}", c as u32);
1172 }
1173 c => escaped.push(c),
1174 }
1175 }
1176 escaped.push('"');
1177 escaped
1178}
1179
1180fn to_json_value(value: &QueryValue) -> String {
1181 match value {
1182 QueryValue::Null => "null".to_owned(),
1183 QueryValue::Integer(number) => number.to_string(),
1184 QueryValue::Real(number) if number.is_finite() => number.to_string(),
1185 QueryValue::Real(_) => "null".to_owned(),
1186 QueryValue::Text(text) => escape_json_string(text),
1187 }
1188}
1189
1190fn escape_markdown_cell(value: &str) -> String {
1191 value.replace('|', "\\|").replace('\n', "<br>")
1192}
1193
1194fn escape_xml_text(value: &str) -> String {
1195 value
1196 .replace('&', "&")
1197 .replace('<', "<")
1198 .replace('>', ">")
1199 .replace('"', """)
1200 .replace('\'', "'")
1201}
1202
1203fn escape_xml_tag(value: &str) -> String {
1204 let sanitized = value
1205 .chars()
1206 .map(|character| {
1207 if character.is_alphanumeric() || character == '_' || character == '-' {
1208 character
1209 } else {
1210 '_'
1211 }
1212 })
1213 .collect::<String>();
1214
1215 if sanitized.is_empty() {
1216 "element".to_owned()
1217 } else if sanitized.chars().next().unwrap_or('x').is_numeric() {
1218 format!("_{sanitized}")
1219 } else {
1220 sanitized
1221 }
1222}
1223
1224fn format_blob(value: &[u8]) -> String {
1225 let mut formatted = String::from("0x");
1226 for byte in value {
1227 let _ = write!(&mut formatted, "{byte:02x}");
1228 }
1229
1230 formatted
1231}
1232
1233fn quote_identifier(identifier: &str) -> String {
1234 format!("\"{}\"", identifier.replace('"', "\"\""))
1235}
1236
1237fn sanitize_table_name(name: &str) -> String {
1238 let sanitized = name
1239 .chars()
1240 .map(|character| {
1241 if character.is_alphanumeric() || character == '_' {
1242 character
1243 } else {
1244 '_'
1245 }
1246 })
1247 .collect::<String>()
1248 .trim_matches('_')
1249 .to_string();
1250
1251 if sanitized.is_empty() {
1252 "sheet_view".to_owned()
1253 } else {
1254 sanitized
1255 }
1256}
1257
1258fn normalize_view_key(name: &str) -> String {
1259 name.to_ascii_lowercase()
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264 use std::{
1265 fs,
1266 path::PathBuf,
1267 time::{SystemTime, UNIX_EPOCH},
1268 };
1269
1270 use super::*;
1271
1272 fn temp_path(name: &str) -> PathBuf {
1273 let unique = SystemTime::now()
1274 .duration_since(UNIX_EPOCH)
1275 .expect("time should move forward")
1276 .as_nanos();
1277 std::env::temp_dir().join(format!("query-forge-{name}-{unique}.xlsx"))
1278 }
1279
1280 fn temp_xml_path(name: &str) -> PathBuf {
1281 let unique = SystemTime::now()
1282 .duration_since(UNIX_EPOCH)
1283 .expect("time should move forward")
1284 .as_nanos();
1285 std::env::temp_dir().join(format!("query-forge-{name}-{unique}.xml"))
1286 }
1287
1288 fn temp_csv_path(name: &str) -> PathBuf {
1289 let unique = SystemTime::now()
1290 .duration_since(UNIX_EPOCH)
1291 .expect("time should move forward")
1292 .as_nanos();
1293 std::env::temp_dir().join(format!("query-forge-{name}-{unique}.csv"))
1294 }
1295
1296 fn temp_jsonl_path(name: &str) -> PathBuf {
1297 let unique = SystemTime::now()
1298 .duration_since(UNIX_EPOCH)
1299 .expect("time should move forward")
1300 .as_nanos();
1301 std::env::temp_dir().join(format!("query-forge-{name}-{unique}.jsonl"))
1302 }
1303
1304 fn temp_json_path(name: &str) -> PathBuf {
1305 let unique = SystemTime::now()
1306 .duration_since(UNIX_EPOCH)
1307 .expect("time should move forward")
1308 .as_nanos();
1309 std::env::temp_dir().join(format!("query-forge-{name}-{unique}.json"))
1310 }
1311
1312 fn temp_markdown_path(name: &str) -> PathBuf {
1313 let unique = SystemTime::now()
1314 .duration_since(UNIX_EPOCH)
1315 .expect("time should move forward")
1316 .as_nanos();
1317 std::env::temp_dir().join(format!("query-forge-{name}-{unique}.md"))
1318 }
1319
1320 fn write_test_workbook(path: &Path) -> Result<()> {
1321 let mut workbook = Workbook::new();
1322 let worksheet = workbook.add_worksheet();
1323
1324 worksheet.write_string(0, 0, "name")?;
1325 worksheet.write_string(0, 1, "price")?;
1326 worksheet.write_string(0, 2, "active")?;
1327 worksheet.write_string(1, 0, "Keyboard")?;
1328 worksheet.write_number(1, 1, 12.5)?;
1329 worksheet.write_boolean(1, 2, true)?;
1330 worksheet.write_string(2, 0, "Cable")?;
1331 worksheet.write_number(2, 1, 5.0)?;
1332 worksheet.write_boolean(2, 2, false)?;
1333
1334 workbook.save(path)?;
1335 Ok(())
1336 }
1337
1338 fn write_test_workbook_on_sheet(path: &Path, sheet_name: &str) -> Result<()> {
1339 let mut workbook = Workbook::new();
1340 let worksheet = workbook.add_worksheet();
1341 worksheet.set_name(sheet_name)?;
1342
1343 worksheet.write_string(0, 0, "name")?;
1344 worksheet.write_string(0, 1, "price")?;
1345 worksheet.write_string(1, 0, "Keyboard")?;
1346 worksheet.write_number(1, 1, 12.5)?;
1347
1348 workbook.save(path)?;
1349 Ok(())
1350 }
1351
1352 fn write_test_xml(path: &Path) -> Result<()> {
1353 fs::write(
1354 path,
1355 r#"<?xml version="1.0" encoding="UTF-8"?>
1356<data>
1357 <row>
1358 <name>Keyboard</name>
1359 <price>12.5</price>
1360 <active>true</active>
1361 </row>
1362 <row>
1363 <name>Cable</name>
1364 <price>5</price>
1365 <active>false</active>
1366 </row>
1367</data>
1368"#,
1369 )?;
1370
1371 Ok(())
1372 }
1373
1374 fn write_test_xml_with_sections(path: &Path) -> Result<()> {
1375 fs::write(
1376 path,
1377 r#"<?xml version="1.0" encoding="UTF-8"?>
1378<root>
1379 <Inventory>
1380 <row>
1381 <name>Keyboard</name>
1382 <price>12.5</price>
1383 </row>
1384 </Inventory>
1385 <Archive>
1386 <row>
1387 <name>Legacy Cable</name>
1388 <price>3.5</price>
1389 </row>
1390 </Archive>
1391</root>
1392"#,
1393 )?;
1394
1395 Ok(())
1396 }
1397
1398 fn write_test_csv(path: &Path) -> Result<()> {
1399 fs::write(
1400 path,
1401 "name,price,active\nKeyboard,12.5,true\nCable,5,false\n",
1402 )?;
1403
1404 Ok(())
1405 }
1406
1407 fn write_test_csv_no_headers(path: &Path) -> Result<()> {
1408 fs::write(path, "Keyboard,12.5,true\nCable,5,false\n")?;
1409
1410 Ok(())
1411 }
1412
1413 fn write_test_jsonl(path: &Path) -> Result<()> {
1414 fs::write(
1415 path,
1416 "{\"name\":\"Keyboard\",\"price\":12.5,\"active\":true}\n{\"name\":\"Cable\",\"price\":5,\"active\":false}\n",
1417 )?;
1418
1419 Ok(())
1420 }
1421
1422 fn write_test_json(path: &Path) -> Result<()> {
1423 fs::write(
1424 path,
1425 "[{\"name\":\"Keyboard\",\"price\":12.5,\"active\":true},{\"name\":\"Cable\",\"price\":5,\"active\":false}]",
1426 )?;
1427
1428 Ok(())
1429 }
1430
1431 fn write_test_json_with_sections(path: &Path) -> Result<()> {
1432 fs::write(
1433 path,
1434 "{\"Inventory\":[{\"name\":\"Keyboard\",\"price\":12.5}],\"Archive\":[{\"name\":\"Legacy Cable\",\"price\":3.5}]}",
1435 )?;
1436
1437 Ok(())
1438 }
1439
1440 fn write_test_markdown_with_tables(path: &Path) -> Result<()> {
1441 fs::write(
1442 path,
1443 r#"# Inventory Report
1444
1445| name | price | active |
1446| --- | ---: | :---: |
1447| Keyboard | 12.5 | true |
1448| Cable | 5 | false |
1449
1450## Archive
1451
1452| name | price |
1453| --- | --- |
1454| Legacy Cable | 3.5 |
1455"#,
1456 )?;
1457
1458 Ok(())
1459 }
1460
1461 #[test]
1462 fn normalizes_duplicate_and_blank_headers() {
1463 let headers = vec![
1464 Data::String("name".into()),
1465 Data::String(String::new()),
1466 Data::String("name".into()),
1467 ];
1468
1469 assert_eq!(
1470 normalize_headers(&headers, 3),
1471 vec!["name", "column2", "name_2"]
1472 );
1473 }
1474
1475 #[test]
1476 fn executes_sql_query_against_sheet() -> Result<()> {
1477 let workbook_path = temp_path("query");
1478 write_test_workbook(&workbook_path)?;
1479
1480 let result = run_query(
1481 &workbook_path,
1482 Some("Sheet1"),
1483 "SELECT name, price FROM sheet WHERE price > 10 ORDER BY price DESC",
1484 true,
1485 )?;
1486
1487 assert_eq!(result.columns, vec!["name", "price"]);
1488 assert_eq!(
1489 result.rows,
1490 vec![vec![
1491 QueryValue::Text("Keyboard".into()),
1492 QueryValue::Real(12.5)
1493 ]]
1494 );
1495
1496 fs::remove_file(workbook_path)?;
1497 Ok(())
1498 }
1499
1500 #[test]
1501 fn executes_sql_query_against_sheet1_alias() -> Result<()> {
1502 let workbook_path = temp_path("query-sheet1-alias");
1503 write_test_workbook(&workbook_path)?;
1504
1505 let result = run_query(
1506 &workbook_path,
1507 Some("Sheet1"),
1508 "SELECT name, price FROM sheet1 WHERE price > 10 ORDER BY price DESC",
1509 true,
1510 )?;
1511
1512 assert_eq!(result.columns, vec!["name", "price"]);
1513 assert_eq!(
1514 result.rows,
1515 vec![vec![
1516 QueryValue::Text("Keyboard".into()),
1517 QueryValue::Real(12.5)
1518 ]]
1519 );
1520
1521 fs::remove_file(workbook_path)?;
1522 Ok(())
1523 }
1524
1525 #[test]
1526 fn executes_sql_query_with_named_parameter() -> Result<()> {
1527 let workbook_path = temp_path("query-with-param");
1528 write_test_workbook(&workbook_path)?;
1529
1530 let result = run_query_with_params(
1531 &workbook_path,
1532 Some("Sheet1"),
1533 "SELECT name, price FROM sheet WHERE price > :min_price ORDER BY price DESC",
1534 &[QueryParam {
1535 name: "min_price".to_owned(),
1536 value: QueryValue::Real(10.0),
1537 }],
1538 true,
1539 )?;
1540
1541 assert_eq!(
1542 result,
1543 QueryResult {
1544 columns: vec!["name".to_owned(), "price".to_owned()],
1545 rows: vec![vec![
1546 QueryValue::Text("Keyboard".to_owned()),
1547 QueryValue::Real(12.5),
1548 ]],
1549 }
1550 );
1551
1552 fs::remove_file(&workbook_path)?;
1553 Ok(())
1554 }
1555
1556 #[test]
1557 fn executes_query_against_multiple_workbooks() -> Result<()> {
1558 let workbook_path_1 = temp_path("multi-1");
1559 let workbook_path_2 = temp_path("multi-2");
1560 write_test_workbook(&workbook_path_1)?;
1561 write_test_workbook(&workbook_path_2)?;
1562
1563 let result = run_query_with_params_multi(
1564 &[workbook_path_1.as_path(), workbook_path_2.as_path()],
1565 Some("Sheet1"),
1566 "SELECT COUNT(*) AS total_rows FROM sheet UNION ALL SELECT COUNT(*) AS total_rows FROM sheet2",
1567 &[],
1568 true,
1569 )?;
1570
1571 assert_eq!(result.columns, vec!["total_rows"]);
1572 assert_eq!(
1573 result.rows,
1574 vec![vec![QueryValue::Integer(2)], vec![QueryValue::Integer(2)]]
1575 );
1576
1577 fs::remove_file(&workbook_path_1)?;
1578 fs::remove_file(&workbook_path_2)?;
1579 Ok(())
1580 }
1581
1582 #[test]
1583 fn executes_query_against_multiple_workbooks_with_distinct_sheet_names() -> Result<()> {
1584 let workbook_path_1 = temp_path("multi-sheet-1");
1585 let workbook_path_2 = temp_path("multi-sheet-2");
1586 write_test_workbook_on_sheet(&workbook_path_1, "Consuntivo")?;
1587 write_test_workbook_on_sheet(&workbook_path_2, "WKL")?;
1588
1589 let result = run_query_with_params_multi_inputs(
1590 &[
1591 WorkbookInput {
1592 path: workbook_path_1.as_path(),
1593 sheet_name: Some("Consuntivo"),
1594 },
1595 WorkbookInput {
1596 path: workbook_path_2.as_path(),
1597 sheet_name: Some("WKL"),
1598 },
1599 ],
1600 "SELECT COUNT(*) AS total_rows FROM sheet UNION ALL SELECT COUNT(*) AS total_rows FROM sheet2",
1601 &[],
1602 true,
1603 )?;
1604
1605 assert_eq!(result.columns, vec!["total_rows"]);
1606 assert_eq!(
1607 result.rows,
1608 vec![vec![QueryValue::Integer(1)], vec![QueryValue::Integer(1)]]
1609 );
1610
1611 fs::remove_file(&workbook_path_1)?;
1612 fs::remove_file(&workbook_path_2)?;
1613 Ok(())
1614 }
1615
1616 #[test]
1617 fn executes_sql_query_against_whole_xml_file() -> Result<()> {
1618 let xml_path = temp_xml_path("xml-whole");
1619 write_test_xml(&xml_path)?;
1620
1621 let result = run_query(
1622 &xml_path,
1623 None,
1624 "SELECT name, price FROM sheet WHERE active = 1 ORDER BY price DESC",
1625 true,
1626 )?;
1627
1628 assert_eq!(result.columns, vec!["name", "price"]);
1629 assert_eq!(
1630 result.rows,
1631 vec![vec![
1632 QueryValue::Text("Keyboard".into()),
1633 QueryValue::Real(12.5)
1634 ]]
1635 );
1636
1637 fs::remove_file(xml_path)?;
1638 Ok(())
1639 }
1640
1641 #[test]
1642 fn executes_sql_query_against_xml_sheet_tag() -> Result<()> {
1643 let xml_path = temp_xml_path("xml-sheet-tag");
1644 write_test_xml_with_sections(&xml_path)?;
1645
1646 let result = run_query(
1647 &xml_path,
1648 Some("Archive"),
1649 "SELECT name, price FROM sheet",
1650 true,
1651 )?;
1652
1653 assert_eq!(result.columns, vec!["name", "price"]);
1654 assert_eq!(
1655 result.rows,
1656 vec![vec![
1657 QueryValue::Text("Legacy Cable".into()),
1658 QueryValue::Real(3.5)
1659 ]]
1660 );
1661
1662 fs::remove_file(xml_path)?;
1663 Ok(())
1664 }
1665
1666 #[test]
1667 fn executes_query_against_heterogeneous_xlsx_and_xml_inputs() -> Result<()> {
1668 let workbook_path = temp_path("mixed-xlsx");
1669 let xml_path = temp_xml_path("mixed-xml");
1670 write_test_workbook(&workbook_path)?;
1671 write_test_xml(&xml_path)?;
1672
1673 let result = run_query_with_params_multi_inputs(
1674 &[
1675 WorkbookInput {
1676 path: workbook_path.as_path(),
1677 sheet_name: Some("Sheet1"),
1678 },
1679 WorkbookInput {
1680 path: xml_path.as_path(),
1681 sheet_name: None,
1682 },
1683 ],
1684 "SELECT COUNT(*) AS total_rows FROM sheet UNION ALL SELECT COUNT(*) AS total_rows FROM sheet2",
1685 &[],
1686 true,
1687 )?;
1688
1689 assert_eq!(result.columns, vec!["total_rows"]);
1690 assert_eq!(
1691 result.rows,
1692 vec![vec![QueryValue::Integer(2)], vec![QueryValue::Integer(2)]]
1693 );
1694
1695 fs::remove_file(&workbook_path)?;
1696 fs::remove_file(&xml_path)?;
1697 Ok(())
1698 }
1699
1700 #[test]
1701 fn executes_sql_query_against_csv_file() -> Result<()> {
1702 let csv_path = temp_csv_path("csv");
1703 write_test_csv(&csv_path)?;
1704
1705 let result = run_query(
1706 &csv_path,
1707 None,
1708 "SELECT name, price FROM sheet WHERE active = 1 ORDER BY price DESC",
1709 true,
1710 )?;
1711
1712 assert_eq!(result.columns, vec!["name", "price"]);
1713 assert_eq!(
1714 result.rows,
1715 vec![vec![
1716 QueryValue::Text("Keyboard".into()),
1717 QueryValue::Real(12.5)
1718 ]]
1719 );
1720
1721 fs::remove_file(csv_path)?;
1722 Ok(())
1723 }
1724
1725 #[test]
1726 fn executes_sql_query_against_csv_without_headers() -> Result<()> {
1727 let csv_path = temp_csv_path("csv-no-headers");
1728 write_test_csv_no_headers(&csv_path)?;
1729
1730 let result = run_query(
1731 &csv_path,
1732 None,
1733 "SELECT column1, column2 FROM sheet WHERE column3 = 1 ORDER BY column2 DESC",
1734 false,
1735 )?;
1736
1737 assert_eq!(result.columns, vec!["column1", "column2"]);
1738 assert_eq!(
1739 result.rows,
1740 vec![vec![
1741 QueryValue::Text("Keyboard".into()),
1742 QueryValue::Real(12.5)
1743 ]]
1744 );
1745
1746 fs::remove_file(csv_path)?;
1747 Ok(())
1748 }
1749
1750 #[test]
1751 fn executes_sql_query_against_jsonl_file() -> Result<()> {
1752 let jsonl_path = temp_jsonl_path("jsonl");
1753 write_test_jsonl(&jsonl_path)?;
1754
1755 let result = run_query(
1756 &jsonl_path,
1757 None,
1758 "SELECT name, price FROM sheet WHERE active = 1 ORDER BY price DESC",
1759 true,
1760 )?;
1761
1762 assert_eq!(result.columns, vec!["name", "price"]);
1763 assert_eq!(
1764 result.rows,
1765 vec![vec![
1766 QueryValue::Text("Keyboard".into()),
1767 QueryValue::Real(12.5)
1768 ]]
1769 );
1770
1771 fs::remove_file(jsonl_path)?;
1772 Ok(())
1773 }
1774
1775 #[test]
1776 fn executes_sql_query_against_json_file() -> Result<()> {
1777 let json_path = temp_json_path("json");
1778 write_test_json(&json_path)?;
1779
1780 let result = run_query(
1781 &json_path,
1782 None,
1783 "SELECT name, price FROM sheet WHERE active = 1 ORDER BY price DESC",
1784 true,
1785 )?;
1786
1787 assert_eq!(result.columns, vec!["name", "price"]);
1788 assert_eq!(
1789 result.rows,
1790 vec![vec![
1791 QueryValue::Text("Keyboard".into()),
1792 QueryValue::Real(12.5)
1793 ]]
1794 );
1795
1796 fs::remove_file(json_path)?;
1797 Ok(())
1798 }
1799
1800 #[test]
1801 fn executes_sql_query_against_json_sheet_key() -> Result<()> {
1802 let json_path = temp_json_path("json-sheet-key");
1803 write_test_json_with_sections(&json_path)?;
1804
1805 let result = run_query(
1806 &json_path,
1807 Some("Archive"),
1808 "SELECT name, price FROM sheet",
1809 true,
1810 )?;
1811
1812 assert_eq!(result.columns, vec!["name", "price"]);
1813 assert_eq!(
1814 result.rows,
1815 vec![vec![
1816 QueryValue::Text("Legacy Cable".into()),
1817 QueryValue::Real(3.5)
1818 ]]
1819 );
1820
1821 fs::remove_file(json_path)?;
1822 Ok(())
1823 }
1824
1825 #[test]
1826 fn executes_sql_query_against_first_markdown_table_by_default() -> Result<()> {
1827 let markdown_path = temp_markdown_path("markdown-default");
1828 write_test_markdown_with_tables(&markdown_path)?;
1829
1830 let result = run_query(
1831 &markdown_path,
1832 None,
1833 "SELECT name, price FROM sheet WHERE active = 1",
1834 true,
1835 )?;
1836
1837 assert_eq!(result.columns, vec!["name", "price"]);
1838 assert_eq!(
1839 result.rows,
1840 vec![vec![
1841 QueryValue::Text("Keyboard".into()),
1842 QueryValue::Real(12.5)
1843 ]]
1844 );
1845
1846 fs::remove_file(markdown_path)?;
1847 Ok(())
1848 }
1849
1850 #[test]
1851 fn executes_sql_query_against_markdown_table_by_numeric_key() -> Result<()> {
1852 let markdown_path = temp_markdown_path("markdown-key");
1853 write_test_markdown_with_tables(&markdown_path)?;
1854
1855 let result = run_query(
1856 &markdown_path,
1857 Some("2"),
1858 "SELECT name, price FROM sheet",
1859 true,
1860 )?;
1861
1862 assert_eq!(result.columns, vec!["name", "price"]);
1863 assert_eq!(
1864 result.rows,
1865 vec![vec![
1866 QueryValue::Text("Legacy Cable".into()),
1867 QueryValue::Real(3.5)
1868 ]]
1869 );
1870
1871 fs::remove_file(markdown_path)?;
1872 Ok(())
1873 }
1874
1875 #[test]
1876 fn executes_query_against_heterogeneous_xlsx_xml_csv_jsonl_json_markdown_inputs() -> Result<()> {
1877 let workbook_path = temp_path("mixed4-xlsx");
1878 let xml_path = temp_xml_path("mixed4-xml");
1879 let csv_path = temp_csv_path("mixed4-csv");
1880 let jsonl_path = temp_jsonl_path("mixed4-jsonl");
1881 let json_path = temp_json_path("mixed4-json");
1882 let markdown_path = temp_markdown_path("mixed4-markdown");
1883 write_test_workbook(&workbook_path)?;
1884 write_test_xml(&xml_path)?;
1885 write_test_csv(&csv_path)?;
1886 write_test_jsonl(&jsonl_path)?;
1887 write_test_json(&json_path)?;
1888 write_test_markdown_with_tables(&markdown_path)?;
1889
1890 let result = run_query_with_params_multi_inputs(
1891 &[
1892 WorkbookInput {
1893 path: workbook_path.as_path(),
1894 sheet_name: Some("Sheet1"),
1895 },
1896 WorkbookInput {
1897 path: xml_path.as_path(),
1898 sheet_name: None,
1899 },
1900 WorkbookInput {
1901 path: csv_path.as_path(),
1902 sheet_name: None,
1903 },
1904 WorkbookInput {
1905 path: jsonl_path.as_path(),
1906 sheet_name: None,
1907 },
1908 WorkbookInput {
1909 path: json_path.as_path(),
1910 sheet_name: None,
1911 },
1912 WorkbookInput {
1913 path: markdown_path.as_path(),
1914 sheet_name: None,
1915 },
1916 ],
1917 "SELECT COUNT(*) AS total_rows FROM sheet UNION ALL SELECT COUNT(*) AS total_rows FROM sheet2 UNION ALL SELECT COUNT(*) AS total_rows FROM sheet3 UNION ALL SELECT COUNT(*) AS total_rows FROM sheet4 UNION ALL SELECT COUNT(*) AS total_rows FROM sheet5 UNION ALL SELECT COUNT(*) AS total_rows FROM sheet6",
1918 &[],
1919 true,
1920 )?;
1921
1922 assert_eq!(result.columns, vec!["total_rows"]);
1923 assert_eq!(
1924 result.rows,
1925 vec![
1926 vec![QueryValue::Integer(2)],
1927 vec![QueryValue::Integer(2)],
1928 vec![QueryValue::Integer(2)],
1929 vec![QueryValue::Integer(2)],
1930 vec![QueryValue::Integer(2)],
1931 vec![QueryValue::Integer(2)]
1932 ]
1933 );
1934
1935 fs::remove_file(&workbook_path)?;
1936 fs::remove_file(&xml_path)?;
1937 fs::remove_file(&csv_path)?;
1938 fs::remove_file(&jsonl_path)?;
1939 fs::remove_file(&json_path)?;
1940 fs::remove_file(&markdown_path)?;
1941 Ok(())
1942 }
1943
1944 #[test]
1945 fn writes_query_result_to_xlsx() -> Result<()> {
1946 let output_path = temp_path("output");
1947 let result = QueryResult {
1948 columns: vec!["item".into(), "total".into()],
1949 rows: vec![vec![
1950 QueryValue::Text("Mouse".into()),
1951 QueryValue::Integer(3),
1952 ]],
1953 };
1954
1955 write_xlsx(&result, &output_path)?;
1956
1957 let written = run_query(
1958 &output_path,
1959 Some("Sheet1"),
1960 "SELECT item, total FROM sheet",
1961 true,
1962 )?;
1963
1964 assert_eq!(written.columns, vec!["item", "total"]);
1965 assert_eq!(
1966 written.rows,
1967 vec![vec![
1968 QueryValue::Text("Mouse".into()),
1969 QueryValue::Real(3.0)
1970 ]]
1971 );
1972
1973 fs::remove_file(output_path)?;
1974 Ok(())
1975 }
1976
1977 #[test]
1978 fn renders_csv_with_escaping() {
1979 let result = QueryResult {
1980 columns: vec!["name".into(), "notes".into()],
1981 rows: vec![vec![
1982 QueryValue::Text("Mouse".into()),
1983 QueryValue::Text("line1,line2".into()),
1984 ]],
1985 };
1986
1987 assert_eq!(render_csv(&result), "name,notes\nMouse,\"line1,line2\"");
1988 }
1989
1990 #[test]
1991 fn renders_text_with_aligned_columns() {
1992 let result = QueryResult {
1993 columns: vec!["mese".into(), "totale_ore".into()],
1994 rows: vec![
1995 vec![QueryValue::Text("2026-01-01".into()), QueryValue::Real(10.5)],
1996 vec![QueryValue::Text("2026-12-01".into()), QueryValue::Integer(2)],
1997 ],
1998 };
1999
2000 assert_eq!(
2001 render_text(&result),
2002 "mese | totale_ore\n-----------+-----------\n2026-01-01 | 10.5 \n2026-12-01 | 2 "
2003 );
2004 }
2005
2006 #[test]
2007 fn renders_jsonl() {
2008 let result = QueryResult {
2009 columns: vec!["item".into(), "stock".into(), "note".into()],
2010 rows: vec![vec![
2011 QueryValue::Text("Desk".into()),
2012 QueryValue::Integer(8),
2013 QueryValue::Null,
2014 ]],
2015 };
2016
2017 assert_eq!(
2018 render_jsonl(&result),
2019 "{\"item\":\"Desk\",\"stock\":8,\"note\":null}"
2020 );
2021 }
2022
2023 #[test]
2024 fn renders_markdown() {
2025 let result = QueryResult {
2026 columns: vec!["category".into(), "total".into()],
2027 rows: vec![vec![
2028 QueryValue::Text("electronics".into()),
2029 QueryValue::Integer(47),
2030 ]],
2031 };
2032
2033 assert_eq!(
2034 render_markdown(&result),
2035 "| category | total |\n| --- | --- |\n| electronics | 47 |"
2036 );
2037 }
2038}