query-forge
Rust CLI to run SQL queries on one or more XLSX/XML/CSV/JSON/JSONL/Markdown/HTML/Feather/Parquet inputs and produce output in table (default), CSV, JSONL, Markdown, HTML, XML, Feather, Parquet, a new XLSX file, or the system clipboard for text-based formats.
query-forge is built for one core job: run SQL on messy local files, fast, with zero database setup.
Installation
From crates.io:
From source (local development):
Quick start
Run an inline query:
Or load the query from an SQL file:
Who it is for
- Analysts and Excel power users who need SQL without moving data to a DB first.
- Data engineers and consultants who reconcile heterogeneous local exports quickly.
- QA, auditors, and operations teams who compare snapshots and validate changes.
Start in 60 seconds (high-value workflows)
Query a local file:
Compare two snapshots:
Create a pivot table:
Inspect schema and data quality quickly:
Use qf tables and qf schema before writing complex joins to confirm table names and inferred types.
Key features
- SQL queries on XLSX/XML/CSV/JSON/JSONL/Markdown/HTML/Feather/Parquet data using in-memory SQLite.
- Multi-file support with automatic table mapping:
table,table2,table3, ... - Explicit table naming:
name=file.xlsxassigns a custom SQL table name for readability. - Sheet selection with
file.xlsx:SheetNameorfile.xlsx#SheetName. - For XML inputs, if no sheet/tag is provided the whole file is used; if a sheet is provided, only the subtree inside that XML tag is used.
- CSV, JSON, JSONL, Markdown, HTML, Feather and Parquet inputs are supported directly (
.csv,.json,.jsonl,.md,.markdown,.html,.htm,.feather,.parquet) and can be mixed with XLSX/XML in the same query. - In
qf query, sheet/tag/key selection is expressed viafile:selectororfile#selector(XLSX, XML, JSON, Markdown, HTML; for Markdown and HTML, selector is 1-based table index); CSV/JSONL/Feather/Parquet do not support selector syntax. - Richer JSON extraction modes via
--json-mode array|object|flatten— handle arrays, objects, and arbitrarily nested JSON documents. - Richer XML extraction modes via
--xml-mode rows|descendants|attributes— extract tabular rows, all leaf text nodes, or element attributes as columns. - Large CSV/JSONL ingestion improvements — CSV rows are parsed in one pass, JSONL is read line-by-line, and SQLite registration uses batched inserts.
- Named SQL parameters from CLI with
--param. - Output export in
table(default),csv,json,jsonl,markdown(aliasmd),html,xml,xlsx,feather,parquet. - Explicit input/output format overrides with
|type, including clipboard pseudo-paths such as@clipboard|csvand@clipboard|json. - Support for headerless sheets with
--no-headers(column1,column2, ...). - Header normalization via
--normalize-headerswith--header-case snake|camel|pascal|screaming-snakeand--dedupe-headers. - CTE chaining via
--with 'name AS (...)'to prepend named subqueries before the main--sqlor--sql-filequery; repeatable for multiple CTEs. - Multi-statement SQL files —
--sql-filefiles may contain several;-separated statements (DDL, DML, or SELECT); all statements except the last are executed in order and the last SELECT is returned. - AI-friendly envelope output via global
--ai-modeforquery,tables,schema,inspect,diff,pivot, andconv, with structured JSON data, metadata, warnings, and errors. - Capabilities discovery via
qf capabilities(JSON contract for commands, formats, AI-mode support, and error-code families). - Shell completion scripts for bash, zsh, fish, elvish, and PowerShell via
qf completions <SHELL>. - Man page generation via
qf man-page. - Dataset diffing via
qf diffwith key-based or positional comparison, schema-only mode, and diff-friendly exit codes. - Pivot tables via
qf pivotfor cross-tabulation and aggregation; always computes as static data via SQL CASE WHEN crosstab, compatible with all output formats.
Examples
Multiple inputs
Explicit table names
Assign readable names to each input for use in SQL queries:
Without a name prefix, tables are automatically named table, table2, table3, etc.
Explicit format overrides
Use |type to force the input or output format when the extension is missing, ambiguous, or intentionally different from the real data:
|
The same syntax works with selectors:
|
You can also use |type on outputs, and clipboard paths must use this syntax (for example @clipboard|csv and @clipboard|json).
Parameterized query
SELECT *
FROM table
WHERE "Elemento WBS" LIKE '%' || :wbs || '%'
AND CAST("ore" AS REAL) > :min_ore;
Parameters:
Multiple values for the same parameter (separated by , or ;):
XML input
Use the whole XML file:
Use only a specific XML tag as sheet:
CSV/JSON/JSONL/Markdown/HTML/Feather/Parquet input
CSV:
JSONL:
JSON (array at root):
JSON with key selection as sheet:
Markdown (first table by default):
Markdown with table index key (second table):
HTML (first <table> by default):
HTML with table index key (second table):
Feather:
Parquet:
Clipboard input/output
Use @clipboard|type as a pseudo-path to read text-based datasets from the system clipboard or copy text-based results back to it.
In PowerShell, quote these values so @ is not parsed as splatting, for example --input '@clipboard|csv' and --output '@clipboard|json'.
Supported clipboard input types are csv, json, jsonl, md/markdown, html/htm, and xml. Clipboard output supports text, csv, json, jsonl, markdown/md, html, and xml. Extension syntax such as @clipboard.csv is not supported.
Large CSV/JSONL workloads
For larger local CSV and JSONL files, query-forge now avoids full-file buffering during load and batches inserts into SQLite, which reduces memory pressure and improves ingestion throughput.
AI mode (--ai-mode)
Use global --ai-mode to wrap output into a stable JSON envelope.
Supported commands in AI mode: query, tables, schema, inspect, diff, pivot, conv.
The envelope contains:
oksuccess flagschema_versioncommanddata(rows/columns)metadata(timings, inputs, optional output target)warningserror(nullon success, object on failure)
When a command fails in --ai-mode, query-forge emits a JSON error envelope with a stable error.code (for example QF_INVALID_ARGUMENT, QF_INPUT_NOT_FOUND, QF_SQL_ERROR).
Capabilities discovery (qf capabilities)
Use qf capabilities as first call from AI tools to discover supported commands and formats.
Output is JSON and includes:
- command inventory
- per-command
supports_ai_mode - output-format lists
- known error-code families
Type inference controls
By default, query-forge tries to infer SQLite-compatible types from incoming values. Use these flags to make the behavior explicit or tune it for locale-specific data:
--infer-typesexplicitly enables typed ingestion.--all-textdisables inference and preserves all values as text.--decimal-commainterprets values like12,50as decimals.--date-formatparses matching date strings during inference.--null-values,--true-values, and--false-valuesadd custom tokens for conversion.
JSON extraction modes
Control how JSON documents are mapped to rows with --json-mode.
array (default) — each element of a top-level JSON array becomes a row:
# [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
object — each key-value pair of a JSON object becomes a row with key and value columns:
# {"revenue": 1200, "cost": 800, "profit": 400}
flatten — recursively flattens nested objects and arrays into a single row per document element, using dot-separated paths as column names:
# {"user":{"name":"Alice","address":{"city":"London"}},"tags":["a","b"]}
XML extraction modes
Control how XML documents are mapped to rows with --xml-mode.
rows (default) — detects and extracts tabular rows from the XML structure (original behaviour):
descendants — collects every leaf text element (no child elements) as a row with tag and value columns, regardless of nesting depth:
# Useful for semi-structured XML where you want all text values
attributes — collects all elements that have at least one attribute; each attribute name becomes a column, and non-empty text content is exposed as a value column:
# <items><item id="1" type="A">hello</item><item id="2" type="B">world</item></items>
Header normalization
Use --normalize-headers to sanitize column names before they are registered as SQLite columns. Non-alphanumeric characters are replaced with underscores, leading/trailing underscores are trimmed, and consecutive underscores are collapsed. Combine with --header-case to choose a naming convention.
snake — lowercase with underscores (e.g. Product ID → product_id):
camel — camelCase, no underscores (e.g. Product ID → productId):
pascal — PascalCase, no underscores (e.g. Product ID → ProductId):
screaming-snake — UPPER_SNAKE_CASE (e.g. Product ID → PRODUCT_ID):
Add --dedupe-headers to automatically append numeric suffixes when two normalized names would collide.
Export
Table (default, console-friendly output):
Or explicitly with --format table:
CSV:
JSONL:
JSON:
Markdown:
HTML:
XML:
XLSX:
Feather:
Parquet:
CTE chaining (--with)
Prepend named CTE definitions before the main query with --with. Each value is a name AS (...) expression; the WITH keyword is added automatically. Repeat the flag for multiple CTEs.
Works with both --sql and --sql-file.
Multi-statement SQL files
A .sql file passed to --sql-file may contain multiple ;-separated statements. All statements except the last are executed as DDL or DML (e.g. CREATE TEMP TABLE, INSERT). The last statement must be a SELECT and its result is the command output.
-- analysis.sql
CREATE TEMP TABLE recent AS
SELECT * FROM table WHERE year >= 2024;
CREATE TEMP TABLE summary AS
SELECT category, SUM(revenue) AS total
FROM recent
GROUP BY category;
SELECT category, total
FROM summary
ORDER BY total DESC;
Semicolon splitting respects single-quoted strings, '' escape sequences, -- line comments, and /* */ block comments.
Dataset diff (qf diff)
Compare two snapshots by primary key:
Side-by-side output with all classes:
Schema-only comparison:
Pivot tables (qf pivot)
Compute a cross-tabulation (pivot table) from any supported input.
Frequency table — count rows by a single dimension:
Output:
category | count
electronics | 2
office | 2
Crosstab — sum a value column broken down by two dimensions (--rows × --cols):
Output:
category | false | true
electronics | 2 | 32
office | 50 | 80
Export to any format — the result is static data, compatible with all output formats including xlsx:
# Export pivot as CSV
# Export pivot as XLSX (static cells, not a native Excel PivotTable)
Inspect the generated SQL with --show-sql:
Prints the CASE WHEN crosstab query to stderr before executing it.
Supported aggregations: count (default), sum, avg, min, max.
--values is required for sum, avg, min, max; optional for count.
Note on static output:
qf pivotalways computes the pivot table via a SQL CASE WHEN crosstab query and writes the result as static data, regardless of the chosen output format. For xlsx output this means pre-computed cells rather than a native Excel PivotTable (whichrust_xlsxwriterdoes not currently support). Use--show-sqlto retrieve the generated SQL for use in other tools that support native pivot tables.
Ready-to-use examples
The examples/ folder contains:
inventory.xlsx,inventory.xml,inventory.csv,inventory.json,inventory.jsonl,inventory.html,inventory.md,inventory.feather,snapshot-before.csv,snapshot-after.csvas inputs.active-products.txt,active-products.csv,active-products.jsonl,active-products.md,active-products.xml,active-products.html,active-products.feather,active-products-from-xml.csv,active-products-from-csv.csv,active-products-from-json.csv,active-products-from-jsonl.csv,active-products-from-html.csv,active-products-from-markdown.csv,active-products-from-feather.csv,stock-by-category.xlsx,diff-products.txt,diff-products-side-by-side.md,diff-schema.json,pivot-count-by-category.txt,pivot-stock-by-category.csv,pivot-stock-by-category.xlsxas generated outputs.commands.shwith runnable commands from the repository root.
Regenerate the examples locally:
Documentation
- Crate: https://crates.io/crates/query-forge
- API docs: https://docs.rs/query-forge
- Repository: https://github.com/mad4j/query-forge
- Roadmap: ROADMAP.md
Comparison
query-forge is designed as a focused CLI for running SQL queries across heterogeneous local files with a uniform --input model. Nearby tools exist, but they usually cover only part of the same problem.
| Tool | SQL queries | XLSX | XML | CSV | JSON/JSONL | Markdown tables | Primary focus |
|---|---|---|---|---|---|---|---|
query-forge |
Yes | Yes | Yes | Yes | Yes | Yes | Unified SQL CLI over heterogeneous document-style inputs |
duckdb |
Yes | Partial | Partial | Yes | Yes | No | General-purpose analytical SQL engine |
csvkit |
Partial | Via conversion | No | Yes | Limited | No | CSV tooling and CSV-oriented SQL workflows |
qsv / xsv / miller |
Limited / No | No | No | Yes | Limited | No | Fast tabular text processing |
jq |
No | No | No | No | Yes | No | JSON transformation and filtering |
xmlstarlet |
No | No | Yes | No | No | No | XML querying and transformation |
VisiData |
Partial / Interactive | Yes | Limited | Yes | Yes | Limited | Interactive data exploration |
Notes:
duckdbis the closest alternative when the core requirement is "run SQL on local data files", especially for CSV and JSON, but it does not provide the same single-purpose workflow for XLSX/XML/Markdown inputs.csvkit,qsv,xsv, andmillerare strong tabular-data tools, but they are centered on delimited text rather than a unified multi-format SQL interface.jqandxmlstarletare powerful for JSON and XML respectively, but they use format-specific query languages instead of SQL.VisiDatais excellent for interactive inspection, whilequery-forgeis oriented to batch execution and scriptable SQL queries.
Shell completions and man page
Shell completions
Print a completion script for your shell and source it:
# Bash — write to ~/.bash_completion (overwrite to avoid duplicates)
# Zsh — add to a directory on $fpath, e.g. ~/.zfunc/
# Fish
# Elvish
# PowerShell
# Then add `. "$Env:UserProfile\qf_completions.ps1"` to your $PROFILE
Packagers can pipe the output directly to the appropriate system path during installation.
Man page
Print the man page to stdout and install it:
# Preview
|
# Install system-wide
|
# Write to a file
Development
Running benchmarks
Reproducible benchmarks are provided for every supported input and output format (CSV, JSONL, JSON, Markdown, HTML, XML, XLSX, Parquet) using Criterion.
Three benchmark groups are measured for 1,000-row datasets:
| Group | What is timed |
|---|---|
load/<format> |
File I/O + parsing → in-memory SheetData |
query/<format> |
Full pipeline: load + SQLite ingest + GROUP BY aggregation |
output/<format> |
Render a pre-built result to the target format |
HTML reports with latency distributions and regression detection are written to target/criterion/ after each run.
License
Released under the MIT license. See LICENSE.