{
  "markdown": "# Quarry\n\n> **The database workbench built for the AI era** — one kernel, many faces (CLI / GUI / MCP / agent skill).\n\n[![CI](https://github.com/Wangggym/quarry/actions/workflows/ci.yml/badge.svg)](https://github.com/Wangggym/quarry/actions/workflows/ci.yml)\n[![Coverage ≥95%](https://img.shields.io/badge/coverage-%E2%89%A595%25-brightgreen)](TESTING.md)\n[![Tests](https://img.shields.io/badge/tests-723-brightgreen)](TESTING.md)\n[![PyPI](https://img.shields.io/pypi/v/quarry-db)](https://pypi.org/project/quarry-db/)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://pypi.org/project/quarry-db/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n\n[中文文档 →](README.zh-CN.md) · [Website →](https://quarry.yiminlab.site)\n\n![Quarry demo](site/assets/demo.svg)\n\nEvery database tool you know — DBeaver, TablePlus, pgAdmin — assumes a *human* at the keyboard. But increasingly, the entity running your queries is an **AI agent**, and agents need different guarantees:\n\n- **Results a machine can parse**, not a screen a human can read\n- **Safety rails that live in the kernel**, so no client can forget them\n- **Deterministic error contracts** (stable exit codes), not stack traces to scrape\n- **Configuration as files**, not clicks — so it can be versioned, diffed, and shared with agents\n\nQuarry inverts the traditional design: it is a **query kernel with an agent-safe contract first**, and the human faces (CLI, GUI) are thin shells grown from the same kernel. Whether a query comes from a person in the browser, a script in CI, or Claude running a skill, it passes through the exact same safety rails and returns the exact same structured result.\n\n## Philosophy\n\n1. **One core, many faces.** Connection management, query execution, schema introspection, and safety rails live in an importable kernel (`quarry.core`). The CLI (`qy`), the GUI, the MCP server, and agent skills are thin shells. Fix a bug once, every face gets it.\n\n2. **Read-only by default; escalation is explicit and graduated.** Writes and DDL are blocked (exit code `8`) unless you pass `--write`. Production connections require an *additional* confirmation on top of `--write`. Every query gets an automatic `LIMIT 500` unless you opt out. Because the rails are in the kernel, an agent cannot bypass them by picking a different entry point.\n\n3. **A contract machines can trust.** Every query returns `{columns, rows, rowCount, truncated, elapsedMs, engine, sql}`. Exit codes are stable API: `0` ok, `2` connection error, `3` SQL error, `8` safety block. An agent can branch on outcomes without parsing prose.\n\n4. **Workspace as code.** A workspace is just a directory: `connections.toml` + `queries/**/*.sql` (named queries with `-- @meta` headers). It lives in *your* repo, versioned by git, shared between teammates and agents alike. The kernel itself carries zero business logic and zero secrets.\n\n5. **Nearly zero dependencies.** Pure stdlib. PostgreSQL goes through your system `psql`, Redis through `redis-cli`, SSH tunnels through system `ssh`. MySQL is one optional `pymysql`. No Electron, no daemon, no cloud.\n\n## Install\n\n```bash\npipx install quarry-db        # or: pip install quarry-db\nqy --help\n```\n\nPostgreSQL uses the system `psql` binary; MySQL needs `pip install \"quarry-db[mysql]\"`.\n\n## Quickstart\n\n```bash\nmkdir my-workspace && cd my-workspace\ncat > connections.toml <<'EOF'\n[shop]\nurl    = \"postgresql://user:pass@localhost:5432/shop\"\nengine = \"postgres\"\nenv    = \"dev\"\nEOF\n\nqy connections                       # list connections\nqy exec shop --sql \"select * from customers\"\nqy schema shop customers             # table structure (\\d+)\nqy gui                               # browser data grid\n```\n\n## Workspace\n\nA workspace directory is the source of connections + queries:\n\n```\nmy-workspace/\n├── connections.toml      # [key] url / engine / env / group / notes\n└── queries/<db>/*.sql    # named queries (with -- @meta headers)\n```\n\nResolution order: `--workspace PATH` → `~/.config/quarry/config.toml` → current directory.\n\n## CLI reference\n\n| Command | Purpose |\n|---------|---------|\n| `qy connections [list\\|add\\|set\\|remove\\|test]` | Manage connections |\n| `qy ping <db>\\|--all [--timeout N] [--format text\\|json]` | Reachability probe (ok/fail + latency; exit 1 if any fail) |\n| `qy exec <db> --sql \"...\" [--format json\\|ndjson\\|csv\\|table] [--timeout N]` | Run ad-hoc SQL |\n| `qy speedtest <db> [--env dev] [--bytes N] [--runs N]` | Benchmark the current PostgreSQL/MySQL tunnel path |\n| `qy schema <db> <table>` | Live table structure |\n| `qy run <name> [k=v ...]` | Run a saved named query |\n| `qy save <name> --db X --sql \"...\"` | Save a named query |\n| `qy list / describe / validate / fingerprint / audit` | Manage named queries |\n| `qy workspace list/add/remove` | Manage aggregated workspaces |\n| `qy up/down/status [--format text\\|json]` | Workspace tunnel keep-alive keeper |\n| `qy local up/down/status/sync [--engine postgres\\|redis\\|all]` | Local dev containers (see below) |\n| `qy gui` | Launch the local GUI |\n| `qy mcp [--write]` | Serve the MCP face over stdio (for AI agents) |\n\n## MCP (the agent-native face)\n\n`qy mcp` speaks the Model Context Protocol over stdio — pure stdlib, no SDK dependency. Agents get six tools (`list_connections`, `list_tables`, `describe_table`, `exec_sql`, `list_saved_queries`, `run_saved_query`) with the exact same kernel rails: read-only unless the server was started with `--write` *and* the call passes `write: true`; a prod env additionally requires `confirm_prod: true`.\n\n```bash\n# Claude Code\nclaude mcp add quarry -- qy mcp --workspace ~/my-workspace\n```\n\n```json\n// or any MCP client (.mcp.json)\n{ \"mcpServers\": { \"quarry\": { \"command\": \"qy\", \"args\": [\"mcp\", \"--workspace\", \"/path/to/workspace\"] } } }\n```\n\nPublished in the [MCP Registry](https://registry.modelcontextprotocol.io/) as `mcp-name: io.github.Wangggym/quarry`.\n\n## Safety rails (the AI-native moat)\n\n- **Read-only by default**: writes/DDL blocked with exit code `8`; `--write` to allow\n- **Automatic row cap**: `run_query()` injects `LIMIT 500`; raise with `--max-rows N`\n- **Graduated prod protection**: all envs default read-only → dev needs `--write` → prod needs `--write` *plus* an interactive confirmation (`--yes` for automation)\n- **Stable exit-code contract**: `0` ok / `2` connection / `3` SQL / `8` safety block\n\n## Timeouts\n\nQuery execution and connection establishment (including SSH tunnel setup) are capped independently, so an unreachable host fails fast instead of eating the whole query budget:\n\n- **Connect timeout**: 15s, fixed — bounds tunnel/dial only.\n- **Execute timeout**: 300s default for the CLI/GUI, 120s for MCP (agents should converge faster).\n\nThe effective execute timeout is resolved in priority order:\n\n1. `--timeout N` (CLI, on `qy exec`/`qy run`)\n2. `QUARRY_TIMEOUT` env var\n3. the connection's `timeout` field in `connections.toml` (set via `qy connections add/set --timeout N`)\n4. the default above\n\n```toml\n[shop_prod]\nurl     = \"postgresql://…prod…/shop\"\ntimeout = 600   # this connection alone gets 10 minutes\n```\n\nOn PostgreSQL, `qy` also sets a server-side `statement_timeout` (~90% of the execute timeout) before running the query; on MySQL/MariaDB it sets the equivalent session variable (`MAX_EXECUTION_TIME` / `max_statement_time`, whichever the server supports) best-effort. Either way, the database itself cancels a runaway query and reports the real reason — instead of the client giving up and leaving the query running server-side. A timeout error always tells you how to raise it (`--timeout`, `QUARRY_TIMEOUT`, or the connection's `timeout` setting). `--timeout` and the `timeout` field must be a positive number of seconds.\n\n## As a library (what the GUI and agents use)\n\n```python\nfrom quarry import configure_workspace, get_connection, run_query\n\nconfigure_workspace(\"~/my-workspace\")\nres = run_query(get_connection(\"shop\"), \"select * from customers\")\nprint(res.to_dict())   # {columns, rows, rowCount, truncated, elapsedMs, engine, sql}\n```\n\n## SSH tunnels\n\nFor databases only reachable via a bastion, add `ssh_*` fields and `qy` opens the tunnel automatically (system `ssh`, zero dependencies):\n\n```toml\n[internal_db]\nurl      = \"postgresql://user:pass@127.0.0.1:5432/appdb\"\nengine   = \"postgres\"\nssh_host = \"bastion.example.com\"\nssh_user = \"ubuntu\"\nssh_key  = \"~/.ssh/id_ed25519\"\n```\n\nNeptune participates in the same tunnel path now: if a Neptune connection has\n`ssh_host` (plus optional `ssh_user`/`ssh_key`/`ssh_port`), it joins tunnel\npooling/keep-alive the same way as Postgres/MySQL/Redis.\n\n### Workspace keep-alive (`qy up/down/status`)\n\nIf you query the same SSH-backed connections repeatedly (CLI + GUI + MCP), run\nthe workspace keeper once and reuse warm forwards across processes:\n\n```bash\nqy up                    # start keeper for current workspace (also turns keep-alive on)\nqy status                # text status: keeper + per-connection tunnel state\nqy status --format json  # machine-readable state (up/reconnecting/down)\nqy down                  # stop keeper\n```\n\n`keep_alive=true` + `reconnect=true` are persisted per workspace in\n`~/.config/quarry/config.toml`. When reconnect is enabled, dropped tunnels are\nre-opened with exponential backoff and reported as `reconnecting` in both `qy\nstatus` and the GUI header badge. If keep-alive is enabled but the keeper is\ndown, cold `qy exec`/`qy run` still work (legacy behavior) and print a one-line\nhint to stderr suggesting `qy up`.\n\n### Proxy (for throttled tunnels)\n\nIf an SSH tunnel's throughput is throttled (a cross-border bastion, for example — the handshake connects fine but data crawls), route it through your machine's HTTP(S) proxy instead:\n\n```bash\nqy proxy              # show the discovered proxy + each workspace's toggle state\nqy proxy on           # enable for the current workspace\nqy proxy off          # disable it again\nqy exec mydb --no-proxy --sql \"...\"   # skip the proxy for one call, even if enabled\n```\n\nThe proxy is auto-discovered — macOS system proxy settings first (`scutil --proxy`), falling back to `ALL_PROXY`/`HTTPS_PROXY` — and the toggle is persisted per workspace in `config.toml` (never `connections.toml`). It only affects connections with `ssh_host` (tunneled via `ProxyCommand`) and Neptune's direct HTTPS requests; a direct (non-tunneled) DB connection is unaffected, and `qy connections add/set` warns if you enable the proxy for a connection with no `ssh_host`. If the proxy is enabled but nothing is listening on its port, `qy` falls back to a direct connection instead of erroring; targets covered by the system proxy's exceptions list (loopback, private CIDR ranges) are never proxied.\n\n#### Confirming the proxy is actually in effect\n\nBecause the fallback-to-direct behavior above is silent by design (a query still has to run), it's worth knowing how to check whether a given call actually went through the proxy:\n\n- **`qy` output**: if a workspace has the proxy enabled but a call still ran direct, `qy exec`/`qy run` print a one-line reason to stderr — no proxy discovered, discovered but nothing listening on its port, or the target is covered by the proxy's exceptions list. `--no-proxy` suppresses this (you asked for direct, so there's nothing to report).\n- **`qy proxy`**: besides the discovered proxy and each workspace's toggle, it lists every pooled SSH tunnel — ssh target, local port, whether it's actually routed through the proxy (and which address), and whether the underlying `ssh` process is still alive. Add `--format json` for a `tunnels` array with the same fields, handy for scripting.\n- **GUI**: an env pill in the sidebar shows a small badge when that connection's tunnel is routed through the proxy; the workspace manager shows each workspace's proxy toggle alongside the currently discovered proxy address. Both are computed server-side from the same logic `qy` uses, not guessed in the browser.\n\n## Redis\n\n`engine = \"redis\"` (uses system `redis-cli`). Queries are redis commands:\n\n```bash\nqy exec cache --sql \"SCAN 0 COUNT 100\"\nqy exec cache --sql \"HGETALL user:42\"\n```\n\nRead-only rail applies here too: `GET/SCAN/TYPE/TTL/HGETALL` pass; `SET/DEL/FLUSHALL` are blocked without `--write`. In the GUI, redis keys are clickable with TYPE-aware value display.\n\n## Groups & env-sets\n\nConnections can be organized into **project folders** (`group`) and **env-sets** (same `db`, different `env`, shared schema):\n\n```toml\n[shop_dev]\nurl = \"postgresql://…dev…/shop\";  group = \"shop\"; db = \"shop\"; env = \"dev\"\n[shop_prod]\nurl = \"postgresql://…prod…/shop\"; group = \"shop\"; db = \"shop\"; env = \"prod\"\n```\n\n- Connections with the same `db` fold into one env-set — one saved query runs against any environment: `qy exec shop --env prod`\n- `qy connections add/set` accepts `--db` and `--group`; when a new key such as `shop_prod --env prod` matches an existing env-set, Quarry inherits that identity automatically\n- If one env member omits `group` but every grouped sibling agrees, CLI/GUI/MCP keep the logical DB together in that group instead of creating a duplicate under `OTHER`\n- Unspecified env defaults to `dev` (the safest)\n- The GUI shows an environment switcher (prod turns red)\n\n## Multiple workspaces\n\n`qy` aggregates all workspaces listed in `~/.config/quarry/config.toml` — one GUI/CLI over all your projects:\n\n```bash\nqy workspace add ~/projects/acme/db-workspace\nqy workspace add ~/projects/side-project/db\nqy connections    # both projects, grouped\nqy gui            # sidebar shows both groups side by side\n```\n\n`--workspace a:b` (os.pathsep-separated) works as a temporary override; the first directory is primary for writes.\n\n## Local dev containers\n\nWhen a locally-running service shares a remote (dev) database, every read/write\ncrosses the public network — and a test/e2e run that hammers the DB gets flaky\non the round trips. `qy local` runs Postgres/Redis in a docker container so the\nservice talks only to `localhost`:\n\n```bash\nqy local up shop            # start local Postgres + register a shop `local` connection\nqy connections              # shop now shows a [local] env alongside [dev]\nqy run active_customers --env local\n\nqy local status             # running? which port / image?\nqy local sync shop          # copy dev schema into local (staging db + rename swap)\nqy local down               # stop, keep the data volume (data survives)\nqy local down --purge       # stop + delete the volume (next up is an empty DB)\n```\n\nOne shared Postgres container hosts a logical database per connection key (fixed\nport `5433`; redis `6380`), and data lives on a named docker volume. Requires a\ndocker daemon; the image tag is overridable with `--image`.\n\n## GUI\n\n![Quarry GUI](site/assets/gui-dark.png)\n\n`qy gui` — a local, zero-build web GUI (Slate & Copper theme, light/dark):\n\n- Grouped sidebar tree with env switcher (prod turns red), connection health dots\n- **Multi-tab editor** — each tab remembers its SQL + connection, across restarts\n- SQL highlighting + local autocomplete (keywords / tables / columns)\n- **EXPLAIN button** — one click to the query plan\n- Type-aware data grid: sorting, column resize, **keyboard navigation** (arrows + Enter), cell inspection with a **collapsible JSON tree**\n- CSV/JSON export, **searchable query history** (with connection + time)\n- TYPE-aware Redis key browsing\n- **Update check** — a background thread polls PyPI once every 24h and shows\n  a header badge (with the upgrade command + release notes) when a newer\n  `quarry-db` is out. Editable/dev installs are skipped automatically; set\n  `QUARRY_UPDATE_CHECK=0` to disable it entirely.\n\n## Roadmap\n\n- Column types in the result contract for all engines\n- SQLite & DuckDB engines (zero-setup local demo)\n- Redis key-namespace folding tree\n- Cross-environment schema/data diff\n- Write audit log (who ran what, where, when)\n- Single-binary distribution\n\n## Development & testing\n\n```bash\npip install -e \".[dev]\"\ncreatedb quarry_test && psql quarry_test -f tests/seed.sql   # or: make seed\nmake test        # layered run with a per-layer PASS/FAIL summary\n```\n\n**723 tests in four layers**, each auto-classified so you can run any slice:\n\n| Layer | Count | Covers | Needs |\n|-------|------:|--------|-------|\n| `unit` | 568 | pure logic + mocked engines (safety rails, SQL skeleton, params, formatters, cache) | nothing |\n| `integration` | 110 | in-process against a real DB, incl. the GUI HTTP API and CLI/MCP dispatch | Postgres |\n| `e2e` | 45 | the real `qy` CLI and `qy mcp` stdio server as subprocesses | Postgres |\n| `browser` | 20 | the **real GUI frontend** driven in headless Chromium (Playwright) | Postgres + Playwright |\n\nDB/engine-backed tests skip automatically when the engine is unreachable, so the\nsuite stays green on a bare machine; CI provides the engines and runs everything.\n\n**Coverage is gated at ≥95%** (unit + integration) and currently sits at **99.6%**.\n\n### Seeing test status at a glance\n\n- **On GitHub:** the CI badge above is live — it goes red if any layer *or* the\n  coverage gate fails. Per-commit and per-PR results show under the **Actions** tab\n  and as PR checks.\n- **Locally, pass/fail:** `make test` prints a colored per-layer summary; run one\n  layer with `make test-unit` / `test-integration` / `test-e2e` / `test-browser`.\n- **Locally, coverage:** `make cov` enforces the gate and writes an HTML report —\n  open `htmlcov/index.html` for a line-by-line view of exactly what's covered.\n\nSee [TESTING.md](TESTING.md) for the full architecture, fixtures, and CI layout,\nand [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.\n\nQuarry is developed and tested on macOS and Linux. Windows is currently untested (the psql/ssh integration and port takeover are Unix-flavored) — PRs welcome.\n\n## License\n\n[MIT](LICENSE)\n",
  "bytes": 17835,
  "sha": "2f565cbc85a8d93eb114830bf8d17e8102b3493a105fd75bdea3d481404e119f",
  "repo_slug": "wangggym/quarry",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_wangggym_quarry_9e7a6dba/readme"
}