Skip to content

Commit 06b66aa

Browse files
committed
implement SchemaEditor.add/remove_constraint()
And also creating uniques in create_model() and add_field(), and removing them in remove_field().
1 parent d222bd8 commit 06b66aa

File tree

2 files changed

+78
-41
lines changed

2 files changed

+78
-41
lines changed

django_mongodb/features.py

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1313
supports_collation_on_charfield = False
1414
supports_column_check_constraints = False
1515
supports_date_lookup_using_string = False
16+
supports_deferrable_unique_constraints = False
1617
supports_explaining_query_execution = True
1718
supports_expression_defaults = False
1819
supports_expression_indexes = False
@@ -22,6 +23,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
2223
# BSON Date type doesn't support microsecond precision.
2324
supports_microsecond_precision = False
2425
supports_paramstyle_pyformat = False
26+
# Not implemented.
27+
supports_partial_indexes = False
2528
supports_select_difference = False
2629
supports_select_intersection = False
2730
supports_sequence_reset = False
@@ -69,28 +72,15 @@ class DatabaseFeatures(BaseDatabaseFeatures):
6972
"backends.tests.ThreadTests.test_pass_connection_between_threads",
7073
"backends.tests.ThreadTests.test_closing_non_shared_connections",
7174
"backends.tests.ThreadTests.test_default_connection_thread_local",
72-
# AddField
73-
"schema.tests.SchemaTests.test_add_unique_charfield",
7475
# AlterField
7576
"schema.tests.SchemaTests.test_alter_field_fk_to_o2o",
76-
"schema.tests.SchemaTests.test_alter_field_o2o_keeps_unique",
7777
"schema.tests.SchemaTests.test_alter_field_o2o_to_fk",
78-
"schema.tests.SchemaTests.test_alter_int_pk_to_int_unique",
7978
# AlterField (unique)
8079
"schema.tests.SchemaTests.test_indexes",
8180
"schema.tests.SchemaTests.test_unique",
82-
"schema.tests.SchemaTests.test_unique_and_reverse_m2m",
8381
# alter_unique_together
8482
"migrations.test_operations.OperationTests.test_alter_unique_together",
8583
"schema.tests.SchemaTests.test_unique_together",
86-
# add/remove_constraint
87-
"introspection.tests.IntrospectionTests.test_get_constraints",
88-
"migrations.test_operations.OperationTests.test_add_partial_unique_constraint",
89-
"migrations.test_operations.OperationTests.test_create_model_with_partial_unique_constraint",
90-
"migrations.test_operations.OperationTests.test_remove_partial_unique_constraint",
91-
"schema.tests.SchemaTests.test_composed_constraint_with_fk",
92-
"schema.tests.SchemaTests.test_remove_ignored_unique_constraint_not_create_fk_index",
93-
"schema.tests.SchemaTests.test_unique_constraint",
9484
}
9585
# $bitAnd, #bitOr, and $bitXor are new in MongoDB 6.3.
9686
_django_test_expected_failures_bitwise = {
@@ -181,24 +171,6 @@ def django_test_expected_failures(self):
181171
"model_fields.test_autofield.SmallAutoFieldTests",
182172
"queries.tests.TestInvalidValuesRelation.test_invalid_values",
183173
},
184-
"MongoDB does not enforce UNIQUE constraints.": {
185-
"auth_tests.test_basic.BasicTestCase.test_unicode_username",
186-
"auth_tests.test_migrations.ProxyModelWithSameAppLabelTests.test_migrate_with_existing_target_permission",
187-
"constraints.tests.UniqueConstraintTests.test_database_constraint",
188-
"contenttypes_tests.test_operations.ContentTypeOperationsTests.test_content_type_rename_conflict",
189-
"contenttypes_tests.test_operations.ContentTypeOperationsTests.test_existing_content_type_rename",
190-
"custom_pk.tests.CustomPKTests.test_unique_pk",
191-
"force_insert_update.tests.ForceInsertInheritanceTests.test_force_insert_with_existing_grandparent",
192-
"get_or_create.tests.GetOrCreateTestsWithManualPKs.test_create_with_duplicate_primary_key",
193-
"get_or_create.tests.GetOrCreateTestsWithManualPKs.test_savepoint_rollback",
194-
"get_or_create.tests.GetOrCreateThroughManyToMany.test_something",
195-
"get_or_create.tests.UpdateOrCreateTests.test_manual_primary_key_test",
196-
"get_or_create.tests.UpdateOrCreateTestsWithManualPKs.test_create_with_duplicate_primary_key",
197-
"introspection.tests.IntrospectionTests.test_get_constraints_unique_indexes_orders",
198-
"model_fields.test_filefield.FileFieldTests.test_unique_when_same_filename",
199-
"one_to_one.tests.OneToOneTests.test_multiple_o2o",
200-
"queries.test_bulk_update.BulkUpdateTests.test_database_routing_batch_atomicity",
201-
},
202174
"MongoDB does not enforce PositiveIntegerField constraint.": {
203175
"model_fields.test_integerfield.PositiveIntegerFieldTests.test_negative_values",
204176
},

django_mongodb/schema.py

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
2-
from django.db.models import Index
2+
from django.db.models import Index, UniqueConstraint
33
from pymongo import ASCENDING, DESCENDING
44
from pymongo.operations import IndexModel
55

@@ -29,18 +29,23 @@ def create_model(self, model):
2929

3030
def _create_model_indexes(self, model):
3131
"""
32-
Create all indexes (field indexes, index_together, Meta.indexes) for
33-
the specified model.
32+
Create all indexes (field indexes & uniques, Meta.index_together,
33+
Meta.constraints, Meta.indexes) for the specified model.
3434
"""
3535
if not model._meta.managed or model._meta.proxy or model._meta.swapped:
3636
return
37-
# Field indexes
37+
# Field indexes and uniques
3838
for field in model._meta.local_fields:
3939
if self._field_should_be_indexed(model, field):
4040
self._add_field_index(model, field)
41+
elif self._field_should_have_unique(field):
42+
self._add_field_unique(model, field)
4143
# Meta.index_together (RemovedInDjango51Warning)
4244
for field_names in model._meta.index_together:
4345
self._add_composed_index(model, field_names)
46+
# Meta.constraints
47+
for constraint in model._meta.constraints:
48+
self.add_constraint(model, constraint)
4449
# Meta.indexes
4550
for index in model._meta.indexes:
4651
self.add_index(model, index)
@@ -62,9 +67,11 @@ def add_field(self, model, field):
6267
self.get_collection(model._meta.db_table).update_many(
6368
{}, [{"$set": {column: self.effective_default(field)}}]
6469
)
65-
# Add an index, if required.
70+
# Add an index or unique, if required.
6671
if self._field_should_be_indexed(model, field):
6772
self._add_field_index(model, field)
73+
elif self._field_should_have_unique(field):
74+
self._add_field_unique(model, field)
6875

6976
def _alter_field(
7077
self,
@@ -110,6 +117,8 @@ def remove_field(self, model, field):
110117
self.get_collection(model._meta.db_table).update_many({}, {"$unset": {column: ""}})
111118
if self._field_should_be_indexed(model, field):
112119
self._remove_field_index(model, field)
120+
elif self._field_should_have_unique(field):
121+
self._remove_field_unique(model, field)
113122

114123
def alter_index_together(self, model, old_index_together, new_index_together):
115124
olds = {tuple(fields) for fields in old_index_together}
@@ -124,9 +133,21 @@ def alter_index_together(self, model, old_index_together, new_index_together):
124133
def alter_unique_together(self, model, old_unique_together, new_unique_together):
125134
pass
126135

127-
def add_index(self, model, index, field=None):
136+
def add_index(self, model, index, field=None, unique=False):
128137
if index.contains_expressions:
129138
return
139+
kwargs = {}
140+
if unique:
141+
filter_expression = {}
142+
if field:
143+
filter_expression[field.column] = {"$type": field.db_type(self.connection)}
144+
else:
145+
for field_name, _ in index.fields_orders:
146+
field_ = model._meta.get_field(field_name)
147+
filter_expression[field_.column] = {"$type": field_.db_type(self.connection)}
148+
# Use partialFilterExpression to allow multiple null values for a
149+
# unique constraint.
150+
kwargs = {"partialFilterExpression": filter_expression, "unique": True}
130151
index_orders = (
131152
[(field.column, ASCENDING)]
132153
if field
@@ -137,7 +158,7 @@ def add_index(self, model, index, field=None):
137158
for field_name, order in index.fields_orders
138159
]
139160
)
140-
idx = IndexModel(index_orders, name=index.name)
161+
idx = IndexModel(index_orders, name=index.name, **kwargs)
141162
self.get_collection(model._meta.db_table).create_indexes([idx])
142163

143164
def _add_composed_index(self, model, field_names):
@@ -202,13 +223,57 @@ def _remove_field_index(self, model, field):
202223
)
203224
collection.drop_index(index_names[0])
204225

205-
def add_constraint(self, model, constraint):
206-
pass
226+
def add_constraint(self, model, constraint, field=None):
227+
if isinstance(constraint, UniqueConstraint) and self._unique_supported(
228+
condition=constraint.condition,
229+
deferrable=constraint.deferrable,
230+
include=constraint.include,
231+
expressions=constraint.expressions,
232+
nulls_distinct=constraint.nulls_distinct,
233+
):
234+
idx = Index(fields=constraint.fields, name=constraint.name)
235+
self.add_index(model, idx, field=field, unique=True)
236+
237+
def _add_field_unique(self, model, field):
238+
name = str(self._unique_constraint_name(model._meta.db_table, [field.column]))
239+
constraint = UniqueConstraint(fields=[field.name], name=name)
240+
self.add_constraint(model, constraint, field=field)
207241

208242
def remove_constraint(self, model, constraint):
209-
pass
243+
if isinstance(constraint, UniqueConstraint) and self._unique_supported(
244+
condition=constraint.condition,
245+
deferrable=constraint.deferrable,
246+
include=constraint.include,
247+
expressions=constraint.expressions,
248+
nulls_distinct=constraint.nulls_distinct,
249+
):
250+
idx = Index(fields=constraint.fields, name=constraint.name)
251+
self.remove_index(model, idx)
252+
253+
def _remove_field_unique(self, model, field):
254+
# Find the unique constraint for this field
255+
meta_constraint_names = {constraint.name for constraint in model._meta.constraints}
256+
constraint_names = self._constraint_names(
257+
model,
258+
[field.column],
259+
unique=True,
260+
primary_key=False,
261+
exclude=meta_constraint_names,
262+
)
263+
if len(constraint_names) != 1:
264+
num_found = len(constraint_names)
265+
raise ValueError(
266+
f"Found wrong number ({num_found}) of unique constraints for "
267+
f"{model._meta.db_table}.{field.column}."
268+
)
269+
self.get_collection(model._meta.db_table).drop_index(constraint_names[0])
210270

211271
def alter_db_table(self, model, old_db_table, new_db_table):
212272
if old_db_table == new_db_table:
213273
return
214274
self.get_collection(old_db_table).rename(new_db_table)
275+
276+
def _field_should_have_unique(self, field):
277+
db_type = field.db_type(self.connection)
278+
# The _id column is automatically unique.
279+
return db_type and field.unique and field.column != "_id"

0 commit comments

Comments
 (0)