Skip to content

Commit 5a421fc

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 37af0e3 commit 5a421fc

File tree

2 files changed

+75
-40
lines changed

2 files changed

+75
-40
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
@@ -76,28 +79,15 @@ class DatabaseFeatures(BaseDatabaseFeatures):
7679
"backends.tests.ThreadTests.test_pass_connection_between_threads",
7780
"backends.tests.ThreadTests.test_closing_non_shared_connections",
7881
"backends.tests.ThreadTests.test_default_connection_thread_local",
79-
# AddField
80-
"schema.tests.SchemaTests.test_add_unique_charfield",
8182
# AlterField
8283
"schema.tests.SchemaTests.test_alter_field_fk_to_o2o",
83-
"schema.tests.SchemaTests.test_alter_field_o2o_keeps_unique",
8484
"schema.tests.SchemaTests.test_alter_field_o2o_to_fk",
85-
"schema.tests.SchemaTests.test_alter_int_pk_to_int_unique",
8685
# AlterField (unique)
8786
"schema.tests.SchemaTests.test_indexes",
8887
"schema.tests.SchemaTests.test_unique",
89-
"schema.tests.SchemaTests.test_unique_and_reverse_m2m",
9088
# alter_unique_together
9189
"migrations.test_operations.OperationTests.test_alter_unique_together",
9290
"schema.tests.SchemaTests.test_unique_together",
93-
# add/remove_constraint
94-
"introspection.tests.IntrospectionTests.test_get_constraints",
95-
"migrations.test_operations.OperationTests.test_add_partial_unique_constraint",
96-
"migrations.test_operations.OperationTests.test_create_model_with_partial_unique_constraint",
97-
"migrations.test_operations.OperationTests.test_remove_partial_unique_constraint",
98-
"schema.tests.SchemaTests.test_composed_constraint_with_fk",
99-
"schema.tests.SchemaTests.test_remove_ignored_unique_constraint_not_create_fk_index",
100-
"schema.tests.SchemaTests.test_unique_constraint",
10191
}
10292
# $bitAnd, #bitOr, and $bitXor are new in MongoDB 6.3.
10393
_django_test_expected_failures_bitwise = {
@@ -188,24 +178,6 @@ def django_test_expected_failures(self):
188178
"model_fields.test_autofield.SmallAutoFieldTests",
189179
"queries.tests.TestInvalidValuesRelation.test_invalid_values",
190180
},
191-
"MongoDB does not enforce UNIQUE constraints.": {
192-
"auth_tests.test_basic.BasicTestCase.test_unicode_username",
193-
"auth_tests.test_migrations.ProxyModelWithSameAppLabelTests.test_migrate_with_existing_target_permission",
194-
"constraints.tests.UniqueConstraintTests.test_database_constraint",
195-
"contenttypes_tests.test_operations.ContentTypeOperationsTests.test_content_type_rename_conflict",
196-
"contenttypes_tests.test_operations.ContentTypeOperationsTests.test_existing_content_type_rename",
197-
"custom_pk.tests.CustomPKTests.test_unique_pk",
198-
"force_insert_update.tests.ForceInsertInheritanceTests.test_force_insert_with_existing_grandparent",
199-
"get_or_create.tests.GetOrCreateTestsWithManualPKs.test_create_with_duplicate_primary_key",
200-
"get_or_create.tests.GetOrCreateTestsWithManualPKs.test_savepoint_rollback",
201-
"get_or_create.tests.GetOrCreateThroughManyToMany.test_something",
202-
"get_or_create.tests.UpdateOrCreateTests.test_manual_primary_key_test",
203-
"get_or_create.tests.UpdateOrCreateTestsWithManualPKs.test_create_with_duplicate_primary_key",
204-
"introspection.tests.IntrospectionTests.test_get_constraints_unique_indexes_orders",
205-
"model_fields.test_filefield.FileFieldTests.test_unique_when_same_filename",
206-
"one_to_one.tests.OneToOneTests.test_multiple_o2o",
207-
"queries.test_bulk_update.BulkUpdateTests.test_database_routing_batch_atomicity",
208-
},
209181
"MongoDB does not enforce PositiveIntegerField constraint.": {
210182
"model_fields.test_integerfield.PositiveIntegerFieldTests.test_negative_values",
211183
},

django_mongodb/schema.py

Lines changed: 72 additions & 9 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.operations import IndexModel
44

55
from .query import wrap_database_errors
@@ -17,18 +17,23 @@ def create_model(self, model):
1717

1818
def _create_model_indexes(self, model):
1919
"""
20-
Create all indexes (field indexes, index_together, Meta.indexes) for
21-
the specified model.
20+
Create all indexes (field indexes & uniques, index_together,
21+
Meta.constraints, Meta.indexes) for the specified model.
2222
"""
2323
if not model._meta.managed or model._meta.proxy or model._meta.swapped:
2424
return
25-
# Field indexes
25+
# Field indexes and uniques
2626
for field in model._meta.local_fields:
2727
if self._field_should_be_indexed(model, field):
2828
self._add_field_index(model, field)
29+
elif self._field_should_have_unique(field):
30+
self._add_field_unique(model, field)
2931
# Meta.index_together (RemovedInDjango51Warning)
3032
for field_names in model._meta.index_together:
3133
self._add_composed_index(model, field_names)
34+
# Meta.constraints
35+
for constraint in model._meta.constraints:
36+
self.add_constraint(model, constraint)
3237
# Meta.indexes
3338
for index in model._meta.indexes:
3439
self.add_index(model, index)
@@ -50,9 +55,11 @@ def add_field(self, model, field):
5055
self.connection.database[model._meta.db_table].update_many(
5156
{}, [{"$set": {column: self.effective_default(field)}}]
5257
)
53-
# Add an index, if required.
58+
# Add an index or unique, if required.
5459
if self._field_should_be_indexed(model, field):
5560
self._add_field_index(model, field)
61+
elif self._field_should_have_unique(field):
62+
self._add_field_unique(model, field)
5663

5764
def _alter_field(
5865
self,
@@ -102,6 +109,8 @@ def remove_field(self, model, field):
102109
self.connection.database[model._meta.db_table].update_many({}, {"$unset": {column: ""}})
103110
if self._field_should_be_indexed(model, field):
104111
self._remove_field_index(model, field)
112+
elif self._field_should_have_unique(field):
113+
self._remove_field_unique(model, field)
105114

106115
def alter_index_together(self, model, old_index_together, new_index_together):
107116
olds = {tuple(fields) for fields in old_index_together}
@@ -116,9 +125,19 @@ def alter_index_together(self, model, old_index_together, new_index_together):
116125
def alter_unique_together(self, model, old_unique_together, new_unique_together):
117126
pass
118127

119-
def add_index(self, model, index, field=None):
128+
def add_index(self, model, index, field=None, unique=False):
120129
if index.contains_expressions:
121130
return
131+
kwargs = {}
132+
if unique:
133+
filter_expression = {}
134+
if field:
135+
filter_expression[field.column] = {"$type": field.db_type(self.connection)}
136+
else:
137+
for field_name, _ in index.fields_orders:
138+
field_ = model._meta.get_field(field_name)
139+
filter_expression[field_.column] = {"$type": field_.db_type(self.connection)}
140+
kwargs = {"partialFilterExpression": filter_expression, "unique": True}
122141
index_orders = (
123142
[(field.column, 1)]
124143
if field
@@ -130,6 +149,7 @@ def add_index(self, model, index, field=None):
130149
idx = IndexModel(
131150
index_orders,
132151
name=index.name,
152+
**kwargs,
133153
)
134154
self.connection.database[model._meta.db_table].create_indexes([idx])
135155

@@ -196,13 +216,56 @@ def _remove_field_index(self, model, field):
196216
# is to look at its name (refs #28053).
197217
collection.drop_index(index_name)
198218

199-
def add_constraint(self, model, constraint):
200-
pass
219+
def add_constraint(self, model, constraint, field=None):
220+
if isinstance(constraint, UniqueConstraint) and self._unique_supported(
221+
condition=constraint.condition,
222+
deferrable=constraint.deferrable,
223+
include=constraint.include,
224+
expressions=constraint.expressions,
225+
nulls_distinct=constraint.nulls_distinct,
226+
):
227+
idx = Index(fields=constraint.fields, name=constraint.name)
228+
self.add_index(model, idx, field=field, unique=True)
229+
230+
def _add_field_unique(self, model, field):
231+
constraint = UniqueConstraint(
232+
fields=[field.name], name=f"{model._meta.db_table}_{field.column}_key"
233+
)
234+
self.add_constraint(model, constraint, field=field)
201235

202236
def remove_constraint(self, model, constraint):
203-
pass
237+
if isinstance(constraint, UniqueConstraint) and self._unique_supported(
238+
condition=constraint.condition,
239+
deferrable=constraint.deferrable,
240+
include=constraint.include,
241+
expressions=constraint.expressions,
242+
nulls_distinct=constraint.nulls_distinct,
243+
):
244+
idx = Index(fields=constraint.fields, name=constraint.name)
245+
self.remove_index(model, idx)
246+
247+
def _remove_field_unique(self, model, field, strict=True):
248+
# Find the unique constraint for this field
249+
meta_constraint_names = {constraint.name for constraint in model._meta.constraints}
250+
constraint_names = self._constraint_names(
251+
model,
252+
[field.column],
253+
unique=True,
254+
primary_key=False,
255+
exclude=meta_constraint_names,
256+
)
257+
if strict and len(constraint_names) != 1:
258+
raise ValueError(
259+
f"Found wrong number ({len(constraint_names)}) of unique "
260+
f"constraints for {model._meta.db_table}.{field.column}"
261+
)
262+
for constraint_name in constraint_names:
263+
self.connection.database[model._meta.db_table].drop_index(constraint_name)
204264

205265
def alter_db_table(self, model, old_db_table, new_db_table):
206266
if old_db_table == new_db_table:
207267
return
208268
self.connection.database[old_db_table].rename(new_db_table)
269+
270+
def _field_should_have_unique(self, field):
271+
return field.unique and field.column != "_id"

0 commit comments

Comments
 (0)