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.
Files changed (103) hide show
  1. {django_postpone_index-0.0.2/django_postpone_index.egg-info → django_postpone_index-0.0.4}/PKG-INFO +39 -1
  2. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/README.md +38 -0
  3. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/base_tests.py +28 -5
  4. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/settings.py +2 -6
  5. django_postpone_index-0.0.4/dev/test_explicit_constraint/tests.py +54 -0
  6. {django_postpone_index-0.0.2/dev/test_gis_index → django_postpone_index-0.0.4/dev/test_explicit_index}/tests.py +0 -1
  7. django_postpone_index-0.0.4/dev/test_fields/tests.py +43 -0
  8. django_postpone_index-0.0.4/dev/test_fields_rename/migrations/0001_initial.py +72 -0
  9. django_postpone_index-0.0.4/dev/test_fields_rename/migrations/0002_auto_20260204_0917.py +87 -0
  10. django_postpone_index-0.0.4/dev/test_fields_rename/models.py +81 -0
  11. {django_postpone_index-0.0.2/dev/test_fields → django_postpone_index-0.0.4/dev/test_fields_rename}/tests.py +0 -1
  12. {django_postpone_index-0.0.2/dev/test_explicit_constraint → django_postpone_index-0.0.4/dev/test_gis_index}/tests.py +0 -1
  13. {django_postpone_index-0.0.2/dev/test_explicit_index → django_postpone_index-0.0.4/dev/test_index_together}/tests.py +0 -1
  14. django_postpone_index-0.0.4/dev/test_model_rename/migrations/0001_initial.py +72 -0
  15. django_postpone_index-0.0.4/dev/test_model_rename/migrations/0002_auto_20260204_1037.py +38 -0
  16. django_postpone_index-0.0.4/dev/test_model_rename/models.py +60 -0
  17. django_postpone_index-0.0.4/dev/test_model_rename/tests.py +9 -0
  18. django_postpone_index-0.0.4/dev/test_unique_together/tests.py +72 -0
  19. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4/django_postpone_index.egg-info}/PKG-INFO +39 -1
  20. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/django_postpone_index.egg-info/SOURCES.txt +12 -0
  21. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/_version.py +3 -3
  22. django_postpone_index-0.0.4/postpone_index/contrib/postgis/__init__.py +0 -0
  23. django_postpone_index-0.0.4/postpone_index/contrib/postgres/__init__.py +0 -0
  24. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/contrib/postgres/schema.py +33 -12
  25. django_postpone_index-0.0.4/postpone_index/management/__init__.py +0 -0
  26. django_postpone_index-0.0.4/postpone_index/management/commands/__init__.py +0 -0
  27. django_postpone_index-0.0.4/postpone_index/models.py +130 -0
  28. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/utils.py +23 -5
  29. django_postpone_index-0.0.2/dev/test_index_together/tests.py +0 -10
  30. django_postpone_index-0.0.2/dev/test_unique_together/tests.py +0 -10
  31. django_postpone_index-0.0.2/postpone_index/models.py +0 -61
  32. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/.github/workflows/ci.yml +0 -0
  33. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/.gitignore +0 -0
  34. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/__init__.py +0 -0
  35. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/urls.py +0 -0
  36. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/utils.py +0 -0
  37. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/config/wsgi.py +0 -0
  38. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/manage.py +0 -0
  39. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/__init__.py +0 -0
  40. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/migrations/0001_initial.py +0 -0
  41. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/migrations/0002_auto_20260130_1044.py +0 -0
  42. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/migrations/0003_auto_20260130_1052.py +0 -0
  43. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/migrations/__init__.py +0 -0
  44. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_constraint/models.py +0 -0
  45. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/__init__.py +0 -0
  46. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/migrations/0001_initial.py +0 -0
  47. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/migrations/0002_auto_20260130_1044.py +0 -0
  48. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/migrations/0003_auto_20260130_1052.py +0 -0
  49. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/migrations/__init__.py +0 -0
  50. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_explicit_index/models.py +0 -0
  51. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/__init__.py +0 -0
  52. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/migrations/0001_initial.py +0 -0
  53. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/migrations/0002_auto_20260130_1258.py +0 -0
  54. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/migrations/0003_auto_20260130_1259.py +0 -0
  55. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/migrations/__init__.py +0 -0
  56. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_fields/models.py +0 -0
  57. {django_postpone_index-0.0.2/dev/test_gis_index → django_postpone_index-0.0.4/dev/test_fields_rename}/__init__.py +0 -0
  58. {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
  59. {django_postpone_index-0.0.2/dev/test_ignore_migration → django_postpone_index-0.0.4/dev/test_gis_index}/__init__.py +0 -0
  60. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_gis_index/migrations/0001_initial.py +0 -0
  61. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_gis_index/migrations/0002_gisindex1_mline.py +0 -0
  62. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_gis_index/migrations/0003_auto_20260202_1156.py +0 -0
  63. {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
  64. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_gis_index/models.py +0 -0
  65. {django_postpone_index-0.0.2/dev/test_index_together → django_postpone_index-0.0.4/dev/test_ignore_migration}/__init__.py +0 -0
  66. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/migrations/0001_initial.py +0 -0
  67. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/migrations/0002_auto_20260130_1044.py +0 -0
  68. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/migrations/0003_auto_20260130_1052.py +0 -0
  69. {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
  70. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/models.py +0 -0
  71. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_ignore_migration/tests.py +0 -0
  72. {django_postpone_index-0.0.2/dev/test_unique_together → django_postpone_index-0.0.4/dev/test_index_together}/__init__.py +0 -0
  73. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_index_together/migrations/0001_initial.py +0 -0
  74. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_index_together/migrations/0002_auto_20260130_1044.py +0 -0
  75. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_index_together/migrations/0003_auto_20260130_1053.py +0 -0
  76. {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
  77. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_index_together/models.py +0 -0
  78. {django_postpone_index-0.0.2/postpone_index/contrib → django_postpone_index-0.0.4/dev/test_model_rename}/__init__.py +0 -0
  79. {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
  80. {django_postpone_index-0.0.2/postpone_index/contrib/postgres → django_postpone_index-0.0.4/dev/test_unique_together}/__init__.py +0 -0
  81. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_unique_together/migrations/0001_initial.py +0 -0
  82. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_unique_together/migrations/0002_auto_20260130_1044.py +0 -0
  83. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_unique_together/migrations/0003_auto_20260130_1053.py +0 -0
  84. {django_postpone_index-0.0.2/postpone_index/management → django_postpone_index-0.0.4/dev/test_unique_together/migrations}/__init__.py +0 -0
  85. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/dev/test_unique_together/models.py +0 -0
  86. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/django_postpone_index.egg-info/dependency_links.txt +0 -0
  87. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/django_postpone_index.egg-info/requires.txt +0 -0
  88. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/django_postpone_index.egg-info/top_level.txt +0 -0
  89. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/__init__.py +0 -0
  90. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/admin.py +0 -0
  91. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/apps.py +0 -0
  92. {django_postpone_index-0.0.2/postpone_index/management/commands → django_postpone_index-0.0.4/postpone_index/contrib}/__init__.py +0 -0
  93. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/contrib/postgis/base.py +0 -0
  94. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/contrib/postgis/schema.py +0 -0
  95. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/contrib/postgres/base.py +0 -0
  96. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/management/commands/apply_postponed.py +0 -0
  97. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/migration_utils.py +0 -0
  98. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/sql/start.sql +0 -0
  99. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/postpone_index/testing_utils.py +0 -0
  100. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/pyproject.toml +0 -0
  101. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/setup.cfg +0 -0
  102. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/setup.py +0 -0
  103. {django_postpone_index-0.0.2 → django_postpone_index-0.0.4}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-postpone-index
3
- Version: 0.0.2
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
- migrations = None # Replace in child to the list of migrations to test
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
- # Baseline: default settings (tests run with POSTPONE_INDEX_IGNORE=True).
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()
@@ -7,4 +7,3 @@ class ModuleTest(base_tests.TestCase):
7
7
  __doc__ = __doc__
8
8
 
9
9
  module_name = __name__.split('.')[0]
10
- migrations = ('0001', '0002', '0003')
@@ -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
@@ -7,4 +7,3 @@ class ModuleTest(base_tests.TestCase):
7
7
  __doc__ = __doc__
8
8
 
9
9
  module_name = __name__.split('.')[0]
10
- migrations = ('0001', '0002', '0003')
@@ -7,4 +7,3 @@ class ModuleTest(base_tests.TestCase):
7
7
  __doc__ = __doc__
8
8
 
9
9
  module_name = __name__.split('.')[0]
10
- migrations = ('0001', '0002', '0003')
@@ -7,4 +7,3 @@ class ModuleTest(base_tests.TestCase):
7
7
  __doc__ = __doc__
8
8
 
9
9
  module_name = __name__.split('.')[0]
10
- migrations = ('0001', '0002', '0003')
@@ -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
+ ]