p.enthalabs

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.

![Image 1: npm](https://www.npmjs.com/package/querydrift)![Image 2: CI](https://github.com/AALXX/QueryDrift/actions)![Image 3: license](https://github.com/AALXX/QueryDrift/blob/main/LICENSE)![Image 4: node](https://nodejs.org/)

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

![Image 5: querydrift catching an N+1 regression in CI](https://github.com/AALXX/QueryDrift/blob/main/demo.gif)

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