Skip to content

Commit 1e70d2c

Browse files
authored
Add Django 3 support (#19)
1 parent 752227d commit 1e70d2c

File tree

10 files changed

+222
-75
lines changed

10 files changed

+222
-75
lines changed

.github/workflows/main.yml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
branches:
9+
- master
10+
11+
env:
12+
DATABASE_URL: "mssql://SA:MyPassword42@localhost:1433/default?isolation_level=read committed&driver=ODBC Driver 17 for SQL Server"
13+
DATABASE_URL_OTHER: "mssql://SA:MyPassword42@localhost:1433/other?isolation_level=read committed&driver=ODBC Driver 17 for SQL Server"
14+
15+
jobs:
16+
linting:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v1
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v1
23+
with:
24+
python-version: "3.8"
25+
26+
- name: Install
27+
run: |
28+
python -m pip install --upgrade pip
29+
pip install flake8
30+
- name: Linting
31+
run: |
32+
flake8
33+
34+
build:
35+
runs-on: ${{ matrix.os }}
36+
37+
strategy:
38+
fail-fast: false
39+
matrix:
40+
os: [ubuntu-latest, windows-latest]
41+
tox_env:
42+
- "py36-django22"
43+
- "py36-django30"
44+
45+
- "py37-django22"
46+
- "py37-django30"
47+
48+
- "py38-django30"
49+
50+
include:
51+
- python: "3.6"
52+
tox_env: "py36-django22"
53+
54+
- python: "3.6"
55+
tox_env: "py36-django30"
56+
57+
- python: "3.7"
58+
tox_env: "py37-django22"
59+
60+
- python: "3.7"
61+
tox_env: "py37-django30"
62+
63+
- python: "3.8"
64+
tox_env: "py38-django30"
65+
66+
67+
steps:
68+
- uses: actions/checkout@v2
69+
- uses: actions/checkout@v2
70+
with:
71+
repository: django/django
72+
path: django
73+
- name: Set up Python
74+
uses: actions/setup-python@v1
75+
with:
76+
python-version: ${{ matrix.python }}
77+
78+
- name: Install Linux deps
79+
if: matrix.os == 'ubuntu-latest'
80+
run: |
81+
curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
82+
curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
83+
sudo apt-get update
84+
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql17 g++ unixodbc-dev libmemcached-dev
85+
86+
- name: Install Windows deps
87+
if: matrix.os == 'windows-latest'
88+
run: |
89+
powershell wget https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.3.1.1_x64.msi -OutFile msodbcsql_17.3.1.1_x64.msi
90+
powershell "Start-Process msiexec.exe -Wait -ArgumentList '/I msodbcsql_17.3.1.1_x64.msi /qn /norestart IACCEPTMSODBCSQLLICENSETERMS=YES'"
91+
92+
- name: Install
93+
run: |
94+
python -m pip install --upgrade pip wheel setuptools
95+
pip install tox tox-venv
96+
97+
- name: Test Linux
98+
if: matrix.os == 'ubuntu-latest'
99+
run: |
100+
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=MyPassword42' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu
101+
tox -e ${{ matrix.tox_env }}
102+
103+
- name: Test Windows
104+
if: matrix.os == 'windows-latest'
105+
run: |
106+
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=MyPassword42' -p 1433:1433 -d christianacca/mssql-server-windows-express:1809
107+
tox -e ${{ matrix.tox_env }}

.travis.yml

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,30 @@ branches:
77
only:
88
- master
99

10-
templates:
11-
mssql: &mssql DATABASE_URL="mssql://SA:MyPassword42@localhost:1433/default?isolation_level=read committed&driver=ODBC Driver 17 for SQL Server" DATABASE_URL_OTHER="mssql://SA:MyPassword42@localhost:1433/other?isolation_level=read committed&driver=ODBC Driver 17 for SQL Server"
12-
1310
env:
1411
global:
1512
- PYTHONPATH=$PYTHONPATH:$TRAVIS_BUILD_DIR/django
13+
- DATABASE_URL="mssql://SA:MyPassword42@localhost:1433/default?isolation_level=read committed&driver=ODBC Driver 17 for SQL Server"
14+
- DATABASE_URL_OTHER="mssql://SA:MyPassword42@localhost:1433/other?isolation_level=read committed&driver=ODBC Driver 17 for SQL Server"
15+
16+
services: docker
17+
18+
templates:
19+
linux_before_install: &linux_before_install
20+
- docker pull mcr.microsoft.com/mssql/server:2017-latest-ubuntu
21+
- docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=MyPassword42' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu
22+
- curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
23+
- curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
24+
- sudo apt-get update
25+
- sudo ACCEPT_EULA=Y apt-get install -y msodbcsql17 g++ unixodbc-dev
26+
27+
win_before_install: &win_before_install
28+
- docker pull christianacca/mssql-server-windows-express:1803
29+
- docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=MyPassword42' -p 1433:1433 -d christianacca/mssql-server-windows-express:1803
30+
- wget https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.3.1.1_x64.msi
31+
- powershell "Start-Process msiexec.exe -Wait -ArgumentList '/I msodbcsql_17.3.1.1_x64.msi /qn /norestart IACCEPTMSODBCSQLLICENSETERMS=YES'"
32+
- choco install python3 --version 3.7.2
33+
- export PATH="/c/Python37:/c/Python37/Scripts:$PATH"
1634

1735
matrix:
1836
include:
@@ -21,39 +39,28 @@ matrix:
2139
install: pip install flake8==3.7.1
2240
script: flake8
2341

24-
- python: "3.7"
25-
services: docker
26-
before_install:
27-
- docker pull mcr.microsoft.com/mssql/server:2017-latest-ubuntu
28-
- docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=MyPassword42' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu
29-
- curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
30-
- curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
31-
- sudo apt-get update
32-
- sudo ACCEPT_EULA=Y apt-get install -y msodbcsql17 g++ unixodbc-dev
33-
env:
34-
- *mssql
35-
36-
- os: windows
37-
language: sh
38-
python: "3.7"
39-
services: docker
40-
before_install:
41-
- docker pull christianacca/mssql-server-windows-express:1803
42-
- docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=MyPassword42' -p 1433:1433 -d christianacca/mssql-server-windows-express:1803
43-
- wget https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.3.1.1_x64.msi
44-
- powershell "Start-Process msiexec.exe -Wait -ArgumentList '/I msodbcsql_17.3.1.1_x64.msi /qn /norestart IACCEPTMSODBCSQLLICENSETERMS=YES'"
45-
- choco install python3 --version 3.7.2
46-
- export PATH="/c/Python37:/c/Python37/Scripts:$PATH"
47-
- pip install "django>=2.2,<2.3"
48-
env:
49-
- *mssql
42+
- { before_install: *linux_before_install, python: "3.6", os: linux, env: TOX_ENV=py36-django22 }
43+
- { before_install: *linux_before_install, python: "3.6", os: linux, env: TOX_ENV=py36-django30 }
44+
45+
- { before_install: *linux_before_install, python: "3.7", os: linux, env: TOX_ENV=py37-django22 }
46+
- { before_install: *linux_before_install, python: "3.7", os: linux, env: TOX_ENV=py37-django30 }
47+
48+
- { before_install: *linux_before_install, python: "3.8", os: linux, env: TOX_ENV=py38-django30 }
49+
50+
- { before_install: *win_before_install, language: sh, python: "3.6", os: windows, env: TOX_ENV=py36-django22 }
51+
- { before_install: *win_before_install, language: sh, python: "3.6", os: windows, env: TOX_ENV=py36-django30 }
52+
53+
- { before_install: *win_before_install, language: sh, python: "3.7", os: windows, env: TOX_ENV=py37-django22 }
54+
- { before_install: *win_before_install, language: sh, python: "3.7", os: windows, env: TOX_ENV=py37-django30 }
55+
56+
- { before_install: *win_before_install, language: sh, python: "3.8", os: windows, env: TOX_ENV=py38-django30 }
57+
58+
5059

5160
install:
5261
- python -m pip install --upgrade pip wheel setuptools
53-
- pip install -e .["tests"]
54-
- git clone --branch=stable/2.2.x https://github.com/django/django.git --depth=1
62+
- pip install tox tox-travis tox-venv
63+
- git clone https://github.com/django/django.git
5564

5665
script:
57-
- pip install -r django/tests/requirements/py3.txt
58-
- python manage.py test
59-
- ./test.sh
66+
- tox -e $TOX_ENV

setup.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
try:
2-
from setuptools import setup
3-
except ImportError:
4-
from distutils.core import setup
1+
from setuptools import find_packages, setup
52

63
CLASSIFIERS = [
74
'License :: OSI Approved :: BSD License',
@@ -13,6 +10,8 @@
1310
'Programming Language :: Python :: 3.5',
1411
'Programming Language :: Python :: 3.6',
1512
'Programming Language :: Python :: 3.7',
13+
'Framework :: Django :: 2.2',
14+
'Framework :: Django :: 3.0',
1615
]
1716

1817
setup(
@@ -24,13 +23,10 @@
2423
author_email='[email protected]',
2524
url='https://github.com/ESSolutions/django-mssql-backend',
2625
license='BSD',
27-
packages=['sql_server', 'sql_server.pyodbc'],
26+
packages=find_packages(),
2827
install_requires=[
2928
'pyodbc>=3.0',
3029
],
31-
extras_require={
32-
'tests': ['dj-database-url==0.5.0'],
33-
},
3430
classifiers=CLASSIFIERS,
3531
keywords='django',
3632
)

sql_server/pyodbc/base.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
import time
77

88
from django.core.exceptions import ImproperlyConfigured
9-
from django import VERSION
10-
11-
if VERSION[:3] < (2, 2, 0) or VERSION[:2] >= (2, 3):
12-
raise ImproperlyConfigured("Django %d.%d.%d is not supported." % VERSION[:3])
139

1410
try:
1511
import pyodbc as Database
@@ -94,6 +90,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
9490
'PositiveIntegerField': 'int',
9591
'PositiveSmallIntegerField': 'smallint',
9692
'SlugField': 'nvarchar(%(max_length)s)',
93+
'SmallAutoField': 'smallint IDENTITY (1, 1)',
9794
'SmallIntegerField': 'smallint',
9895
'TextField': 'nvarchar(max)',
9996
'TimeField': 'time',

sql_server/pyodbc/compiler.py

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import types
22
from itertools import chain
33

4+
import django
45
from django.db.models.aggregates import Avg, Count, StdDev, Variance
56
from django.db.models.expressions import Exists, OrderBy, Ref, Subquery, Value
67
from django.db.models.functions import (
@@ -370,9 +371,9 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
370371
# Finally do cleanup - get rid of the joins we created above.
371372
self.query.reset_refcounts(refcounts_before)
372373

373-
def compile(self, node, select_format=False):
374+
def compile(self, node, *args, **kwargs):
374375
node = self._as_microsoft(node)
375-
return super().compile(node, select_format)
376+
return super().compile(node, *args, **kwargs)
376377

377378
def collapse_group_by(self, expressions, having):
378379
expressions = super().collapse_group_by(expressions, having)
@@ -421,6 +422,25 @@ def _as_microsoft(self, node):
421422

422423

423424
class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler):
425+
def get_returned_fields(self):
426+
if django.VERSION >= (3, 0, 0):
427+
return self.returning_fields
428+
return self.return_id
429+
430+
def fix_auto(self, sql, opts, fields, qn):
431+
if opts.auto_field is not None:
432+
# db_column is None if not explicitly specified by model field
433+
auto_field_column = opts.auto_field.db_column or opts.auto_field.column
434+
columns = [f.column for f in fields]
435+
if auto_field_column in columns:
436+
id_insert_sql = []
437+
table = qn(opts.db_table)
438+
sql_format = 'SET IDENTITY_INSERT %s ON; %s; SET IDENTITY_INSERT %s OFF'
439+
for q, p in sql:
440+
id_insert_sql.append((sql_format % (table, q, table), p))
441+
sql = id_insert_sql
442+
443+
return sql
424444

425445
def as_sql(self):
426446
# We don't need quote_name_unless_alias() here, since these are all
@@ -447,38 +467,28 @@ def as_sql(self):
447467
# queries and generate their own placeholders. Doing that isn't
448468
# necessary and it should be possible to use placeholders and
449469
# expressions in bulk inserts too.
450-
can_bulk = (not self.return_id and self.connection.features.has_bulk_insert) and self.query.fields
470+
can_bulk = (not self.get_returned_fields() and self.connection.features.has_bulk_insert) and self.query.fields
451471

452472
placeholder_rows, param_rows = self.assemble_as_sql(fields, value_rows)
453473

454-
if self.return_id and self.connection.features.can_return_id_from_insert:
474+
if self.get_returned_fields() and self.connection.features.can_return_id_from_insert:
455475
result.insert(0, 'SET NOCOUNT ON')
456476
result.append((values_format + ';') % ', '.join(placeholder_rows[0]))
457477
params = [param_rows[0]]
458478
result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)')
459-
return [(" ".join(result), tuple(chain.from_iterable(params)))]
460-
461-
if can_bulk:
462-
result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows))
463-
sql = [(" ".join(result), tuple(p for ps in param_rows for p in ps))]
479+
sql = [(" ".join(result), tuple(chain.from_iterable(params)))]
464480
else:
465-
sql = [
466-
(" ".join(result + [values_format % ", ".join(p)]), vals)
467-
for p, vals in zip(placeholder_rows, param_rows)
468-
]
481+
if can_bulk:
482+
result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows))
483+
sql = [(" ".join(result), tuple(p for ps in param_rows for p in ps))]
484+
else:
485+
sql = [
486+
(" ".join(result + [values_format % ", ".join(p)]), vals)
487+
for p, vals in zip(placeholder_rows, param_rows)
488+
]
469489

470490
if self.query.fields:
471-
if opts.auto_field is not None:
472-
# db_column is None if not explicitly specified by model field
473-
auto_field_column = opts.auto_field.db_column or opts.auto_field.column
474-
columns = [f.column for f in fields]
475-
if auto_field_column in columns:
476-
id_insert_sql = []
477-
table = qn(opts.db_table)
478-
sql_format = 'SET IDENTITY_INSERT %s ON; %s; SET IDENTITY_INSERT %s OFF'
479-
for q, p in sql:
480-
id_insert_sql.append((sql_format % (table, q, table), p))
481-
sql = id_insert_sql
491+
sql = self.fix_auto(sql, opts, fields, qn)
482492

483493
return sql
484494

sql_server/pyodbc/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
77
allow_sliced_subqueries_with_in = False
88
can_introspect_autofield = True
99
can_introspect_small_integer_field = True
10+
can_return_columns_from_insert = True
1011
can_return_id_from_insert = True
1112
can_use_chunked_reads = False
1213
for_update_after_from = True

sql_server/pyodbc/operations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.conf import settings
66
from django.db.backends.base.operations import BaseDatabaseOperations
77
from django.utils import timezone
8-
from django.utils.encoding import force_text
8+
from django.utils.encoding import force_str
99

1010
import pytz
1111

@@ -403,7 +403,7 @@ def tablespace_sql(self, tablespace, inline=False):
403403
def prep_for_like_query(self, x):
404404
"""Prepares a value for use in a LIKE query."""
405405
# http://msdn2.microsoft.com/en-us/library/ms179859.aspx
406-
return force_text(x).replace('\\', '\\\\').replace('[', '[[]').replace('%', '[%]').replace('_', '[_]')
406+
return force_str(x).replace('\\', '\\\\').replace('[', '[[]').replace('%', '[%]').replace('_', '[_]')
407407

408408
def prep_for_iexact_query(self, x):
409409
"""

sql_server/pyodbc/schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from django.db.models import Index
1111
from django.db.models.fields import AutoField, BigAutoField
1212
from django.db.transaction import TransactionManagementError
13-
from django.utils.encoding import force_text
13+
from django.utils.encoding import force_str
1414

1515

1616
class Statement(DjStatement):
@@ -874,7 +874,7 @@ def quote_value(self, value):
874874
elif isinstance(value, str):
875875
return "'%s'" % value.replace("'", "''")
876876
elif isinstance(value, (bytes, bytearray, memoryview)):
877-
return "0x%s" % force_text(binascii.hexlify(value))
877+
return "0x%s" % force_str(binascii.hexlify(value))
878878
elif isinstance(value, bool):
879879
return "1" if value else "0"
880880
else:

0 commit comments

Comments
 (0)