query-forge 0.2.0

Run SQL queries on XLSX/XML/CSV/JSON/JSONL/Markdown inputs and export results as text, CSV, JSONL, Markdown, XML, or XLSX
Documentation
use std::path::PathBuf;

use clap::{ArgAction, ArgGroup, Args, Parser, Subcommand, ValueEnum};

#[derive(Debug, Parser)]
#[command(author, version, about = "Query XLSX/XML/CSV/JSON/JSONL/Markdown datasets with SQL")]
pub(crate) struct Cli {
    #[command(subcommand)]
    pub(crate) command: Commands,
}

#[derive(Debug, Subcommand)]
pub(crate) enum Commands {
    /// Execute a SQL query against one or more input files.
    #[command(
        after_long_help = "Examples:\n\
    qf query --input ./input.xlsx:Sheet1 --sql \"SELECT * FROM table\"\n\
    qf query --input ./inventory.xml:Inventory --sql \"SELECT product, price FROM table WHERE active = 1\"\n\
    qf query --input ./inventory.csv --sql \"SELECT product, price FROM table WHERE active = 1\"\n\
    qf query --input ./inventory.json:Inventory --sql \"SELECT product, price FROM table WHERE active = 1\"\n\
    qf query --input ./inventory.jsonl --sql \"SELECT product, price FROM table WHERE active = 1\"\n\
    qf query --input ./a.xlsx:Sheet1 --input ./b.csv --sql \"SELECT * FROM table UNION ALL SELECT * FROM table2\"\n\
    qf query --input sales=./sales.xlsx:Q1 --input costs=./costs.csv --sql \"SELECT * FROM sales JOIN costs ON sales.id = costs.id\"\n\
    qf query --input ./inventory.md --sql \"SELECT product, price FROM table WHERE active = 1\"\n\
    qf query --input ./inventory.md:2 --sql \"SELECT product, price FROM table\"\n\
    qf query --input ./input.xlsx:Sheet1 --query-file ./query.sql --output ./result.csv --format csv"
    )]
    Query(QueryCommand),

    /// List logical SQL tables generated from inputs.
    #[command(
        after_long_help = "Examples:\n\
    qf tables --input sales=./sales.xlsx:Q1 --input costs=./costs.csv\n\
    qf tables --input ./inventory.json:Inventory --format json\n\
    qf tables --input ./inventory.xml:Inventory --input ./inventory.csv --format markdown"
    )]
    Tables(TablesCommand),

    /// Show per-table column names and inferred SQL-compatible types.
    #[command(
        after_long_help = "Examples:\n\
    qf schema --input ./inventory.json:Inventory\n\
    qf schema --input sales=./sales.xlsx:Q1 --input costs=./costs.csv --format json\n\
    qf schema --input ./inventory.xml:Inventory --format markdown"
    )]
    Schema(SchemaCommand),

    /// Return a compact diagnostic summary (tables, row counts, column counts, null density, and sample rows).
    #[command(
        after_long_help = "Examples:\n\
    qf inspect --input ./inventory.md:2 --sample 5\n\
    qf inspect --input sales=./sales.xlsx:Q1 --input costs=./costs.csv --format json\n\
    qf inspect --input ./inventory.csv --stats --format markdown"
    )]
    Inspect(InspectCommand),
}

#[derive(Debug, Args)]
#[command(group(
    ArgGroup::new("query_source")
        .required(true)
        .args(["sql", "query_file"])
))]
pub(crate) struct QueryCommand {
    #[arg(
        short,
        long,
        required = true,
        action = ArgAction::Append,
        value_name = "[NAME=]PATH[:SHEET|KEY]",
        long_help = "Input dataset path. Supported formats: .xlsx, .xml, .csv, .json, .jsonl, .md, .markdown.\n\
Optionally prefix with a table name: NAME=PATH (e.g. sales=./sales.xlsx:Q1).\n\
Without a name prefix, tables are named 'table', 'table2', 'table3', ...\n\
For XLSX use PATH:SheetName (or PATH#SheetName) to select a worksheet.\n\
For XML use PATH:TagName (or PATH#TagName) to select a subtree; without sheet/tag the whole file is used.\n\
For JSON use PATH:Key (or PATH#Key) to select a top-level key; without key the whole JSON root is used.\n\
For Markdown use PATH:N (or PATH#N), where N is the 1-based table number; without key the first table is used.\n\
CSV and JSONL do not support sheet/key selection."
    )]
    pub(crate) input: Vec<String>,
    #[arg(
        long,
        alias = "query",
        short = 'q',
        conflicts_with = "query_file",
        long_help = "SQL query to execute. Use 'table', 'table2', 'table3', ... or explicit table names.\n\
The flag --query is also accepted as an alias."
    )]
    pub(crate) sql: Option<String>,
    #[arg(long = "query-file", conflicts_with = "sql")]
    pub(crate) query_file: Option<PathBuf>,
    #[arg(long = "param", value_name = "NAME=VALUE", action = ArgAction::Append)]
    pub(crate) params: Vec<String>,
    #[arg(short, long)]
    pub(crate) output: Option<PathBuf>,
    #[arg(long, value_enum)]
    pub(crate) format: Option<OutputFormat>,
    #[arg(long = "no-headers")]
    pub(crate) no_headers: bool,
}

#[derive(Debug, Args)]
pub(crate) struct TablesCommand {
    #[arg(
        short,
        long,
        required = true,
        action = ArgAction::Append,
        value_name = "[NAME=]PATH[:SHEET|KEY]",
        long_help = "Input dataset path. Supported formats: .xlsx, .xml, .csv, .json, .jsonl, .md, .markdown.\n\
Optionally prefix with a table name: NAME=PATH (e.g. sales=./sales.xlsx:Q1).\n\
Without a name prefix, tables are named 'table', 'table2', 'table3', ...\n\
For XLSX use PATH:SheetName to select a worksheet.\n\
For XML use PATH:TagName to select a subtree.\n\
For JSON use PATH:Key to select a top-level key.\n\
For Markdown use PATH:N for the 1-based table index."
    )]
    pub(crate) input: Vec<String>,
    #[arg(
        short,
        long,
        long_help = "Default sheet/tag/key applied to every --input that does not already specify one."
    )]
    pub(crate) sheet: Option<String>,
    #[arg(long, value_enum, default_value = "text")]
    pub(crate) format: InspectionFormat,
}

#[derive(Debug, Args)]
pub(crate) struct SchemaCommand {
    #[arg(
        short,
        long,
        required = true,
        action = ArgAction::Append,
        value_name = "[NAME=]PATH[:SHEET|KEY]",
        long_help = "Input dataset path. Supported formats: .xlsx, .xml, .csv, .json, .jsonl, .md, .markdown.\n\
Optionally prefix with a table name: NAME=PATH (e.g. sales=./sales.xlsx:Q1).\n\
Without a name prefix, tables are named 'table', 'table2', 'table3', ...\n\
For XLSX use PATH:SheetName to select a worksheet.\n\
For XML use PATH:TagName to select a subtree.\n\
For JSON use PATH:Key to select a top-level key.\n\
For Markdown use PATH:N for the 1-based table index."
    )]
    pub(crate) input: Vec<String>,
    #[arg(
        short,
        long,
        long_help = "Default sheet/tag/key applied to every --input that does not already specify one."
    )]
    pub(crate) sheet: Option<String>,
    #[arg(long, value_enum, default_value = "text")]
    pub(crate) format: InspectionFormat,
}

#[derive(Debug, Args)]
pub(crate) struct InspectCommand {
    #[arg(
        short,
        long,
        required = true,
        action = ArgAction::Append,
        value_name = "[NAME=]PATH[:SHEET|KEY]",
        long_help = "Input dataset path. Supported formats: .xlsx, .xml, .csv, .json, .jsonl, .md, .markdown.\n\
Optionally prefix with a table name: NAME=PATH (e.g. sales=./sales.xlsx:Q1).\n\
Without a name prefix, tables are named 'table', 'table2', 'table3', ...\n\
For XLSX use PATH:SheetName to select a worksheet.\n\
For XML use PATH:TagName to select a subtree.\n\
For JSON use PATH:Key to select a top-level key.\n\
For Markdown use PATH:N for the 1-based table index."
    )]
    pub(crate) input: Vec<String>,
    #[arg(
        short,
        long,
        long_help = "Default sheet/tag/key applied to every --input that does not already specify one."
    )]
    pub(crate) sheet: Option<String>,
    #[arg(long, value_enum, default_value = "text")]
    pub(crate) format: InspectionFormat,
    /// Number of sample rows to include per table (default: 5).
    #[arg(long, default_value = "5")]
    pub(crate) sample: usize,
    /// Enable additional metrics (distinct count, min/max for numeric columns).
    #[arg(long)]
    pub(crate) stats: bool,
}

#[derive(Clone, Debug, ValueEnum)]
pub(crate) enum OutputFormat {
    Text,
    Csv,
    Jsonl,
    Markdown,
    Xlsx,
    Xml,
}

#[derive(Clone, Debug, ValueEnum)]
pub(crate) enum InspectionFormat {
    Text,
    Json,
    Markdown,
}