Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
pip install flake8
- name: Linting
run: |
flake8
flake8 --exclude testapp

build:
runs-on: ${{ matrix.os }}
Expand All @@ -41,11 +41,14 @@ jobs:
tox_env:
- "py36-django22"
- "py36-django30"
- "py36-django31"

- "py37-django22"
- "py37-django30"
- "py37-django31"

- "py38-django30"
- "py38-django31"

include:
- python: "3.6"
Expand All @@ -54,15 +57,24 @@ jobs:
- python: "3.6"
tox_env: "py36-django30"

- python: "3.6"
tox_env: "py36-django31"

- python: "3.7"
tox_env: "py37-django22"

- python: "3.7"
tox_env: "py37-django30"

- python: "3.7"
tox_env: "py37-django31"

- python: "3.8"
tox_env: "py38-django30"

- python: "3.8"
tox_env: "py38-django31"


steps:
- uses: actions/checkout@v2
Expand Down
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,25 @@ matrix:

- { before_install: *linux_before_install, python: "3.6", os: linux, env: TOX_ENV=py36-django22 }
- { before_install: *linux_before_install, python: "3.6", os: linux, env: TOX_ENV=py36-django30 }
- { before_install: *linux_before_install, python: "3.6", os: linux, env: TOX_ENV=py36-django31 }

- { before_install: *linux_before_install, python: "3.7", os: linux, env: TOX_ENV=py37-django22 }
- { before_install: *linux_before_install, python: "3.7", os: linux, env: TOX_ENV=py37-django30 }
- { before_install: *linux_before_install, python: "3.7", os: linux, env: TOX_ENV=py37-django31 }

- { before_install: *linux_before_install, python: "3.8", os: linux, env: TOX_ENV=py38-django30 }
- { before_install: *linux_before_install, python: "3.8", os: linux, env: TOX_ENV=py38-django31 }

- { before_install: *win_before_install, language: sh, python: "3.6", os: windows, env: TOX_ENV=py36-django22 }
- { before_install: *win_before_install, language: sh, python: "3.6", os: windows, env: TOX_ENV=py36-django30 }
- { before_install: *win_before_install, language: sh, python: "3.6", os: windows, env: TOX_ENV=py36-django31 }

- { before_install: *win_before_install, language: sh, python: "3.7", os: windows, env: TOX_ENV=py37-django22 }
- { before_install: *win_before_install, language: sh, python: "3.7", os: windows, env: TOX_ENV=py37-django30 }
- { before_install: *win_before_install, language: sh, python: "3.7", os: windows, env: TOX_ENV=py37-django31 }

- { before_install: *win_before_install, language: sh, python: "3.8", os: windows, env: TOX_ENV=py38-django30 }
- { before_install: *win_before_install, language: sh, python: "3.8", os: windows, env: TOX_ENV=py38-django31 }



Expand Down
8 changes: 5 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,12 @@ Dictionary. Current available keys are:
definition present in the ``freetds.conf`` FreeTDS configuration file
instead of a hostname or an IP address.

But if this option is present and it's value is ``True``, this
special behavior is turned off.
But if this option is present and its value is ``True``, this
special behavior is turned off. Instead, connections to the database
server will be established using ``HOST`` and ``PORT`` options, without
requiring ``freetds.conf`` to be configured.

See http://www.freetds.org/userguide/dsnless.htm for more information.
See https://www.freetds.org/userguide/dsnless.html for more information.

- unicode_results

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

setup(
name='django-mssql-backend',
version='2.8.0',
version='2.8.1',
description='Django backend for Microsoft SQL Server',
long_description=open('README.rst').read(),
author='ES Solutions AB',
Expand Down
26 changes: 14 additions & 12 deletions sql_server/pyodbc/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -89,12 +89,14 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'OneToOneField': 'int',
'PositiveIntegerField': 'int',
'PositiveSmallIntegerField': 'smallint',
'PositiveBigIntegerField': 'bigint',
'SlugField': 'nvarchar(%(max_length)s)',
'SmallAutoField': 'smallint IDENTITY (1, 1)',
'SmallIntegerField': 'smallint',
'TextField': 'nvarchar(max)',
'TimeField': 'time',
'UUIDField': 'char(32)',
'JSONField': 'nvarchar(max)',
}
data_type_check_constraints = {
'PositiveIntegerField': '[%(column)s] >= 0',
Expand Down
13 changes: 10 additions & 3 deletions sql_server/pyodbc/creation.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import binascii
import os

import django
from django.db.backends.base.creation import BaseDatabaseCreation


class DatabaseCreation(BaseDatabaseCreation):
@property
def cursor(self):
if django.VERSION >= (3, 1):
return self.connection._nodb_cursor

return self.connection._nodb_connection.cursor

def _destroy_test_db(self, test_database_name, verbosity):
"""
Expand All @@ -14,7 +21,7 @@ def _destroy_test_db(self, test_database_name, verbosity):
# ourselves. Connect to the previous database (not the test database)
# to do so, because it's not allowed to delete a database while being
# connected to it.
with self.connection._nodb_connection.cursor() as cursor:
with self.cursor() as cursor:
to_azure_sql_db = self.connection.to_azure_sql_db
if not to_azure_sql_db:
cursor.execute("ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
Expand All @@ -36,7 +43,7 @@ def enable_clr(self):
This function will not fail if current user doesn't have
permissions to enable clr, and clr is already enabled
"""
with self._nodb_connection.cursor() as cursor:
with self.cursor() as cursor:
# check whether clr is enabled
cursor.execute('''
SELECT value FROM sys.configurations
Expand Down Expand Up @@ -86,7 +93,7 @@ def install_regex_clr(self, database_name):

self.enable_clr()

with self._nodb_connection.cursor() as cursor:
with self.cursor() as cursor:
for s in sql:
cursor.execute(s)

Expand Down
14 changes: 14 additions & 0 deletions sql_server/pyodbc/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@


class DatabaseFeatures(BaseDatabaseFeatures):
can_introspect_json_field = False
has_native_json_field = False
has_native_uuid_field = False
allow_sliced_subqueries_with_in = False
can_introspect_autofield = True
Expand All @@ -22,6 +24,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
requires_literal_defaults = True
requires_sqlparse_for_splitting = False
supports_boolean_expr_in_select_clause = False
supports_deferrable_unique_constraints = False
supports_ignore_conflicts = False
supports_index_on_text_field = False
supports_paramstyle_pyformat = False
Expand All @@ -33,6 +36,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_timezones = False
supports_transactions = True
uses_savepoints = True
supports_order_by_nulls_modifier = False
supports_order_by_is_nulls = False
order_by_nulls_first = True

@cached_property
def has_bulk_insert(self):
Expand All @@ -53,3 +59,11 @@ def supports_partial_indexes(self):
@cached_property
def supports_functions_in_partial_indexes(self):
return self.connection.sql_server_version > 2005

@cached_property
def introspected_field_types(self):
return {
**super().introspected_field_types,
'GenericIPAddressField': 'CharField',
'PositiveBigIntegerField': 'BigIntegerField'
}
36 changes: 35 additions & 1 deletion sql_server/pyodbc/functions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django import VERSION
from django.db.models import BooleanField
from django.db.models.functions import Cast
from django.db.models.functions.math import ATan2, Log, Ln, Round
from django.db.models.functions.math import ATan2, Log, Ln, Mod, Round
from django.db.models.expressions import Case, Exists, OrderBy, When
from django.db.models.lookups import Lookup

Expand All @@ -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
) 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_is_nulls:
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)

Expand All @@ -26,6 +54,10 @@ def sqlserver_ln(self, compiler, connection, **extra_context):
return self.as_sql(compiler, connection, function='LOG', **extra_context)


def sqlserver_mod(self, compiler, connection, **extra_context):
return self.as_sql(compiler, connection, template='%(expressions)s', arg_joiner='%%', **extra_context)


def sqlserver_round(self, compiler, connection, **extra_context):
return self.as_sql(compiler, connection, template='%(function)s(%(expressions)s, 0)', **extra_context)

Expand Down Expand Up @@ -77,6 +109,7 @@ def sqlserver_orderby(self, compiler, connection):
ATan2.as_microsoft = sqlserver_atan2
Log.as_microsoft = sqlserver_log
Ln.as_microsoft = sqlserver_ln
Mod.as_microsoft = sqlserver_mod
Round.as_microsoft = sqlserver_round

if DJANGO3:
Expand All @@ -85,3 +118,4 @@ def sqlserver_orderby(self, compiler, connection):
Exists.as_microsoft = sqlserver_exists

OrderBy.as_microsoft = sqlserver_orderby
OrderBy.as_sql = sqlserver_as_sql
5 changes: 4 additions & 1 deletion sql_server/pyodbc/introspection.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import pyodbc as Database
from collections import namedtuple

from django.db.backends.base.introspection import (
BaseDatabaseIntrospection, FieldInfo, TableInfo,
BaseDatabaseIntrospection, TableInfo,
)
from django.db.models.indexes import Index

SQL_AUTOFIELD = -777555
SQL_BIGAUTOFIELD = -777444

FieldInfo = namedtuple('FieldInfo', 'name type_code display_size internal_size precision scale null_ok default')


class DatabaseIntrospection(BaseDatabaseIntrospection):
# Map type codes to Django Field types.
Expand Down
Loading