Recipe: CI#
pytest-queryguard fails the pytest run, so every CI system that runs pytest enforces it. This page is about where the findings show up. For code scanning alerts with the GitHub Action and the SARIF file, see CI and GitHub Action.
GitHub Actions#
Plain pytest, with a JUnit file kept as an artifact:
name: Tests
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: pip install -e ".[test]" # your project, with pytest-queryguard in its test extra
- run: pytest --junitxml=report.xml
- uses: actions/upload-artifact@v7
if: always()
with:
name: junit
path: report.xml
The finding is in the job log as a normal pytest failure. With the action (see CI and GitHub Action) it also becomes an annotated code scanning alert.
GitLab CI#
GitLab shows JUnit failures in the merge request when the file is declared as a report:
tests:
image: python:3.12
script:
- pip install -e ".[test]"
- pytest --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
The text of a finding is in the failure message of the test case in that
file, so it appears in the merge request's test report. This is what the
file holds for the N+1 test of the sync example (generated with
pytest --junitxml=report.xml tests/test_orders.py -k list_orders; the time, timestamp and
hostname are replaced):
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="1" skipped="0" tests="1" time="0.043" timestamp="..." hostname="..."><testcase classname="tests.test_orders" name="test_list_orders" time="0.043"><failure message="Failed: queryguard: N+1 detected Order.customer was lazy-loaded 50 times first at app/orders.py:10 in list_orders kind: relationship-load label: Order.customer sql: SELECT customer.id, customer.name FROM customer WHERE customer.id = ? fix: load it with the parent query, e.g. select(Order).options(joinedload(Order.customer))">queryguard: N+1 detected
Order.customer was lazy-loaded 50 times
first at app/orders.py:10 in list_orders
kind: relationship-load label: Order.customer
sql: SELECT customer.id, customer.name FROM customer WHERE customer.id = ?
fix: load it with the parent query, e.g.
select(Order).options(joinedload(Order.customer))</failure></testcase></testsuite></testsuites>
GitLab's code quality and SAST report formats are not produced; the SARIF output is for GitHub code scanning. The GitLab snippet was written from GitLab's documentation and the JUnit file above was produced by running pytest; the pipeline itself was not run on GitLab.
pre-commit#
pre-commit does not fit. A hook there runs on the files of a commit, and
pytest-queryguard looks at the SQL that a test run sends, so there is nothing
it can check in a changed file without running the tests against a database.
Run it in CI and in your normal test command, and use a pytest hook in
repo: local only if you already run your suite from pre-commit.
Keep the baseline honest#
Run the full suite with --queryguard-update-baseline in a scheduled job and
open a pull request when the file shrinks. Do not let that job commit to
the main branch; the baseline should change only through review (see
Adopting on a legacy codebase).