Skip to main content

django-query-contract

CI PyPI Python versions Django versions Docs Coverage Ruff License

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

django_query_contract-0.1.0.tar.gz (130.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_query_contract-0.1.0-py3-none-any.whl (22.8 kB view details)

Uploaded Python 3

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

Hashes for django_query_contract-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c461855272be8d3c56ca6f1d1b18e4c50a3fcbb286db4d1634e7dab93077cd68
MD5 3e2c588506d63f7e83d9e187e56ff7d5
BLAKE2b-256 70b953f264fc6a5e9959b7b0f87f4e926a0c9d73e5a240edc96a81a23b701a40

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_query_contract-0.1.0.tar.gz:

Publisher: release.yml on Artui/django-query-contract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_query_contract-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_query_contract-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 271701c458f9de39ca28b5335dc8da0539d729a2920557c20dd71650887171e3
MD5 f871aa5b0b64550d61d2e0c7ce3e1973
BLAKE2b-256 c4aafc96dff9974e6be8909787f44df414300157c1c4ab0fd93e2692af0ea6b4

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page