1
1
from django .apps .registry import Apps
2
+ from django .db import DatabaseError
2
3
from django .db import models as django_models
3
4
from django .db .migrations import Migration
4
- from django .db .migrations .exceptions import IrreversibleError
5
+ from django .db .migrations .exceptions import IrreversibleError , MigrationSchemaMissing
5
6
from django .db .migrations .operations .fields import FieldOperation
6
7
from django .db .migrations .operations .models import (
7
8
DeleteModel ,
15
16
__all__ = ["patch_migrations" , "patch_migration_recorder" , "patch_migration" ]
16
17
17
18
19
+ def _should_distribute_migrations (connection ):
20
+ """
21
+ Check if the connection is configured for distributed migrations.
22
+ """
23
+ return getattr (connection , "distributed_migrations" , False ) and getattr (
24
+ connection , "migration_cluster" , None
25
+ )
26
+
27
+
28
+ def _get_model_table_name (connection ):
29
+ """
30
+ Return the name of the table that will be used by the MigrationRecorder.
31
+ If distributed migrations are enabled, return the distributed table name.
32
+ Otherwise, return the regular django_migrations table name.
33
+ """
34
+ if _should_distribute_migrations (connection ):
35
+ return "distributed_django_migrations"
36
+ return "django_migrations"
37
+
38
+
18
39
def patch_migrations ():
19
40
patch_migration_recorder ()
20
41
patch_migration ()
@@ -29,22 +50,63 @@ def Migration(self):
29
50
if self ._migration_class is None :
30
51
if self .connection .vendor == "clickhouse" :
31
52
from clickhouse_backend import models
53
+ from clickhouse_backend .models import currentDatabase
32
54
33
- class Migration (models .ClickhouseModel ):
34
- app = models .StringField (max_length = 255 )
35
- name = models .StringField (max_length = 255 )
36
- applied = models .DateTime64Field (default = now )
37
- deleted = models .BoolField (default = False )
55
+ # Only create a distributed migration model if the connection
56
+ # has distributed migrations enabled and a migration cluster is set.
57
+ # otherwise, create a regular merge tree.
58
+ if _should_distribute_migrations (self .connection ):
38
59
39
- class Meta :
40
- apps = Apps ()
41
- app_label = "migrations"
42
- db_table = "django_migrations"
43
- engine = models .MergeTree (order_by = ("app" , "name" ))
44
- cluster = getattr (self .connection , "migration_cluster" , None )
60
+ class _Migration (models .ClickhouseModel ):
61
+ app = models .StringField (max_length = 255 )
62
+ name = models .StringField (max_length = 255 )
63
+ applied = models .DateTime64Field (default = now )
64
+ deleted = models .BoolField (default = False )
45
65
46
- def __str__ (self ):
47
- return "Migration %s for %s" % (self .name , self .app )
66
+ class Meta :
67
+ apps = Apps ()
68
+ app_label = "migrations"
69
+ db_table = "django_migrations"
70
+ engine = models .MergeTree (order_by = ("app" , "name" ))
71
+ cluster = getattr (self .connection , "migration_cluster" )
72
+
73
+ def __str__ (self ):
74
+ return "Migration %s for %s" % (self .name , self .app )
75
+
76
+ class Migration (models .ClickhouseModel ):
77
+ app = models .StringField (max_length = 255 )
78
+ name = models .StringField (max_length = 255 )
79
+ applied = models .DateTime64Field (default = now )
80
+ deleted = models .BoolField (default = False )
81
+
82
+ class Meta :
83
+ apps = Apps ()
84
+ app_label = "migrations"
85
+ db_table = _get_model_table_name (self .connection )
86
+ engine = models .Distributed (
87
+ getattr (self .connection , "migration_cluster" ),
88
+ currentDatabase (),
89
+ _Migration ._meta .db_table ,
90
+ models .Rand (),
91
+ )
92
+ cluster = getattr (self .connection , "migration_cluster" )
93
+
94
+ Migration ._meta .local_model_class = _Migration
95
+
96
+ else :
97
+
98
+ class Migration (models .ClickhouseModel ):
99
+ app = models .StringField (max_length = 255 )
100
+ name = models .StringField (max_length = 255 )
101
+ applied = models .DateTime64Field (default = now )
102
+ deleted = models .BoolField (default = False )
103
+
104
+ class Meta :
105
+ apps = Apps ()
106
+ app_label = "migrations"
107
+ db_table = _get_model_table_name (self .connection )
108
+ engine = models .MergeTree (order_by = ("app" , "name" ))
109
+ cluster = getattr (self .connection , "migration_cluster" )
48
110
49
111
else :
50
112
@@ -69,15 +131,45 @@ def has_table(self):
69
131
# Assert migration table won't be deleted once created.
70
132
if not getattr (self , "_has_table" , False ):
71
133
with self .connection .cursor () as cursor :
134
+ table = self .Migration ._meta .db_table
72
135
tables = self .connection .introspection .table_names (cursor )
73
- self ._has_table = self . Migration . _meta . db_table in tables
136
+ self ._has_table = table in tables
74
137
if self ._has_table and self .connection .vendor == "clickhouse" :
75
138
# fix https://github.com/jayvynl/django-clickhouse-backend/issues/51
76
139
cursor .execute (
77
- "ALTER table django_migrations ADD COLUMN IF NOT EXISTS deleted Bool"
140
+ f "ALTER table { table } ADD COLUMN IF NOT EXISTS deleted Bool"
78
141
)
79
142
return self ._has_table
80
143
144
+ def ensure_schema (self ):
145
+ """Ensure the table exists and has the correct schema."""
146
+ # If the table's there, that's fine - we've never changed its schema
147
+ # in the codebase.
148
+ if self .has_table ():
149
+ return
150
+
151
+ # In case of distributed migrations, we need to ensure the local model exists first and
152
+ # then create the distributed model.
153
+ try :
154
+ with self .connection .schema_editor () as editor :
155
+ if (
156
+ editor .connection .vendor == "clickhouse"
157
+ and _should_distribute_migrations (editor .connection )
158
+ ):
159
+ with editor .connection .cursor () as cursor :
160
+ tables = editor .connection .introspection .table_names (cursor )
161
+ local_model_class = self .Migration ._meta .local_model_class
162
+ local_table = local_model_class ._meta .db_table
163
+ if local_table not in tables :
164
+ # Create the local model first
165
+ editor .create_model (self .Migration ._meta .local_model_class )
166
+
167
+ editor .create_model (self .Migration )
168
+ except DatabaseError as exc :
169
+ raise MigrationSchemaMissing (
170
+ "Unable to create the django_migrations table (%s)" % exc
171
+ )
172
+
81
173
def migration_qs (self ):
82
174
if self .connection .vendor == "clickhouse" :
83
175
return self .Migration .objects .using (self .connection .alias ).filter (
@@ -118,6 +210,7 @@ def flush(self):
118
210
119
211
MigrationRecorder .Migration = property (Migration )
120
212
MigrationRecorder .has_table = has_table
213
+ MigrationRecorder .ensure_schema = ensure_schema
121
214
MigrationRecorder .migration_qs = property (migration_qs )
122
215
MigrationRecorder .record_applied = record_applied
123
216
MigrationRecorder .record_unapplied = record_unapplied
@@ -136,13 +229,15 @@ def apply(self, project_state, schema_editor, collect_sql=False):
136
229
"""
137
230
applied_on_remote = False
138
231
if getattr (schema_editor .connection , "migration_cluster" , None ):
232
+ _table = _get_model_table_name (schema_editor .connection )
233
+
139
234
with schema_editor .connection .cursor () as cursor :
140
235
cursor .execute (
141
236
"select EXISTS(select 1 from clusterAllReplicas(%s, currentDatabase(), %s)"
142
237
" where app=%s and name=%s and deleted=false)" ,
143
238
[
144
239
schema_editor .connection .migration_cluster ,
145
- "django_migrations" ,
240
+ _table ,
146
241
self .app_label ,
147
242
self .name ,
148
243
],
@@ -203,13 +298,15 @@ def unapply(self, project_state, schema_editor, collect_sql=False):
203
298
"""
204
299
unapplied_on_remote = False
205
300
if getattr (schema_editor .connection , "migration_cluster" , None ):
301
+ _table = _get_model_table_name (schema_editor .connection )
302
+
206
303
with schema_editor .connection .cursor () as cursor :
207
304
cursor .execute (
208
305
"select EXISTS(select 1 from clusterAllReplicas(%s, currentDatabase(), %s)"
209
306
" where app=%s and name=%s and deleted=true)" ,
210
307
[
211
308
schema_editor .connection .migration_cluster ,
212
- "django_migrations" ,
309
+ _table ,
213
310
self .app_label ,
214
311
self .name ,
215
312
],
0 commit comments