diff --git a/README.rst b/README.rst index b563110f..b2b8e4e9 100644 --- a/README.rst +++ b/README.rst @@ -193,6 +193,11 @@ Dictionary. Current available keys are: Integer. Sets the back off time in seconds for reries of the database connection process. Default value is ``5``. +- supports_nullable_unique_constraints + + Boolean. Supports nullable unique constraints. + Default value is ``False``. + backend-specific settings ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/sql_server/pyodbc/base.py b/sql_server/pyodbc/base.py index 2194e37d..65a041fe 100644 --- a/sql_server/pyodbc/base.py +++ b/sql_server/pyodbc/base.py @@ -201,7 +201,7 @@ def __init__(self, *args, **kwargs): ops[op] = '%s COLLATE %s' % (sql, collation) self.operators.update(ops) - self.features = DatabaseFeatures(self) + self.features = DatabaseFeatures(self, opts.get('supports_nullable_unique_constraints', False)) self.ops = DatabaseOperations(self) self.client = DatabaseClient(self) self.creation = DatabaseCreation(self) diff --git a/sql_server/pyodbc/features.py b/sql_server/pyodbc/features.py index 06f6286a..5316babf 100644 --- a/sql_server/pyodbc/features.py +++ b/sql_server/pyodbc/features.py @@ -30,3 +30,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_timezones = False supports_transactions = True uses_savepoints = True + + def __init__(self, connection, supports_nullable_unique_constraints=False): + BaseDatabaseFeatures.__init__(self, connection) + self.supports_nullable_unique_constraints = supports_nullable_unique_constraints diff --git a/sql_server/pyodbc/schema.py b/sql_server/pyodbc/schema.py index 5b4c1a4a..2c5c9e84 100644 --- a/sql_server/pyodbc/schema.py +++ b/sql_server/pyodbc/schema.py @@ -51,6 +51,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) @@ -269,7 +271,14 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, 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.null and self.connection.features.supports_nullable_unique_constraints: + 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? if (not old_field.db_index and new_field.db_index and not new_field.unique and not @@ -398,6 +407,13 @@ 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 and self.connection.features.supports_nullable_unique_constraints: + 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']: @@ -452,6 +468,13 @@ def create_model(self, model): definition, extra_params = self.column_sql(model, field) if definition is None: continue + + if field.null and field.unique and self.connection.features.supports_nullable_unique_constraints: + 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']: