{
  "markdown": "<p align=\"center\">\n  <img src=\"logo.svg\" alt=\"csvql\" width=\"420\"/>\n</p>\n\n[![CI](https://github.com/melihbirim/csvql/actions/workflows/ci.yml/badge.svg)](https://github.com/melihbirim/csvql/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)\n[![Release](https://img.shields.io/github/v/release/melihbirim/csvql)](https://github.com/melihbirim/csvql/releases)\n\n**The analytical CSV query engine for AI agents.**\n\nRun SQL analytics — `GROUP BY`, aggregates, joins, time-series — on CSV files **in place**: no database, no import, no ingest. csvql ships as an [MCP](https://modelcontextprotocol.io/) server, so an LLM can query a gigabyte file for a few hundred tokens instead of pasting it (impossible) into context. A single static binary written in Zig. Your data never leaves your machine.\n\n> A database is something you load your data *into*. csvql is a query you run on the data where it already lives.\n\n**Read-only and on-prem by design.** csvql only runs `SELECT` — it has no `INSERT`/`UPDATE`/`DELETE`/`DROP` and physically cannot modify your data. It makes zero network calls, needs no cloud, and runs fully air-gapped. **Our next north star:** the safe way to give AI agents query access to corporate data — run csvql *next to the data* on your own servers (read-only, nothing leaves the box) instead of shipping files out to an LLM.\n\n### Token economics: query files instead of pasting them\n\nPasting a 417 MB CSV into an LLM costs **230 million tokens** — it fits no context window. Over MCP, the agent queries the file in place and gets back only the answer:\n\n| Question an agent asks | Tokens used |\n| ---------------------- | ----------- |\n| *\"How many trips per cab type?\"* | **43** |\n| *\"Which year was busiest?\"* | **49** |\n| *\"Average fare by passenger count?\"* | **123** |\n\nSame answers, **~1,000–500,000× fewer tokens** — flat, regardless of file size. One command wires it into Claude: [`csvql install`](#setup). Measure it yourself: [`bench/bench_tokens.py`](bench/bench_tokens.py).\n\n```bash\n$ csvql \"SELECT cab_type, COUNT(*) FROM 'trips.csv' GROUP BY cab_type\"\ncab_type,COUNT(*)\ngreen,32447\nyellow,967553\n  0.05s — no import, queried straight off the file\n```\n\n[Website](https://melihbirim.github.io/csvql/) · [Quick Start](#quick-start) · [Installation](#installation) · [Performance](#performance) · [SQL Reference](#sql-reference) · [Docs](#documentation)\n\n---\n\n## Quick Start\n\ncsvql auto-detects SQL or simple mode from your input:\n\n```bash\n# SQL mode\ncsvql \"SELECT name, salary FROM 'data.csv' WHERE age > 30 ORDER BY salary DESC LIMIT 10\"\n\n# Simple mode — same query, shorter syntax\ncsvql data.csv \"name,salary\" \"age>30\" 10 \"salary:desc\"\n\n# Just browse a file\ncsvql data.csv\n```\n\n### Unix Pipes\n\n```bash\ncat data.csv | csvql \"SELECT name, age FROM '-' WHERE age > 25\"\ncsvql \"SELECT * FROM 'data.csv' WHERE status = 'active'\" > output.csv\ncsvql \"SELECT email FROM 'users.csv'\" | wc -l\n```\n\n### Flags\n\n| Flag                 | Short | Description                                         |\n| -------------------- | ----- | --------------------------------------------------- |\n| `--no-header`        |       | Suppress header row in output                       |\n| `--no-input-header`  |       | Treat the first row as data; auto-name columns `c1`..`cN` |\n| `-o`, `--output <file>` |    | Write results to a file instead of stdout           |\n| `--delimiter <char>` | `-d`  | Field delimiter (default `,`). Use `\\t` for TSV     |\n| `--json`             |       | Output as a JSON array (`[{...}, ...]`)             |\n| `--jsonl`            |       | Output as JSONL / NDJSON (one JSON object per line) |\n| `--threads <N>`      |       | Worker threads for parallel execution; `0` uses automatic detection |\n| `--strict`           |       | Error on a WHERE numeric comparison against a non-numeric value instead of silently skipping that row (see [CORRECTNESS.md](CORRECTNESS.md#strict-and-exit-codes)) |\n| `--version`          | `-v`  | Show version                                        |\n| `--help`             | `-h`  | Show help                                           |\n| `--mcp`              |       | Start as an MCP server (stdio JSON-RPC transport)   |\n| `--root <dir>`       |       | Confine file access to a directory (repeatable via commas) |\n| `--audit <file>`     |       | Append a JSONL audit record per query (timestamp, SQL)     |\n\n```bash\n# TSV file\ncsvql \"SELECT name, salary FROM 'data.tsv'\" -d $'\\t'\n\n# Pipe into another tool that expects no header\ncsvql \"SELECT name, age FROM 'data.csv'\" --no-header | awk -F, '{print $2}'\n\n# TSV input, no header in output\ncat data.tsv | csvql \"SELECT * FROM '-'\" -d $'\\t' --no-header\n```\n\n## Installation\n\n### Homebrew (macOS / Linux)\n\n```bash\nbrew install melihbirim/csvql/csvql\n```\n\nOr in two steps if you plan to install multiple tools from this tap:\n\n```bash\nbrew tap melihbirim/csvql\nbrew install csvql\n```\n\n> `melihbirim/csvql` is the tap (the formula repository), and the trailing `/csvql` is the formula name inside it.\n\n### Prebuilt Binaries\n\nDownload from [GitHub Releases](https://github.com/melihbirim/csvql/releases):\n\n```bash\n# macOS (Apple Silicon)\ncurl -L https://github.com/melihbirim/csvql/releases/latest/download/csvql-macos-aarch64.tar.gz | tar xz\nsudo mv csvql-macos-aarch64 /usr/local/bin/csvql\n\n# macOS (Intel)\ncurl -L https://github.com/melihbirim/csvql/releases/latest/download/csvql-macos-x86_64.tar.gz | tar xz\nsudo mv csvql-macos-x86_64 /usr/local/bin/csvql\n\n# Linux (x86_64)\ncurl -L https://github.com/melihbirim/csvql/releases/latest/download/csvql-linux-x86_64.tar.gz | tar xz\nsudo mv csvql-linux-x86_64 /usr/local/bin/csvql\n```\n\n### Build from Source\n\nRequires [Zig](https://ziglang.org/) 0.13.0+ (tested with 0.15.2):\n\n```bash\ngit clone https://github.com/melihbirim/csvql.git\ncd csvql\nzig build -Doptimize=ReleaseFast\nsudo cp zig-out/bin/csvql /usr/local/bin/\n```\n\n## Performance\n\n**2M rows, 56 MB CSV, Apple M2 Pro** — aggregates on the raw CSV (best-of-5):\n\n| Query                        | csvql      | DuckDB | Speedup   |\n| ----------------------------- | ---------- | ------ | --------- |\n| `SELECT COUNT(*)` scalar      | **0.012s** | 0.136s | **11.3x** |\n| `COUNT(*) GROUP BY`           | **0.020s** | 0.146s | **7.3x**  |\n| `JOIN SELECT *` (2M × 6)      | **0.088s** | 7.832s | **89x**   |\n\n**NYC Taxi, 20M rows, 8 GB CSV** — raw CSV, no ingest, both engines: **~3.2x** faster, **~6x** less memory, and **0 bytes** of extra storage (DuckDB's fast path needs a 2.1 GB native store first). At this scale csvql reads raw CSV about as fast as `cat` — the read itself is the bound, not parsing.\n\nFull breakdown (LIKE, multi-table JOIN, subqueries, memory/storage, methodology): **[BENCHMARKS.md](BENCHMARKS.md)**. Reproduce any number yourself: [`bench/bench_all.sh`](bench/bench_all.sh).\n\n## SQL Reference\n\n`SELECT`/`FROM`/`WHERE`/`GROUP BY`/`HAVING`/`ORDER BY`/`LIMIT`/`OFFSET`, `JOIN`, subquery `IN`/`NOT IN`, `LIKE`/`ILIKE`/`BETWEEN`/`IS NULL`/`AND`/`OR`/`NOT`, aggregates (`COUNT`/`SUM`/`AVG`/`MIN`/`MAX`/`VARIANCE`/`STDDEV`/`MEDIAN`/`GROUP_CONCAT`), `CASE WHEN`, and scalar functions (`UPPER`/`LOWER`/`TRIM`/`CONCAT`/`SUBSTR`/`REPLACE`/`SPLIT_PART`/`ROUND`/`CAST`/`COALESCE`/`STRFTIME`/`DATEDIFF`/`DATEADD`/and more).\n\n```bash\ncsvql \"SELECT department, COUNT(*), AVG(salary) FROM 'data.csv' WHERE salary > 50000 GROUP BY department HAVING COUNT(*) > 10 ORDER BY department\"\ncsvql \"SELECT e.name, d.dept_name FROM 'employees.csv' e JOIN 'departments.csv' d ON e.dept_id = d.id WHERE d.dept_name = 'Engineering'\"\ncsvql \"SELECT id FROM 'orders.csv' WHERE customer_id IN (SELECT id FROM 'customers.csv' WHERE region = 'EU')\"\n```\n\nFull syntax table, runnable examples for every feature, known differences from DuckDB, and current limitations: **[SQL_REFERENCE.md](SQL_REFERENCE.md)**.\n\nPositional \"simple mode\" is also available for quick one-off filters without writing SQL: `csvql data.csv \"name,salary\" \"age>30\" 10 \"salary:desc\"` — see [SIMPLE_QUERY_LANGUAGE.md](SIMPLE_QUERY_LANGUAGE.md).\n\n## MCP Server\n\ncsvql ships as a [Model Context Protocol](https://modelcontextprotocol.io/) server, letting AI assistants (Claude, Copilot, etc.) query your CSV files directly.\n\n```bash\ncsvql --mcp\n```\n\n### Why query instead of paste?\n\nA 1 MB CSV costs **~560,000 tokens** to paste into an LLM — it doesn't even fit a 200K-token context window. Pasting a real dataset is impossible past a few hundred KB, and expensive long before that. With `csvql --mcp` the agent *queries* the file instead and gets back only the rows it asked for:\n\n| CSV size | Paste into context | Query via `csvql --mcp` | Savings |\n| -------- | ------------------ | ----------------------- | ------- |\n| 1 MB     | 559K tokens ❌ *(overflows)* | ~540 tokens | **1,000x** |\n| 10 MB    | 5.6M tokens ❌      | ~550 tokens | **10,000x** |\n| 100 MB   | 55M tokens ❌       | ~565 tokens | **98,000x** |\n| 417 MB   | 230M tokens ❌      | ~560 tokens | **~410,000x** |\n\nThe query cost is **flat** — it's the SQL plus a few result rows, independent of file size — so a 417 MB file costs the same ~560 tokens as a 1 MB one. Five real questions, answered against DuckDB's NYC-taxi data; token counts via `tiktoken` (exact `cl100k`). Reproduce: [`bench/bench_tokens.py`](bench/bench_tokens.py). Your data never leaves your machine.\n\n### Exposed Tools\n\n| Tool | Description |\n|------|-------------|\n| `csv_query(sql)` | Execute any supported SQL query, returns results as JSON |\n| `csv_schema(file)` | Column names and sample rows for a CSV file |\n| `csv_list(directory?)` | List CSV files in a directory |\n\n### Supported Queries via MCP\n\n`csv_query` accepts the full SQL dialect supported by csvql. You can ask your AI assistant things like:\n\n| Natural language prompt | SQL sent to `csv_query` |\n|---|---|\n| \"Show me the top 10 customers by revenue\" | `SELECT customer, SUM(revenue) AS total FROM 'sales.csv' GROUP BY customer ORDER BY total DESC LIMIT 10` |\n| \"How many orders per month in 2025?\" | `SELECT STRFTIME('%Y-%m', order_date) AS month, COUNT(*) AS orders FROM 'orders.csv' WHERE order_date BETWEEN '2025-01-01' AND '2025-12-31' GROUP BY month ORDER BY 1` |\n| \"How long does delivery take on average?\" | `SELECT AVG(DATEDIFF('hour', shipped_at, delivered_at)) AS avg_hours FROM 'orders.csv' WHERE delivered_at != ''` |\n| \"Flag orders where picking exceeded SLA\" | `SELECT order_id, DATEDIFF('minute', ordered_at, picked_at) AS mins FROM 'orders.csv' WHERE picked_at != ''` (scalar functions in WHERE not yet supported — filter by `mins > 90` in your shell) |\n| \"Add 2-day estimated delivery to shipments\" | `SELECT order_id, DATEADD('day', 2, shipped_at) AS est_delivery FROM 'orders.csv' WHERE shipped_at != ''` |\n| \"Which employees have no department?\" | `SELECT name FROM 'employees.csv' WHERE department IS NULL` |\n| \"List all cities, deduplicated, sorted\" | `SELECT DISTINCT city FROM 'data.csv' ORDER BY city` |\n| \"Average salary by department, only > 80k avg\" | `SELECT department, AVG(salary) AS avg_sal FROM 'data.csv' GROUP BY department HAVING AVG(salary) > 80000 ORDER BY avg_sal DESC` |\n| \"Join orders with customers, filter by region\" | `SELECT o.id, c.name FROM 'orders.csv' o JOIN 'customers.csv' c ON o.customer_id = c.id WHERE c.region = 'West'` |\n| \"Salaries in range 50k–70k\" | `SELECT name, salary FROM 'data.csv' WHERE salary BETWEEN 50000 AND 70000 ORDER BY salary` |\n| \"Employees not in London or Paris\" | `SELECT name, city FROM 'data.csv' WHERE NOT city IN ('London', 'Paris')` |\n\n**Full WHERE clause support:** `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `BETWEEN`, `IN`, `IS NULL`, `IS NOT NULL`, `NOT`, `AND`, `OR`\n\n**Full SELECT support:** column projections, `AS` aliases, `DISTINCT`, `COUNT`/`SUM`/`AVG`/`MIN`/`MAX`/`VARIANCE`/`STDDEV`/`MEDIAN`/`GROUP_CONCAT`, `GROUP BY`, `HAVING`, `ORDER BY` (by name, alias, or position), `LIMIT`, `STRFTIME()`, `DATE_PART()`, `JOIN`, `UPPER`/`LOWER`/`TRIM`/`LENGTH`/`SUBSTR`/`REPLACE`/`SPLIT_PART`/`GREATEST`/`LEAST`, `ABS`/`SIGN`/`CEIL`/`FLOOR`/`MOD`/`ROUND`, `COALESCE`, `CAST`, `DATEDIFF`, `DATEADD`, `EXTRACT`\n\n### Setup\n\n**One command (recommended)** — registers csvql in Claude Code and Claude Desktop, no manual config:\n\n```bash\ncsvql install          # add --print to dry-run first\n```\n\nIt runs `claude mcp add` for Claude Code (if the CLI is present) and merges an `mcpServers.csvql` entry into the Claude Desktop config, preserving your other servers. Restart Claude afterward.\n\n**Claude Desktop (one-click)** — grab the `csvql-<platform>.mcpb` for your OS from [Releases](https://github.com/melihbirim/csvql/releases) and open it in Claude Desktop (Settings → Extensions). No terminal. Build it yourself with [`scripts/build-mcpb.sh`](scripts/build-mcpb.sh).\n\n<details>\n<summary>Manual config (if you prefer)</summary>\n\n**VS Code (Copilot)** — create `.vscode/mcp.json` in your workspace:\n\n```json\n{\n  \"servers\": {\n    \"csvql\": {\n      \"type\": \"stdio\",\n      \"command\": \"/usr/local/bin/csvql\",\n      \"args\": [\"--mcp\"]\n    }\n  }\n}\n```\n\n**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"csvql\": {\n      \"command\": \"/usr/local/bin/csvql\",\n      \"args\": [\"--mcp\"]\n    }\n  }\n}\n```\n\n</details>\n\nOnce connected, you can ask your AI assistant to query CSV files directly:\n> *\"What are the top 5 product categories by revenue this year?\"*\n\n### Remote & on-prem: query data where it lives\n\nBig files are hard to download — so run csvql **on the server next to the data** and connect over SSH. Only the SQL query and the small result cross the wire; the data never leaves the box. No open port, no reverse proxy — it rides your existing SSH keys and audit trail:\n\n```jsonc\n// client MCP config — csvql runs on the remote server\n{ \"command\": \"ssh\", \"args\": [\"analyst@dataserver\", \"csvql\", \"--mcp\", \"--root\", \"/data\"] }\n```\n\n**`--root` sandboxes file access.** With `--root /data`, queries can only read files under `/data` — `SELECT * FROM '/etc/passwd'` and `../` traversal are rejected. Always set `--root` when exposing csvql to an agent or another user. Pair it with a restricted OS user and a read-only mount for defense in depth.\n\n**Read-only by construction:** csvql only runs `SELECT` — it has no `INSERT`/`UPDATE`/`DELETE`/`DROP` and cannot modify your data. It makes zero outbound network calls and runs fully air-gapped. Full posture and hardening guidance in [SECURITY.md](SECURITY.md).\n\n## Language Libraries\n\ncsvql ships as a native library for Python and Node.js — same SIMD engine, same performance, no subprocess.\n\n```bash\npip install csvql-query          # Python: csvql.query(), .query_df(), .query_csv()\nzig build node -Doptimize=ReleaseFast   # Node.js: require('csvql-query')\n```\n\nFull API, options (delimiter/comment/skip-empty-lines), memory comparisons against `csv-parse`/`papaparse`, and a runnable ETL example: **[docs/NODE.md](docs/NODE.md)** · **[docs/PYTHON.md](docs/PYTHON.md)**.\n\n## Documentation\n\n| Document                                             | Description                                         |\n| ---------------------------------------------------- | --------------------------------------------------- |\n| [SQL_REFERENCE.md](SQL_REFERENCE.md)                 | Full SQL syntax, runnable examples, DuckDB differences, limitations |\n| [BENCHMARKS.md](BENCHMARKS.md)                       | Detailed performance analysis vs DuckDB, ClickHouse |\n| [CORRECTNESS.md](CORRECTNESS.md)                     | What's tested against DuckDB, how, known gaps, error behaviour |\n| [ARCHITECTURE.md](ARCHITECTURE.md)                   | Engine design, optimization techniques              |\n| [SECURITY.md](SECURITY.md)                           | Security posture, network/disk-write guarantees, hardening |\n| [SIMPLE_QUERY_LANGUAGE.md](SIMPLE_QUERY_LANGUAGE.md) | Simple mode syntax reference                        |\n| [docs/NODE.md](docs/NODE.md)                         | Node.js library: full API, options, ETL example     |\n| [docs/PYTHON.md](docs/PYTHON.md)                     | Python library: full API                            |\n| [docs/LIBRARY.md](docs/LIBRARY.md)                   | Using the CSV parser as a Zig library               |\n| [CONTRIBUTING.md](CONTRIBUTING.md)                   | Contribution guidelines                             |\n\n## Roadmap\n\n| Feature                             | Issue                                                | Status              |\n| ----------------------------------- | ---------------------------------------------------- | ------------------- |\n| `--no-header` / `--delimiter` flags | [#12](https://github.com/melihbirim/csvql/issues/12) | ✅ shipped (v0.5.0) |\n| `LIKE` operator in WHERE            | [#13](https://github.com/melihbirim/csvql/issues/13) | ✅ shipped          |\n| `--json` / `--jsonl` output format  | [#14](https://github.com/melihbirim/csvql/issues/14) | ✅ shipped          |\n| `HAVING` clause                     |                                                      | ✅ shipped          |\n| `STRFTIME()` date bucketing         |                                                      | ✅ shipped          |\n| MCP server (`--mcp`)                |                                                      | ✅ shipped          |\n| `AS` alias in SELECT & ORDER BY     |                                                      | ✅ shipped          |\n| `BETWEEN low AND high`              |                                                      | ✅ shipped          |\n| `IS NULL` / `IS NOT NULL`           |                                                      | ✅ shipped          |\n| `NOT` prefix for conditions         |                                                      | ✅ shipped          |\n| `ORDER BY` positional (`ORDER BY 1`)|                                                      | ✅ shipped          |\n| `GROUP BY` alias (`GROUP BY month`) |                                                      | ✅ shipped          |\n| `CASE WHEN` inside aggregates       |                                                      | ✅ shipped          |\n| `ILIKE` in WHERE                    |                                                      | ✅ shipped          |\n| `UPPER`, `LOWER`, `TRIM`, `LENGTH`, `SUBSTR` in SELECT |                             | ✅ shipped          |\n| `ABS`, `SIGN`, `CEIL`, `FLOOR`, `MOD` in SELECT |                                         | ✅ shipped          |\n| `ROUND(col)` / `ROUND(col, n)` in SELECT |                                               | ✅ shipped          |\n| `COALESCE` in SELECT                |                                                      | ✅ shipped          |\n| `CAST` in SELECT                    |                                                      | ✅ shipped          |\n| `DATE_PART()`, `DATEDIFF`, `DATEADD`, `EXTRACT` |                                          | ✅ shipped          |\n| `JOIN` (inner, hash join)           |                                                      | ✅ shipped          |\n| `--threads` parallelism control     | [#51](https://github.com/melihbirim/csvql/issues/51) | ✅ shipped (v1.7.0) |\n| `--no-input-header` (headerless CSVs, `c1..cN`) | [#49](https://github.com/melihbirim/csvql/issues/49) | ✅ shipped (v1.7.0) |\n| MCP token guardrails + `csvql install` + `.mcpb` bundle | [#54](https://github.com/melihbirim/csvql/issues/54) | ✅ shipped (v1.7.0) |\n| `--root` file-access sandbox        | [#58](https://github.com/melihbirim/csvql/issues/58) | ✅ shipped (v1.7.0) |\n| `--audit` query log                 | [#62](https://github.com/melihbirim/csvql/issues/62) | ✅ shipped (v1.7.0) |\n| `-o`/`--output <file>`               | [#71](https://github.com/melihbirim/csvql/issues/71) | ✅ shipped (v1.8.0) |\n| `REPLACE`, `SPLIT_PART`, `GREATEST`, `LEAST` | [#67](https://github.com/melihbirim/csvql/issues/67) | ✅ shipped (v1.8.0) |\n| `VARIANCE`, `STDDEV`, `MEDIAN`, `GROUP_CONCAT` | [#50](https://github.com/melihbirim/csvql/issues/50) | ✅ shipped (v1.9.0) |\n| HTTP/SSE MCP transport (shared service) | [#60](https://github.com/melihbirim/csvql/issues/60) | planned             |\n| `OFFSET` clause | [#70](https://github.com/melihbirim/csvql/issues/70) | ✅ shipped |\n| `--markdown` output | [#72](https://github.com/melihbirim/csvql/issues/72) | help wanted |\n| Shell completions (bash/zsh) | [#73](https://github.com/melihbirim/csvql/issues/73) | help wanted |\n\n## Contributing\n\nContributions welcome — bug reports, performance improvements, features, docs. See [CONTRIBUTING.md](CONTRIBUTING.md).\n\nNew here? The [**good first issues**](https://github.com/melihbirim/csvql/labels/good%20first%20issue) are scoped with file pointers and clear done-when criteria — a great place to start (new SQL functions, output formats, and more).\n\n## License\n\nMIT — see [LICENSE.md](LICENSE.md).\n\n---\n\n**Built with Zig** · **9x faster than DuckDB** · **MCP Server** · [GitHub](https://github.com/melihbirim/csvql)\n",
  "bytes": 20988,
  "sha": "4e055860c14bef44a42bf5f4d14ae90e686dd46d600b02949e4bd39f1c85b9bb",
  "repo_slug": "melihbirim/csvql",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_melihbirim_csvql_649193f2/readme"
}