diff --git a/sql_server/pyodbc/features.py b/sql_server/pyodbc/features.py index f8056e01..823b7ee9 100644 --- a/sql_server/pyodbc/features.py +++ b/sql_server/pyodbc/features.py @@ -16,7 +16,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): ignores_quoted_identifier_case = True requires_literal_defaults = True requires_sqlparse_for_splitting = 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 b97efeef..20b73aed 100644 --- a/sql_server/pyodbc/schema.py +++ b/sql_server/pyodbc/schema.py @@ -52,6 +52,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_default_sql(self, model, old_field, new_field, drop=False): """ @@ -202,6 +204,11 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, # is to look at its name (refs #28053). continue self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_name)) + # Drop any unique nullable index/constraints, we'll remake them later if need be + if old_field.unique and old_field.null: + index_names = self._constraint_names(model, [old_field.column], unique=True, index=True) + for index_name in index_names: + self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_name)) # Change check constraints? if old_db_params['check'] != new_db_params['check'] and old_db_params['check']: constraint_names = self._constraint_names(model, [old_field.column], check=True) @@ -307,9 +314,13 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, if post_actions: for sql, params in post_actions: self.execute(sql, params) - # Added a unique? - if not old_field.unique and new_field.unique: - self.execute(self._create_unique_sql(model, [new_field.column])) + if new_field.unique: + if new_field.null: + self.execute( + self._create_index_sql(model, [new_field], sql=self.sql_create_unique_null, suffix="_uniq") + ) + elif not old_field.unique: + 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: @@ -483,6 +494,10 @@ def add_field(self, model, field): # It might not actually have a column behind it if definition is None: return + if 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']: @@ -525,6 +540,10 @@ def create_model(self, model): definition, extra_params = self.column_sql(model, field) if definition is None: continue + if 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']: @@ -709,7 +728,7 @@ def remove_field(self, model, field): }) # Drop unique constraints, SQL Server requires explicit deletion for name, infodict in constraints.items(): - if field.column in infodict['columns'] and infodict['unique'] and not infodict['primary_key']: + if field.column in infodict['columns'] and infodict['unique'] and not infodict['primary_key'] and not infodict['index']: self.execute(self.sql_delete_unique % { "table": self.quote_name(model._meta.db_table), "name": self.quote_name(name),