Zero-config CI gate for Laravel query regressions.
Baseline query counts per test, surface N+1 patterns, and fail builds when tests drift — without manual per-test assertions.
Zero-config CI gate for Laravel. Auto-instruments your test suite, baselines query counts per test, and fails PRs that introduce N+1s or query regressions — without you adding a single assertion.
Existing Laravel query tools either run only in dev mode (beyondcode/laravel-query-detector, Debugbar) or require devs to opt-in per test with manual assertions (mattiasgeniar/phpunit-query-count-assertions). Neither catches regressions automatically in CI on an existing suite.
QueryGuard does. Install, baseline once, and from then on every PR that pushes a test's query count past its baseline (or introduces a new N+1 pattern) fails the build with a precise diff.
composer require --dev laramint/queryguardRegister the PHPUnit extension in phpunit.xml:
<extensions>
<bootstrap class="QueryGuard\PHPUnit\QueryGuardExtension"/>
</extensions># Record the baseline (do this once, commit the file).
php artisan queryguard:baseline
# In CI, this exits non-zero on regression:
php artisan queryguard:check
# Or run phpunit directly with the env var:
QUERYGUARD_MODE=check vendor/bin/phpunitCommit tests/.queryguard-baseline.json to git — PR diffs naturally show "this test went from 4 to 17 queries."
use QueryGuard\Attributes\QueryBudget;
#[QueryBudget(max: 5)]
public function test_index_is_fast(): void { /* ... */ }- name: QueryGuard
run: |
php artisan queryguard:check --markdown > queryguard.md || EXIT=$?
gh pr comment "$PR_NUMBER" --body-file queryguard.md
exit ${EXIT:-0}
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ github.token }}php artisan vendor:publish --tag=queryguard-configThen edit config/queryguard.php for tolerances, ignore patterns, slow-query threshold, and N+1 detection threshold.
- The PHPUnit extension hooks
testPrepared/testFinishedand registers aDB::listencallback that records every query per-test. - Each query's SQL is normalized into a stable signature (literals stripped,
IN (?,?,?)collapsed, keywords lowercased) so the same logical query matches across runs. - At end of run, the recorded profiles are either written to
tests/.queryguard-baseline.json(inbaselinemode) or diffed against it (incheckmode). - Any of the following exits the run non-zero:
- A test's query count exceeds
baseline + tolerance - The same query signature is executed more than
n_plus_one.thresholdtimes in a single test - A
#[QueryBudget]is exceeded
- A test's query count exceeds
Slow queries and new query signatures are reported as warnings (non-fatal) by default.
MIT.
