npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

querydrift

v0.2.0

Published

Fail your CI when a PR introduces N+1 query regressions. Node.js + PostgreSQL.

Readme

querydrift

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.

npm CI license node

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

querydrift catching an N+1 regression in CI

Real run against Postgres — see demo/. Rendered with vhs.


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

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

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

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

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

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

| 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

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

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

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

MIT