django-postpone-index 0.0.1__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 (88) hide show
  1. django_postpone_index-0.0.1/.github/workflows/ci.yml +147 -0
  2. django_postpone_index-0.0.1/.gitignore +29 -0
  3. django_postpone_index-0.0.1/PKG-INFO +276 -0
  4. django_postpone_index-0.0.1/README.md +227 -0
  5. django_postpone_index-0.0.1/dev/config/__init__.py +0 -0
  6. django_postpone_index-0.0.1/dev/config/base_tests.py +183 -0
  7. django_postpone_index-0.0.1/dev/config/settings.py +169 -0
  8. django_postpone_index-0.0.1/dev/config/urls.py +9 -0
  9. django_postpone_index-0.0.1/dev/config/utils.py +28 -0
  10. django_postpone_index-0.0.1/dev/config/wsgi.py +14 -0
  11. django_postpone_index-0.0.1/dev/manage.py +24 -0
  12. django_postpone_index-0.0.1/dev/test_explicit_constraint/__init__.py +0 -0
  13. django_postpone_index-0.0.1/dev/test_explicit_constraint/migrations/0001_initial.py +23 -0
  14. django_postpone_index-0.0.1/dev/test_explicit_constraint/migrations/0002_auto_20260130_1044.py +17 -0
  15. django_postpone_index-0.0.1/dev/test_explicit_constraint/migrations/0003_auto_20260130_1052.py +21 -0
  16. django_postpone_index-0.0.1/dev/test_explicit_constraint/migrations/__init__.py +0 -0
  17. django_postpone_index-0.0.1/dev/test_explicit_constraint/models.py +31 -0
  18. django_postpone_index-0.0.1/dev/test_explicit_constraint/tests.py +10 -0
  19. django_postpone_index-0.0.1/dev/test_explicit_index/__init__.py +0 -0
  20. django_postpone_index-0.0.1/dev/test_explicit_index/migrations/0001_initial.py +22 -0
  21. django_postpone_index-0.0.1/dev/test_explicit_index/migrations/0002_auto_20260130_1044.py +17 -0
  22. django_postpone_index-0.0.1/dev/test_explicit_index/migrations/0003_auto_20260130_1052.py +21 -0
  23. django_postpone_index-0.0.1/dev/test_explicit_index/migrations/__init__.py +0 -0
  24. django_postpone_index-0.0.1/dev/test_explicit_index/models.py +14 -0
  25. django_postpone_index-0.0.1/dev/test_explicit_index/tests.py +10 -0
  26. django_postpone_index-0.0.1/dev/test_fields/__init__.py +0 -0
  27. django_postpone_index-0.0.1/dev/test_fields/migrations/0001_initial.py +44 -0
  28. django_postpone_index-0.0.1/dev/test_fields/migrations/0002_auto_20260130_1258.py +41 -0
  29. django_postpone_index-0.0.1/dev/test_fields/migrations/0003_auto_20260130_1259.py +33 -0
  30. django_postpone_index-0.0.1/dev/test_fields/migrations/__init__.py +0 -0
  31. django_postpone_index-0.0.1/dev/test_fields/models.py +43 -0
  32. django_postpone_index-0.0.1/dev/test_fields/tests.py +10 -0
  33. django_postpone_index-0.0.1/dev/test_gis_index/__init__.py +0 -0
  34. django_postpone_index-0.0.1/dev/test_gis_index/migrations/0001_initial.py +22 -0
  35. django_postpone_index-0.0.1/dev/test_gis_index/migrations/0002_gisindex1_mline.py +20 -0
  36. django_postpone_index-0.0.1/dev/test_gis_index/migrations/0003_auto_20260202_1156.py +24 -0
  37. django_postpone_index-0.0.1/dev/test_gis_index/migrations/__init__.py +0 -0
  38. django_postpone_index-0.0.1/dev/test_gis_index/models.py +13 -0
  39. django_postpone_index-0.0.1/dev/test_gis_index/tests.py +10 -0
  40. django_postpone_index-0.0.1/dev/test_ignore_migration/__init__.py +0 -0
  41. django_postpone_index-0.0.1/dev/test_ignore_migration/migrations/0001_initial.py +25 -0
  42. django_postpone_index-0.0.1/dev/test_ignore_migration/migrations/0002_auto_20260130_1044.py +19 -0
  43. django_postpone_index-0.0.1/dev/test_ignore_migration/migrations/0003_auto_20260130_1052.py +21 -0
  44. django_postpone_index-0.0.1/dev/test_ignore_migration/migrations/__init__.py +0 -0
  45. django_postpone_index-0.0.1/dev/test_ignore_migration/models.py +31 -0
  46. django_postpone_index-0.0.1/dev/test_ignore_migration/tests.py +51 -0
  47. django_postpone_index-0.0.1/dev/test_index_together/__init__.py +0 -0
  48. django_postpone_index-0.0.1/dev/test_index_together/migrations/0001_initial.py +43 -0
  49. django_postpone_index-0.0.1/dev/test_index_together/migrations/0002_auto_20260130_1044.py +21 -0
  50. django_postpone_index-0.0.1/dev/test_index_together/migrations/0003_auto_20260130_1053.py +21 -0
  51. django_postpone_index-0.0.1/dev/test_index_together/migrations/__init__.py +0 -0
  52. django_postpone_index-0.0.1/dev/test_index_together/models.py +39 -0
  53. django_postpone_index-0.0.1/dev/test_index_together/tests.py +10 -0
  54. django_postpone_index-0.0.1/dev/test_unique_together/__init__.py +0 -0
  55. django_postpone_index-0.0.1/dev/test_unique_together/migrations/0001_initial.py +43 -0
  56. django_postpone_index-0.0.1/dev/test_unique_together/migrations/0002_auto_20260130_1044.py +21 -0
  57. django_postpone_index-0.0.1/dev/test_unique_together/migrations/0003_auto_20260130_1053.py +21 -0
  58. django_postpone_index-0.0.1/dev/test_unique_together/migrations/__init__.py +0 -0
  59. django_postpone_index-0.0.1/dev/test_unique_together/models.py +39 -0
  60. django_postpone_index-0.0.1/dev/test_unique_together/tests.py +10 -0
  61. django_postpone_index-0.0.1/django_postpone_index.egg-info/PKG-INFO +276 -0
  62. django_postpone_index-0.0.1/django_postpone_index.egg-info/SOURCES.txt +86 -0
  63. django_postpone_index-0.0.1/django_postpone_index.egg-info/dependency_links.txt +1 -0
  64. django_postpone_index-0.0.1/django_postpone_index.egg-info/requires.txt +2 -0
  65. django_postpone_index-0.0.1/django_postpone_index.egg-info/top_level.txt +1 -0
  66. django_postpone_index-0.0.1/postpone_index/__init__.py +4 -0
  67. django_postpone_index-0.0.1/postpone_index/_version.py +34 -0
  68. django_postpone_index-0.0.1/postpone_index/admin.py +29 -0
  69. django_postpone_index-0.0.1/postpone_index/apps.py +8 -0
  70. django_postpone_index-0.0.1/postpone_index/contrib/__init__.py +0 -0
  71. django_postpone_index-0.0.1/postpone_index/contrib/postgis/__init__.py +0 -0
  72. django_postpone_index-0.0.1/postpone_index/contrib/postgis/base.py +11 -0
  73. django_postpone_index-0.0.1/postpone_index/contrib/postgis/schema.py +11 -0
  74. django_postpone_index-0.0.1/postpone_index/contrib/postgres/__init__.py +0 -0
  75. django_postpone_index-0.0.1/postpone_index/contrib/postgres/base.py +11 -0
  76. django_postpone_index-0.0.1/postpone_index/contrib/postgres/schema.py +230 -0
  77. django_postpone_index-0.0.1/postpone_index/management/__init__.py +0 -0
  78. django_postpone_index-0.0.1/postpone_index/management/commands/__init__.py +0 -0
  79. django_postpone_index-0.0.1/postpone_index/management/commands/apply_postponed.py +251 -0
  80. django_postpone_index-0.0.1/postpone_index/migration_utils.py +20 -0
  81. django_postpone_index-0.0.1/postpone_index/models.py +61 -0
  82. django_postpone_index-0.0.1/postpone_index/sql/start.sql +14 -0
  83. django_postpone_index-0.0.1/postpone_index/testing_utils.py +37 -0
  84. django_postpone_index-0.0.1/postpone_index/utils.py +111 -0
  85. django_postpone_index-0.0.1/pyproject.toml +17 -0
  86. django_postpone_index-0.0.1/setup.cfg +4 -0
  87. django_postpone_index-0.0.1/setup.py +53 -0
  88. django_postpone_index-0.0.1/tox.ini +57 -0
@@ -0,0 +1,147 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "**"
7
+ tags:
8
+ - "v*"
9
+ - "*.*.*"
10
+ - "*.*.*.*"
11
+ pull_request:
12
+
13
+ jobs:
14
+ syntax:
15
+ name: tox (syntax)
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+ cache: pip
28
+
29
+ - name: Install build tools
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ python -m pip install --upgrade tox
33
+
34
+ - name: Run tox
35
+ run: |
36
+ tox run -e syntax
37
+
38
+ test:
39
+ name: tox (-f ${{ matrix.tox_factor }})
40
+ runs-on: ubuntu-latest
41
+ strategy:
42
+ fail-fast: false
43
+ matrix:
44
+ include:
45
+ - python-version: "3.10"
46
+ tox_factor: py310
47
+ - python-version: "3.11"
48
+ tox_factor: py311
49
+ - python-version: "3.12"
50
+ tox_factor: py312
51
+ - python-version: "3.13"
52
+ tox_factor: py313
53
+ - python-version: "3.14"
54
+ tox_factor: py3.14
55
+
56
+ services:
57
+ postgres:
58
+ image: postgis/postgis:15-3.4
59
+ env:
60
+ POSTGRES_USER: test
61
+ POSTGRES_PASSWORD: test
62
+ POSTGRES_DB: postpone_index
63
+ ports:
64
+ - 5432:5432
65
+ options: >-
66
+ --health-cmd "pg_isready -U test -d postpone_index"
67
+ --health-interval 10s
68
+ --health-timeout 5s
69
+ --health-retries 10
70
+
71
+ steps:
72
+ - name: Checkout
73
+ uses: actions/checkout@v4
74
+ with:
75
+ fetch-depth: 0
76
+
77
+ - name: Set up Python
78
+ uses: actions/setup-python@v5
79
+ with:
80
+ python-version: ${{ matrix.python-version }}
81
+ allow-prereleases: true
82
+ cache: pip
83
+
84
+ - name: Install build tools
85
+ run: |
86
+ python -m pip install --upgrade pip
87
+ python -m pip install --upgrade tox
88
+
89
+ - name: Install necessary libraries
90
+ run: |
91
+ sudo apt-get update
92
+ sudo apt-get install -y gdal-bin libgdal-dev libgeos-dev
93
+
94
+ - name: Prepare databases
95
+ env:
96
+ PGPASSWORD: test
97
+ run: |
98
+ until pg_isready -h 127.0.0.1 -p 5432 -U test -d postpone_index; do sleep 1; done
99
+ createdb -h 127.0.0.1 -p 5432 -U test postpone_index_additional
100
+
101
+ - name: Run tox
102
+ run: |
103
+ tox run -f ${{ matrix.tox_factor }}
104
+
105
+ publish:
106
+ name: Publish to PyPI
107
+ runs-on: ubuntu-latest
108
+ needs: [syntax, test]
109
+ if: startsWith(github.ref, 'refs/tags/')
110
+ steps:
111
+ - name: Checkout
112
+ uses: actions/checkout@v4
113
+ with:
114
+ fetch-depth: 0
115
+
116
+ - name: Set up Python
117
+ uses: actions/setup-python@v5
118
+ with:
119
+ python-version: "3.12"
120
+ cache: pip
121
+
122
+ - name: Determine if this is a release tag
123
+ id: release
124
+ env:
125
+ TAG_NAME: ${{ github.ref_name }}
126
+ run: |
127
+ python - <<'PY'
128
+ import os, re
129
+ tag = os.environ.get("TAG_NAME", "")
130
+ is_release = bool(re.match(r"^(?:v)?\d+\.\d+\.\d+(?:\.\d+)?$", tag))
131
+ with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
132
+ f.write(f"is_release={str(is_release).lower()}\n")
133
+ print(f"tag={tag} is_release={is_release}")
134
+ PY
135
+
136
+ - name: Build distributions
137
+ if: steps.release.outputs.is_release == 'true'
138
+ run: |
139
+ python -m pip install --upgrade pip
140
+ python -m pip install --upgrade build
141
+ python -m build
142
+
143
+ - name: Publish distributions to PyPI
144
+ if: steps.release.outputs.is_release == 'true'
145
+ uses: pypa/gh-action-pypi-publish@release/v1
146
+ with:
147
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,29 @@
1
+ docs/_build
2
+ build/
3
+ dist/
4
+ *.egg-info
5
+ locale
6
+ *.pyc
7
+ *.pyo
8
+ *.sqlite*
9
+ lcov.info
10
+ benchmarks/results/
11
+ coverage.xml
12
+ .coverage
13
+ htmlcov/
14
+ .svn/
15
+ .tox/
16
+ .vscode/
17
+ .venv/
18
+ .clangd
19
+ __pycache__/
20
+ .ropeproject
21
+ polib.py
22
+ reversion
23
+ tinymce
24
+ pip-log.txt
25
+ MANIFEST
26
+ dist
27
+
28
+ # Generated by setuptools-scm during builds
29
+ postpone_index/_version.py
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-postpone-index
3
+ Version: 0.0.1
4
+ Summary: Postpone index creation to provide Zero Downtime Migration feature
5
+ Home-page: https://github.com/nnseva/django-postpone-index
6
+ Author: Vsevolod Novikov
7
+ Author-email: nnseva@gmail.com
8
+ License: LGPLv3
9
+ Project-URL: Source, https://github.com/nnseva/django-postpone-index
10
+ Project-URL: Issues, https://github.com/nnseva/django-postpone-index/issues
11
+ Keywords: zero-downtime-migration,django,migration,zero-downtime,downtime,postgres,postgresql
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Classifier: Framework :: Django
20
+ Classifier: Framework :: Django :: 2.2
21
+ Classifier: Framework :: Django :: 3.0
22
+ Classifier: Framework :: Django :: 3.1
23
+ Classifier: Framework :: Django :: 3.2
24
+ Classifier: Framework :: Django :: 4.0
25
+ Classifier: Framework :: Django :: 4.1
26
+ Classifier: Framework :: Django :: 4.2
27
+ Classifier: Framework :: Django :: 5.0
28
+ Classifier: Framework :: Django :: 5.1
29
+ Classifier: Framework :: Django :: 5.2
30
+ Classifier: Development Status :: 4 - Beta
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: Topic :: Software Development :: Libraries
33
+ Requires-Python: >=3.10
34
+ Description-Content-Type: text/markdown
35
+ Requires-Dist: django2-postgres-backport
36
+ Requires-Dist: django-unlimited-char
37
+ Dynamic: author
38
+ Dynamic: author-email
39
+ Dynamic: classifier
40
+ Dynamic: description
41
+ Dynamic: description-content-type
42
+ Dynamic: home-page
43
+ Dynamic: keywords
44
+ Dynamic: license
45
+ Dynamic: project-url
46
+ Dynamic: requires-dist
47
+ Dynamic: requires-python
48
+ Dynamic: summary
49
+
50
+ [![Tests](https://github.com/nnseva/django-postpone-index/actions/workflows/ci.yml/badge.svg)](https://github.com/nnseva/django-postpone-index/actions/workflows/ci.yml)
51
+
52
+ # Django Postpone Index
53
+
54
+ This package provides modules and tools to postpone any index creation instead doing it inside the migration,
55
+ to provide *Zero Downtime Migration* feature.
56
+
57
+ The package is now using the PostgresSQL-specific `CREATE INDEX CONCURRENTLY` SQL command, so is applicable
58
+ only to the PostgreSQL backend.
59
+
60
+ ## Installation
61
+
62
+ *Stable version* from the PyPi package repository
63
+ ```bash
64
+ pip install django-postpone-index
65
+ ```
66
+
67
+ *Last development version* from the GitHub source version control system
68
+ ```
69
+ pip install git+git://github.com/nnseva/django-postpone-index.git
70
+ ```
71
+
72
+ ## Problem Description
73
+
74
+ Large data leads to long index creation time.
75
+
76
+ When the migration is automatically created, it executes all SQL commands creating index inside a transaction.
77
+
78
+ Large data and index creation inside a transaction lead to long-term table lock which blocks any data writting to the table.
79
+
80
+ On the other side, `CREATE INDEX CONCURRENTLY` SQL command may solve the problem, but this SQL command can not be executed inside a transaction block.
81
+
82
+ The `AddIndexConcurrently` might be created in a separate migration, moving out the automatically generated `AddIndex` from the migration,
83
+ but not all indexes are created using `AddIndex`.
84
+
85
+ ## Solution
86
+
87
+ All index creation SQL commands (as well as unique constraints creation) are catched
88
+ and postponed using a special `PostponedSQL` model (the `DROP INDEX` and `DROP CONSTRAINT` SQL commands
89
+ are still executed immediately).
90
+
91
+ When the migration is finished, the postponed indexes may be created in a separate process
92
+ using `CREATE INDEX CONCURRENTLY` SQL command by the `apply_postponed` management command.
93
+ Apart from the standard migration, this process doesn't lock the whole table for a long time.
94
+
95
+ Failed index creation statements don't lead to the command failure
96
+ (until a special command line parameter passed). Every failed statement is stored
97
+ as erroneous instead. When the data is fixed, you can execute the `apply_postponed` management
98
+ command again to restore the failed indexes.
99
+
100
+ ## Complex Use Cases
101
+
102
+ The following complex use cases are processed by the package.
103
+
104
+ - Several create/drop pairs. There can be several create/drop index pairs if several migrations applied at once.
105
+ - Back Migration. The both, forward and backward migrations are processed.
106
+ - Implicit index drop while removing the table. The Django doesn't issue a separate SQL to drop indexes of the dropped table.
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
+
109
+ ## Using
110
+
111
+ Include the `postpone_index` application in `setting.py`:
112
+
113
+ ```python
114
+ INSTALLED_APPS = [
115
+ ...
116
+ 'postpone_index',
117
+ ...
118
+ ]
119
+ ```
120
+
121
+ Use `pospone_index.contrib.postgres` or `postpone_index.contrib.postgis` engines instead of the Django-provided in `settings.py`:
122
+
123
+ ```python
124
+ DATABASES = {
125
+ 'default': {
126
+ 'ENGINE': 'postpone_index.contrib.postgres',
127
+ ...
128
+ }
129
+ }
130
+ ```
131
+
132
+ If you provide your own database engine instead of the Django-provided, you can also
133
+ combine `pospone_index.contrib.postgres.schema.DatabaseSchemaEditorMixin` with your own Database Schema Editor, f.e.:
134
+
135
+ `mybackend/schema.py`
136
+ ```python
137
+ from django.db.backends.postgresql.schema import DatabaseSchemaEditor as _DatabaseSchemaEditor
138
+ from pospone_index.contrib.postgres.schema import DatabaseSchemaEditorMixin
139
+
140
+ class PostponeIndexDatabaseSchemaEditor(DatabaseSchemaEditorMixin, _DatabaseSchemaEditor):
141
+ # Your own code
142
+ ...
143
+ ```
144
+
145
+ `mybackend/base.py`
146
+ ```python
147
+ from django.db.backends.postgresql.base import (
148
+ DatabaseWrapper as _DatabaseWrapper,
149
+ )
150
+
151
+ from mybackend.schema import PostponeIndexDatabaseSchemaEditor
152
+
153
+
154
+ class DatabaseWrapper(_DatabaseWrapper):
155
+ """Database wrapper"""
156
+
157
+ SchemaEditorClass = PostponeIndexDatabaseSchemaEditor
158
+ # Your own code
159
+ ...
160
+ ```
161
+
162
+ Execute `apply_postponed` management command every time after the `migrate` management command to create new postponed indexes.
163
+
164
+ Monitor `PostponedSQL` model instances to see errors on the SQL execution.
165
+
166
+ After the data is fixed, you can try to recreate the postponed invalid indexes just
167
+ calling the `apply_postponed` migration command again. All not-applied indexes will be tried to create again.
168
+
169
+ **NOTICE** the `apply_postponed` management command doesn't have any explicit locking mechanics. Avoid starting this
170
+ command concurrently with itself or another `migrate` command on the same database.
171
+
172
+ ## Django testing
173
+
174
+ Django migrates testing database before tests. Always use `POSTPONE_INDEX_IGNORE = True` settings to avoid postpone index
175
+ for the testing database.
176
+
177
+ If you want to check your own migration with the postpone index switched on,
178
+ use the `postpone_index.testing_utils.TestCase` and `override_settings` Django feature with the following trick:
179
+
180
+ ```python
181
+ from django.core.management import call_command
182
+ from django.test import override_settings
183
+ from postpone_index.models import PostponedSQL
184
+ from postpone_index import testing_utils
185
+
186
+ class ModuleTest(testing_utils.TestCase):
187
+ # Notice that the base TestCase is TransactionalTestCase
188
+
189
+ @classmethod
190
+ def setUpClass(cls):
191
+ # If you want to have customized setUpClass, call the method of the base class
192
+ super().setUpClass()
193
+
194
+ @classmethod
195
+ def tearDownClass(cls):
196
+ # If you want to have customized tearDownClass, call the method of the base class
197
+ super().tearDownClass()
198
+
199
+ def test_my_special_migration_case(self):
200
+ """Explicitly check my migration with postpone_index"""
201
+
202
+ module_to_check = "my_module" # Your Django App
203
+ migration_before_the_check = "0005" # Just before your migration
204
+ migration_to_check = "0006" # The migration you check
205
+
206
+ # Notice that POSTPONED_INDEX_IGNORE is True by default while testing
207
+ call_command('migrate', module_to_check, migration_before_the_check)
208
+
209
+ with override_settings(
210
+ POSTPONE_INDEX_IGNORE=False
211
+ ):
212
+ # Here we can check how it's going with `postpone_index` activated
213
+
214
+ # Check whether your migration works as expected with postponed indexes
215
+ call_command('migrate', module_to_check, migration_to_check)
216
+
217
+ # Here you can check how the module works before apply_postponed
218
+ ...
219
+
220
+ # Check whether the indexes applied properly. The `-x` parameter
221
+ # causes exception on errors
222
+ call_command('apply_postponed', 'run', '-x')
223
+ ```
224
+
225
+ ## Django settings
226
+
227
+ ### `POSTPONE_INDEX_IGNORE`
228
+
229
+ The setting totally switches off the functionality of the package.
230
+
231
+ Always use this setting in the test environment to avoid using postponed index creation for the test database.
232
+
233
+ May be used in a heterogeneous database environment to switch off the package functionality on unsupported databases.
234
+
235
+ ### `POSTPONE_INDEX_ADMIN_IGNORE`
236
+
237
+ The `PostponedSQL` model admin view is switched on by default. You can totally switch it off,
238
+ or create your own admin class instead. Use `postpone_index.admin.PostponedSQLAdminMixin` as a base class if necessary.
239
+
240
+ ## Django database
241
+
242
+ The Django supports heterogeneous database environment in a single project. Every single database has it's own
243
+ state of migrations executed by the `manage.py migrate --database <alias>`.
244
+
245
+ The `apply_postponed` command also supports selection of the database alias using similar syntax:
246
+
247
+ ```bash
248
+
249
+ # The 'default' database alias is used as a default
250
+ python manage.py migrate
251
+ python manage.py apply_postponed
252
+
253
+ # A non-default database alias parameter has similar syntax
254
+ python manage.py migrate --database another-postgres-database
255
+ python manage.py apply_postponed --database another-postgres-database
256
+ ```
257
+
258
+ Use `POSTPONE_INDEX_IGNORE=1` environment to switch off the package functionality on migrations running on unsupported database engines like:
259
+
260
+ ```bash
261
+ POSTPONE_INDEX_IGNORE=1 python manage.py migrate --database non-postgres-database
262
+ ```
263
+
264
+ ## Special migrations to avoid postpone index
265
+
266
+ Sometimes you may need to avoid the `postpone_index` applied to a single migration.
267
+
268
+ Just include the `PostponeIndexIgnoreMigrationMixin` into a base class list for your special migration:
269
+
270
+ ```python
271
+ from django.db import migrations, models
272
+ from postpone_index.migration_utils import PostponeIndexIgnoreMigrationMixin
273
+
274
+ class Migration(PostponeIndexIgnoreMigrationMixin, migrations.Migration):
275
+ ...
276
+ ```
@@ -0,0 +1,227 @@
1
+ [![Tests](https://github.com/nnseva/django-postpone-index/actions/workflows/ci.yml/badge.svg)](https://github.com/nnseva/django-postpone-index/actions/workflows/ci.yml)
2
+
3
+ # Django Postpone Index
4
+
5
+ This package provides modules and tools to postpone any index creation instead doing it inside the migration,
6
+ to provide *Zero Downtime Migration* feature.
7
+
8
+ The package is now using the PostgresSQL-specific `CREATE INDEX CONCURRENTLY` SQL command, so is applicable
9
+ only to the PostgreSQL backend.
10
+
11
+ ## Installation
12
+
13
+ *Stable version* from the PyPi package repository
14
+ ```bash
15
+ pip install django-postpone-index
16
+ ```
17
+
18
+ *Last development version* from the GitHub source version control system
19
+ ```
20
+ pip install git+git://github.com/nnseva/django-postpone-index.git
21
+ ```
22
+
23
+ ## Problem Description
24
+
25
+ Large data leads to long index creation time.
26
+
27
+ When the migration is automatically created, it executes all SQL commands creating index inside a transaction.
28
+
29
+ Large data and index creation inside a transaction lead to long-term table lock which blocks any data writting to the table.
30
+
31
+ On the other side, `CREATE INDEX CONCURRENTLY` SQL command may solve the problem, but this SQL command can not be executed inside a transaction block.
32
+
33
+ The `AddIndexConcurrently` might be created in a separate migration, moving out the automatically generated `AddIndex` from the migration,
34
+ but not all indexes are created using `AddIndex`.
35
+
36
+ ## Solution
37
+
38
+ All index creation SQL commands (as well as unique constraints creation) are catched
39
+ and postponed using a special `PostponedSQL` model (the `DROP INDEX` and `DROP CONSTRAINT` SQL commands
40
+ are still executed immediately).
41
+
42
+ When the migration is finished, the postponed indexes may be created in a separate process
43
+ using `CREATE INDEX CONCURRENTLY` SQL command by the `apply_postponed` management command.
44
+ Apart from the standard migration, this process doesn't lock the whole table for a long time.
45
+
46
+ Failed index creation statements don't lead to the command failure
47
+ (until a special command line parameter passed). Every failed statement is stored
48
+ as erroneous instead. When the data is fixed, you can execute the `apply_postponed` management
49
+ command again to restore the failed indexes.
50
+
51
+ ## Complex Use Cases
52
+
53
+ The following complex use cases are processed by the package.
54
+
55
+ - Several create/drop pairs. There can be several create/drop index pairs if several migrations applied at once.
56
+ - Back Migration. The both, forward and backward migrations are processed.
57
+ - Implicit index drop while removing the table. The Django doesn't issue a separate SQL to drop indexes of the dropped table.
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
+
60
+ ## Using
61
+
62
+ Include the `postpone_index` application in `setting.py`:
63
+
64
+ ```python
65
+ INSTALLED_APPS = [
66
+ ...
67
+ 'postpone_index',
68
+ ...
69
+ ]
70
+ ```
71
+
72
+ Use `pospone_index.contrib.postgres` or `postpone_index.contrib.postgis` engines instead of the Django-provided in `settings.py`:
73
+
74
+ ```python
75
+ DATABASES = {
76
+ 'default': {
77
+ 'ENGINE': 'postpone_index.contrib.postgres',
78
+ ...
79
+ }
80
+ }
81
+ ```
82
+
83
+ If you provide your own database engine instead of the Django-provided, you can also
84
+ combine `pospone_index.contrib.postgres.schema.DatabaseSchemaEditorMixin` with your own Database Schema Editor, f.e.:
85
+
86
+ `mybackend/schema.py`
87
+ ```python
88
+ from django.db.backends.postgresql.schema import DatabaseSchemaEditor as _DatabaseSchemaEditor
89
+ from pospone_index.contrib.postgres.schema import DatabaseSchemaEditorMixin
90
+
91
+ class PostponeIndexDatabaseSchemaEditor(DatabaseSchemaEditorMixin, _DatabaseSchemaEditor):
92
+ # Your own code
93
+ ...
94
+ ```
95
+
96
+ `mybackend/base.py`
97
+ ```python
98
+ from django.db.backends.postgresql.base import (
99
+ DatabaseWrapper as _DatabaseWrapper,
100
+ )
101
+
102
+ from mybackend.schema import PostponeIndexDatabaseSchemaEditor
103
+
104
+
105
+ class DatabaseWrapper(_DatabaseWrapper):
106
+ """Database wrapper"""
107
+
108
+ SchemaEditorClass = PostponeIndexDatabaseSchemaEditor
109
+ # Your own code
110
+ ...
111
+ ```
112
+
113
+ Execute `apply_postponed` management command every time after the `migrate` management command to create new postponed indexes.
114
+
115
+ Monitor `PostponedSQL` model instances to see errors on the SQL execution.
116
+
117
+ After the data is fixed, you can try to recreate the postponed invalid indexes just
118
+ calling the `apply_postponed` migration command again. All not-applied indexes will be tried to create again.
119
+
120
+ **NOTICE** the `apply_postponed` management command doesn't have any explicit locking mechanics. Avoid starting this
121
+ command concurrently with itself or another `migrate` command on the same database.
122
+
123
+ ## Django testing
124
+
125
+ Django migrates testing database before tests. Always use `POSTPONE_INDEX_IGNORE = True` settings to avoid postpone index
126
+ for the testing database.
127
+
128
+ If you want to check your own migration with the postpone index switched on,
129
+ use the `postpone_index.testing_utils.TestCase` and `override_settings` Django feature with the following trick:
130
+
131
+ ```python
132
+ from django.core.management import call_command
133
+ from django.test import override_settings
134
+ from postpone_index.models import PostponedSQL
135
+ from postpone_index import testing_utils
136
+
137
+ class ModuleTest(testing_utils.TestCase):
138
+ # Notice that the base TestCase is TransactionalTestCase
139
+
140
+ @classmethod
141
+ def setUpClass(cls):
142
+ # If you want to have customized setUpClass, call the method of the base class
143
+ super().setUpClass()
144
+
145
+ @classmethod
146
+ def tearDownClass(cls):
147
+ # If you want to have customized tearDownClass, call the method of the base class
148
+ super().tearDownClass()
149
+
150
+ def test_my_special_migration_case(self):
151
+ """Explicitly check my migration with postpone_index"""
152
+
153
+ module_to_check = "my_module" # Your Django App
154
+ migration_before_the_check = "0005" # Just before your migration
155
+ migration_to_check = "0006" # The migration you check
156
+
157
+ # Notice that POSTPONED_INDEX_IGNORE is True by default while testing
158
+ call_command('migrate', module_to_check, migration_before_the_check)
159
+
160
+ with override_settings(
161
+ POSTPONE_INDEX_IGNORE=False
162
+ ):
163
+ # Here we can check how it's going with `postpone_index` activated
164
+
165
+ # Check whether your migration works as expected with postponed indexes
166
+ call_command('migrate', module_to_check, migration_to_check)
167
+
168
+ # Here you can check how the module works before apply_postponed
169
+ ...
170
+
171
+ # Check whether the indexes applied properly. The `-x` parameter
172
+ # causes exception on errors
173
+ call_command('apply_postponed', 'run', '-x')
174
+ ```
175
+
176
+ ## Django settings
177
+
178
+ ### `POSTPONE_INDEX_IGNORE`
179
+
180
+ The setting totally switches off the functionality of the package.
181
+
182
+ Always use this setting in the test environment to avoid using postponed index creation for the test database.
183
+
184
+ May be used in a heterogeneous database environment to switch off the package functionality on unsupported databases.
185
+
186
+ ### `POSTPONE_INDEX_ADMIN_IGNORE`
187
+
188
+ The `PostponedSQL` model admin view is switched on by default. You can totally switch it off,
189
+ or create your own admin class instead. Use `postpone_index.admin.PostponedSQLAdminMixin` as a base class if necessary.
190
+
191
+ ## Django database
192
+
193
+ The Django supports heterogeneous database environment in a single project. Every single database has it's own
194
+ state of migrations executed by the `manage.py migrate --database <alias>`.
195
+
196
+ The `apply_postponed` command also supports selection of the database alias using similar syntax:
197
+
198
+ ```bash
199
+
200
+ # The 'default' database alias is used as a default
201
+ python manage.py migrate
202
+ python manage.py apply_postponed
203
+
204
+ # A non-default database alias parameter has similar syntax
205
+ python manage.py migrate --database another-postgres-database
206
+ python manage.py apply_postponed --database another-postgres-database
207
+ ```
208
+
209
+ Use `POSTPONE_INDEX_IGNORE=1` environment to switch off the package functionality on migrations running on unsupported database engines like:
210
+
211
+ ```bash
212
+ POSTPONE_INDEX_IGNORE=1 python manage.py migrate --database non-postgres-database
213
+ ```
214
+
215
+ ## Special migrations to avoid postpone index
216
+
217
+ Sometimes you may need to avoid the `postpone_index` applied to a single migration.
218
+
219
+ Just include the `PostponeIndexIgnoreMigrationMixin` into a base class list for your special migration:
220
+
221
+ ```python
222
+ from django.db import migrations, models
223
+ from postpone_index.migration_utils import PostponeIndexIgnoreMigrationMixin
224
+
225
+ class Migration(PostponeIndexIgnoreMigrationMixin, migrations.Migration):
226
+ ...
227
+ ```
File without changes