django-postpone-index 0.0.2__tar.gz → 0.0.4__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_postpone_index-0.0.2/django_postpone_index.egg-info → django_postpone_index-0.0.4}/PKG-INFO +39 -1
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/README.md +38 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/base_tests.py +28 -5
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/settings.py +2 -6
- django_postpone_index-0.0.4/dev/test_explicit_constraint/tests.py +54 -0
- {django_postpone_index-0.0.2/dev/test_gis_index → django_postpone_index-0.0.4/dev/test_explicit_index}/tests.py +0 -1
- django_postpone_index-0.0.4/dev/test_fields/tests.py +43 -0
- django_postpone_index-0.0.4/dev/test_fields_rename/migrations/0001_initial.py +72 -0
- django_postpone_index-0.0.4/dev/test_fields_rename/migrations/0002_auto_20260204_0917.py +87 -0
- django_postpone_index-0.0.4/dev/test_fields_rename/models.py +81 -0
- {django_postpone_index-0.0.2/dev/test_fields → django_postpone_index-0.0.4/dev/test_fields_rename}/tests.py +0 -1
- {django_postpone_index-0.0.2/dev/test_explicit_constraint → django_postpone_index-0.0.4/dev/test_gis_index}/tests.py +0 -1
- {django_postpone_index-0.0.2/dev/test_explicit_index → django_postpone_index-0.0.4/dev/test_index_together}/tests.py +0 -1
- django_postpone_index-0.0.4/dev/test_model_rename/migrations/0001_initial.py +72 -0
- django_postpone_index-0.0.4/dev/test_model_rename/migrations/0002_auto_20260204_1037.py +38 -0
- django_postpone_index-0.0.4/dev/test_model_rename/models.py +60 -0
- django_postpone_index-0.0.4/dev/test_model_rename/tests.py +9 -0
- django_postpone_index-0.0.4/dev/test_unique_together/tests.py +72 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4/django_postpone_index.egg-info}/PKG-INFO +39 -1
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/django_postpone_index.egg-info/SOURCES.txt +12 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/_version.py +3 -3
- django_postpone_index-0.0.4/postpone_index/contrib/postgis/__init__.py +0 -0
- django_postpone_index-0.0.4/postpone_index/contrib/postgres/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/contrib/postgres/schema.py +33 -12
- django_postpone_index-0.0.4/postpone_index/management/__init__.py +0 -0
- django_postpone_index-0.0.4/postpone_index/management/commands/__init__.py +0 -0
- django_postpone_index-0.0.4/postpone_index/models.py +130 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/utils.py +23 -5
- django_postpone_index-0.0.2/dev/test_index_together/tests.py +0 -10
- django_postpone_index-0.0.2/dev/test_unique_together/tests.py +0 -10
- django_postpone_index-0.0.2/postpone_index/models.py +0 -61
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/.github/workflows/ci.yml +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/.gitignore +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/urls.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/utils.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/wsgi.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/manage.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/migrations/0001_initial.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/migrations/0002_auto_20260130_1044.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/migrations/0003_auto_20260130_1052.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/migrations/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/models.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/migrations/0001_initial.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/migrations/0002_auto_20260130_1044.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/migrations/0003_auto_20260130_1052.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/migrations/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/models.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/migrations/0001_initial.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/migrations/0002_auto_20260130_1258.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/migrations/0003_auto_20260130_1259.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/migrations/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/models.py +0 -0
- {django_postpone_index-0.0.2/dev/test_gis_index → django_postpone_index-0.0.4/dev/test_fields_rename}/__init__.py +0 -0
- {django_postpone_index-0.0.2/dev/test_gis_index → django_postpone_index-0.0.4/dev/test_fields_rename}/migrations/__init__.py +0 -0
- {django_postpone_index-0.0.2/dev/test_ignore_migration → django_postpone_index-0.0.4/dev/test_gis_index}/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_gis_index/migrations/0001_initial.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_gis_index/migrations/0002_gisindex1_mline.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_gis_index/migrations/0003_auto_20260202_1156.py +0 -0
- {django_postpone_index-0.0.2/dev/test_ignore_migration → django_postpone_index-0.0.4/dev/test_gis_index}/migrations/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_gis_index/models.py +0 -0
- {django_postpone_index-0.0.2/dev/test_index_together → django_postpone_index-0.0.4/dev/test_ignore_migration}/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/migrations/0001_initial.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/migrations/0002_auto_20260130_1044.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/migrations/0003_auto_20260130_1052.py +0 -0
- {django_postpone_index-0.0.2/dev/test_index_together → django_postpone_index-0.0.4/dev/test_ignore_migration}/migrations/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/models.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/tests.py +0 -0
- {django_postpone_index-0.0.2/dev/test_unique_together → django_postpone_index-0.0.4/dev/test_index_together}/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_index_together/migrations/0001_initial.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_index_together/migrations/0002_auto_20260130_1044.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_index_together/migrations/0003_auto_20260130_1053.py +0 -0
- {django_postpone_index-0.0.2/dev/test_unique_together → django_postpone_index-0.0.4/dev/test_index_together}/migrations/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_index_together/models.py +0 -0
- {django_postpone_index-0.0.2/postpone_index/contrib → django_postpone_index-0.0.4/dev/test_model_rename}/__init__.py +0 -0
- {django_postpone_index-0.0.2/postpone_index/contrib/postgis → django_postpone_index-0.0.4/dev/test_model_rename/migrations}/__init__.py +0 -0
- {django_postpone_index-0.0.2/postpone_index/contrib/postgres → django_postpone_index-0.0.4/dev/test_unique_together}/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_unique_together/migrations/0001_initial.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_unique_together/migrations/0002_auto_20260130_1044.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_unique_together/migrations/0003_auto_20260130_1053.py +0 -0
- {django_postpone_index-0.0.2/postpone_index/management → django_postpone_index-0.0.4/dev/test_unique_together/migrations}/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_unique_together/models.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/django_postpone_index.egg-info/dependency_links.txt +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/django_postpone_index.egg-info/requires.txt +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/django_postpone_index.egg-info/top_level.txt +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/admin.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/apps.py +0 -0
- {django_postpone_index-0.0.2/postpone_index/management/commands → django_postpone_index-0.0.4/postpone_index/contrib}/__init__.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/contrib/postgis/base.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/contrib/postgis/schema.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/contrib/postgres/base.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/management/commands/apply_postponed.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/migration_utils.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/sql/start.sql +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/testing_utils.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/pyproject.toml +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/setup.cfg +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/setup.py +0 -0
- {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/tox.ini +0 -0
{django_postpone_index-0.0.2/django_postpone_index.egg-info → django_postpone_index-0.0.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-postpone-index
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Postpone index creation to provide Zero Downtime Migration feature
|
|
5
5
|
Home-page: https://github.com/nnseva/django-postpone-index
|
|
6
6
|
Author: Vsevolod Novikov
|
|
@@ -105,6 +105,7 @@ The following complex use cases are processed by the package.
|
|
|
105
105
|
- Back Migration. The both, forward and backward migrations are processed.
|
|
106
106
|
- Implicit index drop while removing the table. The Django doesn't issue a separate SQL to drop indexes of the dropped table.
|
|
107
107
|
- Implicit index drop while removing the field. The Django doesn't issue a separate SQL to drop indexes related to the dropped column.
|
|
108
|
+
- Rename field (column) name or model (table) name
|
|
108
109
|
|
|
109
110
|
## Using
|
|
110
111
|
|
|
@@ -169,6 +170,43 @@ calling the `apply_postponed` migration command again. All not-applied indexes w
|
|
|
169
170
|
**NOTICE** the `apply_postponed` management command doesn't have any explicit locking mechanics. Avoid starting this
|
|
170
171
|
command concurrently with itself or another `migrate` command on the same database.
|
|
171
172
|
|
|
173
|
+
## Intermediate migration state
|
|
174
|
+
|
|
175
|
+
Apart from standard Django migrations, using the `postpone_index` package leads to the *intermediate migration state*
|
|
176
|
+
after the `migrate` management command finished:
|
|
177
|
+
|
|
178
|
+
- new model structure is applied
|
|
179
|
+
- indexes to be deleted are deleted
|
|
180
|
+
- indexes to be created are *not created yet*
|
|
181
|
+
|
|
182
|
+
You should be aware that if you introduce a new unique index or constraint, the database does not control uniqueness
|
|
183
|
+
based on not yet created indexes at this time.
|
|
184
|
+
|
|
185
|
+
Your code works now as expected everywhere, except the code which is based on new unique constraints introduced in applied migrations.
|
|
186
|
+
|
|
187
|
+
Apply the `apply_postponed run` management command to make these new indexes work.
|
|
188
|
+
|
|
189
|
+
Any error while `apply_postponed run` execution is stored in the `PostponedSQL` model instance.
|
|
190
|
+
|
|
191
|
+
You can see erroneous lines using `apply_postponed list` command. See the `[E]` mark at the start of the line.
|
|
192
|
+
|
|
193
|
+
You also can see the error details using the format parameter of the `apply_postponed list -f '... %(error)s'` management command.
|
|
194
|
+
|
|
195
|
+
The `apply_postponed run -x` breaks execution on any error. You can see the error in the standard error or logging streams.
|
|
196
|
+
|
|
197
|
+
The `apply_postponed run` (without `-x` parameter) doesn't stop on error, but outputs warning to the log stream instead.
|
|
198
|
+
|
|
199
|
+
When the error happened, it most probably is caused by the non-unique records. Fix the data and try to execute
|
|
200
|
+
`apply_postponed run` again to create an index.
|
|
201
|
+
|
|
202
|
+
After the successfull `apply_postponed run` execution, the migration state is finalised to be equal as if you applied the migration
|
|
203
|
+
without `postpone_index` package at all.
|
|
204
|
+
|
|
205
|
+
The `apply_postponed run` management command marks all successfully executed `PostponedSQL` instances as `done`. You can see `[X]` mark
|
|
206
|
+
at the start of the line produced by `apply_ponsponed list` management command.
|
|
207
|
+
|
|
208
|
+
You can cleanup `done` instances using `apply_postponed cleanup` management command. This step is optional.
|
|
209
|
+
|
|
172
210
|
## Django testing
|
|
173
211
|
|
|
174
212
|
Django migrates testing database before tests. Always use `POSTPONE_INDEX_IGNORE = True` settings to avoid postpone index
|
|
@@ -56,6 +56,7 @@ The following complex use cases are processed by the package.
|
|
|
56
56
|
- Back Migration. The both, forward and backward migrations are processed.
|
|
57
57
|
- Implicit index drop while removing the table. The Django doesn't issue a separate SQL to drop indexes of the dropped table.
|
|
58
58
|
- Implicit index drop while removing the field. The Django doesn't issue a separate SQL to drop indexes related to the dropped column.
|
|
59
|
+
- Rename field (column) name or model (table) name
|
|
59
60
|
|
|
60
61
|
## Using
|
|
61
62
|
|
|
@@ -120,6 +121,43 @@ calling the `apply_postponed` migration command again. All not-applied indexes w
|
|
|
120
121
|
**NOTICE** the `apply_postponed` management command doesn't have any explicit locking mechanics. Avoid starting this
|
|
121
122
|
command concurrently with itself or another `migrate` command on the same database.
|
|
122
123
|
|
|
124
|
+
## Intermediate migration state
|
|
125
|
+
|
|
126
|
+
Apart from standard Django migrations, using the `postpone_index` package leads to the *intermediate migration state*
|
|
127
|
+
after the `migrate` management command finished:
|
|
128
|
+
|
|
129
|
+
- new model structure is applied
|
|
130
|
+
- indexes to be deleted are deleted
|
|
131
|
+
- indexes to be created are *not created yet*
|
|
132
|
+
|
|
133
|
+
You should be aware that if you introduce a new unique index or constraint, the database does not control uniqueness
|
|
134
|
+
based on not yet created indexes at this time.
|
|
135
|
+
|
|
136
|
+
Your code works now as expected everywhere, except the code which is based on new unique constraints introduced in applied migrations.
|
|
137
|
+
|
|
138
|
+
Apply the `apply_postponed run` management command to make these new indexes work.
|
|
139
|
+
|
|
140
|
+
Any error while `apply_postponed run` execution is stored in the `PostponedSQL` model instance.
|
|
141
|
+
|
|
142
|
+
You can see erroneous lines using `apply_postponed list` command. See the `[E]` mark at the start of the line.
|
|
143
|
+
|
|
144
|
+
You also can see the error details using the format parameter of the `apply_postponed list -f '... %(error)s'` management command.
|
|
145
|
+
|
|
146
|
+
The `apply_postponed run -x` breaks execution on any error. You can see the error in the standard error or logging streams.
|
|
147
|
+
|
|
148
|
+
The `apply_postponed run` (without `-x` parameter) doesn't stop on error, but outputs warning to the log stream instead.
|
|
149
|
+
|
|
150
|
+
When the error happened, it most probably is caused by the non-unique records. Fix the data and try to execute
|
|
151
|
+
`apply_postponed run` again to create an index.
|
|
152
|
+
|
|
153
|
+
After the successfull `apply_postponed run` execution, the migration state is finalised to be equal as if you applied the migration
|
|
154
|
+
without `postpone_index` package at all.
|
|
155
|
+
|
|
156
|
+
The `apply_postponed run` management command marks all successfully executed `PostponedSQL` instances as `done`. You can see `[X]` mark
|
|
157
|
+
at the start of the line produced by `apply_ponsponed list` management command.
|
|
158
|
+
|
|
159
|
+
You can cleanup `done` instances using `apply_postponed cleanup` management command. This step is optional.
|
|
160
|
+
|
|
123
161
|
## Django testing
|
|
124
162
|
|
|
125
163
|
Django migrates testing database before tests. Always use `POSTPONE_INDEX_IGNORE = True` settings to avoid postpone index
|
|
@@ -2,19 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
from django.apps import apps
|
|
4
4
|
from django.core.management import call_command
|
|
5
|
-
from django.db import DEFAULT_DB_ALIAS, connections
|
|
5
|
+
from django.db import DEFAULT_DB_ALIAS, connection, connections
|
|
6
|
+
from django.db.migrations.loader import MigrationLoader
|
|
6
7
|
from django.test import override_settings
|
|
7
8
|
|
|
8
9
|
from postpone_index import testing_utils
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
def _list_migrations(app_name):
|
|
13
|
+
"""Lists all module migrations"""
|
|
14
|
+
|
|
15
|
+
loader = MigrationLoader(connection, ignore_no_migrations=True)
|
|
16
|
+
graph = loader.graph
|
|
17
|
+
ret = []
|
|
18
|
+
for node in graph.leaf_nodes(app_name):
|
|
19
|
+
for plan_node in graph.forwards_plan(node):
|
|
20
|
+
if plan_node[0] == app_name:
|
|
21
|
+
ret.append(plan_node[1])
|
|
22
|
+
return ret
|
|
23
|
+
|
|
24
|
+
|
|
11
25
|
class TestCase(testing_utils.TestCase):
|
|
12
26
|
__doc__ = __doc__
|
|
13
27
|
|
|
14
28
|
maxDiff = None
|
|
15
29
|
|
|
16
30
|
module_name = None # Replace in child to the name of the module to test
|
|
17
|
-
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def migrations(self):
|
|
34
|
+
"""All module migrations"""
|
|
35
|
+
return _list_migrations(self.module_name)
|
|
18
36
|
|
|
19
37
|
@classmethod
|
|
20
38
|
def setUpClass(cls):
|
|
@@ -71,14 +89,18 @@ class TestCase(testing_utils.TestCase):
|
|
|
71
89
|
def test_002_migrate_equaliry(self):
|
|
72
90
|
"""Test and compare migrations with and without postpone index using introspection"""
|
|
73
91
|
|
|
92
|
+
# Baseline: default settings (tests run with POSTPONE_INDEX_IGNORE=True).
|
|
93
|
+
call_command('migrate', self.module_name, 'zero')
|
|
94
|
+
|
|
95
|
+
migration_before = 'zero'
|
|
96
|
+
|
|
74
97
|
for migration_id in self.migrations:
|
|
75
|
-
#
|
|
76
|
-
call_command('migrate', self.module_name, 'zero')
|
|
98
|
+
# Store introspection
|
|
77
99
|
call_command('migrate', self.module_name, migration_id)
|
|
78
100
|
baseline = self.introspect_app_schema(self.module_name)
|
|
101
|
+
call_command('migrate', self.module_name, migration_before)
|
|
79
102
|
|
|
80
103
|
# With postpone_index enabled.
|
|
81
|
-
call_command('migrate', self.module_name, 'zero')
|
|
82
104
|
with override_settings(POSTPONE_INDEX_IGNORE=False):
|
|
83
105
|
call_command('migrate', self.module_name, migration_id)
|
|
84
106
|
call_command('apply_postponed', 'run', '-x')
|
|
@@ -93,6 +115,7 @@ class TestCase(testing_utils.TestCase):
|
|
|
93
115
|
migration_id,
|
|
94
116
|
),
|
|
95
117
|
)
|
|
118
|
+
migration_before = migration_id
|
|
96
119
|
|
|
97
120
|
@staticmethod
|
|
98
121
|
def _field_info_to_dict(field_info):
|
|
@@ -38,6 +38,8 @@ INSTALLED_APPS = [
|
|
|
38
38
|
'config',
|
|
39
39
|
'test_unique_together',
|
|
40
40
|
'test_fields',
|
|
41
|
+
'test_fields_rename',
|
|
42
|
+
'test_model_rename',
|
|
41
43
|
'test_explicit_constraint',
|
|
42
44
|
'test_explicit_index',
|
|
43
45
|
'test_ignore_migration',
|
|
@@ -124,11 +126,6 @@ LOGGING = {
|
|
|
124
126
|
}
|
|
125
127
|
},
|
|
126
128
|
'loggers': {
|
|
127
|
-
'postpone_index.contrib.postgres.schema': {
|
|
128
|
-
'handlers': ['console'],
|
|
129
|
-
'level': os.environ.get('POSTPONE_INDEX_LOGGING', 'INFO'),
|
|
130
|
-
'propagate': False,
|
|
131
|
-
},
|
|
132
129
|
'postpone_index': {
|
|
133
130
|
'handlers': ['console'],
|
|
134
131
|
'level': os.environ.get('POSTPONE_INDEX_LOGGING', 'INFO'),
|
|
@@ -139,7 +136,6 @@ LOGGING = {
|
|
|
139
136
|
'level': os.environ.get('CONFIG_LOGGING', 'INFO'),
|
|
140
137
|
'propagate': False,
|
|
141
138
|
},
|
|
142
|
-
# Again, default Django configuration to email unhandled exceptions
|
|
143
139
|
'django.db': {
|
|
144
140
|
'handlers': ['console'],
|
|
145
141
|
'level': os.environ.get('DATABASE_LOGGING', 'INFO'),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Module Tests"""
|
|
2
|
+
|
|
3
|
+
from config import base_tests
|
|
4
|
+
|
|
5
|
+
from django.core.management import call_command
|
|
6
|
+
from django.db import IntegrityError
|
|
7
|
+
from django.test import override_settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModuleTest(base_tests.TestCase):
|
|
11
|
+
__doc__ = __doc__
|
|
12
|
+
|
|
13
|
+
module_name = __name__.split('.')[0]
|
|
14
|
+
|
|
15
|
+
def test_004_migrate_with_bad_unique_data(self):
|
|
16
|
+
"""Test migrations step by step"""
|
|
17
|
+
with override_settings(
|
|
18
|
+
POSTPONE_INDEX_IGNORE=True
|
|
19
|
+
):
|
|
20
|
+
call_command('migrate', self.module_name, 'zero')
|
|
21
|
+
with override_settings(
|
|
22
|
+
POSTPONE_INDEX_IGNORE=False
|
|
23
|
+
):
|
|
24
|
+
call_command('migrate', self.module_name, '0002')
|
|
25
|
+
|
|
26
|
+
from test_explicit_constraint.models import ExplicitConstraint1
|
|
27
|
+
|
|
28
|
+
# Duplicate field1/field2/field3 - ok without apply_postponed
|
|
29
|
+
ExplicitConstraint1.objects.create(field1='qwerty', field2='uiop', field3='asdfg')
|
|
30
|
+
duplicate = ExplicitConstraint1.objects.create(field1='qwerty', field2='uiop', field3='asdfg')
|
|
31
|
+
|
|
32
|
+
with self.assertRaises(IntegrityError):
|
|
33
|
+
# Generates error on duplicate records
|
|
34
|
+
call_command('apply_postponed', 'run', '-x')
|
|
35
|
+
duplicate.delete() # remove duplicate
|
|
36
|
+
|
|
37
|
+
call_command('apply_postponed', 'run', '-x') # Now it should be OK
|
|
38
|
+
call_command('apply_postponed', 'cleanup')
|
|
39
|
+
self._assert_postponed_sql_empty()
|
|
40
|
+
|
|
41
|
+
call_command('migrate', self.module_name, '0003')
|
|
42
|
+
|
|
43
|
+
# Duplicate field1/field2/field3 - ok
|
|
44
|
+
# after the migration deleted old unique and not yet created a new one
|
|
45
|
+
duplicate = ExplicitConstraint1.objects.create(field1='qwerty', field2='uiop', field3='asdfg')
|
|
46
|
+
|
|
47
|
+
with self.assertRaises(IntegrityError):
|
|
48
|
+
# Generates error on duplicate records
|
|
49
|
+
call_command('apply_postponed', 'run', '-x')
|
|
50
|
+
duplicate.delete() # remove duplicate
|
|
51
|
+
|
|
52
|
+
call_command('apply_postponed', 'run', '-x') # Now it should be OK
|
|
53
|
+
call_command('apply_postponed', 'cleanup')
|
|
54
|
+
self._assert_postponed_sql_empty()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Module Tests"""
|
|
2
|
+
|
|
3
|
+
from config import base_tests
|
|
4
|
+
|
|
5
|
+
from django.core.management import call_command
|
|
6
|
+
from django.db import IntegrityError
|
|
7
|
+
from django.test import override_settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModuleTest(base_tests.TestCase):
|
|
11
|
+
__doc__ = __doc__
|
|
12
|
+
|
|
13
|
+
module_name = __name__.split('.')[0]
|
|
14
|
+
|
|
15
|
+
def test_004_migrate_with_bad_unique_data(self):
|
|
16
|
+
"""Test migrations step by step"""
|
|
17
|
+
with override_settings(
|
|
18
|
+
POSTPONE_INDEX_IGNORE=True
|
|
19
|
+
):
|
|
20
|
+
call_command('migrate', self.module_name, 'zero')
|
|
21
|
+
with override_settings(
|
|
22
|
+
POSTPONE_INDEX_IGNORE=False
|
|
23
|
+
):
|
|
24
|
+
call_command('migrate', self.module_name, '0002')
|
|
25
|
+
call_command('apply_postponed', 'run', '-x')
|
|
26
|
+
call_command('apply_postponed', 'cleanup')
|
|
27
|
+
self._assert_postponed_sql_empty()
|
|
28
|
+
|
|
29
|
+
from test_fields.models import UniqueField1
|
|
30
|
+
|
|
31
|
+
# Duplicate field1
|
|
32
|
+
UniqueField1.objects.create(field1='qwerty')
|
|
33
|
+
duplicate = UniqueField1.objects.create(field1='qwerty')
|
|
34
|
+
|
|
35
|
+
call_command('migrate', self.module_name, '0003') # should be OK
|
|
36
|
+
with self.assertRaises(IntegrityError):
|
|
37
|
+
# Generates error on duplicate records
|
|
38
|
+
call_command('apply_postponed', 'run', '-x')
|
|
39
|
+
duplicate.delete() # remove duplicate
|
|
40
|
+
|
|
41
|
+
call_command('apply_postponed', 'run', '-x') # Now it should be OK
|
|
42
|
+
call_command('apply_postponed', 'cleanup')
|
|
43
|
+
self._assert_postponed_sql_empty()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Generated by Django 2.2 on 2026-02-04 09:15
|
|
2
|
+
|
|
3
|
+
import django
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='ExplicitConstraint1',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('field1', models.CharField(max_length=10)),
|
|
20
|
+
('field2', models.CharField(max_length=10)),
|
|
21
|
+
],
|
|
22
|
+
),
|
|
23
|
+
migrations.CreateModel(
|
|
24
|
+
name='ExplicitIndex1',
|
|
25
|
+
fields=[
|
|
26
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
27
|
+
('field1', models.CharField(max_length=10)),
|
|
28
|
+
('field2', models.CharField(max_length=10)),
|
|
29
|
+
],
|
|
30
|
+
),
|
|
31
|
+
migrations.CreateModel(
|
|
32
|
+
name='UniqueField1',
|
|
33
|
+
fields=[
|
|
34
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
35
|
+
('field1', models.CharField(max_length=10, unique=True)),
|
|
36
|
+
],
|
|
37
|
+
),
|
|
38
|
+
migrations.CreateModel(
|
|
39
|
+
name='UniqueTogether1',
|
|
40
|
+
fields=[
|
|
41
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
42
|
+
('field1', models.CharField(max_length=10)),
|
|
43
|
+
('field2', models.CharField(max_length=10)),
|
|
44
|
+
],
|
|
45
|
+
options={
|
|
46
|
+
'unique_together': {('field1', 'field2')},
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
migrations.AddIndex(
|
|
50
|
+
model_name='explicitindex1',
|
|
51
|
+
index=models.Index(fields=['field1', 'field2'], name='explicit_rename_index1_index'),
|
|
52
|
+
),
|
|
53
|
+
migrations.AddConstraint(
|
|
54
|
+
model_name='explicitconstraint1',
|
|
55
|
+
constraint=models.UniqueConstraint(condition=models.Q(('field1', '<empty>'), ('field2', '<empty>'), _negated=True), fields=('field1', 'field2'), name='explicit_rename_constraint1_non_empty'),
|
|
56
|
+
),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
if django.VERSION < (5, 1):
|
|
60
|
+
operations += [
|
|
61
|
+
migrations.CreateModel(
|
|
62
|
+
name='IndexTogether1',
|
|
63
|
+
fields=[
|
|
64
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
65
|
+
('field1', models.CharField(max_length=10)),
|
|
66
|
+
('field2', models.CharField(max_length=10)),
|
|
67
|
+
],
|
|
68
|
+
options={
|
|
69
|
+
'index_together': {('field1', 'field2')},
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Generated by Django 2.2 on 2026-02-04 09:17
|
|
2
|
+
|
|
3
|
+
import django
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('test_fields_rename', '0001_initial'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.RemoveConstraint(
|
|
15
|
+
model_name='explicitconstraint1',
|
|
16
|
+
name='explicit_rename_constraint1_non_empty',
|
|
17
|
+
),
|
|
18
|
+
migrations.RemoveIndex(
|
|
19
|
+
model_name='explicitindex1',
|
|
20
|
+
name='explicit_rename_index1_index',
|
|
21
|
+
),
|
|
22
|
+
migrations.RenameField(
|
|
23
|
+
model_name='explicitconstraint1',
|
|
24
|
+
old_name='field1',
|
|
25
|
+
new_name='renamed_field1',
|
|
26
|
+
),
|
|
27
|
+
migrations.RenameField(
|
|
28
|
+
model_name='explicitconstraint1',
|
|
29
|
+
old_name='field2',
|
|
30
|
+
new_name='renamed_field2',
|
|
31
|
+
),
|
|
32
|
+
migrations.RenameField(
|
|
33
|
+
model_name='explicitindex1',
|
|
34
|
+
old_name='field1',
|
|
35
|
+
new_name='renamed_field1',
|
|
36
|
+
),
|
|
37
|
+
migrations.RenameField(
|
|
38
|
+
model_name='explicitindex1',
|
|
39
|
+
old_name='field2',
|
|
40
|
+
new_name='renamed_field2',
|
|
41
|
+
),
|
|
42
|
+
migrations.RenameField(
|
|
43
|
+
model_name='uniquefield1',
|
|
44
|
+
old_name='field1',
|
|
45
|
+
new_name='renamed_field1',
|
|
46
|
+
),
|
|
47
|
+
migrations.RenameField(
|
|
48
|
+
model_name='uniquetogether1',
|
|
49
|
+
old_name='field1',
|
|
50
|
+
new_name='renamed_field1',
|
|
51
|
+
),
|
|
52
|
+
migrations.RenameField(
|
|
53
|
+
model_name='uniquetogether1',
|
|
54
|
+
old_name='field2',
|
|
55
|
+
new_name='renamed_field2',
|
|
56
|
+
),
|
|
57
|
+
migrations.AlterUniqueTogether(
|
|
58
|
+
name='uniquetogether1',
|
|
59
|
+
unique_together={('renamed_field1', 'renamed_field2')},
|
|
60
|
+
),
|
|
61
|
+
migrations.AddIndex(
|
|
62
|
+
model_name='explicitindex1',
|
|
63
|
+
index=models.Index(fields=['renamed_field1', 'renamed_field2'], name='explicit_rename_index1_index'),
|
|
64
|
+
),
|
|
65
|
+
migrations.AddConstraint(
|
|
66
|
+
model_name='explicitconstraint1',
|
|
67
|
+
constraint=models.UniqueConstraint(condition=models.Q(('renamed_field1', '<empty>'), ('renamed_field2', '<empty>'), _negated=True), fields=('renamed_field1', 'renamed_field2'), name='explicit_rename_constraint1_non_empty'),
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
if django.VERSION < (5, 1):
|
|
72
|
+
operations += [
|
|
73
|
+
migrations.RenameField(
|
|
74
|
+
model_name='indextogether1',
|
|
75
|
+
old_name='field1',
|
|
76
|
+
new_name='renamed_field1',
|
|
77
|
+
),
|
|
78
|
+
migrations.RenameField(
|
|
79
|
+
model_name='indextogether1',
|
|
80
|
+
old_name='field2',
|
|
81
|
+
new_name='renamed_field2',
|
|
82
|
+
),
|
|
83
|
+
migrations.AlterIndexTogether(
|
|
84
|
+
name='indextogether1',
|
|
85
|
+
index_together={('renamed_field1', 'renamed_field2')},
|
|
86
|
+
),
|
|
87
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import django
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UniqueField1(models.Model):
|
|
6
|
+
"""Sequential migrations with unique field"""
|
|
7
|
+
|
|
8
|
+
# Changing history
|
|
9
|
+
# field1 = models.CharField(max_length=10, unique=True) # 0001
|
|
10
|
+
renamed_field1 = models.CharField(max_length=10, unique=True) # 0002
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UniqueTogether1(models.Model):
|
|
14
|
+
"""Sequential migrations with unique-together fields"""
|
|
15
|
+
|
|
16
|
+
# Changing history
|
|
17
|
+
# field1 = models.CharField(max_length=10) # 0001
|
|
18
|
+
# field2 = models.CharField(max_length=10) # 0001
|
|
19
|
+
renamed_field1 = models.CharField(max_length=10) # 0002
|
|
20
|
+
renamed_field2 = models.CharField(max_length=10) # 0002
|
|
21
|
+
|
|
22
|
+
class Meta:
|
|
23
|
+
# unique_together = (('field1', 'field2'),)
|
|
24
|
+
unique_together = (('renamed_field1', 'renamed_field2'),)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if django.VERSION < (5, 1):
|
|
28
|
+
class IndexTogether1(models.Model):
|
|
29
|
+
"""Sequential migrations with index-together fields"""
|
|
30
|
+
|
|
31
|
+
# Changing history
|
|
32
|
+
# field1 = models.CharField(max_length=10) # 0001
|
|
33
|
+
# field2 = models.CharField(max_length=10) # 0001
|
|
34
|
+
renamed_field1 = models.CharField(max_length=10) # 0002
|
|
35
|
+
renamed_field2 = models.CharField(max_length=10) # 0002
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
# index_together = (('field1', 'field2'),) # 0001
|
|
39
|
+
index_together = (('renamed_field1', 'renamed_field2'),) # 0002
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ExplicitConstraint1(models.Model):
|
|
43
|
+
"""Sequential migrations with explicit named constraint in Meta"""
|
|
44
|
+
|
|
45
|
+
# Changing history
|
|
46
|
+
# field1 = models.CharField(max_length=10) # 0001
|
|
47
|
+
# field2 = models.CharField(max_length=10) # 0001
|
|
48
|
+
renamed_field1 = models.CharField(max_length=10) # 0002
|
|
49
|
+
renamed_field2 = models.CharField(max_length=10) # 0002
|
|
50
|
+
|
|
51
|
+
class Meta:
|
|
52
|
+
# 0001
|
|
53
|
+
# constraints = [
|
|
54
|
+
# models.UniqueConstraint(
|
|
55
|
+
# fields=['field1', 'field2'],
|
|
56
|
+
# condition=~(models.Q(field1='<empty>') & models.Q(field2='<empty>')),
|
|
57
|
+
# name='explicit_rename_constraint1_non_empty'
|
|
58
|
+
# )
|
|
59
|
+
# ]
|
|
60
|
+
# 0002
|
|
61
|
+
constraints = [
|
|
62
|
+
models.UniqueConstraint(
|
|
63
|
+
fields=['renamed_field1', 'renamed_field2'],
|
|
64
|
+
condition=~(models.Q(renamed_field1='<empty>') & models.Q(renamed_field2='<empty>')),
|
|
65
|
+
name='explicit_rename_constraint1_non_empty'
|
|
66
|
+
)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ExplicitIndex1(models.Model):
|
|
71
|
+
"""Sequential migrations with explicit named index in Meta"""
|
|
72
|
+
|
|
73
|
+
# field1 = models.CharField(max_length=10) # 0001
|
|
74
|
+
# field2 = models.CharField(max_length=10) # 0001
|
|
75
|
+
renamed_field1 = models.CharField(max_length=10) # 0002
|
|
76
|
+
renamed_field2 = models.CharField(max_length=10) # 0002
|
|
77
|
+
|
|
78
|
+
class Meta:
|
|
79
|
+
"""Modified Meta"""
|
|
80
|
+
# indexes = [models.Index(fields=['field1', 'field2'], name='explicit_rename_index1_index')] # 0001
|
|
81
|
+
indexes = [models.Index(fields=['renamed_field1', 'renamed_field2'], name='explicit_rename_index1_index')] # 0002
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Generated by Django 2.2 on 2026-02-04 10:34
|
|
2
|
+
|
|
3
|
+
import django
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='ExplicitConstraint1',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('field1', models.CharField(max_length=10)),
|
|
20
|
+
('field2', models.CharField(max_length=10)),
|
|
21
|
+
],
|
|
22
|
+
),
|
|
23
|
+
migrations.CreateModel(
|
|
24
|
+
name='ExplicitIndex1',
|
|
25
|
+
fields=[
|
|
26
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
27
|
+
('field1', models.CharField(max_length=10)),
|
|
28
|
+
('field2', models.CharField(max_length=10)),
|
|
29
|
+
],
|
|
30
|
+
),
|
|
31
|
+
migrations.CreateModel(
|
|
32
|
+
name='UniqueField1',
|
|
33
|
+
fields=[
|
|
34
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
35
|
+
('field1', models.CharField(max_length=10, unique=True)),
|
|
36
|
+
],
|
|
37
|
+
),
|
|
38
|
+
migrations.CreateModel(
|
|
39
|
+
name='UniqueTogether1',
|
|
40
|
+
fields=[
|
|
41
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
42
|
+
('field1', models.CharField(max_length=10)),
|
|
43
|
+
('field2', models.CharField(max_length=10)),
|
|
44
|
+
],
|
|
45
|
+
options={
|
|
46
|
+
'unique_together': {('field1', 'field2')},
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
migrations.AddIndex(
|
|
50
|
+
model_name='explicitindex1',
|
|
51
|
+
index=models.Index(fields=['field1', 'field2'], name='explicit_model_index1_index'),
|
|
52
|
+
),
|
|
53
|
+
migrations.AddConstraint(
|
|
54
|
+
model_name='explicitconstraint1',
|
|
55
|
+
constraint=models.UniqueConstraint(condition=models.Q(('field1', '<empty>'), ('field2', '<empty>'), _negated=True), fields=('field1', 'field2'), name='explicit_model_constraint1_non_empty'),
|
|
56
|
+
),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
if django.VERSION < (5, 1):
|
|
60
|
+
operations += [
|
|
61
|
+
migrations.CreateModel(
|
|
62
|
+
name='IndexTogether1',
|
|
63
|
+
fields=[
|
|
64
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
65
|
+
('field1', models.CharField(max_length=10)),
|
|
66
|
+
('field2', models.CharField(max_length=10)),
|
|
67
|
+
],
|
|
68
|
+
options={
|
|
69
|
+
'index_together': {('field1', 'field2')},
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
]
|