django-postgres-denormalize 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_postgres_denormalize-1.0.0/CHANGELOG.rst +9 -0
- django_postgres_denormalize-1.0.0/LICENSE +23 -0
- django_postgres_denormalize-1.0.0/MANIFEST.in +20 -0
- django_postgres_denormalize-1.0.0/PKG-INFO +154 -0
- django_postgres_denormalize-1.0.0/README.rst +107 -0
- django_postgres_denormalize-1.0.0/assets/icon-128.png +0 -0
- django_postgres_denormalize-1.0.0/assets/icon-16.png +0 -0
- django_postgres_denormalize-1.0.0/assets/icon-256.png +0 -0
- django_postgres_denormalize-1.0.0/assets/icon-32.png +0 -0
- django_postgres_denormalize-1.0.0/assets/icon-48.png +0 -0
- django_postgres_denormalize-1.0.0/assets/icon-512.png +0 -0
- django_postgres_denormalize-1.0.0/assets/icon-64.png +0 -0
- django_postgres_denormalize-1.0.0/assets/icon-small.svg +16 -0
- django_postgres_denormalize-1.0.0/assets/icon.svg +16 -0
- django_postgres_denormalize-1.0.0/assets/social-preview.png +0 -0
- django_postgres_denormalize-1.0.0/assets/social-preview.svg +21 -0
- django_postgres_denormalize-1.0.0/docs/Makefile +14 -0
- django_postgres_denormalize-1.0.0/docs/_static/custom.css +96 -0
- django_postgres_denormalize-1.0.0/docs/_templates/layout.html +30 -0
- django_postgres_denormalize-1.0.0/docs/conf.py +96 -0
- django_postgres_denormalize-1.0.0/docs/getting_started/alternatives.rst +125 -0
- django_postgres_denormalize-1.0.0/docs/getting_started/installation.rst +39 -0
- django_postgres_denormalize-1.0.0/docs/getting_started/overview.rst +114 -0
- django_postgres_denormalize-1.0.0/docs/getting_started/when_to_use.rst +87 -0
- django_postgres_denormalize-1.0.0/docs/guides/batch_updates.rst +82 -0
- django_postgres_denormalize-1.0.0/docs/guides/drift.rst +101 -0
- django_postgres_denormalize-1.0.0/docs/guides/exclusions.rst +81 -0
- django_postgres_denormalize-1.0.0/docs/guides/fields.rst +179 -0
- django_postgres_denormalize-1.0.0/docs/guides/indexing.rst +107 -0
- django_postgres_denormalize-1.0.0/docs/guides/migrations.rst +86 -0
- django_postgres_denormalize-1.0.0/docs/guides/triggers.rst +195 -0
- django_postgres_denormalize-1.0.0/docs/index.rst +52 -0
- django_postgres_denormalize-1.0.0/docs/integrations/djanquiltdb.rst +17 -0
- django_postgres_denormalize-1.0.0/docs/reference/api.rst +65 -0
- django_postgres_denormalize-1.0.0/docs/reference/changelog.rst +5 -0
- django_postgres_denormalize-1.0.0/docs/reference/checks.rst +78 -0
- django_postgres_denormalize-1.0.0/docs/reference/settings.rst +122 -0
- django_postgres_denormalize-1.0.0/pyproject.toml +198 -0
- django_postgres_denormalize-1.0.0/setup.cfg +4 -0
- django_postgres_denormalize-1.0.0/src/django_postgres_denormalize.egg-info/PKG-INFO +154 -0
- django_postgres_denormalize-1.0.0/src/django_postgres_denormalize.egg-info/SOURCES.txt +79 -0
- django_postgres_denormalize-1.0.0/src/django_postgres_denormalize.egg-info/dependency_links.txt +1 -0
- django_postgres_denormalize-1.0.0/src/django_postgres_denormalize.egg-info/requires.txt +22 -0
- django_postgres_denormalize-1.0.0/src/django_postgres_denormalize.egg-info/top_level.txt +1 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/__init__.py +36 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/apps.py +18 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/batch.py +130 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/chains.py +121 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/checks.py +381 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/conf.py +170 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/fields.py +716 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/operations.py +139 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/py.typed +0 -0
- django_postgres_denormalize-1.0.0/src/postgres_denormalize/triggers.py +597 -0
- django_postgres_denormalize-1.0.0/tests/catalog/__init__.py +0 -0
- django_postgres_denormalize-1.0.0/tests/catalog/apps.py +5 -0
- django_postgres_denormalize-1.0.0/tests/catalog/exclusions.py +6 -0
- django_postgres_denormalize-1.0.0/tests/catalog/migrations/0001_initial.py +292 -0
- django_postgres_denormalize-1.0.0/tests/catalog/migrations/__init__.py +0 -0
- django_postgres_denormalize-1.0.0/tests/catalog/models.py +59 -0
- django_postgres_denormalize-1.0.0/tests/config/__init__.py +0 -0
- django_postgres_denormalize-1.0.0/tests/config/settings.py +55 -0
- django_postgres_denormalize-1.0.0/tests/isolated/__init__.py +5 -0
- django_postgres_denormalize-1.0.0/tests/isolated/apps.py +5 -0
- django_postgres_denormalize-1.0.0/tests/isolated/test_checks.py +320 -0
- django_postgres_denormalize-1.0.0/tests/isolated/test_fields.py +870 -0
- django_postgres_denormalize-1.0.0/tests/isolated/test_operations.py +30 -0
- django_postgres_denormalize-1.0.0/tests/isolated/test_triggers.py +241 -0
- django_postgres_denormalize-1.0.0/tests/manage.py +10 -0
- django_postgres_denormalize-1.0.0/tests/tenancy/__init__.py +0 -0
- django_postgres_denormalize-1.0.0/tests/tenancy/apps.py +5 -0
- django_postgres_denormalize-1.0.0/tests/tenancy/migrations/0001_initial.py +232 -0
- django_postgres_denormalize-1.0.0/tests/tenancy/migrations/__init__.py +0 -0
- django_postgres_denormalize-1.0.0/tests/tenancy/models.py +52 -0
- django_postgres_denormalize-1.0.0/tests/test_batch.py +225 -0
- django_postgres_denormalize-1.0.0/tests/test_chains.py +55 -0
- django_postgres_denormalize-1.0.0/tests/test_checks.py +233 -0
- django_postgres_denormalize-1.0.0/tests/test_conf.py +221 -0
- django_postgres_denormalize-1.0.0/tests/test_fields.py +337 -0
- django_postgres_denormalize-1.0.0/tests/test_operations.py +215 -0
- django_postgres_denormalize-1.0.0/tests/test_triggers.py +667 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
v1.0.0 (2026-10-18)
|
|
2
|
+
-------------------
|
|
3
|
+
Added:
|
|
4
|
+
* ``postgres_denormalize.fields.DenormalizedForeignKey``, a foreign key that stores a copy of the foreign key at the
|
|
5
|
+
end of a lookup path.
|
|
6
|
+
* ``postgres_denormalize.fields.DenormalizedField``, a column that stores a copy of any other column at the end of a
|
|
7
|
+
lookup path.
|
|
8
|
+
* Backfill tools as a migration operation (``postgres_denormalize.operations.BackfillDenormalized``) and a function
|
|
9
|
+
(``postgres_denormalize.operations.backfill_denormalized``).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2026, DjanQuiltDB Project
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
|
5
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
6
|
+
list of conditions and the following disclaimer.
|
|
7
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
8
|
+
this list of conditions and the following disclaimer in the documentation
|
|
9
|
+
and/or other materials provided with the distribution.
|
|
10
|
+
* Neither the name of the copyright holder nor the names of its
|
|
11
|
+
contributors may be used to endorse or promote products derived from
|
|
12
|
+
this software without specific prior written permission.
|
|
13
|
+
|
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
15
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
16
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
17
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
18
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
19
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
20
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
21
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
23
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# The sdist is what downstream packagers build and test from, so it carries the whole test project and the docs, not
|
|
2
|
+
# just the importable package. Without this, setuptools' defaults ship tests/test_*.py but none of the fixture apps or
|
|
3
|
+
# settings they import, and the suite in the sdist cannot run.
|
|
4
|
+
|
|
5
|
+
include CHANGELOG.rst
|
|
6
|
+
include LICENSE
|
|
7
|
+
include README.rst
|
|
8
|
+
|
|
9
|
+
recursive-include src/postgres_denormalize py.typed
|
|
10
|
+
|
|
11
|
+
graft docs
|
|
12
|
+
graft tests
|
|
13
|
+
|
|
14
|
+
# docs/conf.py points html_logo and html_favicon at ../assets, so a docs build from the sdist fails without these -- and
|
|
15
|
+
# .readthedocs.yaml sets fail_on_warning, which turns a missing logo into a build failure rather than a shrug.
|
|
16
|
+
graft assets
|
|
17
|
+
|
|
18
|
+
prune docs/_build
|
|
19
|
+
global-exclude *.py[cod]
|
|
20
|
+
global-exclude __pycache__
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-postgres-denormalize
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Trigger-maintained copies of ancestor columns for Django on PostgreSQL.
|
|
5
|
+
Author-email: DjanQuiltDB Project <djanquiltdb@portal42.net>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/djanquiltdb/django-postgres-denormalize
|
|
8
|
+
Project-URL: Documentation, https://django-postgres-denormalize.readthedocs.io/
|
|
9
|
+
Project-URL: Source, https://github.com/djanquiltdb/django-postgres-denormalize
|
|
10
|
+
Project-URL: Issues, https://github.com/djanquiltdb/django-postgres-denormalize/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/djanquiltdb/django-postgres-denormalize/blob/master/CHANGELOG.rst
|
|
12
|
+
Keywords: django,database,postgresql,denormalization,triggers
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Framework :: Django
|
|
21
|
+
Classifier: Framework :: Django :: 6.0
|
|
22
|
+
Classifier: Framework :: Django :: 6.1
|
|
23
|
+
Classifier: Topic :: Database
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.14
|
|
26
|
+
Description-Content-Type: text/x-rst
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: django<7.0,>=6.0
|
|
29
|
+
Requires-Dist: django-pgtrigger<5,>=4.17
|
|
30
|
+
Provides-Extra: lint
|
|
31
|
+
Requires-Dist: ruff==0.15.21; extra == "lint"
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: coverage; extra == "test"
|
|
34
|
+
Requires-Dist: psycopg[binary]>=3.0.0; extra == "test"
|
|
35
|
+
Requires-Dist: dj-database-url; extra == "test"
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: sphinx<10,>=8; extra == "docs"
|
|
38
|
+
Requires-Dist: sphinx-rtd-theme<4,>=3.1; extra == "docs"
|
|
39
|
+
Requires-Dist: psycopg[binary]>=3.0.0; extra == "docs"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: django-postgres-denormalize[docs,lint,test]; extra == "dev"
|
|
42
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
43
|
+
Requires-Dist: tox>=4.21; extra == "dev"
|
|
44
|
+
Requires-Dist: build; extra == "dev"
|
|
45
|
+
Requires-Dist: twine; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
.. image:: https://raw.githubusercontent.com/DjanQuiltDB/django-postgres-denormalize/master/assets/icon-128.png
|
|
49
|
+
:alt: django-postgres-denormalize
|
|
50
|
+
:width: 128
|
|
51
|
+
|
|
52
|
+
===========================
|
|
53
|
+
django-postgres-denormalize
|
|
54
|
+
===========================
|
|
55
|
+
|
|
56
|
+
.. image:: https://img.shields.io/pypi/v/django-postgres-denormalize.svg
|
|
57
|
+
:target: https://pypi.org/project/django-postgres-denormalize/
|
|
58
|
+
:alt: PyPI
|
|
59
|
+
|
|
60
|
+
.. image:: https://img.shields.io/pypi/pyversions/django-postgres-denormalize.svg
|
|
61
|
+
:target: https://pypi.org/project/django-postgres-denormalize/
|
|
62
|
+
:alt: Supported Python versions
|
|
63
|
+
|
|
64
|
+
.. image:: https://img.shields.io/pypi/frameworkversions/django/django-postgres-denormalize.svg
|
|
65
|
+
:target: https://pypi.org/project/django-postgres-denormalize/
|
|
66
|
+
:alt: Supported Django versions
|
|
67
|
+
|
|
68
|
+
.. image:: https://img.shields.io/badge/postgres-17%20%7C%2018-4169e1?logo=postgresql&logoColor=white
|
|
69
|
+
:target: https://www.postgresql.org/
|
|
70
|
+
:alt: Supported PostgreSQL versions
|
|
71
|
+
|
|
72
|
+
.. image:: https://github.com/djanquiltdb/django-postgres-denormalize/actions/workflows/ci.yml/badge.svg
|
|
73
|
+
:target: https://github.com/djanquiltdb/django-postgres-denormalize/actions/workflows/ci.yml
|
|
74
|
+
:alt: CI
|
|
75
|
+
|
|
76
|
+
.. image:: https://readthedocs.org/projects/django-postgres-denormalize/badge/?version=latest
|
|
77
|
+
:target: https://django-postgres-denormalize.readthedocs.io/
|
|
78
|
+
:alt: Documentation
|
|
79
|
+
|
|
80
|
+
.. image:: https://img.shields.io/pypi/l/django-postgres-denormalize.svg
|
|
81
|
+
:target: https://github.com/djanquiltdb/django-postgres-denormalize/blob/master/LICENSE
|
|
82
|
+
:alt: BSD-3-Clause licence
|
|
83
|
+
|
|
84
|
+
PostgreSQL cannot index, sort or constrain on a value that is stored in another table. A value that is several joins
|
|
85
|
+
away, such as the tenant that a row belongs to through its parent, cannot be part of an index or a unique constraint on
|
|
86
|
+
the row's table, and every query that filters or orders by it pays for the joins.
|
|
87
|
+
|
|
88
|
+
This package lets a model store a copy of such a value in an ordinary column, and keeps the copy correct with
|
|
89
|
+
PostgreSQL triggers. That works for every write, whether it comes from the ORM, a bulk update, raw SQL or the previous
|
|
90
|
+
release that is still serving during a rolling deploy. The model declares the lookup path and nothing else;
|
|
91
|
+
``makemigrations`` writes the column, the triggers and their changes.
|
|
92
|
+
|
|
93
|
+
Full documentation is at https://django-postgres-denormalize.readthedocs.io/.
|
|
94
|
+
|
|
95
|
+
.. code-block:: python
|
|
96
|
+
|
|
97
|
+
from django.db import models
|
|
98
|
+
from postgres_denormalize.fields import DenormalizedField, DenormalizedForeignKey
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Task(models.Model):
|
|
102
|
+
project = models.ForeignKey('Project', on_delete=models.CASCADE)
|
|
103
|
+
title = models.CharField(max_length=100)
|
|
104
|
+
# A copy of project.tenant: a real foreign key, its target and on_delete rule inferred from the path.
|
|
105
|
+
tenant = DenormalizedForeignKey('project__tenant')
|
|
106
|
+
# A copy of project.name: a column of the same type as the source.
|
|
107
|
+
project_name = DenormalizedField('project__name')
|
|
108
|
+
|
|
109
|
+
class Meta:
|
|
110
|
+
ordering = ['project_name', 'title']
|
|
111
|
+
constraints = [models.UniqueConstraint(fields=['tenant', 'title'], name='unique_task_title_per_tenant')]
|
|
112
|
+
indexes = [models.Index(fields=['tenant', 'project_name', 'title'], name='task_listing')]
|
|
113
|
+
|
|
114
|
+
From then on, saving a task computes its copies in the database and reads them back onto the instance. Moving a
|
|
115
|
+
project to another tenant, or renaming it, rewrites the copies of its tasks in the same transaction, but only the
|
|
116
|
+
copies that change.
|
|
117
|
+
|
|
118
|
+
**Use it for** values that an index or a unique constraint needs, and values that most queries sort or filter by across
|
|
119
|
+
several joins. **Do not use it** to save a join for rendering, because ``select_related`` does that for free. Do not use
|
|
120
|
+
it either for values whose source is often updated in bulk by code that you cannot wrap in one of its batch helpers.
|
|
121
|
+
See `when to use it, and when not to
|
|
122
|
+
<https://django-postgres-denormalize.readthedocs.io/en/latest/getting_started/when_to_use.html>`_, and the
|
|
123
|
+
`alternatives <https://django-postgres-denormalize.readthedocs.io/en/latest/getting_started/alternatives.html>`_ page,
|
|
124
|
+
which compares it with django-pgconstraints and django-computedfields.
|
|
125
|
+
|
|
126
|
+
Installation
|
|
127
|
+
------------
|
|
128
|
+
|
|
129
|
+
.. code-block:: bash
|
|
130
|
+
|
|
131
|
+
pip install django-postgres-denormalize
|
|
132
|
+
|
|
133
|
+
.. code-block:: python
|
|
134
|
+
|
|
135
|
+
INSTALLED_APPS = [
|
|
136
|
+
# ...
|
|
137
|
+
'pgtrigger',
|
|
138
|
+
'postgres_denormalize',
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
Requires PostgreSQL and django-pgtrigger, with pgtrigger's migration support turned on (the default). See the
|
|
142
|
+
`settings reference <https://django-postgres-denormalize.readthedocs.io/en/latest/reference/settings.html>`_ for the
|
|
143
|
+
options.
|
|
144
|
+
|
|
145
|
+
Relationship to DjanQuiltDB
|
|
146
|
+
---------------------------
|
|
147
|
+
|
|
148
|
+
While this library was written in conjunction with `DjanQuiltDB <https://github.com/djanquiltdb/djanquiltdb>`_ and the
|
|
149
|
+
libraries were designed to work together seamlessly in projects where both are installed, both libraries are written to
|
|
150
|
+
be used as standalone libraries as well. For using django-postgres-denormalize, it is not necessary to use or be
|
|
151
|
+
familiar with DjanQuiltDB.
|
|
152
|
+
|
|
153
|
+
If you wish to use DjanQuiltDB features for the copies that django-postgres-denormalize keeps, you can install the
|
|
154
|
+
``djanquiltdb[postgres-denormalize]`` extra. For more information, please refer to the DjanQuiltDB documentation.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
.. image:: https://raw.githubusercontent.com/DjanQuiltDB/django-postgres-denormalize/master/assets/icon-128.png
|
|
2
|
+
:alt: django-postgres-denormalize
|
|
3
|
+
:width: 128
|
|
4
|
+
|
|
5
|
+
===========================
|
|
6
|
+
django-postgres-denormalize
|
|
7
|
+
===========================
|
|
8
|
+
|
|
9
|
+
.. image:: https://img.shields.io/pypi/v/django-postgres-denormalize.svg
|
|
10
|
+
:target: https://pypi.org/project/django-postgres-denormalize/
|
|
11
|
+
:alt: PyPI
|
|
12
|
+
|
|
13
|
+
.. image:: https://img.shields.io/pypi/pyversions/django-postgres-denormalize.svg
|
|
14
|
+
:target: https://pypi.org/project/django-postgres-denormalize/
|
|
15
|
+
:alt: Supported Python versions
|
|
16
|
+
|
|
17
|
+
.. image:: https://img.shields.io/pypi/frameworkversions/django/django-postgres-denormalize.svg
|
|
18
|
+
:target: https://pypi.org/project/django-postgres-denormalize/
|
|
19
|
+
:alt: Supported Django versions
|
|
20
|
+
|
|
21
|
+
.. image:: https://img.shields.io/badge/postgres-17%20%7C%2018-4169e1?logo=postgresql&logoColor=white
|
|
22
|
+
:target: https://www.postgresql.org/
|
|
23
|
+
:alt: Supported PostgreSQL versions
|
|
24
|
+
|
|
25
|
+
.. image:: https://github.com/djanquiltdb/django-postgres-denormalize/actions/workflows/ci.yml/badge.svg
|
|
26
|
+
:target: https://github.com/djanquiltdb/django-postgres-denormalize/actions/workflows/ci.yml
|
|
27
|
+
:alt: CI
|
|
28
|
+
|
|
29
|
+
.. image:: https://readthedocs.org/projects/django-postgres-denormalize/badge/?version=latest
|
|
30
|
+
:target: https://django-postgres-denormalize.readthedocs.io/
|
|
31
|
+
:alt: Documentation
|
|
32
|
+
|
|
33
|
+
.. image:: https://img.shields.io/pypi/l/django-postgres-denormalize.svg
|
|
34
|
+
:target: https://github.com/djanquiltdb/django-postgres-denormalize/blob/master/LICENSE
|
|
35
|
+
:alt: BSD-3-Clause licence
|
|
36
|
+
|
|
37
|
+
PostgreSQL cannot index, sort or constrain on a value that is stored in another table. A value that is several joins
|
|
38
|
+
away, such as the tenant that a row belongs to through its parent, cannot be part of an index or a unique constraint on
|
|
39
|
+
the row's table, and every query that filters or orders by it pays for the joins.
|
|
40
|
+
|
|
41
|
+
This package lets a model store a copy of such a value in an ordinary column, and keeps the copy correct with
|
|
42
|
+
PostgreSQL triggers. That works for every write, whether it comes from the ORM, a bulk update, raw SQL or the previous
|
|
43
|
+
release that is still serving during a rolling deploy. The model declares the lookup path and nothing else;
|
|
44
|
+
``makemigrations`` writes the column, the triggers and their changes.
|
|
45
|
+
|
|
46
|
+
Full documentation is at https://django-postgres-denormalize.readthedocs.io/.
|
|
47
|
+
|
|
48
|
+
.. code-block:: python
|
|
49
|
+
|
|
50
|
+
from django.db import models
|
|
51
|
+
from postgres_denormalize.fields import DenormalizedField, DenormalizedForeignKey
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Task(models.Model):
|
|
55
|
+
project = models.ForeignKey('Project', on_delete=models.CASCADE)
|
|
56
|
+
title = models.CharField(max_length=100)
|
|
57
|
+
# A copy of project.tenant: a real foreign key, its target and on_delete rule inferred from the path.
|
|
58
|
+
tenant = DenormalizedForeignKey('project__tenant')
|
|
59
|
+
# A copy of project.name: a column of the same type as the source.
|
|
60
|
+
project_name = DenormalizedField('project__name')
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
ordering = ['project_name', 'title']
|
|
64
|
+
constraints = [models.UniqueConstraint(fields=['tenant', 'title'], name='unique_task_title_per_tenant')]
|
|
65
|
+
indexes = [models.Index(fields=['tenant', 'project_name', 'title'], name='task_listing')]
|
|
66
|
+
|
|
67
|
+
From then on, saving a task computes its copies in the database and reads them back onto the instance. Moving a
|
|
68
|
+
project to another tenant, or renaming it, rewrites the copies of its tasks in the same transaction, but only the
|
|
69
|
+
copies that change.
|
|
70
|
+
|
|
71
|
+
**Use it for** values that an index or a unique constraint needs, and values that most queries sort or filter by across
|
|
72
|
+
several joins. **Do not use it** to save a join for rendering, because ``select_related`` does that for free. Do not use
|
|
73
|
+
it either for values whose source is often updated in bulk by code that you cannot wrap in one of its batch helpers.
|
|
74
|
+
See `when to use it, and when not to
|
|
75
|
+
<https://django-postgres-denormalize.readthedocs.io/en/latest/getting_started/when_to_use.html>`_, and the
|
|
76
|
+
`alternatives <https://django-postgres-denormalize.readthedocs.io/en/latest/getting_started/alternatives.html>`_ page,
|
|
77
|
+
which compares it with django-pgconstraints and django-computedfields.
|
|
78
|
+
|
|
79
|
+
Installation
|
|
80
|
+
------------
|
|
81
|
+
|
|
82
|
+
.. code-block:: bash
|
|
83
|
+
|
|
84
|
+
pip install django-postgres-denormalize
|
|
85
|
+
|
|
86
|
+
.. code-block:: python
|
|
87
|
+
|
|
88
|
+
INSTALLED_APPS = [
|
|
89
|
+
# ...
|
|
90
|
+
'pgtrigger',
|
|
91
|
+
'postgres_denormalize',
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
Requires PostgreSQL and django-pgtrigger, with pgtrigger's migration support turned on (the default). See the
|
|
95
|
+
`settings reference <https://django-postgres-denormalize.readthedocs.io/en/latest/reference/settings.html>`_ for the
|
|
96
|
+
options.
|
|
97
|
+
|
|
98
|
+
Relationship to DjanQuiltDB
|
|
99
|
+
---------------------------
|
|
100
|
+
|
|
101
|
+
While this library was written in conjunction with `DjanQuiltDB <https://github.com/djanquiltdb/djanquiltdb>`_ and the
|
|
102
|
+
libraries were designed to work together seamlessly in projects where both are installed, both libraries are written to
|
|
103
|
+
be used as standalone libraries as well. For using django-postgres-denormalize, it is not necessary to use or be
|
|
104
|
+
familiar with DjanQuiltDB.
|
|
105
|
+
|
|
106
|
+
If you wish to use DjanQuiltDB features for the copies that django-postgres-denormalize keeps, you can install the
|
|
107
|
+
``djanquiltdb[postgres-denormalize]`` extra. For more information, please refer to the DjanQuiltDB documentation.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<path d="M15,42V36H49V42" fill="none" stroke="#31936D" stroke-width="5"
|
|
3
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
|
|
5
|
+
<rect x="16" y="3" width="32" height="19" rx="5" fill="#44B78B"/>
|
|
6
|
+
<rect x="23" y="10" width="18" height="5" rx="2.5" fill="#FFC757"/>
|
|
7
|
+
|
|
8
|
+
<rect x="1" y="42" width="29" height="19" rx="5" fill="#31936D"/>
|
|
9
|
+
<rect x="7" y="49" width="17" height="5" rx="2.5" fill="#FFC757"/>
|
|
10
|
+
|
|
11
|
+
<rect x="34" y="42" width="29" height="19" rx="5" fill="#1E6F50"/>
|
|
12
|
+
<rect x="40" y="49" width="17" height="5" rx="2.5" fill="#FFC757"/>
|
|
13
|
+
|
|
14
|
+
<path d="M37,21L25,37H32L28,47L41,31H34L37,21Z" fill="#FFC757"
|
|
15
|
+
stroke="#FFC757" stroke-width="2" stroke-linejoin="round"/>
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<path d="M16,43V37Q16,34 19,34H45Q48,34 48,37V43" fill="none" stroke="#31936D" stroke-width="3.5"
|
|
3
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
|
|
5
|
+
<rect x="19" y="5" width="26" height="17" rx="4.5" fill="#44B78B"/>
|
|
6
|
+
<rect x="25.5" y="11.6" width="13" height="3.8" rx="1.9" fill="#FFC757"/>
|
|
7
|
+
|
|
8
|
+
<rect x="3" y="43" width="26" height="16" rx="4.5" fill="#31936D"/>
|
|
9
|
+
<rect x="9.5" y="49.1" width="13" height="3.8" rx="1.9" fill="#FFC757"/>
|
|
10
|
+
|
|
11
|
+
<rect x="35" y="43" width="26" height="16" rx="4.5" fill="#1E6F50"/>
|
|
12
|
+
<rect x="41.5" y="49.1" width="13" height="3.8" rx="1.9" fill="#FFC757"/>
|
|
13
|
+
|
|
14
|
+
<path d="M35.5,22.5L27,34.5H32.5L29.5,42.5L38.5,30.5H33L35.5,22.5Z" fill="#FFC757"
|
|
15
|
+
stroke="#FFC757" stroke-width="1.2" stroke-linejoin="round"/>
|
|
16
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 640" width="1280" height="640">
|
|
2
|
+
<rect width="1280" height="640" fill="#092e20"/>
|
|
3
|
+
<g transform="translate(98 192) scale(4)">
|
|
4
|
+
<path d="M16,43V37Q16,34 19,34H45Q48,34 48,37V43" fill="none" stroke="#31936D" stroke-width="3.5"
|
|
5
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
<rect x="19" y="5" width="26" height="17" rx="4.5" fill="#44B78B"/>
|
|
7
|
+
<rect x="25.5" y="11.6" width="13" height="3.8" rx="1.9" fill="#FFC757"/>
|
|
8
|
+
<rect x="3" y="43" width="26" height="16" rx="4.5" fill="#31936D"/>
|
|
9
|
+
<rect x="9.5" y="49.1" width="13" height="3.8" rx="1.9" fill="#FFC757"/>
|
|
10
|
+
<rect x="35" y="43" width="26" height="16" rx="4.5" fill="#1E6F50"/>
|
|
11
|
+
<rect x="41.5" y="49.1" width="13" height="3.8" rx="1.9" fill="#FFC757"/>
|
|
12
|
+
<path d="M35.5,22.5L27,34.5H32.5L29.5,42.5L38.5,30.5H33L35.5,22.5Z" fill="#FFC757"
|
|
13
|
+
stroke="#FFC757" stroke-width="1.2" stroke-linejoin="round"/>
|
|
14
|
+
</g>
|
|
15
|
+
|
|
16
|
+
<g font-family="Noto Sans, DejaVu Sans, Liberation Sans, sans-serif">
|
|
17
|
+
<text id="title" x="420" y="296" font-size="53" font-weight="700" fill="#ffffff">django-postgres-denormalize</text>
|
|
18
|
+
<rect x="423" y="328" width="170" height="5" fill="#FFC757"/>
|
|
19
|
+
<text id="tagline" x="420" y="385" font-size="31" font-weight="400" fill="#44B78B">Trigger-maintained copies of ancestor columns</text>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Minimal makefile for Sphinx documentation
|
|
2
|
+
|
|
3
|
+
SPHINXOPTS ?= -W
|
|
4
|
+
SPHINXBUILD ?= sphinx-build
|
|
5
|
+
SOURCEDIR = .
|
|
6
|
+
BUILDDIR = _build
|
|
7
|
+
|
|
8
|
+
help:
|
|
9
|
+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
10
|
+
|
|
11
|
+
.PHONY: help Makefile
|
|
12
|
+
|
|
13
|
+
%: Makefile
|
|
14
|
+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--dpd-ink: #092e20;
|
|
3
|
+
--dpd-gold: #FFC757;
|
|
4
|
+
--dpd-mint: #E3F4EE;
|
|
5
|
+
--dpd-mint-soft: #EFF9F5;
|
|
6
|
+
--dpd-mint-deep: #CBEBDF;
|
|
7
|
+
--dpd-mint-line: #ABDFCB;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.wy-side-nav-search > a.wordmark {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
gap: 0.6em;
|
|
15
|
+
padding: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.wy-side-nav-search > a.wordmark img.logo {
|
|
19
|
+
flex: none;
|
|
20
|
+
width: 72px;
|
|
21
|
+
height: 72px;
|
|
22
|
+
margin: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.wy-side-nav-search > a.wordmark .name {
|
|
26
|
+
position: relative;
|
|
27
|
+
padding-bottom: 4px;
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
font-size: 1.3em;
|
|
30
|
+
font-weight: 700;
|
|
31
|
+
line-height: 1.05;
|
|
32
|
+
text-align: left;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.wy-side-nav-search > a.wordmark .name::after {
|
|
36
|
+
content: "";
|
|
37
|
+
position: absolute;
|
|
38
|
+
bottom: 0;
|
|
39
|
+
left: 0;
|
|
40
|
+
width: 23%;
|
|
41
|
+
height: 2px;
|
|
42
|
+
background: var(--dpd-gold);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.wy-nav-side {
|
|
46
|
+
background: var(--dpd-mint);
|
|
47
|
+
color: var(--dpd-ink);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.wy-menu-vertical a {
|
|
51
|
+
color: var(--dpd-ink);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.wy-menu-vertical a:hover {
|
|
55
|
+
background-color: var(--dpd-mint-deep);
|
|
56
|
+
color: var(--dpd-ink);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.wy-menu-vertical header,
|
|
60
|
+
.wy-menu-vertical p.caption {
|
|
61
|
+
color: #1E6F50;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.wy-menu-vertical li.current {
|
|
65
|
+
background: var(--dpd-mint-soft);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.wy-menu-vertical li.current a {
|
|
69
|
+
border-right: 1px solid var(--dpd-mint-line);
|
|
70
|
+
color: var(--dpd-ink);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.wy-menu-vertical li.current a:hover {
|
|
74
|
+
background: var(--dpd-mint-deep);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.wy-menu-vertical li.current > a,
|
|
78
|
+
.wy-menu-vertical li.on a {
|
|
79
|
+
background: #ffffff;
|
|
80
|
+
color: var(--dpd-ink);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.wy-menu-vertical li.current > a:hover,
|
|
84
|
+
.wy-menu-vertical li.on a:hover {
|
|
85
|
+
background: #ffffff;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.wy-menu-vertical li.toctree-l1.current > a {
|
|
89
|
+
border-top: 1px solid var(--dpd-mint-line);
|
|
90
|
+
border-bottom: 1px solid var(--dpd-mint-line);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.wy-menu-vertical li.toctree-l2.current > a,
|
|
94
|
+
.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a {
|
|
95
|
+
background: var(--dpd-mint-deep);
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{% extends "!layout.html" %}
|
|
2
|
+
|
|
3
|
+
{%- block sidebartitle %}
|
|
4
|
+
|
|
5
|
+
{%- set _logo_url = logo_url|default(pathto('_static/' + (logo or ""), 1)) %}
|
|
6
|
+
{%- set _root_doc = root_doc|default(master_doc) %}
|
|
7
|
+
<a href="{{ pathto(_root_doc) }}" class="wordmark">
|
|
8
|
+
{%- if logo or logo_url %}
|
|
9
|
+
<img src="{{ _logo_url }}" class="logo" alt="{{ _('Logo') }}"/>
|
|
10
|
+
{%- endif %}
|
|
11
|
+
<span class="name">django-<br/>postgres-<br/>denormalize</span>
|
|
12
|
+
</a>
|
|
13
|
+
|
|
14
|
+
{%- if READTHEDOCS or DEBUG %}
|
|
15
|
+
{%- if theme_version_selector or theme_language_selector %}
|
|
16
|
+
<div class="switch-menus">
|
|
17
|
+
<div class="version-switch"></div>
|
|
18
|
+
<div class="language-switch"></div>
|
|
19
|
+
</div>
|
|
20
|
+
{%- endif %}
|
|
21
|
+
{%- endif %}
|
|
22
|
+
|
|
23
|
+
{%- include "searchbox.html" %}
|
|
24
|
+
|
|
25
|
+
{%- endblock %}
|
|
26
|
+
|
|
27
|
+
{%- block mobile_nav %}
|
|
28
|
+
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
|
29
|
+
<a href="{{ pathto(master_doc) }}">django-postgres-denormalize</a>
|
|
30
|
+
{%- endblock %}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# django-postgres-denormalize documentation build configuration file.
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.abspath('../src/'))
|
|
10
|
+
|
|
11
|
+
from django.conf import settings # noqa: E402
|
|
12
|
+
|
|
13
|
+
from postgres_denormalize import __version__ # noqa: E402
|
|
14
|
+
|
|
15
|
+
settings.configure()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# -- General configuration ------------------------------------------------
|
|
19
|
+
|
|
20
|
+
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.githubpages', 'sphinx_rtd_theme']
|
|
21
|
+
|
|
22
|
+
source_suffix = '.rst'
|
|
23
|
+
|
|
24
|
+
master_doc = 'index'
|
|
25
|
+
|
|
26
|
+
project = 'django-postgres-denormalize'
|
|
27
|
+
copyright = '{} DjanQuiltDB Project'.format(datetime.date.today().year) # noqa: A001
|
|
28
|
+
author = 'DjanQuiltDB Project'
|
|
29
|
+
|
|
30
|
+
release = version = __version__
|
|
31
|
+
|
|
32
|
+
language = 'en'
|
|
33
|
+
|
|
34
|
+
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
35
|
+
|
|
36
|
+
pygments_style = 'sphinx'
|
|
37
|
+
|
|
38
|
+
todo_include_todos = False
|
|
39
|
+
|
|
40
|
+
# Members are documented in source order, which follows the order a reader meets them in.
|
|
41
|
+
autodoc_member_order = 'bysource'
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# -- Options for HTML output ----------------------------------------------
|
|
45
|
+
|
|
46
|
+
html_theme = 'sphinx_rtd_theme'
|
|
47
|
+
|
|
48
|
+
# The header the sidebar and the narrow-screen bar share, in the dark green the social preview card uses.
|
|
49
|
+
html_theme_options = {'style_nav_header_background': '#092e20'}
|
|
50
|
+
|
|
51
|
+
# docs/_templates/layout.html restyles that header; without this it is not picked up at all.
|
|
52
|
+
templates_path = ['_templates']
|
|
53
|
+
|
|
54
|
+
html_static_path = ['_static']
|
|
55
|
+
|
|
56
|
+
# The favicon is a raster rather than the logo itself: below about 32 pixels the lightning bolt blurs into the branch
|
|
57
|
+
# beneath it, so those sizes are rendered from assets/icon-small.svg, which is drawn for them.
|
|
58
|
+
html_logo = '../assets/icon.svg'
|
|
59
|
+
html_favicon = '../assets/icon-32.png'
|
|
60
|
+
|
|
61
|
+
# Only to style the sidebar header the template above replaces; see the file itself.
|
|
62
|
+
html_css_files = ['custom.css']
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# -- Options for HTMLHelp output ------------------------------------------
|
|
66
|
+
|
|
67
|
+
htmlhelp_basename = project
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# -- Options for LaTeX output ---------------------------------------------
|
|
71
|
+
|
|
72
|
+
latex_elements = {}
|
|
73
|
+
|
|
74
|
+
latex_documents = [
|
|
75
|
+
(master_doc, 'django-postgres-denormalize.tex', '{} Documentation'.format(project), author, 'manual'),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# -- Options for manual page output ---------------------------------------
|
|
80
|
+
|
|
81
|
+
man_pages = [(master_doc, project, '{} Documentation'.format(project), [author], 1)]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# -- Options for Texinfo output -------------------------------------------
|
|
85
|
+
|
|
86
|
+
texinfo_documents = [
|
|
87
|
+
(
|
|
88
|
+
master_doc,
|
|
89
|
+
project,
|
|
90
|
+
'{} Documentation'.format(project),
|
|
91
|
+
author,
|
|
92
|
+
project,
|
|
93
|
+
'Trigger-maintained copies of ancestor columns for Django on PostgreSQL.',
|
|
94
|
+
'Miscellaneous',
|
|
95
|
+
),
|
|
96
|
+
]
|