django-query-contract
A query-capture engine for Django, and a pytest plugin over it.
Every statement is recorded with a normalised SQL fingerprint and the call stack
that emitted it, through connection.execute_wrapper(). When a query-count
assertion fails, the failure gains a paragraph saying which shape repeated and
where it came from. When a block runs past the ceiling on Django's own query
log, that is reported instead of papered over.
Install
pip install django-query-contract
The pytest plugin loads itself. There is nothing to add to INSTALLED_APPS, and
no fixture to request.
It ships no count assertion, on purpose
django_assert_num_queries is the assertion. It is typed, it handles
connection= / using= and a custom failure note, and it yields the captured
queries. Keep writing it:
def test_listing_is_flat(django_assert_num_queries, authors):
with django_assert_num_queries(2):
render_author_list()
When it fails, the failure now carries a diagnosis underneath it:
------------------------------ django-query-contract ------------------------------
13 statements captured: 13 on 'default'.
Repeated statement shapes:
11 x #1, #2, #3, #4, #5, #6, #7, #8, ...
SELECT "shop_book"."id", "shop_book"."author_id", "shop_book"."title" FROM "shop_book" WHERE "shop_book"."author_id" = %s
from shop/views.py:16 in render_author_list
2 shape(s) ran once.
The ceiling nobody mentions
assertNumQueries and django_assert_num_queries both count through
CaptureQueriesContext, which slices connection.queries between two absolute
indices. That log is a deque(maxlen=connection.queries_limit) -- 9000 by
default -- so once it rotates the indices no longer point at what they did.
Measured against Django 6.1:
| Already in the log | Queries in the block | Reported |
|---|---|---|
| 0 | 8999 | 8999 |
| 0 | 9001 | 9000 |
| 8990 | 100 | 10 |
| 9000 | 5 | 0 |
The last row is a passing django_assert_max_num_queries(1) around five real
queries, and the regime it happens in -- thousands of statements in one block --
is exactly the N+1-at-scale case worth catching.
Capture here rides on execute_wrapper, which has no bound, so this package
raises a QueryLogCeilingWarning naming the test, the real count and the number
the assertion was handed.
Reading the capture directly
from django_query_contract import QueryCapture
with QueryCapture() as capture:
render_author_list()
for fingerprint, records in capture.by_fingerprint().items():
if len(records) > 1:
print(len(records), records[0].call_site, fingerprint)
A QueryRecord carries the statement, its fingerprint, the connection alias and
vendor, the parameter count and the call stack. It carries no parameters --
a bulk_create is one execution and ten thousand values, and a runtime reader
of this capture has no business holding them -- and no duration, because a
performance assertion that mentions milliseconds is a flaky test with extra
steps.
QueryCapture.from_capture_context(...) builds one from the
CaptureQueriesContext that django_assert_num_queries yields. It is honestly
degraded: no call stacks, no parameter counts, and no ceiling, because a count
taken from a rotated deque cannot report what it lost.
Where the capture stops
execute_wrapper wraps Django's cursor wrapper, so it sees execute and
executemany and nothing else. A statement issued on the raw driver connection
is invisible, and so is a driver API that is neither -- psycopg 3's
cursor.copy(), for instance. Django's own query log has the same blind spot,
so the two agree, and there is a test that pins it rather than a note that
assumes it.
Turning it off
[pytest]
query_contract = false
query_contract_stack_depth = 25
or --no-query-contract for one run.
Status
Early. The capture engine and the pytest diagnosis. N+1 by (call stack, fingerprint), the growth assertion, call-site attribution and plan capture come next.
Full documentation: https://artui.github.io/django-query-contract/
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_query_contract-0.1.0.tar.gz.
File metadata
- Download URL: django_query_contract-0.1.0.tar.gz
- Upload date:
- Size: 130.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c461855272be8d3c56ca6f1d1b18e4c50a3fcbb286db4d1634e7dab93077cd68
|
|
| MD5 |
3e2c588506d63f7e83d9e187e56ff7d5
|
|
| BLAKE2b-256 |
70b953f264fc6a5e9959b7b0f87f4e926a0c9d73e5a240edc96a81a23b701a40
|
Provenance
The following attestation bundles were made for django_query_contract-0.1.0.tar.gz:
Publisher:
release.yml on Artui/django-query-contract
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_query_contract-0.1.0.tar.gz -
Subject digest:
c461855272be8d3c56ca6f1d1b18e4c50a3fcbb286db4d1634e7dab93077cd68 - Sigstore transparency entry: 2685008208
- Sigstore integration time:
-
Permalink:
Artui/django-query-contract@3bcb0e9c299ba0ca64e5571f2b25f35233738eba -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Artui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3bcb0e9c299ba0ca64e5571f2b25f35233738eba -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_query_contract-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_query_contract-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
271701c458f9de39ca28b5335dc8da0539d729a2920557c20dd71650887171e3
|
|
| MD5 |
f871aa5b0b64550d61d2e0c7ce3e1973
|
|
| BLAKE2b-256 |
c4aafc96dff9974e6be8909787f44df414300157c1c4ab0fd93e2692af0ea6b4
|
Provenance
The following attestation bundles were made for django_query_contract-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Artui/django-query-contract
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_query_contract-0.1.0-py3-none-any.whl -
Subject digest:
271701c458f9de39ca28b5335dc8da0539d729a2920557c20dd71650887171e3 - Sigstore transparency entry: 2685008223
- Sigstore integration time:
-
Permalink:
Artui/django-query-contract@3bcb0e9c299ba0ca64e5571f2b25f35233738eba -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Artui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3bcb0e9c299ba0ca64e5571f2b25f35233738eba -
Trigger Event:
push
-
Statement type: