Skip to content

Commit 57c18e6

Browse files
committed
add index creation/deletion support to SchemaEditor.alter_field()
1 parent 846fc6f commit 57c18e6

File tree

2 files changed

+182
-5
lines changed

2 files changed

+182
-5
lines changed

django_mongodb/features.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,12 @@ class DatabaseFeatures(BaseDatabaseFeatures):
8888
# AddField
8989
"schema.tests.SchemaTests.test_add_unique_charfield",
9090
# AlterField
91-
"schema.tests.SchemaTests.test_alter_field_add_index_to_integerfield",
9291
"schema.tests.SchemaTests.test_alter_field_fk_to_o2o",
9392
"schema.tests.SchemaTests.test_alter_field_o2o_keeps_unique",
9493
"schema.tests.SchemaTests.test_alter_field_o2o_to_fk",
9594
"schema.tests.SchemaTests.test_alter_int_pk_to_int_unique",
96-
# AlterField (db_index)
97-
"schema.tests.SchemaTests.test_alter_renames_index",
98-
"schema.tests.SchemaTests.test_indexes",
99-
"schema.tests.SchemaTests.test_remove_db_index_doesnt_remove_custom_indexes",
10095
# AlterField (unique)
96+
"schema.tests.SchemaTests.test_indexes",
10197
"schema.tests.SchemaTests.test_unique",
10298
"schema.tests.SchemaTests.test_unique_and_reverse_m2m",
10399
# alter_unique_together

django_mongodb/schema.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.core.exceptions import FieldError
12
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
23
from django.db.models import Index
34
from pymongo.operations import IndexModel
@@ -57,6 +58,93 @@ def add_field(self, model, field):
5758
index.name = self._create_index_name(model._meta.db_table, [field.column])
5859
self.add_index(model, index, field=field)
5960

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+
60148
def _alter_field(
61149
self,
62150
model,
@@ -67,17 +155,110 @@ def _alter_field(
67155
old_db_params,
68156
new_db_params,
69157
strict=False,
158+
old_db_table=None,
70159
):
71160
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)
72192
# Have they renamed the column?
73193
if old_field.column != new_field.column:
74194
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)
75204
# Replace NULL with the field default if the field and was changed from
76205
# NULL to NOT NULL.
77206
if new_field.has_default() and old_field.null and not new_field.null:
78207
column = new_field.column
79208
default = self.effective_default(new_field)
80209
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+
)
81262

82263
def remove_field(self, model, field):
83264
# Remove implicit M2M tables.

0 commit comments

Comments
 (0)