GitHub - AALXX/QueryDrift: n+1, postgresql, prisma, ci, testing
**Fail your CI when a pull request introduces an N+1 query regression.**
For Node.js / TypeScript projects on PostgreSQL — Prisma or raw `pg`, Vitest or Jest.

Quick start · How it works · AI agents · CLI · FAQ

Real run against Postgres — see `demo/`. Rendered with vhs.
- * *
Why
[](https://github.com/AALXX/QueryDrift#why)
N+1 queries are the most common silent performance regression in ORM-backed apps. A query that runs once per request quietly becomes one query _per row_ — fine against 50 rows in dev, a production incident against 50,000. They pass code review because the diff looks innocent, and they pass tests because the test database is small.
**querydrift turns that into a failing CI check.** You capture a baseline of every query pattern your tests fire, commit it, and every PR is diffed against it. When a test starts firing the same query 3× more than baseline, CI fails and names the test, the pattern, and the count.
- **Root-cause attribution.** Failures point at the exact `file:line` in _your_ code that fired the repeating query — the call inside the loop — not just the SQL. Best-effort, from async stack traces; no instrumentation of your code.
- **No server.** The baseline is a JSON file in your repo. CI runs fully offline.
- **No account, no API key, no telemetry.** Nothing leaves your machine, ever.
- **No per-test annotations.** It captures _all_ queries automatically and attributes them to the test that fired them — you don't decorate your tests.
- **Single install.**`npm i -D querydrift`. No native binaries, no Docker, no daemon.
Quick start
[](https://github.com/AALXX/QueryDrift#quick-start) **1. Install**
npm install --save-dev querydrift
**2. Activate the interceptor in your test setup file**
// vitest.setup.ts (or jest.setup.ts) import { setupPg, setupPrisma } from 'querydrift'
setupPg() // raw pg — must run before any test imports pg
For Prisma, pass your client (constructed with query-event logging):
import { setupPrisma } from 'querydrift' import { prisma } from './src/db' // new PrismaClient({ log: [{ emit: 'event', level: 'query' }] })
setupPrisma(prisma)
> **Prisma 7:** works unchanged where `$on('query')` is available. If your setup drops `$on` (client extensions, some driver-adapter configs), use `setupPg()` instead — Prisma 7's `@prisma/adapter-pg` runs on `pg`, so the driver-level patch captures everything, with better `file:line` attribution too. Don't enable both, or queries are counted twice.
> **Vitest:** set `globals: true` in your config. querydrift reads the current test name from the global `expect` to attribute queries; without it, queries can't be matched to a test. (Jest exposes `expect` globally by default.)
**3. Capture the baseline on `main` and commit it**
npx querydrift baseline git add .querydrift/baseline.json git commit -m "chore: add querydrift baseline"
**4. Add the check to CI** — copy `examples/github-actions.yml` into your `.github/workflows/`, or add one step to your existing test job:
- name: querydrift N+1 check
run: npx querydrift check
That's it. querydrift re-runs your suite internally, so it doesn't need a separate test step. PRs now fail on N+1 regressions.
How it works
[](https://github.com/AALXX/QueryDrift#how-it-works)
``` test suite runs ─▶ interceptor captures every SQL query, grouped by test │ ▼ normalize: WHERE id = 1 ─▶ where id = ? (literals stripped) fingerprint each unique query shape │ ┌───────────────┴───────────────┐ ▼ ▼ querydrift baseline querydrift check → .querydrift/baseline.json → diff current run vs baseline → committed to main → fail (exit 1) on regression ```
Queries are normalized before comparison, so `WHERE id = 1` and `WHERE id = 2` are the same pattern and counts stay stable across runs. A regression is a known pattern whose count jumps past the threshold (default 3×), or a brand-new pattern that runs ≥3 times. The baseline is committed, so a regression is `git blame`-able to the PR that introduced it.
Use with AI agents
[](https://github.com/AALXX/QueryDrift#use-with-ai-agents) querydrift is designed to be driven by a coding agent (Claude Code, Cursor, opencode, …), not just read by a human:
- **Machine-readable output.**`querydrift check --format json` writes **only** JSON to stdout — the test runner's own output is routed to stderr, so an agent can `JSON.parse` stdout with no scraping.
- **Self-describing failures.** Each failure names the test, the normalized query pattern, baseline-vs-current counts, and (when attributable) the `site` — the `file:line` that fired the query. The agent doesn't search for the loop; it's told where it is. A `hint` field spells out the fix.
- **Standard exit codes.**`0` pass, `1` regression, `2` setup error — an agent reads the result without parsing prose.
- **No interactivity.** Every command runs non-interactively in CI or a sandbox.
$ querydrift check --format json { "ok": false, "testsChecked": 1, "failures": [ { "test": "GET /users returns all users with their posts", "kind": "new", "normalized": "select id, title from posts where user_id = ?", "baselineCount": 0, "currentCount": 12, "site": { "file": "src/api/users.ts", "line": 42, "fn": "getUserPosts" } } ], "warnings": [], "hint": "Use Prisma's `include` at the top level instead of querying inside a loop. Re-run `querydrift baseline` on main to accept the new pattern." }
A typical automated loop: run `check --format json` → read `failures[].site` (or `test` + `normalized` when unattributed) → hoist the per-row query at that line into a top-level `include`/join → re-run until `ok: true`.
Ready-made agent integrations
[](https://github.com/AALXX/QueryDrift#ready-made-agent-integrations) This repo ships drop-in commands so your agent runs that loop for you:
- **Claude Code — skill (recommended).** Copy `.claude/skills/querydrift/` into your project's `.claude/skills/` (or `~/.claude/skills/`). It **auto-activates** when Claude sees an N+1 or slow-query context — no command to remember. Claude runs `check --format json` and fixes the regressions.
- **Claude Code — slash command.** Prefer explicit? Copy `.claude/commands/querydrift.md` instead; then `/querydrift` checks and fixes, `/querydrift baseline` recaptures.
- **opencode.** Copy `.opencode/command/querydrift.md` into your project's `.opencode/command/`.
- **Cursor / Codex / any shell-capable agent.** Point it at `querydrift check --format json` — the same JSON contract works as a rule or command.
These are thin glue over the JSON interface above — no MCP server or plugin runtime required.
CLI reference
[](https://github.com/AALXX/QueryDrift#cli-reference)
``` querydrift baseline Capture a baseline from the current test run querydrift check Re-run the suite and fail (exit 1) on a regression ```
| Flag | Command | Default | | --- | --- | --- | | `--test-command <cmd>` | both | auto-detected (`npx vitest run` / `npx jest --runInBand`) | | `--test-runner <r>` | both | auto-detected (`vitest` | `jest`) | | `--output <path>` | baseline | `.querydrift/baseline.json` | | `--baseline <path>` | check | `.querydrift/baseline.json` | | `--threshold <n>` | check | `3` — N+1 multiplier that triggers a failure | | `--fail-on-warning` | check | off — total-count regressions warn but don't fail | | `--format <fmt>` | check | `text` (use `json` for agents/tooling) |
Exit codes
[](https://github.com/AALXX/QueryDrift#exit-codes) | Code | Meaning | CI behavior | | --- | --- | --- | | `0` | No regressions | passes | | `1` | N+1 regression detected | fails — blocks merge | | `2` | Setup error (no baseline, no test runner) | fails — needs a human |
Updating the baseline
[](https://github.com/AALXX/QueryDrift#updating-the-baseline) When a PR intentionally changes queries, re-run `querydrift baseline` on the branch and commit the updated file. The baseline diff shows up in the PR, so reviewers see exactly which patterns changed and can judge whether they're expected.
FAQ
[](https://github.com/AALXX/QueryDrift#faq) **Does it need a database in CI?** Yes — querydrift runs your real test suite, so it needs whatever your tests need (a Postgres service in CI). It captures the SQL your code actually sends.
**Does it slow tests down?** Negligibly. The interceptor records the SQL string and delegates the original call untouched; `COPY`/`LISTEN`/`NOTIFY` and friends are unaffected.
**MySQL / SQLite / Drizzle / TypeORM?** Not in v1. PostgreSQL via Prisma or raw `pg` only.
**Why is a failure missing its `Origin:` line?** Attribution reads the async stack at query time and keeps the first frame in your repo. It's best-effort: Prisma's `$on('query')` event fires outside your call stack (use `setupPg()` with a driver adapter for attributed Prisma 7 queries), and some await boundaries lose frames. An unattributed failure still names the test and pattern — detection never depends on attribution.
**Is the baseline committed?** Yes — `.querydrift/baseline.json` is your source of truth. Add only the run artifacts to `.gitignore`:
.querydrift/last-run.json .querydrift/*.tmp
Contributing
[](https://github.com/AALXX/QueryDrift#contributing)
npm install npm test # unit tests, no database needed npm run build
The `demo/` suite runs against a real Postgres — see `demo/README.md`.
License
[](https://github.com/AALXX/QueryDrift#license) MIT