django-postpone-index 0.0.1__py3-none-any.whl
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.1.dist-info/METADATA +276 -0
- django_postpone_index-0.0.1.dist-info/RECORD +12 -0
- django_postpone_index-0.0.1.dist-info/WHEEL +5 -0
- django_postpone_index-0.0.1.dist-info/top_level.txt +1 -0
- postpone_index/__init__.py +4 -0
- postpone_index/_version.py +34 -0
- postpone_index/admin.py +29 -0
- postpone_index/apps.py +8 -0
- postpone_index/migration_utils.py +20 -0
- postpone_index/models.py +61 -0
- postpone_index/testing_utils.py +37 -0
- postpone_index/utils.py +111 -0
|
@@ -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
|
+
[](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,12 @@
|
|
|
1
|
+
postpone_index/__init__.py,sha256=s1NgdtOn7SO7scs-uAaq1134F7Luh-JjB5Ghra3D2fQ,84
|
|
2
|
+
postpone_index/_version.py,sha256=qf6R-J7-UyuABBo8c0HgaquJ8bejVbf07HodXgwAwgQ,704
|
|
3
|
+
postpone_index/admin.py,sha256=TAZxaciQkEBNhReYulsU5VopPCyABtMLZvCSxk0CfYE,820
|
|
4
|
+
postpone_index/apps.py,sha256=1ap2HY49uXVXjY9UWK3___fjPe22wqFOSOnuH2kIAxs,200
|
|
5
|
+
postpone_index/migration_utils.py,sha256=02rm77mz_MAufl5vdqKPV1CAd1-3bRk6kSy4MIxKsEo,752
|
|
6
|
+
postpone_index/models.py,sha256=jMsA5hdiL8Xgl-hq7bgVl5NUX6LXlSYpMuX9pD68BbU,1771
|
|
7
|
+
postpone_index/testing_utils.py,sha256=ycxp7ovY6JceOvNpotK7OGAebhdHicXJ-0fWJmaq9fo,1224
|
|
8
|
+
postpone_index/utils.py,sha256=wYVPsQRs3LN-ZtvfSWrDDqFFp0OCkVxW9j7w9yqd47o,3823
|
|
9
|
+
django_postpone_index-0.0.1.dist-info/METADATA,sha256=f6r6FaLnag7wQ4NjfFRPhMDZAffRmXtXK7Bg7o4viLU,10415
|
|
10
|
+
django_postpone_index-0.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
django_postpone_index-0.0.1.dist-info/top_level.txt,sha256=SzF44QR8Fr9ArKXZPY7FSJUlTBKYo_dPNHn2hbqKOrc,15
|
|
12
|
+
django_postpone_index-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
postpone_index
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.0.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 1)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
postpone_index/admin.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Admin interface
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.contrib import admin
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
8
|
+
|
|
9
|
+
from postpone_index.models import PostponedSQL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PostponedSQLAdminMixin:
|
|
13
|
+
list_display = ('d', 'description', 'table', 'db_index')
|
|
14
|
+
list_display_links = ('d', 'description')
|
|
15
|
+
search_fields = ('description', 'table', 'db_index')
|
|
16
|
+
list_filter = (
|
|
17
|
+
'table',
|
|
18
|
+
'done'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def d(self, obj):
|
|
22
|
+
return '✅' if obj.done else '🚫' if obj.error else '➡'
|
|
23
|
+
d.short_description = _('Status')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if not getattr(settings, 'POSTPONE_INDEX_IGNORE', False) and not getattr(settings, 'POSTPONE_INDEX_ADMIN_IGNORE', False):
|
|
27
|
+
@admin.register(PostponedSQL)
|
|
28
|
+
class PostponedSQLAdmin(PostponedSQLAdminMixin, admin.ModelAdmin):
|
|
29
|
+
pass
|
postpone_index/apps.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Utility for special migration cases"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PostponeIndexIgnoreMigrationMixin:
|
|
5
|
+
"""Migration class mixin to avoid postpone index"""
|
|
6
|
+
|
|
7
|
+
def apply(self, project_state, schema_editor, *av, **kw):
|
|
8
|
+
"""Override to avoid postpone index"""
|
|
9
|
+
|
|
10
|
+
schema_editor._postpone_index_ignore = True
|
|
11
|
+
ret = super().apply(project_state, schema_editor, *av, **kw)
|
|
12
|
+
schema_editor._postpone_index_ignore = False
|
|
13
|
+
return ret
|
|
14
|
+
|
|
15
|
+
def unapply(self, project_state, schema_editor, *av, **kw):
|
|
16
|
+
"""Override to avoid postpone index"""
|
|
17
|
+
schema_editor._postpone_index_ignore = True
|
|
18
|
+
ret = super().unapply(project_state, schema_editor, *av, **kw)
|
|
19
|
+
schema_editor._postpone_index_ignore = False
|
|
20
|
+
return ret
|
postpone_index/models.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from unlimited_char.fields import CharField
|
|
4
|
+
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PostponedSQL(models.Model):
|
|
10
|
+
ts = models.BigIntegerField(
|
|
11
|
+
primary_key=True, editable=False,
|
|
12
|
+
default=time.time_ns,
|
|
13
|
+
verbose_name=_('Timestamp ns'),
|
|
14
|
+
help_text=_('Timestamp in nanoseconds')
|
|
15
|
+
)
|
|
16
|
+
description = CharField(
|
|
17
|
+
verbose_name=_('Description'),
|
|
18
|
+
help_text=_('Free-form short description')
|
|
19
|
+
)
|
|
20
|
+
sql = models.TextField(
|
|
21
|
+
verbose_name=_('SQL'),
|
|
22
|
+
help_text=_('Original SQL generated on the migration stage'),
|
|
23
|
+
)
|
|
24
|
+
table = CharField(
|
|
25
|
+
blank=True, null=True, db_index=True,
|
|
26
|
+
verbose_name=_('Table'),
|
|
27
|
+
help_text=_('Table name if applied to the table')
|
|
28
|
+
)
|
|
29
|
+
db_index = CharField(
|
|
30
|
+
blank=True, null=True, db_index=True,
|
|
31
|
+
verbose_name=_('Index'),
|
|
32
|
+
help_text=_('Index name if applied to the index')
|
|
33
|
+
)
|
|
34
|
+
fields = CharField(
|
|
35
|
+
blank=True, null=True,
|
|
36
|
+
verbose_name=_('Fields'),
|
|
37
|
+
help_text=_('Field names separated by comma if applied to the fields')
|
|
38
|
+
)
|
|
39
|
+
done = models.BooleanField(
|
|
40
|
+
default=False,
|
|
41
|
+
verbose_name=_('Done'),
|
|
42
|
+
help_text=_('Has the command been applied')
|
|
43
|
+
)
|
|
44
|
+
error = models.TextField(
|
|
45
|
+
blank=True, null=True,
|
|
46
|
+
verbose_name=_('Error'),
|
|
47
|
+
help_text=_('Last error reported when tried to apply')
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def d(self):
|
|
52
|
+
"""Status mark"""
|
|
53
|
+
return '[X]' if self.done else '[E]' if self.error else '[ ]'
|
|
54
|
+
|
|
55
|
+
class Meta:
|
|
56
|
+
managed = False
|
|
57
|
+
verbose_name = _('Postponed SQL')
|
|
58
|
+
verbose_name_plural = _('Postponed SQLs')
|
|
59
|
+
|
|
60
|
+
def __str__(self):
|
|
61
|
+
return self.description
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Testing Utilities"""
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.test import TransactionTestCase
|
|
5
|
+
|
|
6
|
+
from postpone_index.models import PostponedSQL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestCase(TransactionTestCase):
|
|
10
|
+
"""Base module testcase class"""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def _check_postponed_sql_empty(alias='default'):
|
|
14
|
+
try:
|
|
15
|
+
first = PostponedSQL.objects.using(alias).all().first()
|
|
16
|
+
if not first:
|
|
17
|
+
# No record
|
|
18
|
+
return True
|
|
19
|
+
except Exception:
|
|
20
|
+
# No table
|
|
21
|
+
return True
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _assert_postponed_sql_empty(cls, message=None, alias='default'):
|
|
26
|
+
cls.assertTrue(cls._check_postponed_sql_empty(alias=alias), message or 'Postponed SQL list not empty')
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def setUpClass(cls):
|
|
30
|
+
cls.assertTrue(settings.POSTPONE_INDEX_IGNORE, 'Tests should be started with POSTPONE_INDEX_IGNORE=True')
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def tearDownClass(cls):
|
|
34
|
+
if not cls._check_postponed_sql_empty(alias='default'):
|
|
35
|
+
PostponedSQL.objects.using('default').all().delete()
|
|
36
|
+
if not cls._check_postponed_sql_empty(alias='additional'):
|
|
37
|
+
PostponedSQL.objects.using('additional').all().delete()
|
postpone_index/utils.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Some utilities"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import MappingView
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ObjMap(MappingView):
|
|
8
|
+
"""Gives access to any attribute using map protocol, combining with another map for defaults"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, obj, defaults=None):
|
|
11
|
+
"""Initialize by the original object"""
|
|
12
|
+
self._obj = obj
|
|
13
|
+
self._defaults = defaults or {}
|
|
14
|
+
|
|
15
|
+
def __getitem__(self, k):
|
|
16
|
+
"""Override to get access"""
|
|
17
|
+
try:
|
|
18
|
+
return getattr(self._obj, k)
|
|
19
|
+
except AttributeError:
|
|
20
|
+
return self._defaults[k]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Utils:
|
|
24
|
+
"""Utilities class"""
|
|
25
|
+
_create_index_re = re.compile(
|
|
26
|
+
r'^\s*CREATE\s+(?P<unique>UNIQUE\s+)?INDEX\s+(IF\s+NOT\s+EXISTS\s+)?'
|
|
27
|
+
r'(((?P<iq>")?(?P<index_nameq>[^"]+)(?P=iq))|(?P<index_name>[^\s]+))'
|
|
28
|
+
r'\s+ON\s+(?P<only>ONLY\s+)?'
|
|
29
|
+
r'(((?P<tq>")?(?P<table_nameq>[^"]+)(?P=tq))|(?P<table_name>[^\s]+))'
|
|
30
|
+
r'(?P<rest>.*)$',
|
|
31
|
+
re.IGNORECASE | re.MULTILINE
|
|
32
|
+
)
|
|
33
|
+
_drop_index_re = re.compile(
|
|
34
|
+
r'^\s*DROP\s+INDEX\s+(IF\s+EXISTS\s+)?'
|
|
35
|
+
r'(((?P<iq>")?(?P<index_nameq>[^"]+)(?P=iq))|(?P<index_name>[^\s]+))'
|
|
36
|
+
r'(?P<rest>.*)$',
|
|
37
|
+
re.IGNORECASE | re.MULTILINE
|
|
38
|
+
)
|
|
39
|
+
_drop_table_re = re.compile(
|
|
40
|
+
r'^\s*DROP\s+TABLE\s+(IF\s+EXISTS\s+)?'
|
|
41
|
+
r'(((?P<tq>")?(?P<table_nameq>[^"]+)(?P=tq))|(?P<table_name>[^\s]+))'
|
|
42
|
+
r'(?P<rest>.*)$',
|
|
43
|
+
re.IGNORECASE | re.MULTILINE
|
|
44
|
+
)
|
|
45
|
+
_add_constraint_re = re.compile(
|
|
46
|
+
r'^\s*ALTER\s+TABLE\s+'
|
|
47
|
+
r'(((?P<tq>")?(?P<table_nameq>[^"]+)(?P=tq))|(?P<table_name>[^\s]+))'
|
|
48
|
+
r'\s+ADD\s+CONSTRAINT\s+'
|
|
49
|
+
r'(((?P<iq>")?(?P<index_nameq>[^"]+)(?P=iq))|(?P<index_name>[^\s]+))'
|
|
50
|
+
r'\s+UNIQUE\s+'
|
|
51
|
+
r'(?P<rest>.*)$',
|
|
52
|
+
re.IGNORECASE | re.MULTILINE
|
|
53
|
+
)
|
|
54
|
+
_drop_constraint_re = re.compile(
|
|
55
|
+
r'^\s*ALTER\s+TABLE\s+'
|
|
56
|
+
r'(((?P<tq>")?(?P<table_nameq>[^"]+)(?P=tq))|(?P<table_name>[^\s]+))'
|
|
57
|
+
r'\s+DROP\s+CONSTRAINT\s+'
|
|
58
|
+
r'(((?P<iq>")?(?P<index_nameq>[^"]+)(?P=iq))|(?P<index_name>[^\s]+))'
|
|
59
|
+
r'(?P<rest>.*)$',
|
|
60
|
+
re.IGNORECASE | re.MULTILINE
|
|
61
|
+
)
|
|
62
|
+
_drop_column_re = re.compile(
|
|
63
|
+
r'^\s*ALTER\s+TABLE\s+'
|
|
64
|
+
r'(((?P<tq>")?(?P<table_nameq>[^"]+)(?P=tq))|(?P<table_name>[^\s]+))'
|
|
65
|
+
r'\s+DROP\s+COLUMN\s+'
|
|
66
|
+
r'(((?P<cq>")?(?P<column_nameq>[^"]+)(?P=cq))|(?P<column_name>[^\s]+))'
|
|
67
|
+
r'(?P<rest>.*)$',
|
|
68
|
+
re.IGNORECASE | re.MULTILINE
|
|
69
|
+
)
|
|
70
|
+
_column_name_re = re.compile(
|
|
71
|
+
r'(((?P<cq>")(?P<column_nameq>[^"]+)(?P=cq))|(?P<column_name>[^\s]+))',
|
|
72
|
+
re.IGNORECASE | re.MULTILINE
|
|
73
|
+
)
|
|
74
|
+
_enclosed_re = re.compile(
|
|
75
|
+
r'\((?P<enclosed>[^\(\)]*)\)',
|
|
76
|
+
re.IGNORECASE | re.MULTILINE
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def _extract_column_names(cls, rest):
|
|
81
|
+
"""Extracts column names from complex index suffix"""
|
|
82
|
+
rest = cls._extract_enclosed(rest)
|
|
83
|
+
while match := cls._enclosed_re.search(rest):
|
|
84
|
+
rest = rest[:match.start()] + rest[match.end():]
|
|
85
|
+
ret = []
|
|
86
|
+
for column in [c for c in rest.split(',') if c.strip()]:
|
|
87
|
+
match = cls._column_name_re.search(column)
|
|
88
|
+
if not match:
|
|
89
|
+
continue
|
|
90
|
+
ret.append('"%s"' % (match.group('column_nameq') or match.group('column_name')))
|
|
91
|
+
return ret
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def _extract_enclosed(cls, rest):
|
|
95
|
+
"""Extract first enclosed statement with recursive parentheses"""
|
|
96
|
+
deep = 0
|
|
97
|
+
start = 0
|
|
98
|
+
end = 0
|
|
99
|
+
for i, c in enumerate(rest):
|
|
100
|
+
if c == '(':
|
|
101
|
+
if deep == 0:
|
|
102
|
+
start = i + 1
|
|
103
|
+
deep += 1
|
|
104
|
+
elif c == ')':
|
|
105
|
+
deep -= 1
|
|
106
|
+
if deep == 0:
|
|
107
|
+
end = i
|
|
108
|
+
break
|
|
109
|
+
if end == 0:
|
|
110
|
+
return rest
|
|
111
|
+
return rest[start:end]
|