├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.cfg ├── setup.py └── sqlserver_pymssql ├── __init__.py ├── base.py └── compiler.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | build 4 | dist 5 | MANIFEST 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Aymeric Augustin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-pymssql 2 | ============== 3 | 4 | **I'm not maintaining this library because I no longer use SQL Server. 5 | If you'd like to maintain it, please file an issue and let me know.** 6 | 7 | Goals 8 | ----- 9 | 10 | django-pymssql is a Django database backend for Microsoft SQL Server that 11 | works on non-Windows systems. 12 | 13 | It's a small wrapper around django-mssql_ that uses pymssql_ instead of ADO to 14 | connect to SQL Server. 15 | 16 | It should support the same versions of Python, Django and SQL Server as 17 | django-mssql_. 18 | 19 | The original use case was to connect to SQL Server from a Django project 20 | written in Python 3 and running on Linux. 21 | 22 | Status 23 | ------ 24 | 25 | django-pymssql 1.7 almost_ passes Django's test suite with: 26 | 27 | - Python 2.7 or 3.4 28 | - Django 1.7.x + django-mssql 1.6.1 + pymssql 2.1.1 29 | - Microsoft® SQL Server® 2012 Express 30 | 31 | Usage 32 | ----- 33 | 34 | django-pymssql provides a Django database engine called ``sqlserver_pymssql``: 35 | 36 | .. code-block:: python 37 | 38 | DATABASES = { 39 | 'default': { 40 | 'ENGINE': 'sqlserver_pymssql', 41 | 'HOST': '...', 42 | 'NAME': '...', 43 | 'USER': '...', 44 | 'PASSWORD': '...',' 45 | 'OPTIONS': { 46 | # ... 47 | }, 48 | }, 49 | } 50 | 51 | Any parameter accepted by `pymssql.connect`_ can be passed in OPTIONS. 52 | 53 | Alternatives 54 | ------------ 55 | 56 | django-sqlserver_ is a fork of django-mssql_ that supports python-tds_ and 57 | pymssql_ in addition to ADO on Windows. Unfortunately it has diverged and it 58 | lags behind django-mssql_ when it comes to supporting newer Django versions. 59 | 60 | django-pyodbc_ relies on pyodbc_ to connect to SQL Server. It requires a 61 | complex stack that doesn't bring actual benefits. Besides it doesn't appear 62 | to be very mature nor actively maintained. 63 | 64 | Hacking 65 | ------- 66 | 67 | Clone Django, pymssql, django-mssql and django-pymssql and ``pip install -e 68 | .`` each of them in a virtualenv. 69 | 70 | Create a Django tests settings file with the database engine set to 71 | ``'sqlserver_pymssql'`` and credentials for a testing SQL Server instance. 72 | 73 | Go to the ``tests`` subdirectory in a clone of Django and execute 74 | ``./runtests.py --settings=test_pymssql``. 75 | 76 | License 77 | ------- 78 | 79 | django-pymssql is released under the MIT license, like django-mssql_. See the 80 | LICENSE file for details. Note that pymssql_ is released under the LGPL. 81 | 82 | Some database version checking code was borrowed from django-sqlserver_ which 83 | is also released under the MIT license. 84 | 85 | .. _almost: https://github.com/aaugustin/django-pymssql/search?q=failing_tests 86 | .. _django-mssql: http://django-mssql.readthedocs.org/ 87 | .. _django-pyodbc: https://github.com/lionheart/django-pyodbc 88 | .. _django-sqlserver: https://github.com/denisenkom/django-sqlserver 89 | .. _pymssql: http://www.pymssql.org/ 90 | .. _pymssql.connect: http://pymssql.org/en/latest/ref/pymssql.html#pymssql.connect 91 | .. _pyodbc: https://github.com/mkleehammer/pyodbc 92 | .. _python-tds: https://github.com/denisenkom/pytds 93 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import codecs 4 | import os.path 5 | 6 | import setuptools 7 | 8 | root_dir = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | description = ("Django database backend for Microsoft SQL Server " 11 | "that works on non-Windows systems.") 12 | 13 | with codecs.open(os.path.join(root_dir, 'README.rst'), encoding='utf-8') as f: 14 | long_description = f.read() 15 | 16 | setuptools.setup( 17 | name='django-pymssql', 18 | version='1.7.1', 19 | description=description, 20 | long_description=long_description, 21 | url='https://github.com/aaugustin/django-pymssql', 22 | author='Aymeric Augustin', 23 | author_email='aymeric.augustin@m4x.org', 24 | license='BSD', 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Environment :: Web Environment', 28 | 'Framework :: Django', 29 | 'Framework :: Django :: 1.7', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: BSD License', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | ], 39 | packages=[ 40 | 'sqlserver_pymssql', 41 | ], 42 | install_requires=[ 43 | 'Django', 44 | 'django-mssql', 45 | 'pymssql', 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /sqlserver_pymssql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaugustin/django-pymssql/a99ca2f63fd67bc6855340ecb51dbe4f35f6bd06/sqlserver_pymssql/__init__.py -------------------------------------------------------------------------------- /sqlserver_pymssql/base.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils import timezone 4 | 5 | import pymssql as Database 6 | 7 | from sqlserver_ado.base import ( 8 | DatabaseFeatures as _DatabaseFeatures, 9 | DatabaseOperations as _DatabaseOperations, 10 | DatabaseWrapper as _DatabaseWrapper) 11 | 12 | DatabaseError = Database.DatabaseError 13 | IntegrityError = Database.IntegrityError 14 | 15 | VERSION_SQL2000 = 8 16 | VERSION_SQL2005 = 9 17 | VERSION_SQL2008 = 10 18 | 19 | 20 | def _fix_query(query): 21 | # For Django's inspectdb tests -- a model has a non-ASCII column name. 22 | if not isinstance(query, str): 23 | query = query.encode('utf-8') 24 | # For Django's backends and expressions_regress tests. 25 | query = query.replace('%%', '%') 26 | return query 27 | 28 | 29 | def _fix_value(value): 30 | if isinstance(value, datetime.datetime): 31 | if timezone.is_aware(value): 32 | value = timezone.make_naive(value, timezone.utc) 33 | return value 34 | 35 | 36 | def _fix_params(params): 37 | if params is not None: 38 | # pymssql needs a tuple, not another kind of iterable. 39 | params = tuple(_fix_value(value) for value in params) 40 | return params 41 | 42 | 43 | class CursorWrapper(object): 44 | 45 | def __init__(self, cursor): 46 | self.cursor = cursor 47 | 48 | def callproc(self, procname, params=None): 49 | params = _fix_params(params) 50 | return self.cursor.callproc(procname, params) 51 | 52 | def execute(self, query, params=None): 53 | query = _fix_query(query) 54 | params = _fix_params(params) 55 | return self.cursor.execute(query, params) 56 | 57 | def executemany(self, query, param_list): 58 | query = _fix_query(query) 59 | param_list = [_fix_params(params) for params in param_list] 60 | return self.cursor.executemany(query, param_list) 61 | 62 | def __getattr__(self, attr): 63 | return getattr(self.cursor, attr) 64 | 65 | def __iter__(self): 66 | return iter(self.cursor) 67 | 68 | 69 | class DatabaseOperations(_DatabaseOperations): 70 | 71 | compiler_module = "sqlserver_pymssql.compiler" 72 | 73 | 74 | class DatabaseFeatures(_DatabaseFeatures): 75 | 76 | can_introspect_max_length = False 77 | can_introspect_null = False 78 | can_introspect_decimal_field = False 79 | 80 | failing_tests = _DatabaseFeatures.failing_tests.copy() 81 | failing_tests.update({ 82 | 83 | # pymssql doesn't handle binary data correctly. 84 | 'backends.tests.LastExecutedQueryTest' 85 | '.test_query_encoding': [(1, 7)], 86 | 'model_fields.tests.BinaryFieldTests' 87 | '.test_set_and_retrieve': [(1, 7)], 88 | 89 | # pymssql doesn't check parameter counts. 90 | 'backends.tests.ParameterHandlingTest' 91 | '.test_bad_parameter_count': [(1, 7)], 92 | 93 | # Several tests that depend on schema alteration fail at this time. 94 | # This should get fixed in django-mssql when it supports migrations. 95 | 96 | }) 97 | 98 | 99 | class DatabaseWrapper(_DatabaseWrapper): 100 | 101 | Database = Database 102 | 103 | def __init__(self, *args, **kwargs): 104 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 105 | self.features = DatabaseFeatures(self) 106 | self.ops = DatabaseOperations(self) 107 | 108 | def get_connection_params(self): 109 | settings_dict = self.settings_dict 110 | params = { 111 | 'host': settings_dict['HOST'], 112 | 'database': settings_dict['NAME'], 113 | 'user': settings_dict['USER'], 114 | 'password': settings_dict['PASSWORD'] 115 | } 116 | if settings_dict['PORT']: 117 | params['port'] = settings_dict['PORT'] 118 | options = settings_dict.get('OPTIONS', {}) 119 | params.update(options) 120 | return params 121 | 122 | def get_new_connection(self, conn_params): 123 | return Database.connect(**conn_params) 124 | 125 | def init_connection_state(self): 126 | # Not calling super() because we don't care much about version checks. 127 | pass 128 | 129 | def create_cursor(self): 130 | cursor = self.connection.cursor() 131 | return CursorWrapper(cursor) 132 | 133 | def _set_autocommit(self, autocommit): 134 | self.connection.autocommit(autocommit) 135 | 136 | def __get_dbms_version(self, make_connection=True): 137 | """ 138 | Returns the 'DBMS Version' string, or ''. If a connection to the 139 | database has not already been established, a connection will be made 140 | when `make_connection` is True. 141 | """ 142 | if not self.connection and make_connection: 143 | self.connect() 144 | with self.connection.cursor() as cursor: 145 | cursor.execute("SELECT SERVERPROPERTY('productversion')") 146 | return cursor.fetchone()[0] 147 | 148 | def _is_sql2005_and_up(self, conn): 149 | return self._get_major_ver(conn) >= VERSION_SQL2005 150 | 151 | def _is_sql2008_and_up(self, conn): 152 | return self._get_major_ver(conn) >= VERSION_SQL2008 153 | -------------------------------------------------------------------------------- /sqlserver_pymssql/compiler.py: -------------------------------------------------------------------------------- 1 | from sqlserver_ado.compiler import * # noqa 2 | 3 | 4 | # Replace the whole SQLUpdateCompiler with another implementation. 5 | 6 | class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): 7 | 8 | def execute_sql(self, result_type): 9 | # Use a straight pymssql cursor to avoid throwing query counts off. 10 | # (Check Django's update_only_fields tests if you change this.) 11 | cursor = self.connection.connection.cursor() 12 | try: 13 | cursor.execute('SET NOCOUNT OFF') 14 | result = super(SQLUpdateCompiler, self).execute_sql(result_type) 15 | cursor.execute('SET NOCOUNT ON') 16 | return result 17 | finally: 18 | cursor.close() 19 | --------------------------------------------------------------------------------