# Django-assert-queries: Keep your Django queries from exploding
ORMs. Love 'em or hate 'em, they're often part of the job, and a core part of
writing [Django](https://djangoproject.com) webapps. They can make it easy
to write queries that work across databases, but the trade-off is your queries
might explode in number and in complexity.
Who hasn't made this mistake?
```python
for book in Book.objects.all():
print(f'Author: {book.author.name}')
```
Spot the error? We're fetching the associated author once for every book. For
100 books, that's 101 queries!
And that's just a really basic query.
These mistakes happen all the time, and they're not always easy to catch in
unit tests.
That's what the clear-but-unimaginatively-named
[django-assert-queries](https://pypi.org/project/django-assert-queries) is
here to solve. With proper use, this can save companies from costly mistakes.
[We've](https://www.reviewboard.org) found it to be an invaluable tool in our
arsenal.
We'll explore how it does that, but first, let's get things installed.
## Installation and Usage
```shell
$ pip install django-assert-queries
```
``django-assert-queries`` follows [semantic versioning](https://semver.org/),
meaning no surprises when you upgrade.
[Documentation](https://django-assert-queries.readthedocs.io) is available,
covering the whole codebase.
## Let's see it in action
We're going to catch the bug above.
```python
from django_assert_queries import assert_queries
def test():
expected_queries = [
{
'model': Book,
},
]
with assert_queries(expected_queries):
for book in Book.objects.all():
print(f'Book {book.name} by {book.author.name}')
```
When we run that, we get:
```
E AssertionError: Expected 1 queries, but got 101
E
E 100 queries failed to meet expectations.
E
E Query 2:
E model: <class 'django_assert_queries.tests.models.Author'> != None
E tables: {'tests_author'} != {}
E where: Q(id=1) != Q()
E SQL: SELECT "tests_author"."id", "tests_author"."name" FROM "tests_author" WHERE "tests_author"."id" = 1 LIMIT 21
E
E Query 3:
E model: <class 'django_assert_queries.tests.models.Author'> != None
E tables: {'tests_author'} != {}
E where: Q(id=2) != Q()
E SQL: SELECT "tests_author"."id", "tests_author"."name" FROM "tests_author" WHERE "tests_author"."id" = 2 LIMIT 21
E
E Query 4:
E model: <class 'django_assert_queries.tests.models.Author'> != None
E tables: {'tests_author'} != {}
E where: Q(id=3) != Q()
E SQL: SELECT "tests_author"."id", "tests_author"."name" FROM "tests_author" WHERE "tests_author"."id" = 3 LIMIT 21
E
[...]
```
That problem just became a lot more clear. Let's fix this.
```python
from django_assert_queries import assert_queries
def test():
# We'll select-related the authors.
expected_queries = [
{
'model': Book,
'select_related' ('author',),
},
]
with assert_queries(expected_queries):
for book in Book.objects.select_related('author'):
print(f'Book {book.name} by {book.author.name}')
```
These can be a lot more thorough:
```python
def test_complex_query():
expected_queries = [
# Initial query for the books.
{
'model': Book,
'limit': 2,
'only_fields': {'author', 'name'},
'select_related': {'author'},
},
# Initial query for the authors.
{
'model': Author,
'annotations': {
'book_count': Count('books'),
},
'group_by': True,
'num_joins': 1,
'tables': {
'tests_author',
'tests_book',
},
},
# The prefetch-related for all the authors' books.
{
'model': Book,
'where': Q(author__in=list(Author.objects.all())),
},
]
books_queryset = (
Book.objects
.filter(name__in=['Book 1', 'Book 9'])
.only('author', 'name')
.select_related('author')
[:2]
)
authors_queryset = (
Author.objects
.annotate(book_count=Count('books'))
.prefetch_related('books')
)
with assert_queries(expected_queries):
for book in books_queryset:
print(f'Book {book.name} by {book.author.name}')
for author in authors_queryset:
print(f'Author {author.name} published {author.book_count} books:')
for book in author.books.all():
print(f' {book.name}')
```
Here we've got filtering, annotations, field limiting, filtering, joins,
and prefetch-related.
These cover just about everything that Django queries can do, and when used
correctly your unit tests can account for just about every query your
application makes.
## Brought to you by Beanbag
At [Beanbag](https://www.beanbaginc.com), we're all about building better
software development tools.
Our flagship product is [Review Board](https://www.reviewboard.org), one of
the first-ever code review products on the market, and originator for most
now-standard code review features.
We also build these lovely Python packages:
* [beanbag-docutils](https://github.com/beanbaginc/beanbag-docutils/) -
Multi-DPI images, enhanced syntax, and many more add-ons for Sphinx
documentation writers.
* [Djblets](https://github.com/djblets/djblets/) -
Our pack of Django utilities for datagrids, API, privacy, extensions, and
more. Used by Review Board.
* [Grumble](https://github.com/beanbaginc/grumble/) -
For Python print debuggers drowning in print statements.
* [Housekeeping](https://github.com/beanbaginc/housekeeping/) -
Deprecation management for Python codebases of all sizes.
* [kgb](https://github.com/beanbaginc/kgb/) -
Function spies for Python unit tests, a major upgrade from mocks.
* [registries](https://github.com/beanbaginc/registries/) -
Registration management and lookup of objects, for extensible Python
applications.
* [typelets](https://github.com/beanbaginc/typelets/) -
Python typing additions, including comprehensive JSON and JSON-compatible
data structures, symbols, and Django add-ons.
You can see more on [github.com/beanbaginc](https://github.com/beanbaginc) and
[github.com/reviewboard](https://github.com/reviewboard).
Raw data
{
"_id": null,
"home_page": null,
"name": "django-assert-queries",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "database, django, pytest, sql, unit tests",
"author": null,
"author_email": "\"Beanbag, Inc.\" <questions@beanbaginc.com>",
"download_url": "https://files.pythonhosted.org/packages/97/82/18b3772daec45f073d63ef35b5a1e459540ac54c2b092fa8e9414271203d/django_assert_queries-1.0.1.tar.gz",
"platform": null,
"description": "# Django-assert-queries: Keep your Django queries from exploding\n\nORMs. Love 'em or hate 'em, they're often part of the job, and a core part of\nwriting [Django](https://djangoproject.com) webapps. They can make it easy\nto write queries that work across databases, but the trade-off is your queries\nmight explode in number and in complexity.\n\nWho hasn't made this mistake?\n\n```python\nfor book in Book.objects.all():\n print(f'Author: {book.author.name}')\n```\n\nSpot the error? We're fetching the associated author once for every book. For\n100 books, that's 101 queries!\n\nAnd that's just a really basic query.\n\nThese mistakes happen all the time, and they're not always easy to catch in\nunit tests.\n\nThat's what the clear-but-unimaginatively-named\n[django-assert-queries](https://pypi.org/project/django-assert-queries) is\nhere to solve. With proper use, this can save companies from costly mistakes.\n[We've](https://www.reviewboard.org) found it to be an invaluable tool in our\narsenal.\n\nWe'll explore how it does that, but first, let's get things installed.\n\n\n## Installation and Usage\n\n```shell\n$ pip install django-assert-queries\n```\n\n``django-assert-queries`` follows [semantic versioning](https://semver.org/),\nmeaning no surprises when you upgrade.\n\n[Documentation](https://django-assert-queries.readthedocs.io) is available,\ncovering the whole codebase.\n\n\n## Let's see it in action\n\nWe're going to catch the bug above.\n\n```python\nfrom django_assert_queries import assert_queries\n\n\ndef test():\n expected_queries = [\n {\n 'model': Book,\n },\n ]\n\n with assert_queries(expected_queries):\n for book in Book.objects.all():\n print(f'Book {book.name} by {book.author.name}')\n```\n\nWhen we run that, we get:\n\n```\nE AssertionError: Expected 1 queries, but got 101\nE\nE 100 queries failed to meet expectations.\nE\nE Query 2:\nE model: <class 'django_assert_queries.tests.models.Author'> != None\nE tables: {'tests_author'} != {}\nE where: Q(id=1) != Q()\nE SQL: SELECT \"tests_author\".\"id\", \"tests_author\".\"name\" FROM \"tests_author\" WHERE \"tests_author\".\"id\" = 1 LIMIT 21\nE\nE Query 3:\nE model: <class 'django_assert_queries.tests.models.Author'> != None\nE tables: {'tests_author'} != {}\nE where: Q(id=2) != Q()\nE SQL: SELECT \"tests_author\".\"id\", \"tests_author\".\"name\" FROM \"tests_author\" WHERE \"tests_author\".\"id\" = 2 LIMIT 21\nE\nE Query 4:\nE model: <class 'django_assert_queries.tests.models.Author'> != None\nE tables: {'tests_author'} != {}\nE where: Q(id=3) != Q()\nE SQL: SELECT \"tests_author\".\"id\", \"tests_author\".\"name\" FROM \"tests_author\" WHERE \"tests_author\".\"id\" = 3 LIMIT 21\nE\n\n[...]\n```\n\nThat problem just became a lot more clear. Let's fix this.\n\n\n```python\nfrom django_assert_queries import assert_queries\n\n\ndef test():\n # We'll select-related the authors.\n expected_queries = [\n {\n 'model': Book,\n 'select_related' ('author',),\n },\n ]\n\n with assert_queries(expected_queries):\n for book in Book.objects.select_related('author'):\n print(f'Book {book.name} by {book.author.name}')\n```\n\nThese can be a lot more thorough:\n\n```python\ndef test_complex_query():\n expected_queries = [\n # Initial query for the books.\n {\n 'model': Book,\n 'limit': 2,\n 'only_fields': {'author', 'name'},\n 'select_related': {'author'},\n },\n\n # Initial query for the authors.\n {\n 'model': Author,\n 'annotations': {\n 'book_count': Count('books'),\n },\n 'group_by': True,\n 'num_joins': 1,\n 'tables': {\n 'tests_author',\n 'tests_book',\n },\n },\n\n # The prefetch-related for all the authors' books.\n {\n 'model': Book,\n 'where': Q(author__in=list(Author.objects.all())),\n },\n ]\n\n books_queryset = (\n Book.objects\n .filter(name__in=['Book 1', 'Book 9'])\n .only('author', 'name')\n .select_related('author')\n [:2]\n )\n\n authors_queryset = (\n Author.objects\n .annotate(book_count=Count('books'))\n .prefetch_related('books')\n )\n\n with assert_queries(expected_queries):\n for book in books_queryset:\n print(f'Book {book.name} by {book.author.name}')\n\n for author in authors_queryset:\n print(f'Author {author.name} published {author.book_count} books:')\n\n for book in author.books.all():\n print(f' {book.name}')\n```\n\nHere we've got filtering, annotations, field limiting, filtering, joins,\nand prefetch-related.\n\nThese cover just about everything that Django queries can do, and when used\ncorrectly your unit tests can account for just about every query your\napplication makes.\n\n\n## Brought to you by Beanbag\n\nAt [Beanbag](https://www.beanbaginc.com), we're all about building better\nsoftware development tools.\n\nOur flagship product is [Review Board](https://www.reviewboard.org), one of\nthe first-ever code review products on the market, and originator for most\nnow-standard code review features.\n\nWe also build these lovely Python packages:\n\n* [beanbag-docutils](https://github.com/beanbaginc/beanbag-docutils/) -\n Multi-DPI images, enhanced syntax, and many more add-ons for Sphinx\n documentation writers.\n\n* [Djblets](https://github.com/djblets/djblets/) -\n Our pack of Django utilities for datagrids, API, privacy, extensions, and\n more. Used by Review Board.\n\n* [Grumble](https://github.com/beanbaginc/grumble/) -\n For Python print debuggers drowning in print statements.\n\n* [Housekeeping](https://github.com/beanbaginc/housekeeping/) -\n Deprecation management for Python codebases of all sizes.\n\n* [kgb](https://github.com/beanbaginc/kgb/) -\n Function spies for Python unit tests, a major upgrade from mocks.\n\n* [registries](https://github.com/beanbaginc/registries/) -\n Registration management and lookup of objects, for extensible Python\n applications.\n\n* [typelets](https://github.com/beanbaginc/typelets/) -\n Python typing additions, including comprehensive JSON and JSON-compatible\n data structures, symbols, and Django add-ons.\n\nYou can see more on [github.com/beanbaginc](https://github.com/beanbaginc) and\n[github.com/reviewboard](https://github.com/reviewboard).\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Assert and instrument executed database queries using Django.",
"version": "1.0.1",
"project_urls": {
"Documentation": "https://github.com/beanbaginc/django-assert-queries",
"Homepage": "https://github.com/beanbaginc/django-assert-queries",
"Repository": "https://github.com/beanbaginc/django-assert-queries"
},
"split_keywords": [
"database",
" django",
" pytest",
" sql",
" unit tests"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "28c5628880b9b222b6116634037b1d75ef2b13d630d97f1efc3134cfa3e7fa28",
"md5": "b9d4db86454720713b37ce333340603d",
"sha256": "da653570342515c02a8a5acbb6426b5b7a8325130a287cb4b1dcde73f2ddd5b6"
},
"downloads": -1,
"filename": "django_assert_queries-1.0.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "b9d4db86454720713b37ce333340603d",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 29033,
"upload_time": "2024-09-29T04:24:07",
"upload_time_iso_8601": "2024-09-29T04:24:07.548987Z",
"url": "https://files.pythonhosted.org/packages/28/c5/628880b9b222b6116634037b1d75ef2b13d630d97f1efc3134cfa3e7fa28/django_assert_queries-1.0.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "978218b3772daec45f073d63ef35b5a1e459540ac54c2b092fa8e9414271203d",
"md5": "caa5535ab984892da4e838e4f7d613e3",
"sha256": "62d1887e612ccbac51df4749a5f0570f40d503328fcc8020168a29d63afe5be3"
},
"downloads": -1,
"filename": "django_assert_queries-1.0.1.tar.gz",
"has_sig": false,
"md5_digest": "caa5535ab984892da4e838e4f7d613e3",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 29771,
"upload_time": "2024-09-29T04:24:09",
"upload_time_iso_8601": "2024-09-29T04:24:09.166183Z",
"url": "https://files.pythonhosted.org/packages/97/82/18b3772daec45f073d63ef35b5a1e459540ac54c2b092fa8e9414271203d/django_assert_queries-1.0.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-09-29 04:24:09",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "beanbaginc",
"github_project": "django-assert-queries",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"tox": true,
"lcname": "django-assert-queries"
}