Skip to content

Commit db20051

Browse files
authored
Remove unique constraints (fixes #100 and #104) (#106)
* Add support for unique_constraint to introspection, and use it to determine if we should use DROP CONSTRAINT or DROP INDEX when altering or removing a unique (together) constraint. Fixes #100 and #104. In the sys.indexes table, is_unique_constraint is true only when an actual constraint was created (using ALTER TABLE ... ADD CONSTRAINT ... UNIQUE). Because this method is not suitable for nullable fields in practice (you cannot have more than one row with NULL), mssql-django always creates CREATE UNIQUE INDEX instead. django-pyodbc-azure behaved differently and used a unique constraint whenever possible. The problem that arises is that mssql-django assumes that an index is used to enforce all unique constraints, and always uses DROP INDEX to remove it. When migrating a codebase from django-pyodbc-azure to mssql-django, this fails because the database contains actual unique constraints that need to be dropped using "ALTER TABLE ... DROP CONSTRAINT ...". This commit adds support for is_unique_constraint to the introspection, so we can determine if the constraint is enforced by an actual SQL Server constraint or by a unique index. Additionally, places that delete unique constraints have been refactored to use a common function that uses introspection to determine the proper method of deletion. * Also treat primary keys as constraints instead of as indexes. Co-authored-by: Ruben De Visscher <[email protected]>
1 parent d01c01e commit db20051

File tree

2 files changed

+42
-28
lines changed

2 files changed

+42
-28
lines changed

mssql/introspection.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,14 @@ def get_constraints(self, cursor, table_name):
253253
constraints[constraint] = {
254254
"columns": [],
255255
"primary_key": kind.lower() == "primary key",
256+
# In the sys.indexes table, primary key indexes have is_unique_constraint as false,
257+
# but is_unique as true.
256258
"unique": kind.lower() in ["primary key", "unique"],
259+
"unique_constraint": kind.lower() == "unique",
257260
"foreign_key": (ref_table, ref_column) if kind.lower() == "foreign key" else None,
258261
"check": False,
262+
# Potentially misleading: primary key and unique constraints still have indexes attached to them.
263+
# Should probably be updated with the additional info from the sys.indexes table we fetch later on.
259264
"index": False,
260265
}
261266
# Record the details
@@ -280,6 +285,7 @@ def get_constraints(self, cursor, table_name):
280285
"columns": [],
281286
"primary_key": False,
282287
"unique": False,
288+
"unique_constraint": False,
283289
"foreign_key": None,
284290
"check": True,
285291
"index": False,
@@ -291,6 +297,7 @@ def get_constraints(self, cursor, table_name):
291297
SELECT
292298
i.name AS index_name,
293299
i.is_unique,
300+
i.is_unique_constraint,
294301
i.is_primary_key,
295302
i.type,
296303
i.type_desc,
@@ -316,12 +323,13 @@ def get_constraints(self, cursor, table_name):
316323
ic.index_column_id ASC
317324
""", [table_name])
318325
indexes = {}
319-
for index, unique, primary, type_, desc, order, column in cursor.fetchall():
326+
for index, unique, unique_constraint, primary, type_, desc, order, column in cursor.fetchall():
320327
if index not in indexes:
321328
indexes[index] = {
322329
"columns": [],
323330
"primary_key": primary,
324331
"unique": unique,
332+
"unique_constraint": unique_constraint,
325333
"foreign_key": None,
326334
"check": False,
327335
"index": True,

mssql/schema.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,12 @@ def alter_unique_together(self, model, old_unique_together, new_unique_together)
171171
news = {tuple(fields) for fields in new_unique_together}
172172
# Deleted uniques
173173
for fields in olds.difference(news):
174-
self._delete_composed_index(model, fields, {'unique': True}, self.sql_delete_index)
174+
meta_constraint_names = {constraint.name for constraint in model._meta.constraints}
175+
meta_index_names = {constraint.name for constraint in model._meta.indexes}
176+
columns = [model._meta.get_field(field).column for field in fields]
177+
self._delete_unique_constraint_for_columns(
178+
model, columns, exclude=meta_constraint_names | meta_index_names, strict=True)
179+
175180
# Created uniques
176181
if django_version >= (4, 0):
177182
for field_names in news.difference(olds):
@@ -227,7 +232,7 @@ def _model_indexes_sql(self, model):
227232

228233
def _db_table_constraint_names(self, db_table, column_names=None, column_match_any=False,
229234
unique=None, primary_key=None, index=None, foreign_key=None,
230-
check=None, type_=None, exclude=None):
235+
check=None, type_=None, exclude=None, unique_constraint=None):
231236
"""
232237
Return all constraint names matching the columns and conditions. Modified from base `_constraint_names`
233238
`any_column_matches`=False: (default) only return constraints covering exactly `column_names`
@@ -247,6 +252,8 @@ def _db_table_constraint_names(self, db_table, column_names=None, column_match_a
247252
):
248253
if unique is not None and infodict['unique'] != unique:
249254
continue
255+
if unique_constraint is not None and infodict['unique_constraint'] != unique_constraint:
256+
continue
250257
if primary_key is not None and infodict['primary_key'] != primary_key:
251258
continue
252259
if index is not None and infodict['index'] != index:
@@ -299,16 +306,7 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
299306
self.execute(self._delete_constraint_sql(self.sql_delete_fk, model, fk_name))
300307
# Has unique been removed?
301308
if old_field.unique and (not new_field.unique or self._field_became_primary_key(old_field, new_field)):
302-
# Find the unique constraint for this field
303-
constraint_names = self._constraint_names(model, [old_field.column], unique=True, primary_key=False)
304-
if strict and len(constraint_names) != 1:
305-
raise ValueError("Found wrong number (%s) of unique constraints for %s.%s" % (
306-
len(constraint_names),
307-
model._meta.db_table,
308-
old_field.column,
309-
))
310-
for constraint_name in constraint_names:
311-
self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_name))
309+
self._delete_unique_constraint_for_columns(model, [old_field.column], strict=strict)
312310
# Drop incoming FK constraints if the field is a primary key or unique,
313311
# which might be a to_field target, and things are going to change.
314312
drop_foreign_keys = (
@@ -694,21 +692,29 @@ def _delete_unique_constraints(self, model, old_field, new_field, strict=False):
694692
unique_columns.append([old_field.column])
695693
if unique_columns:
696694
for columns in unique_columns:
697-
constraint_names_normal = self._constraint_names(model, columns, unique=True, index=False)
698-
constraint_names_index = self._constraint_names(model, columns, unique=True, index=True)
699-
constraint_names = constraint_names_normal + constraint_names_index
700-
if strict and len(constraint_names) != 1:
701-
raise ValueError("Found wrong number (%s) of unique constraints for %s.%s" % (
702-
len(constraint_names),
703-
model._meta.db_table,
704-
old_field.column,
705-
))
706-
for constraint_name in constraint_names_normal:
707-
self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_name))
708-
# Unique indexes which are not table constraints must be deleted using the appropriate SQL.
709-
# These may exist for example to enforce ANSI-compliant unique constraints on nullable columns.
710-
for index_name in constraint_names_index:
711-
self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_name))
695+
self._delete_unique_constraint_for_columns(model, columns, strict=strict)
696+
697+
def _delete_unique_constraint_for_columns(self, model, columns, strict=False, **constraint_names_kwargs):
698+
constraint_names_unique = self._db_table_constraint_names(
699+
model._meta.db_table, columns, unique=True, unique_constraint=True, **constraint_names_kwargs)
700+
constraint_names_primary = self._db_table_constraint_names(
701+
model._meta.db_table, columns, unique=True, primary_key=True, **constraint_names_kwargs)
702+
constraint_names_normal = constraint_names_unique + constraint_names_primary
703+
constraint_names_index = self._db_table_constraint_names(
704+
model._meta.db_table, columns, unique=True, unique_constraint=False, primary_key=False,
705+
**constraint_names_kwargs)
706+
constraint_names = constraint_names_normal + constraint_names_index
707+
if strict and len(constraint_names) != 1:
708+
raise ValueError("Found wrong number (%s) of unique constraints for columns %s" % (
709+
len(constraint_names),
710+
repr(columns),
711+
))
712+
for constraint_name in constraint_names_normal:
713+
self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_name))
714+
# Unique indexes which are not table constraints must be deleted using the appropriate SQL.
715+
# These may exist for example to enforce ANSI-compliant unique constraints on nullable columns.
716+
for index_name in constraint_names_index:
717+
self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_name))
712718

713719
def _rename_field_sql(self, table, old_field, new_field, new_type):
714720
new_type = self._set_field_new_type_null_status(old_field, new_type)

0 commit comments

Comments
 (0)