diff --git a/sql_server/pyodbc/features.py b/sql_server/pyodbc/features.py index 18945f4d..1530f2f9 100644 --- a/sql_server/pyodbc/features.py +++ b/sql_server/pyodbc/features.py @@ -20,7 +20,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): requires_literal_defaults = True requires_sqlparse_for_splitting = False supports_1000_query_parameters = False - supports_nullable_unique_constraints = False + supports_nullable_unique_constraints = True supports_paramstyle_pyformat = False supports_partially_nullable_unique_constraints = False supports_regex_backreferencing = False diff --git a/sql_server/pyodbc/schema.py b/sql_server/pyodbc/schema.py index fa99cdb6..86e15faa 100644 --- a/sql_server/pyodbc/schema.py +++ b/sql_server/pyodbc/schema.py @@ -50,6 +50,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_delete_table = "DROP TABLE %(table)s" sql_rename_column = "EXEC sp_rename '%(table)s.%(old_column)s', %(new_column)s, 'COLUMN'" sql_rename_table = "EXEC sp_rename %(old_table)s, %(new_table)s" + sql_create_unique_null = "CREATE UNIQUE INDEX %(name)s ON %(table)s(%(columns)s) " \ + "WHERE %(columns)s IS NOT NULL" def _alter_column_type_sql(self, table, old_field, new_field, new_type): new_type = self._set_field_new_type_null_status(old_field, new_type) @@ -280,7 +282,14 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, if (not old_field.unique and new_field.unique) or ( old_field.primary_key and not new_field.primary_key and new_field.unique ): - self.execute(self._create_unique_sql(model, [new_field.column])) + if not new_field.many_to_many and new_field.null: + self.execute( + self._create_index_sql( + model, [new_field], sql=self.sql_create_unique_null, suffix="_uniq" + ) + ) + else: + self.execute(self._create_unique_sql(model, [new_field.column])) # Added an index? # constraint will no longer be used in lieu of an index. The following # lines from the truth table show all True cases; the rest are False: @@ -473,6 +482,13 @@ def add_field(self, model, field): # It might not actually have a column behind it if definition is None: return + + if not field.many_to_many and field.null and field.unique: + definition = definition.replace(' UNIQUE', '') + self.deferred_sql.append(self._create_index_sql( + model, [field], sql=self.sql_create_unique_null, suffix="_uniq" + )) + # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) if db_params['check']: @@ -527,6 +543,13 @@ def create_model(self, model): definition, extra_params = self.column_sql(model, field) if definition is None: continue + + if not field.many_to_many and field.null and field.unique: + definition = definition.replace(' UNIQUE', '') + self.deferred_sql.append(self._create_index_sql( + model, [field], sql=self.sql_create_unique_null, suffix="_uniq" + )) + # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) if db_params['check']: