Skip to main content

query_forge/
lib.rs

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(&parameter_name)
1115                .with_context(|| format!("failed to inspect parameter {parameter_name}"))?
1116            {
1117                statement
1118                    .raw_bind_parameter(index, to_sql_value(&param.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('&', "&amp;")
1197        .replace('<', "&lt;")
1198        .replace('>', "&gt;")
1199        .replace('"', "&quot;")
1200        .replace('\'', "&apos;")
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}