From e55b86461b669c75078051f965e47c96e5e787d9 Mon Sep 17 00:00:00 2001 From: Henrik Ek Date: Fri, 16 Oct 2020 08:25:40 +0200 Subject: [PATCH 1/6] Add native django JSONField support --- sql_server/pyodbc/base.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/sql_server/pyodbc/base.py b/sql_server/pyodbc/base.py index 297a90b8..590fc7c4 100644 --- a/sql_server/pyodbc/base.py +++ b/sql_server/pyodbc/base.py @@ -12,28 +12,28 @@ except ImportError as e: raise ImproperlyConfigured("Error loading pyodbc module: %s" % e) -from django.utils.version import get_version_tuple # noqa +from django.utils.version import get_version_tuple # noqa pyodbc_ver = get_version_tuple(Database.version) if pyodbc_ver < (3, 0): raise ImproperlyConfigured("pyodbc 3.0 or newer is required; you have %s" % Database.version) -from django.conf import settings # noqa -from django.db import NotSupportedError # noqa -from django.db.backends.base.base import BaseDatabaseWrapper # noqa -from django.utils.encoding import smart_str # noqa -from django.utils.functional import cached_property # noqa +from django.conf import settings # noqa +from django.db import NotSupportedError # noqa +from django.db.backends.base.base import BaseDatabaseWrapper # noqa +from django.utils.encoding import smart_str # noqa +from django.utils.functional import cached_property # noqa if hasattr(settings, 'DATABASE_CONNECTION_POOLING'): if not settings.DATABASE_CONNECTION_POOLING: Database.pooling = False -from .client import DatabaseClient # noqa -from .creation import DatabaseCreation # noqa -from .features import DatabaseFeatures # noqa -from .introspection import DatabaseIntrospection # noqa -from .operations import DatabaseOperations # noqa -from .schema import DatabaseSchemaEditor # noqa +from .client import DatabaseClient # noqa +from .creation import DatabaseCreation # noqa +from .features import DatabaseFeatures # noqa +from .introspection import DatabaseIntrospection # noqa +from .operations import DatabaseOperations # noqa +from .schema import DatabaseSchemaEditor # noqa EDITION_AZURE_SQL_DB = 5 @@ -95,6 +95,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): 'TextField': 'nvarchar(max)', 'TimeField': 'time', 'UUIDField': 'char(32)', + 'JSONField': 'nvarchar(max)', } data_type_check_constraints = { 'PositiveIntegerField': '[%(column)s] >= 0', From caf6c2ba7d3d86b4b3632a305197d4dfbe134299 Mon Sep 17 00:00:00 2001 From: Henrik Ek Date: Fri, 16 Oct 2020 08:54:28 +0200 Subject: [PATCH 2/6] Set supports_order_by_nulls_modifier = False --- sql_server/pyodbc/features.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sql_server/pyodbc/features.py b/sql_server/pyodbc/features.py index 1e184217..b05f3942 100644 --- a/sql_server/pyodbc/features.py +++ b/sql_server/pyodbc/features.py @@ -34,6 +34,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_timezones = False supports_transactions = True uses_savepoints = True + supports_order_by_nulls_modifier = False @cached_property def has_bulk_insert(self): From ebc524147714fc0c76f3ce5ed841012553640846 Mon Sep 17 00:00:00 2001 From: Henrik Ek Date: Fri, 16 Oct 2020 14:17:45 +0200 Subject: [PATCH 3/6] Add flag supports_order_by_nulls_first_is_not_null --- sql_server/pyodbc/features.py | 1 + sql_server/pyodbc/functions.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/sql_server/pyodbc/features.py b/sql_server/pyodbc/features.py index b05f3942..9f5d808e 100644 --- a/sql_server/pyodbc/features.py +++ b/sql_server/pyodbc/features.py @@ -35,6 +35,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_transactions = True uses_savepoints = True supports_order_by_nulls_modifier = False + supports_order_by_nulls_first_is_not_null = False @cached_property def has_bulk_insert(self): diff --git a/sql_server/pyodbc/functions.py b/sql_server/pyodbc/functions.py index cc043281..73296859 100644 --- a/sql_server/pyodbc/functions.py +++ b/sql_server/pyodbc/functions.py @@ -12,6 +12,34 @@ class TryCast(Cast): function = 'TRY_CAST' +def sqlserver_as_sql(self, compiler, connection, template=None, **extra_context): + template = template or self.template + if connection.features.supports_order_by_nulls_modifier: + if self.nulls_last: + template = '%s NULLS LAST' % template + elif self.nulls_first: + template = '%s NULLS FIRST' % template + else: + if self.nulls_last and not ( + self.descending and connection.features.order_by_nulls_first + ): + template = '%%(expression)s IS NULL, %s' % template + elif self.nulls_first and not ( + not self.descending and connection.features.order_by_nulls_first + ) and connection.features.supports_order_by_nulls_first_is_not_null: + template = '%%(expression)s IS NOT NULL, %s' % template + connection.ops.check_expression_support(self) + expression_sql, params = compiler.compile(self.expression) + placeholders = { + 'expression': expression_sql, + 'ordering': 'DESC' if self.descending else 'ASC', + **extra_context, + } + template = template or self.template + params *= template.count('%(expression)s') + return (template % placeholders).rstrip(), params + + def sqlserver_atan2(self, compiler, connection, **extra_context): return self.as_sql(compiler, connection, function='ATN2', **extra_context) @@ -85,3 +113,4 @@ def sqlserver_orderby(self, compiler, connection): Exists.as_microsoft = sqlserver_exists OrderBy.as_microsoft = sqlserver_orderby +OrderBy.as_sql = sqlserver_as_sql From 5a39b7809a6cee819c1fb5089baf72b678db3a75 Mon Sep 17 00:00:00 2001 From: Henrik Ek Date: Mon, 19 Oct 2020 23:28:01 +0200 Subject: [PATCH 4/6] Rename feture flag supports_order_by_is_nulls --- sql_server/pyodbc/features.py | 2 +- sql_server/pyodbc/functions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sql_server/pyodbc/features.py b/sql_server/pyodbc/features.py index 9f5d808e..f78ae730 100644 --- a/sql_server/pyodbc/features.py +++ b/sql_server/pyodbc/features.py @@ -35,7 +35,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_transactions = True uses_savepoints = True supports_order_by_nulls_modifier = False - supports_order_by_nulls_first_is_not_null = False + supports_order_by_is_nulls = False @cached_property def has_bulk_insert(self): diff --git a/sql_server/pyodbc/functions.py b/sql_server/pyodbc/functions.py index 73296859..967f5ffb 100644 --- a/sql_server/pyodbc/functions.py +++ b/sql_server/pyodbc/functions.py @@ -22,11 +22,11 @@ def sqlserver_as_sql(self, compiler, connection, template=None, **extra_context) else: if self.nulls_last and not ( self.descending and connection.features.order_by_nulls_first - ): + ) and connection.features.supports_order_by_is_nulls: template = '%%(expression)s IS NULL, %s' % template elif self.nulls_first and not ( not self.descending and connection.features.order_by_nulls_first - ) and connection.features.supports_order_by_nulls_first_is_not_null: + ) and connection.features.supports_order_by_is_nulls: template = '%%(expression)s IS NOT NULL, %s' % template connection.ops.check_expression_support(self) expression_sql, params = compiler.compile(self.expression) From b94524caa7e5f5663a82ecbdd0897bb2a925557f Mon Sep 17 00:00:00 2001 From: Henrik Ek Date: Thu, 22 Oct 2020 12:15:57 +0200 Subject: [PATCH 5/6] Update sql_flush with parameter reset_sequences --- sql_server/pyodbc/operations.py | 82 ++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/sql_server/pyodbc/operations.py b/sql_server/pyodbc/operations.py index 6af85bab..519f8481 100644 --- a/sql_server/pyodbc/operations.py +++ b/sql_server/pyodbc/operations.py @@ -314,7 +314,7 @@ def savepoint_rollback_sql(self, sid): """ return "ROLLBACK TRANSACTION %s" % sid - def sql_flush(self, style, tables, sequences, allow_cascade=False): + def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False): """ Returns a list of SQL statements required to remove all data from the given database tables (without actually removing the tables @@ -329,13 +329,20 @@ def sql_flush(self, style, tables, sequences, allow_cascade=False): The `allow_cascade` argument determines whether truncation may cascade to tables with foreign keys pointing the tables being truncated. """ - if tables: - # Cannot use TRUNCATE on tables that are referenced by a FOREIGN KEY - # So must use the much slower DELETE - from django.db import connections - cursor = connections[self.connection.alias].cursor() - # Try to minimize the risks of the braindeaded inconsistency in - # DBCC CHEKIDENT(table, RESEED, n) behavior. + if not tables: + return [] + + # Cannot use TRUNCATE on tables that are referenced by a FOREIGN KEY + # So must use the much slower DELETE + from django.db import connections + cursor = connections[self.connection.alias].cursor() + # Try to minimize the risks of the braindeaded inconsistency in + # DBCC CHEKIDENT(table, RESEED, n) behavior. + if reset_sequences: + sequences = [ + sequence + for sequence in self.connection.introspection.sequence_list() + ] seqs = [] for seq in sequences: cursor.execute("SELECT COUNT(*) FROM %s" % self.quote_name(seq["table"])) @@ -347,37 +354,36 @@ def sql_flush(self, style, tables, sequences, allow_cascade=False): elem['start_id'] = 1 elem.update(seq) seqs.append(elem) - COLUMNS = "TABLE_NAME, CONSTRAINT_NAME" - WHERE = "CONSTRAINT_TYPE not in ('PRIMARY KEY','UNIQUE')" - cursor.execute( - "SELECT {} FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE {}".format(COLUMNS, WHERE)) - fks = cursor.fetchall() - sql_list = ['ALTER TABLE %s NOCHECK CONSTRAINT %s;' % - (self.quote_name(fk[0]), self.quote_name(fk[1])) for fk in fks] - sql_list.extend(['%s %s %s;' % (style.SQL_KEYWORD('DELETE'), style.SQL_KEYWORD('FROM'), - style.SQL_FIELD(self.quote_name(table))) for table in tables]) - - if self.connection.to_azure_sql_db and self.connection.sql_server_version < 2014: - warnings.warn("Resetting identity columns is not supported " - "on this versios of Azure SQL Database.", - RuntimeWarning) - else: - # Then reset the counters on each table. - sql_list.extend(['%s %s (%s, %s, %s) %s %s;' % ( - style.SQL_KEYWORD('DBCC'), - style.SQL_KEYWORD('CHECKIDENT'), - style.SQL_FIELD(self.quote_name(seq["table"])), - style.SQL_KEYWORD('RESEED'), - style.SQL_FIELD('%d' % seq['start_id']), - style.SQL_KEYWORD('WITH'), - style.SQL_KEYWORD('NO_INFOMSGS'), - ) for seq in seqs]) - - sql_list.extend(['ALTER TABLE %s CHECK CONSTRAINT %s;' % - (self.quote_name(fk[0]), self.quote_name(fk[1])) for fk in fks]) - return sql_list + + COLUMNS = "TABLE_NAME, CONSTRAINT_NAME" + WHERE = "CONSTRAINT_TYPE not in ('PRIMARY KEY','UNIQUE')" + cursor.execute( + "SELECT {} FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE {}".format(COLUMNS, WHERE)) + fks = cursor.fetchall() + sql_list = ['ALTER TABLE %s NOCHECK CONSTRAINT %s;' % + (self.quote_name(fk[0]), self.quote_name(fk[1])) for fk in fks] + sql_list.extend(['%s %s %s;' % (style.SQL_KEYWORD('DELETE'), style.SQL_KEYWORD('FROM'), + style.SQL_FIELD(self.quote_name(table))) for table in tables]) + + if self.connection.to_azure_sql_db and self.connection.sql_server_version < 2014: + warnings.warn("Resetting identity columns is not supported " + "on this versios of Azure SQL Database.", + RuntimeWarning) else: - return [] + # Then reset the counters on each table. + sql_list.extend(['%s %s (%s, %s, %s) %s %s;' % ( + style.SQL_KEYWORD('DBCC'), + style.SQL_KEYWORD('CHECKIDENT'), + style.SQL_FIELD(self.quote_name(seq["table"])), + style.SQL_KEYWORD('RESEED'), + style.SQL_FIELD('%d' % seq['start_id']), + style.SQL_KEYWORD('WITH'), + style.SQL_KEYWORD('NO_INFOMSGS'), + ) for seq in seqs]) + + sql_list.extend(['ALTER TABLE %s CHECK CONSTRAINT %s;' % + (self.quote_name(fk[0]), self.quote_name(fk[1])) for fk in fks]) + return sql_list def start_transaction_sql(self): """ From 4dca253971b4037d7924e68b342a4764972fed85 Mon Sep 17 00:00:00 2001 From: Henrik Ek Date: Thu, 22 Oct 2020 13:02:31 +0200 Subject: [PATCH 6/6] Define seqs variable --- sql_server/pyodbc/operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql_server/pyodbc/operations.py b/sql_server/pyodbc/operations.py index 519f8481..64ef7e69 100644 --- a/sql_server/pyodbc/operations.py +++ b/sql_server/pyodbc/operations.py @@ -338,12 +338,12 @@ def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False cursor = connections[self.connection.alias].cursor() # Try to minimize the risks of the braindeaded inconsistency in # DBCC CHEKIDENT(table, RESEED, n) behavior. + seqs = [] if reset_sequences: sequences = [ sequence for sequence in self.connection.introspection.sequence_list() ] - seqs = [] for seq in sequences: cursor.execute("SELECT COUNT(*) FROM %s" % self.quote_name(seq["table"])) rowcnt = cursor.fetchone()[0]