query-forge 0.7.0

Run SQL queries and dataset diffs on XLSX/XML/CSV/JSON/JSONL/Markdown/HTML/Parquet inputs and export results as text, CSV, JSONL, Markdown, XML, HTML, XLSX, or Parquet
Documentation
use anyhow::{Context, Result, bail};

use crate::{ExtractionOptions, QueryValue, TypeInferenceOptions, XmlMode};
use roxmltree::{Document, Node};

use super::{SheetData, parse_scalar_value};

pub(super) fn load_xml_sheet(
    xml_path: &std::path::Path,
    requested_sheet: Option<&str>,
    inference_options: &TypeInferenceOptions,
    extraction_options: &ExtractionOptions,
) -> Result<SheetData> {
    let xml_content = std::fs::read_to_string(xml_path)
        .with_context(|| format!("failed to read {}", xml_path.display()))?;
    let document = Document::parse(&xml_content)
        .with_context(|| format!("failed to parse XML {}", xml_path.display()))?;

    let root = document.root_element();
    let scope = if let Some(sheet_tag) = requested_sheet {
        root.descendants()
            .find(|node| node.is_element() && node.tag_name().name() == sheet_tag)
            .ok_or_else(|| anyhow::anyhow!("XML tag '{sheet_tag}' not found"))?
    } else {
        root
    };

    let records = match extraction_options.xml_mode {
        XmlMode::Rows => {
            let mut records = collect_xml_records(scope, inference_options);
            if records.is_empty() {
                let fallback = xml_row_from_children(scope, inference_options);
                if fallback.is_empty() {
                    let scope_name = requested_sheet.unwrap_or(scope.tag_name().name());
                    bail!("XML scope '{scope_name}' does not contain tabular data");
                }
                records.push(fallback);
            }
            records
        }
        XmlMode::Descendants => {
            let records = collect_xml_descendants(scope, inference_options);
            if records.is_empty() {
                let scope_name = requested_sheet.unwrap_or(scope.tag_name().name());
                bail!(
                    "XML scope '{scope_name}' does not contain any leaf text elements (--xml-mode descendants)"
                );
            }
            records
        }
        XmlMode::Attributes => {
            let records = collect_xml_attributes_records(scope, inference_options);
            if records.is_empty() {
                let scope_name = requested_sheet.unwrap_or(scope.tag_name().name());
                bail!(
                    "XML scope '{scope_name}' does not contain any elements with attributes (--xml-mode attributes)"
                );
            }
            records
        }
    };

    let mut columns = Vec::new();
    for row in &records {
        for (column, _) in row {
            if !columns.iter().any(|existing| existing == column) {
                columns.push(column.clone());
            }
        }
    }

    let rows = records
        .into_iter()
        .map(|row| {
            columns
                .iter()
                .map(|column| {
                    row.iter()
                        .find(|(key, _)| key == column)
                        .map(|(_, value)| value.clone())
                        .unwrap_or(QueryValue::Null)
                })
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();

    let original_name = requested_sheet
        .map(str::to_owned)
        .unwrap_or_else(|| scope.tag_name().name().to_owned());

    Ok(SheetData {
        original_name,
        columns,
        rows,
    })
}

fn collect_xml_records(
    scope: Node<'_, '_>,
    inference_options: &TypeInferenceOptions,
) -> Vec<Vec<(String, QueryValue)>> {
    let direct_row_nodes = scope
        .children()
        .filter(|node| node.is_element() && node.tag_name().name().eq_ignore_ascii_case("row"))
        .collect::<Vec<_>>();
    if !direct_row_nodes.is_empty() {
        return direct_row_nodes
            .into_iter()
            .map(|row| xml_row_from_children(row, inference_options))
            .filter(|row| !row.is_empty())
            .collect();
    }

    scope
        .descendants()
        .filter(|node| node.is_element() && *node != scope)
        .filter(|node| is_xml_record_candidate(*node))
        .map(|row| xml_row_from_children(row, inference_options))
        .filter(|row| !row.is_empty())
        .collect()
}

fn is_xml_record_candidate(node: Node<'_, '_>) -> bool {
    let children = node
        .children()
        .filter(|child| child.is_element())
        .collect::<Vec<_>>();

    !children.is_empty()
        && children
            .iter()
            .all(|child| !child.children().any(|inner| inner.is_element()))
}

fn xml_row_from_children(
    node: Node<'_, '_>,
    inference_options: &TypeInferenceOptions,
) -> Vec<(String, QueryValue)> {
    node.children()
        .filter(|child| child.is_element())
        .map(|child| {
            let column = child.tag_name().name().to_owned();
            let value = xml_text_to_query_value(&xml_text_content(child), inference_options);
            (column, value)
        })
        .collect()
}

fn xml_text_content(node: Node<'_, '_>) -> String {
    node.text().unwrap_or_default().trim().to_owned()
}

fn xml_text_to_query_value(raw: &str, inference_options: &TypeInferenceOptions) -> QueryValue {
    parse_scalar_value(raw, inference_options)
}

fn collect_xml_descendants(
    scope: Node<'_, '_>,
    inference_options: &TypeInferenceOptions,
) -> Vec<Vec<(String, QueryValue)>> {
    scope
        .descendants()
        .filter(|node| node.is_element() && *node != scope)
        .filter(|node| !node.children().any(|child| child.is_element()))
        .map(|node| {
            let tag = node.tag_name().name().to_owned();
            let value = xml_text_to_query_value(&xml_text_content(node), inference_options);
            vec![
                ("tag".to_owned(), QueryValue::Text(tag)),
                ("value".to_owned(), value),
            ]
        })
        .collect()
}

fn collect_xml_attributes_records(
    scope: Node<'_, '_>,
    inference_options: &TypeInferenceOptions,
) -> Vec<Vec<(String, QueryValue)>> {
    scope
        .descendants()
        .filter(|node| node.is_element() && *node != scope)
        .filter(|node| node.attributes().count() > 0)
        .map(|node| {
            let mut row: Vec<(String, QueryValue)> = node
                .attributes()
                .map(|attr| {
                    let column = attr.name().to_owned();
                    let value = xml_text_to_query_value(attr.value(), inference_options);
                    (column, value)
                })
                .collect();
            let text = xml_text_content(node);
            if !text.is_empty() {
                row.push((
                    "value".to_owned(),
                    xml_text_to_query_value(&text, inference_options),
                ));
            }
            row
        })
        .filter(|row| !row.is_empty())
        .collect()
}