django-query-guard 🛡️
Created by:
Atiqur Rahman
Software QA Engineer | SDET | Test Automation Architect | Microsoft Contributor | Open Source Contributor
📍 Dhaka, Bangladesh | ✉️ rahman.atiqur.pro@gmail.com | 🌐 LinkedIn | 🐙 GitHub Profile
Stop N+1 database queries before they hit production.
django-query-guardis an ultra-fast, zero-dependency Python & Pytest plugin that automatically detects N+1 queries and enforces strict query count limits in your Django test suites and backend code.
📦 Installation (2 Ways to Install)
You can install django-query-guard using either of the two official methods below:
1️⃣ Standard Installation via PyPI (Recommended)
Install the official stable package directly from the PyPI Repository:
pip install django-query-guard
For development and testing tools (Pytest & Pytest-Django):
pip install django-query-guard[dev]
2️⃣ Direct Installation via GitHub (Latest Bleeding-Edge Version)
Install the latest main branch version directly from the GitHub Source Repository:
pip install git+https://github.com/atiqur-rahman-pro/django-query-guard.git
💡 Why Django Developers Need This
In Django, the ORM makes database queries so easy that it’s terrifyingly easy to accidentally write N+1 queries:
# ❌ THE N+1 ACCIDENT
# Fetches 100 users, then executes 100 individual queries for each profile!
# Total: 101 Database Queries! 🐌
users = User.objects.all()
profiles = [user.profile.bio for user in users]
The Solution: django-query-guard
Instead of relying on manual code reviews or checking server logs, django-query-guard turns query limits and N+1 prevention into automated, enforceable CI/CD tests:
# ✅ THE SOLUTION
import pytest
@pytest.mark.django_db
@pytest.mark.query_guard(max_queries=2, detect_n_plus_one=True)
def test_user_profiles_api(client):
response = client.get("/api/users/")
assert response.status_code == 200
If your endpoint accidentally runs 101 queries instead of 2, Pytest fails instantly with an exact breakdown of which query repeated! 💥
🔥 Key Features
- 🎯 Pytest Marker Integration: Simple
@pytest.mark.query_guard(max_queries=N). - 🧠 Smart SQL Normalization: Normalizes SQL queries (e.g.
WHERE id = 1andWHERE id = 2are recognized as the exact same query pattern). - 🛡️ Zero Heavy Dependencies: Built purely on Django's native database execution wrapper and standard library.
- ⚡ Ultra-Fast: Sub-millisecond execution overhead (< 1ms per test).
- 🐍 Python 3.10+ & Django 4.0+ Compatible: Works out-of-the-box with all modern Django versions.
- 📊 HTML Report Generation: Beautiful dark-themed HTML reports with summary cards, test results table, and SQL query details.
- 🔔 CI/CD GitHub Actions Ready: Ready-made workflow template for automatic N+1 detection on every Pull Request.
- 📈 Query Count Trend Tracking: JSON-based run history with regression/improvement detection across Pytest runs.
📖 A to Z Guide: How to Use
1. Using @pytest.mark.query_guard in Pytest
Simply decorate any test function that accesses the database:
import pytest
@pytest.mark.django_db
@pytest.mark.query_guard(max_queries=3)
def test_fetch_dashboard_data(client):
response = client.get("/api/dashboard/")
assert response.status_code == 200
Parameters for query_guard Marker:
| Parameter | Type | Default | Description |
|---|---|---|---|
max_queries |
int |
None |
Maximum allowed total SQL queries. Raises QueryCountExceededError if exceeded. |
detect_n_plus_one |
bool |
True |
Automatically detect N+1 query patterns. |
n_plus_one_threshold |
int |
2 |
Minimum repetitions of a normalized query required to trigger N+1 detection. |
2. Strict N+1 Detection Mode
Even if your total query count is under max_queries, a loop executing duplicate queries will trigger an NPlusOneQueryError:
@pytest.mark.django_db
@pytest.mark.query_guard(detect_n_plus_one=True, n_plus_one_threshold=2)
def test_user_loop():
# Executes SELECT * FROM auth_user WHERE id = ? twice
for user_id in [10, 20]:
User.objects.get(id=user_id)
3. Using as a Context Manager (with query_guard(...))
You can also use query_guard directly inside Django views, Celery tasks, management commands, or standard unit tests:
from django_query_guard import query_guard, NPlusOneQueryError
def process_latest_orders():
with query_guard(max_queries=5, detect_n_plus_one=True):
orders = Order.objects.filter(status="pending").select_related("user")
for order in orders:
print(order.user.email)
4. How to Fix Detected N+1 Queries in Django
When django-query-guard catches an N+1 query, fix it using Django's ORM optimization methods:
Fix 1: Use select_related for Foreign Keys (One-to-One / Many-to-One)
# ❌ Before (N+1 Queries)
books = Book.objects.all()
authors = [book.author.name for book in books]
# ✅ After (1 Query via JOIN)
books = Book.objects.select_related("author").all()
authors = [book.author.name for book in books]
Fix 2: Use prefetch_related for Reverse Foreign Keys / Many-to-Many
# ❌ Before (N+1 Queries)
authors = Author.objects.all()
books = [author.books.all() for author in authors]
# ✅ After (2 Queries Total)
authors = Author.objects.prefetch_related("books").all()
books = [author.books.all() for author in authors]
📊 HTML Report Generation (New in v0.2.0)
Generate a premium dark-themed HTML report after every Pytest run:
pytest --query-guard-report=report.html
The report includes:
- Summary Cards: Total tests, passed/failed counts, total queries, and N+1 detections at a glance.
- Test Results Table: Per-test query count, max allowed, N+1 status, and duration.
- SQL Query Details: Full SQL breakdown for failed tests with exact query text and execution time.
📈 Query Count Trend Tracking (New in v0.2.0)
Track query performance across multiple Pytest runs with automatic regression detection:
pytest --query-guard-trend=.query_guard_history.json
Terminal output after each run:
========================================================================
===================== QUERY GUARD TREND COMPARISON =====================
========================================================================
Current Run: 3 queries across 5 tests
Previous Run: 30 queries across 5 tests
[IMPROVEMENT] Query Delta: -27 queries
========================================================================
Combine both flags for full reporting:
pytest --query-guard-report=report.html --query-guard-trend=.query_guard_history.json
🔔 CI/CD GitHub Actions Integration (New in v0.2.0)
Copy the ready-made workflow file to your project:
mkdir -p .github/workflows
cp query_guard_ci.yml .github/workflows/
Or add to your existing workflow:
- name: Run Query Guard Tests
run: |
pip install django-query-guard
pytest --query-guard-report=report.html --query-guard-trend=.query_guard_history.json -v
- name: Upload Query Guard Report
if: always()
uses: actions/upload-artifact@v4
with:
name: query-guard-report
path: report.html
This ensures every Pull Request is automatically checked for N+1 query regressions before merging!
🛠️ Custom Exceptions
django-query-guard provides explicit exceptions to catch in your application or test suites:
QueryGuardError: Base class for all package exceptions.QueryCountExceededError: Raised when query count exceedsmax_queries.NPlusOneQueryError: Raised when duplicate normalized SQL statements are executed.
📄 License
This project is licensed under the MIT License.
👤 Author & Maintainer
Atiqur Rahman
Software QA Engineer | SDET | Test Automation Architect | Microsoft Contributor | Open Source Contributor
📍 Dhaka, Bangladesh | ✉️ rahman.atiqur.pro@gmail.com | 🌐 LinkedIn | 🐙 GitHub Profile
Release files for django-query-guard 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_query_guard-0.2.1.tar.gz | 20.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_query_guard-0.2.1-py2.py3-none-any.whl | Python 2, Python 3 | none | any | Details |
Total release size: 39.8 kB
Release files / django_query_guard-0.2.1.tar.gz
| Download URL | django_query_guard-0.2.1.tar.gz |
|---|---|
| Size | 20.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
158868fc83d640dab8e52a6c85c9c581eb3489f9e5ca9061cef9b71aff1ac68d
|
|
BLAKE2b-256 checksum How to use checksums |
fedc06b5d6778259e2c6252e8dd85d7ef2cc8d9e7c147a026930f42003e349c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Release files / django_query_guard-0.2.1-py2.py3-none-any.whl
| Download URL | django_query_guard-0.2.1-py2.py3-none-any.whl |
|---|---|
| Size | 18.8 kB |
| Tags | Python 2 Python 3 |
|
SHA-256 checksum How to use checksums |
cd29d46556762d4e77f4ff82a632d88cc2f6a32f9a166c931447d787b4c96e6e
|
|
BLAKE2b-256 checksum How to use checksums |
81d31bb04ae3e05fe6d7ae9c200768ef68e479759201d16f913ebe7e034b3b22
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|