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,92 @@ 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
+ self ._alter_field (
136
+ model ,
137
+ old_field ,
138
+ new_field ,
139
+ old_type ,
140
+ new_type ,
141
+ old_db_params ,
142
+ new_db_params ,
143
+ strict ,
144
+ old_db_table ,
145
+ )
146
+
60
147
def _alter_field (
61
148
self ,
62
149
model ,
@@ -67,17 +154,109 @@ def _alter_field(
67
154
old_db_params ,
68
155
new_db_params ,
69
156
strict = False ,
157
+ old_db_table = None ,
70
158
):
71
159
collection = self .connection .database [model ._meta .db_table ]
160
+ # Removed an index? (no strict check, as multiple indexes are possible)
161
+ # Remove indexes if db_index switched to False or a unique constraint
162
+ # will now be used in lieu of an index. The following lines from the
163
+ # truth table show all True cases; the rest are False:
164
+ #
165
+ # old_field.db_index | old_field.unique | new_field.db_index | new_field.unique
166
+ # ------------------------------------------------------------------------------
167
+ # True | False | False | False
168
+ # True | False | False | True
169
+ # True | False | True | True
170
+ if (
171
+ old_field .db_index
172
+ and not old_field .unique
173
+ and (not new_field .db_index or new_field .unique )
174
+ ):
175
+ # Find the index for this field
176
+ meta_index_names = {index .name for index in model ._meta .indexes }
177
+ # Retrieve only BTREE indexes since this is what's created with
178
+ # db_index=True.
179
+ index_names = self ._constraint_names (
180
+ model ,
181
+ [old_field .column ],
182
+ index = True ,
183
+ type_ = Index .suffix ,
184
+ exclude = meta_index_names ,
185
+ )
186
+ for index_name in index_names :
187
+ # The only way to check if an index was created with
188
+ # db_index=True or with Index(['field'], name='foo')
189
+ # is to look at its name (refs #28053).
190
+ collection .drop_index (index_name )
72
191
# Have they renamed the column?
73
192
if old_field .column != new_field .column :
74
193
collection .update_many ({}, {"$rename" : {old_field .column : new_field .column }})
194
+ # Move index to the new field, if needed.
195
+ if old_field .db_index and new_field .db_index :
196
+ old_db_table = old_db_table or model ._meta .db_table
197
+ old_index = Index (fields = [old_field .name ])
198
+ old_index .name = self ._create_index_name (old_db_table , [old_field .column ])
199
+ self .remove_index (model , old_index )
200
+ new_index = Index (fields = [new_field .name ])
201
+ new_index .name = self ._create_index_name (model ._meta .db_table , [new_field .column ])
202
+ self .add_index (model , new_index , field = new_field )
75
203
# Replace NULL with the field default if the field and was changed from
76
204
# NULL to NOT NULL.
77
205
if new_field .has_default () and old_field .null and not new_field .null :
78
206
column = new_field .column
79
207
default = self .effective_default (new_field )
80
208
collection .update_many ({column : {"$eq" : None }}, [{"$set" : {column : default }}])
209
+ # Added an index? Add an index if db_index switched to True or a unique
210
+ # constraint will no longer be used in lieu of an index. The following
211
+ # lines from the truth table show all True cases; the rest are False:
212
+ #
213
+ # old_field.db_index | old_field.unique | new_field.db_index | new_field.unique
214
+ # ------------------------------------------------------------------------------
215
+ # False | False | True | False
216
+ # False | True | True | False
217
+ # True | True | True | False
218
+ if (
219
+ (not old_field .db_index or old_field .unique )
220
+ and new_field .db_index
221
+ and not new_field .unique
222
+ ):
223
+ index = Index (fields = [new_field .name ])
224
+ index .name = self ._create_index_name (model ._meta .db_table , [new_field .column ])
225
+ self .add_index (model , index )
226
+
227
+ def _alter_many_to_many (self , model , old_field , new_field , strict ):
228
+ """
229
+ Alter M2Ms to repoint their to= endpoints.
230
+
231
+ This is identical to the base class except `old_db_table=...` is added.
232
+ """
233
+ # Rename the through table
234
+ if (
235
+ old_field .remote_field .through ._meta .db_table
236
+ != new_field .remote_field .through ._meta .db_table
237
+ ):
238
+ self .alter_db_table (
239
+ old_field .remote_field .through ,
240
+ old_field .remote_field .through ._meta .db_table ,
241
+ new_field .remote_field .through ._meta .db_table ,
242
+ )
243
+ # Repoint the FK to the other side
244
+ self .alter_field (
245
+ new_field .remote_field .through ,
246
+ # The field that points to the target model is needed, so we can
247
+ # tell alter_field to change it - this is m2m_reverse_field_name()
248
+ # (as opposed to m2m_field_name(), which points to our model).
249
+ old_field .remote_field .through ._meta .get_field (old_field .m2m_reverse_field_name ()),
250
+ new_field .remote_field .through ._meta .get_field (new_field .m2m_reverse_field_name ()),
251
+ old_db_table = old_field .remote_field .through ._meta .db_table ,
252
+ )
253
+ self .alter_field (
254
+ new_field .remote_field .through ,
255
+ # for self-referential models we need to alter field from the other end too
256
+ old_field .remote_field .through ._meta .get_field (old_field .m2m_field_name ()),
257
+ new_field .remote_field .through ._meta .get_field (new_field .m2m_field_name ()),
258
+ old_db_table = old_field .remote_field .through ._meta .db_table ,
259
+ )
81
260
82
261
def remove_field (self , model , field ):
83
262
# Remove implicit M2M tables.
0 commit comments