From 4e280b43206fda753bfe7b89aff75edd139aa2ad Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Tue, 7 Nov 2023 13:14:52 -0800 Subject: [PATCH 01/13] Support db_default field --- mssql/base.py | 2 +- mssql/compiler.py | 3 +- mssql/features.py | 3 ++ mssql/introspection.py | 28 +++++++++++++++ mssql/schema.py | 80 +++++++++++++++++++++++++++++++++++++++--- 5 files changed, 109 insertions(+), 7 deletions(-) diff --git a/mssql/base.py b/mssql/base.py index cf11c506..eb7d3b7d 100644 --- a/mssql/base.py +++ b/mssql/base.py @@ -581,7 +581,7 @@ def format_sql(self, sql, params): sql = smart_str(sql, self.driver_charset) # pyodbc uses '?' instead of '%s' as parameter placeholder. - if params is not None: + if params is not None and params: sql = sql % tuple('?' * len(params)) return sql diff --git a/mssql/compiler.py b/mssql/compiler.py index 2dfe1b6c..95b4904e 100644 --- a/mssql/compiler.py +++ b/mssql/compiler.py @@ -589,10 +589,11 @@ def as_sql(self): params += param_rows result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows)) else: + r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields()) result.insert(0, 'SET NOCOUNT ON') + result.append(r_sql) result.append((values_format + ';') % ', '.join(placeholder_rows[0])) params = [param_rows[0]] - result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)') sql = [(" ".join(result), tuple(chain.from_iterable(params)))] else: if can_bulk: diff --git a/mssql/features.py b/mssql/features.py index faaa0bbb..c1e90849 100644 --- a/mssql/features.py +++ b/mssql/features.py @@ -56,6 +56,9 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_partially_nullable_unique_constraints = True supports_partial_indexes = True supports_functions_in_partial_indexes = True + supports_default_keyword_in_insert = True + supports_expression_defaults = True + supports_default_keyword_in_bulk_insert = True @cached_property def has_zoneinfo_database(self): diff --git a/mssql/introspection.py b/mssql/introspection.py index 5f33bff9..b8280557 100644 --- a/mssql/introspection.py +++ b/mssql/introspection.py @@ -275,6 +275,7 @@ def get_constraints(self, cursor, table_name): # Potentially misleading: primary key and unique constraints still have indexes attached to them. # Should probably be updated with the additional info from the sys.indexes table we fetch later on. "index": False, + "default": False, } # Record the details constraints[constraint]['columns'].append(column) @@ -302,6 +303,32 @@ def get_constraints(self, cursor, table_name): "foreign_key": None, "check": True, "index": False, + "default": False, + } + # Record the details + constraints[constraint]['columns'].append(column) + # Now get DEFAULT constraint columns + cursor.execute(f""" + SELECT d.name AS constraint_name, pc.name AS column_name + FROM sys.default_constraints AS d + INNER JOIN sys.columns pc ON + d.parent_object_id = pc.object_id AND + d.parent_column_id = pc.column_id + WHERE + type_desc = 'DEFAULT_CONSTRAINT' + """) + for constraint, column in cursor.fetchall(): + # If we're the first column, make the record + if constraint not in constraints: + constraints[constraint] = { + "columns": [], + "primary_key": False, + "unique": False, + "unique_constraint": False, + "foreign_key": None, + "check": False, + "index": False, + "default": True, } # Record the details constraints[constraint]['columns'].append(column) @@ -345,6 +372,7 @@ def get_constraints(self, cursor, table_name): "unique_constraint": unique_constraint, "foreign_key": None, "check": False, + "default": False, "index": True, "orders": [], "type": Index.suffix if type_ in (1, 2) else desc.lower(), diff --git a/mssql/schema.py b/mssql/schema.py index 3cf1e110..6c5ad4de 100644 --- a/mssql/schema.py +++ b/mssql/schema.py @@ -19,7 +19,7 @@ Table, ) from django import VERSION as django_version -from django.db.models import Index, UniqueConstraint +from django.db.models import NOT_PROVIDED, Index, UniqueConstraint from django.db.models.fields import AutoField, BigAutoField from django.db.models.sql.where import AND from django.db.transaction import TransactionManagementError @@ -69,6 +69,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_alter_column_type = "ALTER COLUMN %(column)s %(type)s" sql_create_column = "ALTER TABLE %(table)s ADD %(column)s %(definition)s" sql_delete_column = "ALTER TABLE %(table)s DROP COLUMN %(column)s" + sql_delete_default = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s" sql_delete_index = "DROP INDEX %(name)s ON %(table)s" sql_delete_table = """ DECLARE @sql_foreign_constraint_name nvarchar(128) @@ -138,6 +139,48 @@ def _alter_column_default_sql(self, model, old_field, new_field, drop=False): }, params, ) + + def _alter_column_database_default_sql( + self, model, old_field, new_field, drop=False + ): + """ + Hook to specialize column database default alteration. + + Return a (sql, params) fragment to add or drop (depending on the drop + argument) a default to new_field's column. + """ + column = self.quote_name(new_field.column) + + if drop: + # SQL Server requires the name of the default constraint + result = self.execute( + self._sql_select_default_constraint_name % { + "table": self.quote_value(model._meta.db_table), + "column": self.quote_value(new_field.column), + }, + has_result=True + ) + if result: + for row in result: + column = self.quote_name(next(iter(row))) + + sql = self.sql_alter_column_no_default + default_sql = "" + params = [] + else: + sql = self.sql_alter_column_default + default_sql, params = self.db_default_sql(new_field) + + new_db_params = new_field.db_parameters(connection=self.connection) + return ( + sql + % { + "column": column, + "type": new_db_params["type"], + "default": default_sql, + }, + params, + ) def _alter_column_null_sql(self, model, old_field, new_field): """ @@ -460,6 +503,20 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, self._delete_unique_constraints(model, old_field, new_field, strict) # Drop indexes, SQL Server requires explicit deletion self._delete_indexes(model, old_field, new_field) + if new_field.db_default is not NOT_PROVIDED: + if ( + old_field.db_default is NOT_PROVIDED + or new_field.db_default != old_field.db_default + ): + actions.append( + self._alter_column_database_default_sql(model, old_field, new_field) + ) + elif old_field.db_default is not NOT_PROVIDED: + actions.append( + self._alter_column_database_default_sql( + model, old_field, new_field, drop=True + ) + ) # When changing a column NULL constraint to NOT NULL with a given # default value, we need to perform 4 steps: # 1. Add a default for new incoming writes @@ -474,7 +531,8 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, not new_field.null and old_default != new_default and new_default is not None and - not self.skip_default(new_field) + not self.skip_default(new_field) and + new_field.db_default is NOT_PROVIDED ) if needs_database_default: actions.append(self._alter_column_default_sql(model, old_field, new_field)) @@ -503,7 +561,7 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, post_actions.append((create_index_sql_statement, ())) # Only if we have a default and there is a change from NULL to NOT NULL four_way_default_alteration = ( - new_field.has_default() and + (new_field.has_default() or new_field.db_default is not NOT_PROVIDED) and (old_field.null and not new_field.null) ) if actions or null_actions: @@ -525,14 +583,19 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, params, ) if four_way_default_alteration: + if new_field.db_default is NOT_PROVIDED: + default_sql = "%s" + params = [new_default] + else: + default_sql, params = self.db_default_sql(new_field) # Update existing rows with default value self.execute( self.sql_update_with_default % { "table": self.quote_name(model._meta.db_table), "column": self.quote_name(new_field.column), - "default": "%s", + "default": default_sql, }, - [new_default], + params, ) # Since we didn't run a NOT NULL change before we need to do it # now @@ -1288,6 +1351,13 @@ def remove_field(self, model, field): "table": self.quote_name(model._meta.db_table), "name": self.quote_name(name), }) + # Drop default constraint, SQL Server requires explicit deletion + for name, infodict in constraints.items(): + if field.column in infodict['columns'] and infodict['default']: + self.execute(self.sql_delete_default % { + "table": self.quote_name(model._meta.db_table), + "name": self.quote_name(name), + }) # Delete the column sql = self.sql_delete_column % { "table": self.quote_name(model._meta.db_table), From b3538c2750d878c12513bba23585b241b59a18f2 Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Tue, 7 Nov 2023 14:18:32 -0800 Subject: [PATCH 02/13] Remove operations test fixes --- mssql/base.py | 2 +- mssql/compiler.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mssql/base.py b/mssql/base.py index eb7d3b7d..cf11c506 100644 --- a/mssql/base.py +++ b/mssql/base.py @@ -581,7 +581,7 @@ def format_sql(self, sql, params): sql = smart_str(sql, self.driver_charset) # pyodbc uses '?' instead of '%s' as parameter placeholder. - if params is not None and params: + if params is not None: sql = sql % tuple('?' * len(params)) return sql diff --git a/mssql/compiler.py b/mssql/compiler.py index 95b4904e..2dfe1b6c 100644 --- a/mssql/compiler.py +++ b/mssql/compiler.py @@ -589,11 +589,10 @@ def as_sql(self): params += param_rows result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows)) else: - r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields()) result.insert(0, 'SET NOCOUNT ON') - result.append(r_sql) result.append((values_format + ';') % ', '.join(placeholder_rows[0])) params = [param_rows[0]] + result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)') sql = [(" ".join(result), tuple(chain.from_iterable(params)))] else: if can_bulk: From 880bccdac63ae3eb67b4652d5f4db3fbabf6640c Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Tue, 7 Nov 2023 14:36:55 -0800 Subject: [PATCH 03/13] Preliminary fix --- mssql/base.py | 2 +- mssql/compiler.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mssql/base.py b/mssql/base.py index cf11c506..eb7d3b7d 100644 --- a/mssql/base.py +++ b/mssql/base.py @@ -581,7 +581,7 @@ def format_sql(self, sql, params): sql = smart_str(sql, self.driver_charset) # pyodbc uses '?' instead of '%s' as parameter placeholder. - if params is not None: + if params is not None and params: sql = sql % tuple('?' * len(params)) return sql diff --git a/mssql/compiler.py b/mssql/compiler.py index 2dfe1b6c..95b4904e 100644 --- a/mssql/compiler.py +++ b/mssql/compiler.py @@ -589,10 +589,11 @@ def as_sql(self): params += param_rows result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows)) else: + r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields()) result.insert(0, 'SET NOCOUNT ON') + result.append(r_sql) result.append((values_format + ';') % ', '.join(placeholder_rows[0])) params = [param_rows[0]] - result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)') sql = [(" ".join(result), tuple(chain.from_iterable(params)))] else: if can_bulk: From 023b8ca6d05db18ad58fd4e207231d2991c84faf Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Wed, 8 Nov 2023 13:45:38 -0800 Subject: [PATCH 04/13] Check version before accessing db_default --- mssql/schema.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/mssql/schema.py b/mssql/schema.py index 6c5ad4de..01c2ec60 100644 --- a/mssql/schema.py +++ b/mssql/schema.py @@ -503,20 +503,22 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, self._delete_unique_constraints(model, old_field, new_field, strict) # Drop indexes, SQL Server requires explicit deletion self._delete_indexes(model, old_field, new_field) - if new_field.db_default is not NOT_PROVIDED: - if ( - old_field.db_default is NOT_PROVIDED - or new_field.db_default != old_field.db_default - ): + # db_default change? + if django_version >= (5,0): + if new_field.db_default is not NOT_PROVIDED: + if ( + old_field.db_default is NOT_PROVIDED + or new_field.db_default != old_field.db_default + ): + actions.append( + self._alter_column_database_default_sql(model, old_field, new_field) + ) + elif old_field.db_default is not NOT_PROVIDED: actions.append( - self._alter_column_database_default_sql(model, old_field, new_field) - ) - elif old_field.db_default is not NOT_PROVIDED: - actions.append( - self._alter_column_database_default_sql( - model, old_field, new_field, drop=True + self._alter_column_database_default_sql( + model, old_field, new_field, drop=True + ) ) - ) # When changing a column NULL constraint to NOT NULL with a given # default value, we need to perform 4 steps: # 1. Add a default for new incoming writes @@ -531,9 +533,10 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, not new_field.null and old_default != new_default and new_default is not None and - not self.skip_default(new_field) and - new_field.db_default is NOT_PROVIDED + not self.skip_default(new_field) ) + if django_version >= (5,0): + needs_database_default = needs_database_default and new_field.db_default is NOT_PROVIDED if needs_database_default: actions.append(self._alter_column_default_sql(model, old_field, new_field)) # Nullability change? @@ -561,7 +564,7 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, post_actions.append((create_index_sql_statement, ())) # Only if we have a default and there is a change from NULL to NOT NULL four_way_default_alteration = ( - (new_field.has_default() or new_field.db_default is not NOT_PROVIDED) and + (new_field.has_default() or (django_version >= (5,0) and new_field.db_default is not NOT_PROVIDED)) and (old_field.null and not new_field.null) ) if actions or null_actions: @@ -583,11 +586,11 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, params, ) if four_way_default_alteration: - if new_field.db_default is NOT_PROVIDED: + if django_version >= (5,0) and new_field.db_default is not NOT_PROVIDED: + default_sql, params = self.db_default_sql(new_field) + else: default_sql = "%s" params = [new_default] - else: - default_sql, params = self.db_default_sql(new_field) # Update existing rows with default value self.execute( self.sql_update_with_default % { From 4d76857d9fa40afdb2406e95d695067cc5769833 Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Wed, 8 Nov 2023 15:51:35 -0800 Subject: [PATCH 05/13] Skip add field database default test --- testapp/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testapp/settings.py b/testapp/settings.py index f6a19593..642b8814 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -307,6 +307,7 @@ 'constraints.tests.CheckConstraintTests.test_validate_custom_error', 'constraints.tests.CheckConstraintTests.test_validate_nullable_jsonfield', 'constraints.tests.CheckConstraintTests.test_validate_pk_field', + 'migrations.test_operations.OperationTests.test_add_field_database_default', ] REGEX_TESTS = [ From acc440f8c9032090f14607a8ce4175552c74357c Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Fri, 10 Nov 2023 11:45:13 -0800 Subject: [PATCH 06/13] Update returned values from insert --- mssql/compiler.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mssql/compiler.py b/mssql/compiler.py index 95b4904e..b20fb713 100644 --- a/mssql/compiler.py +++ b/mssql/compiler.py @@ -589,11 +589,18 @@ def as_sql(self): params += param_rows result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows)) else: - r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields()) + has_into = 'INTO' in result[0] result.insert(0, 'SET NOCOUNT ON') - result.append(r_sql) + # Use 'OUTPUT' if query contains 'INTO' + if has_into: + r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields()) + if r_sql: + result.append(r_sql) + # Append values result.append((values_format + ';') % ', '.join(placeholder_rows[0])) params = [param_rows[0]] + if not has_into: + result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)') sql = [(" ".join(result), tuple(chain.from_iterable(params)))] else: if can_bulk: From 360004f728ff7977598291d7aca2de26e904f86d Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Fri, 17 Nov 2023 10:52:03 -0800 Subject: [PATCH 07/13] Change can_return_rows_from_bulk_insert default to True --- mssql/base.py | 2 +- mssql/compiler.py | 10 +--------- mssql/features.py | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/mssql/base.py b/mssql/base.py index eb7d3b7d..8f8c69e2 100644 --- a/mssql/base.py +++ b/mssql/base.py @@ -429,7 +429,7 @@ def init_connection_state(self): # Let user choose if driver can return rows from bulk insert since # inserting into tables with triggers causes errors. See issue #130 if (options.get('return_rows_bulk_insert', False)): - self.features_class.can_return_rows_from_bulk_insert = True + self.features_class.can_return_rows_from_bulk_insert = False val = self.get_system_datetime if isinstance(val, str): diff --git a/mssql/compiler.py b/mssql/compiler.py index b20fb713..2dfe1b6c 100644 --- a/mssql/compiler.py +++ b/mssql/compiler.py @@ -589,18 +589,10 @@ def as_sql(self): params += param_rows result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows)) else: - has_into = 'INTO' in result[0] result.insert(0, 'SET NOCOUNT ON') - # Use 'OUTPUT' if query contains 'INTO' - if has_into: - r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields()) - if r_sql: - result.append(r_sql) - # Append values result.append((values_format + ';') % ', '.join(placeholder_rows[0])) params = [param_rows[0]] - if not has_into: - result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)') + result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)') sql = [(" ".join(result), tuple(chain.from_iterable(params)))] else: if can_bulk: diff --git a/mssql/features.py b/mssql/features.py index c1e90849..bb233395 100644 --- a/mssql/features.py +++ b/mssql/features.py @@ -13,7 +13,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): can_introspect_small_integer_field = True can_return_columns_from_insert = True can_return_id_from_insert = True - can_return_rows_from_bulk_insert = False + can_return_rows_from_bulk_insert = True can_rollback_ddl = True can_use_chunked_reads = False for_update_after_from = True From 06b58fc38f56a61709deaeb17ef72210487038f5 Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Fri, 17 Nov 2023 12:31:42 -0800 Subject: [PATCH 08/13] Revert format sql changes --- mssql/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mssql/base.py b/mssql/base.py index 8f8c69e2..0a04cb75 100644 --- a/mssql/base.py +++ b/mssql/base.py @@ -581,7 +581,7 @@ def format_sql(self, sql, params): sql = smart_str(sql, self.driver_charset) # pyodbc uses '?' instead of '%s' as parameter placeholder. - if params is not None and params: + if params is not None: sql = sql % tuple('?' * len(params)) return sql From 8b1e9e0d9adf67e51a5fb3d81072e2e2440e46c2 Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Mon, 20 Nov 2023 09:15:47 -0800 Subject: [PATCH 09/13] Prevent formatting empty params query --- mssql/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mssql/base.py b/mssql/base.py index 0a04cb75..f0be8474 100644 --- a/mssql/base.py +++ b/mssql/base.py @@ -429,7 +429,7 @@ def init_connection_state(self): # Let user choose if driver can return rows from bulk insert since # inserting into tables with triggers causes errors. See issue #130 if (options.get('return_rows_bulk_insert', False)): - self.features_class.can_return_rows_from_bulk_insert = False + self.features_class.can_return_rows_from_bulk_insert = True val = self.get_system_datetime if isinstance(val, str): @@ -581,7 +581,7 @@ def format_sql(self, sql, params): sql = smart_str(sql, self.driver_charset) # pyodbc uses '?' instead of '%s' as parameter placeholder. - if params is not None: + if params is not None and params != []: sql = sql % tuple('?' * len(params)) return sql From becbdd01b8eb075ed4348038f3c48c72bdb28726 Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Mon, 20 Nov 2023 14:18:54 -0800 Subject: [PATCH 10/13] unskip add_field_database_default --- testapp/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testapp/settings.py b/testapp/settings.py index 642b8814..f6a19593 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -307,7 +307,6 @@ 'constraints.tests.CheckConstraintTests.test_validate_custom_error', 'constraints.tests.CheckConstraintTests.test_validate_nullable_jsonfield', 'constraints.tests.CheckConstraintTests.test_validate_pk_field', - 'migrations.test_operations.OperationTests.test_add_field_database_default', ] REGEX_TESTS = [ From 78f1bc6ccaa618a1b480395678631137fbcaa411 Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Tue, 21 Nov 2023 16:01:34 -0800 Subject: [PATCH 11/13] fix add_field_both_defaults test --- mssql/schema.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mssql/schema.py b/mssql/schema.py index 01c2ec60..6dfe5a9a 100644 --- a/mssql/schema.py +++ b/mssql/schema.py @@ -981,7 +981,11 @@ def add_field(self, model, field): self.execute(sql, params) # Drop the default if we need to # (Django usually does not use in-database defaults) - if not self.skip_default(field) and self.effective_default(field) is not None: + if ( + ((django_version >= (5,0) and field.db_default is NOT_PROVIDED) or django_version < (5,0)) + and not self.skip_default(field) + and self.effective_default(field) is not None + ): changes_sql, params = self._alter_column_default_sql(model, None, field, drop=True) sql = self.sql_alter_column % { "table": self.quote_name(model._meta.db_table), From 22203048d85a05e8671132aff9da509cee9cb4d3 Mon Sep 17 00:00:00 2001 From: "Daniel Au (SIMBA TECHNOLOGIES INC)" Date: Wed, 22 Nov 2023 12:09:26 -0800 Subject: [PATCH 12/13] Update nullable default field behavior --- mssql/schema.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mssql/schema.py b/mssql/schema.py index 6dfe5a9a..7c59c4f0 100644 --- a/mssql/schema.py +++ b/mssql/schema.py @@ -957,6 +957,9 @@ def add_field(self, model, field): # It might not actually have a column behind it if definition is None: return + # Nullable columns with default values require 'WITH VALUES' to set existing rows + if 'DEFAULT' in definition and field.null: + definition = definition.replace('NULL', 'WITH VALUES') if (self.connection.features.supports_nullable_unique_constraints and not field.many_to_many and field.null and field.unique): From ee87e485a741a744f20406248055d857264ed2fb Mon Sep 17 00:00:00 2001 From: Mark Shan Date: Tue, 28 Nov 2023 14:55:49 -0800 Subject: [PATCH 13/13] update odbc 17 windows ci --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9be79057..0bb7616d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,7 +102,7 @@ jobs: (Get-Content $pwd/testapp/settings.py).replace('localhost', $IP) | Set-Content $pwd/testapp/settings.py - Invoke-WebRequest https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/17.5.2.1/x64/msodbcsql.msi -OutFile msodbcsql.msi + Invoke-WebRequest https://download.microsoft.com/download/6/f/f/6ffefc73-39ab-4cc0-bb7c-4093d64c2669/en-US/17.10.5.1/x64/msodbcsql.msi -OutFile msodbcsql.msi msiexec /quiet /passive /qn /i msodbcsql.msi IACCEPTMSODBCSQLLICENSETERMS=YES Get-OdbcDriver displayName: Install ODBC