1
+ from django .core .exceptions import FieldError
1
2
from django .db .backends .base .schema import BaseDatabaseSchemaEditor
2
3
from django .db .models import Index
3
4
from pymongo .operations import IndexModel
@@ -57,6 +58,93 @@ def add_field(self, model, field):
57
58
index .name = self ._create_index_name (model ._meta .db_table , [field .column ])
58
59
self .add_index (model , index , field = field )
59
60
61
+ def alter_field (self , model , old_field , new_field , strict = False , old_db_table = None ):
62
+ """
63
+ Allow a field's type, uniqueness, nullability, default, column,
64
+ constraints, etc. to be modified.
65
+ `old_field` is required to compute the necessary changes.
66
+ If `strict` is True, raise errors if the old column does not match
67
+ `old_field` precisely.
68
+
69
+ This is identical to the base class except `old_db_table` is added.
70
+ """
71
+ if not self ._field_should_be_altered (old_field , new_field ):
72
+ return
73
+ # Ensure this field is even column-based
74
+ old_db_params = old_field .db_parameters (connection = self .connection )
75
+ old_type = old_db_params ["type" ]
76
+ new_db_params = new_field .db_parameters (connection = self .connection )
77
+ new_type = new_db_params ["type" ]
78
+ modifying_generated_field = False
79
+ if (old_type is None and old_field .remote_field is None ) or (
80
+ new_type is None and new_field .remote_field is None
81
+ ):
82
+ raise ValueError (
83
+ f"Cannot alter field { old_field } into { new_field } - they do "
84
+ "not properly define db_type (are you using a badly-written "
85
+ "custom field?)"
86
+ )
87
+ if (
88
+ old_type is None
89
+ and new_type is None
90
+ and (
91
+ old_field .remote_field .through
92
+ and new_field .remote_field .through
93
+ and old_field .remote_field .through ._meta .auto_created
94
+ and new_field .remote_field .through ._meta .auto_created
95
+ )
96
+ ):
97
+ self ._alter_many_to_many (model , old_field , new_field , strict )
98
+ return
99
+ if (
100
+ old_type is None
101
+ and new_type is None
102
+ and (
103
+ old_field .remote_field .through
104
+ and new_field .remote_field .through
105
+ and not old_field .remote_field .through ._meta .auto_created
106
+ and not new_field .remote_field .through ._meta .auto_created
107
+ )
108
+ ):
109
+ # Both sides have through models; this is a no-op.
110
+ return
111
+ if old_type is None or new_type is None :
112
+ raise ValueError (
113
+ f"Cannot alter field { old_field } into { new_field } - they are "
114
+ "not compatible types (you cannot alter to or from M2M fields, "
115
+ "or add or remove through= on M2M fields)"
116
+ )
117
+ if old_field .generated != new_field .generated or (
118
+ new_field .generated and old_field .db_persist != new_field .db_persist
119
+ ):
120
+ modifying_generated_field = True
121
+ elif new_field .generated :
122
+ try :
123
+ old_field_sql = old_field .generated_sql (self .connection )
124
+ except FieldError :
125
+ # Field used in a generated field was renamed.
126
+ modifying_generated_field = True
127
+ else :
128
+ new_field_sql = new_field .generated_sql (self .connection )
129
+ modifying_generated_field = old_field_sql != new_field_sql
130
+ if modifying_generated_field :
131
+ raise ValueError (
132
+ f"Modifying GeneratedFields is not supported - the field { new_field } "
133
+ "must be removed and re-added with the new definition."
134
+ )
135
+
136
+ self ._alter_field (
137
+ model ,
138
+ old_field ,
139
+ new_field ,
140
+ old_type ,
141
+ new_type ,
142
+ old_db_params ,
143
+ new_db_params ,
144
+ strict ,
145
+ old_db_table ,
146
+ )
147
+
60
148
def _alter_field (
61
149
self ,
62
150
model ,
@@ -67,17 +155,110 @@ def _alter_field(
67
155
old_db_params ,
68
156
new_db_params ,
69
157
strict = False ,
158
+ old_db_table = None ,
70
159
):
71
160
collection = self .connection .database [model ._meta .db_table ]
161
+ # Removed an index? (no strict check, as multiple indexes are possible)
162
+ # Remove indexes if db_index switched to False or a unique constraint
163
+ # will now be used in lieu of an index. The following lines from the
164
+ # truth table show all True cases; the rest are False:
165
+ #
166
+ # old_field.db_index | old_field.unique | new_field.db_index | new_field.unique
167
+ # ------------------------------------------------------------------------------
168
+ # True | False | False | False
169
+ # True | False | False | True
170
+ # True | False | True | True
171
+ if (
172
+ old_field .db_index
173
+ and not old_field .unique
174
+ and (not new_field .db_index or new_field .unique )
175
+ ):
176
+ # Find the index for this field
177
+ meta_index_names = {index .name for index in model ._meta .indexes }
178
+ # Retrieve only BTREE indexes since this is what's created with
179
+ # db_index=True.
180
+ index_names = self ._constraint_names (
181
+ model ,
182
+ [old_field .column ],
183
+ index = True ,
184
+ type_ = Index .suffix ,
185
+ exclude = meta_index_names ,
186
+ )
187
+ for index_name in index_names :
188
+ # The only way to check if an index was created with
189
+ # db_index=True or with Index(['field'], name='foo')
190
+ # is to look at its name (refs #28053).
191
+ collection .drop_index (index_name )
72
192
# Have they renamed the column?
73
193
if old_field .column != new_field .column :
74
194
collection .update_many ({}, {"$rename" : {old_field .column : new_field .column }})
195
+ # Move index to the new field, if needed.
196
+ if old_field .db_index and new_field .db_index :
197
+ old_db_table = old_db_table or model ._meta .db_table
198
+ old_index = Index (fields = [old_field .name ])
199
+ old_index .name = self ._create_index_name (old_db_table , [old_field .column ])
200
+ self .remove_index (model , old_index )
201
+ new_index = Index (fields = [new_field .name ])
202
+ new_index .name = self ._create_index_name (model ._meta .db_table , [new_field .column ])
203
+ self .add_index (model , new_index , field = new_field )
75
204
# Replace NULL with the field default if the field and was changed from
76
205
# NULL to NOT NULL.
77
206
if new_field .has_default () and old_field .null and not new_field .null :
78
207
column = new_field .column
79
208
default = self .effective_default (new_field )
80
209
collection .update_many ({column : {"$eq" : None }}, [{"$set" : {column : default }}])
210
+ # Added an index? Add an index if db_index switched to True or a unique
211
+ # constraint will no longer be used in lieu of an index. The following
212
+ # lines from the truth table show all True cases; the rest are False:
213
+ #
214
+ # old_field.db_index | old_field.unique | new_field.db_index | new_field.unique
215
+ # ------------------------------------------------------------------------------
216
+ # False | False | True | False
217
+ # False | True | True | False
218
+ # True | True | True | False
219
+ if (
220
+ (not old_field .db_index or old_field .unique )
221
+ and new_field .db_index
222
+ and not new_field .unique
223
+ ):
224
+ index = Index (fields = [new_field .name ])
225
+ index .name = self ._create_index_name (model ._meta .db_table , [new_field .column ])
226
+ self .add_index (model , index )
227
+
228
+ def _alter_many_to_many (self , model , old_field , new_field , strict ):
229
+ """
230
+ Alter M2Ms to repoint their to= endpoints.
231
+
232
+ This is identical to the base class except `old_db_table=...` is added.
233
+ """
234
+
235
+ # Rename the through table
236
+ if (
237
+ old_field .remote_field .through ._meta .db_table
238
+ != new_field .remote_field .through ._meta .db_table
239
+ ):
240
+ self .alter_db_table (
241
+ old_field .remote_field .through ,
242
+ old_field .remote_field .through ._meta .db_table ,
243
+ new_field .remote_field .through ._meta .db_table ,
244
+ )
245
+ # Repoint the FK to the other side
246
+ self .alter_field (
247
+ new_field .remote_field .through ,
248
+ # The field that points to the target model is needed, so we can
249
+ # tell alter_field to change it - this is m2m_reverse_field_name()
250
+ # (as opposed to m2m_field_name(), which points to our model).
251
+ old_field .remote_field .through ._meta .get_field (old_field .m2m_reverse_field_name ()),
252
+ new_field .remote_field .through ._meta .get_field (new_field .m2m_reverse_field_name ()),
253
+ old_db_table = old_field .remote_field .through ._meta .db_table ,
254
+ )
255
+
256
+ self .alter_field (
257
+ new_field .remote_field .through ,
258
+ # for self-referential models we need to alter field from the other end too
259
+ old_field .remote_field .through ._meta .get_field (old_field .m2m_field_name ()),
260
+ new_field .remote_field .through ._meta .get_field (new_field .m2m_field_name ()),
261
+ )
81
262
82
263
def remove_field (self , model , field ):
83
264
# Remove implicit M2M tables.
0 commit comments