Skip to content

Commit

Permalink
Add SQL Server CI coverage
Browse files Browse the repository at this point in the history
Change-Id: Ida0d01ae9bcc0573b86e24fddea620a38c962822
  • Loading branch information
zzzeek committed Aug 31, 2017
1 parent de73c6d commit 2efd89d
Show file tree
Hide file tree
Showing 36 changed files with 382 additions and 268 deletions.
8 changes: 8 additions & 0 deletions doc/build/changelog/unreleased_12/pymssql_sane_rowcount.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. change::
:tags: bug, mssql, orm

Enabled the "sane_rowcount" flag for the pymssql dialect, indicating
that the DBAPI now reports the correct number of rows affected from
an UPDATE or DELETE statement. This impacts mostly the ORM versioning
feature in that it now can verify the number of rows affected on a
target version.
58 changes: 6 additions & 52 deletions lib/sqlalchemy/connectors/pyodbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from .. import util


import sys
import re


Expand All @@ -19,10 +18,8 @@ class PyODBCConnector(Connector):
supports_sane_rowcount_returning = False
supports_sane_multi_rowcount = False

if util.py2k:
# PyODBC unicode is broken on UCS-4 builds
supports_unicode = sys.maxunicode == 65535
supports_unicode_statements = supports_unicode
supports_unicode_statements = True
supports_unicode_binds = True

supports_native_decimal = True
default_paramstyle = 'named'
Expand All @@ -31,21 +28,10 @@ class PyODBCConnector(Connector):
# hold the desired driver name
pyodbc_driver_name = None

# will be set to True after initialize()
# if the freetds.so is detected
freetds = False

# will be set to the string version of
# the FreeTDS driver if freetds is detected
freetds_driver_version = None

# will be set to True after initialize()
# if the libessqlsrv.so is detected
easysoft = False

def __init__(self, supports_unicode_binds=None, **kw):
super(PyODBCConnector, self).__init__(**kw)
self._user_supports_unicode_binds = supports_unicode_binds
if supports_unicode_binds is not None:
self.supports_unicode_binds = supports_unicode_binds

@classmethod
def dbapi(cls):
Expand Down Expand Up @@ -130,40 +116,8 @@ def is_disconnect(self, e, connection, cursor):
else:
return False

def initialize(self, connection):
# determine FreeTDS first. can't issue SQL easily
# without getting unicode_statements/binds set up.

pyodbc = self.dbapi

dbapi_con = connection.connection

_sql_driver_name = dbapi_con.getinfo(pyodbc.SQL_DRIVER_NAME)
self.freetds = bool(re.match(r".*libtdsodbc.*\.so", _sql_driver_name
))
self.easysoft = bool(re.match(r".*libessqlsrv.*\.so", _sql_driver_name
))

if self.freetds:
self.freetds_driver_version = dbapi_con.getinfo(
pyodbc.SQL_DRIVER_VER)

self.supports_unicode_statements = (
not util.py2k or
(not self.freetds and not self.easysoft)
)

if self._user_supports_unicode_binds is not None:
self.supports_unicode_binds = self._user_supports_unicode_binds
elif util.py2k:
self.supports_unicode_binds = (
not self.freetds or self.freetds_driver_version >= '0.91'
) and not self.easysoft
else:
self.supports_unicode_binds = True

# run other initialization which asks for user name, etc.
super(PyODBCConnector, self).initialize(connection)
# def initialize(self, connection):
# super(PyODBCConnector, self).initialize(connection)

def _dbapi_version(self):
if not self.dbapi:
Expand Down
37 changes: 15 additions & 22 deletions lib/sqlalchemy/dialects/mssql/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,17 +560,20 @@ class MyClass(Base):
Rowcount Support / ORM Versioning
---------------------------------
The SQL Server drivers have very limited ability to return the number
of rows updated from an UPDATE or DELETE statement. In particular, the
pymssql driver has no support, whereas the pyodbc driver can only return
this value under certain conditions.
In particular, updated rowcount is not available when OUTPUT INSERTED
is used. This impacts the SQLAlchemy ORM's versioning feature when
server-side versioning schemes are used. When
using pyodbc, the "implicit_returning" flag needs to be set to false
for any ORM mapped class that uses a version_id column in conjunction with
a server-side version generator::
The SQL Server drivers may have limited ability to return the number
of rows updated from an UPDATE or DELETE statement.
As of this writing, the PyODBC driver is not able to return a rowcount when
OUTPUT INSERTED is used. This impacts the SQLAlchemy ORM's versioning feature
in many cases where server-side value generators are in use in that while the
versioning operations can succeed, the ORM cannot always check that an UPDATE
or DELETE statement matched the number of rows expected, which is how it
verifies that the version identifier matched. When this condition occurs, a
warning will be emitted but the operation will proceed.
The use of OUTPUT INSERTED can be disabled by setting the
:paramref:`.Table.implicit_returning` flag to ``False`` on a particular
:class:`.Table`, which in declarative looks like::
class MyTable(Base):
__tablename__ = 'mytable'
Expand All @@ -585,14 +588,10 @@ class MyTable(Base):
'implicit_returning': False
}
Without the implicit_returning flag above, the UPDATE statement will
use ``OUTPUT inserted.timestamp`` and the rowcount will be returned as
-1, causing the versioning logic to fail.
Enabling Snapshot Isolation
---------------------------
Not necessarily specific to SQLAlchemy, SQL Server has a default transaction
SQL Server has a default transaction
isolation mode that locks entire tables, and causes even mildly concurrent
applications to have long held locks and frequent deadlocks.
Enabling snapshot isolation for the database as a whole is recommended
Expand All @@ -606,12 +605,6 @@ class MyTable(Base):
Background on SQL Server snapshot isolation is available at
http://msdn.microsoft.com/en-us/library/ms175095.aspx.
Known Issues
------------
* No support for more than one ``IDENTITY`` column per table
* reflection of indexes does not work with versions older than
SQL Server 2005
"""
import datetime
Expand Down
3 changes: 2 additions & 1 deletion lib/sqlalchemy/dialects/mssql/information_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def __init__(self, bindvalue):
@compiles(_cast_on_2005)
def _compile(element, compiler, **kw):
from . import base
if compiler.dialect.server_version_info < base.MS_2005_VERSION:
if compiler.dialect.server_version_info is None or \
compiler.dialect.server_version_info < base.MS_2005_VERSION:
return compiler.process(element.bindvalue, **kw)
else:
return compiler.process(cast(element.bindvalue, Unicode), **kw)
Expand Down
12 changes: 6 additions & 6 deletions lib/sqlalchemy/dialects/mssql/pymssql.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
`FreeTDS <http://www.freetds.org/>`_. Compatible builds are available for
Linux, MacOSX and Windows platforms.
Modern versions of this driver work very well with SQL Server and
FreeTDS from Linux and is highly recommended.
"""
from .base import MSDialect, MSIdentifierPreparer
from ... import types as sqltypes, util, processors
Expand All @@ -41,7 +44,7 @@ def __init__(self, dialect):


class MSDialect_pymssql(MSDialect):
supports_sane_rowcount = False
supports_native_decimal = True
driver = 'pymssql'

preparer = MSIdentifierPreparer_pymssql
Expand All @@ -68,10 +71,6 @@ def dbapi(cls):
"the 1.0 series of the pymssql DBAPI.")
return module

def __init__(self, **params):
super(MSDialect_pymssql, self).__init__(**params)
self.use_scope_identity = True

def _get_server_version_info(self, connection):
vers = connection.scalar("select @@version")
m = re.match(
Expand Down Expand Up @@ -111,6 +110,7 @@ def set_isolation_level(self, connection, level):
else:
connection.autocommit(False)
super(MSDialect_pymssql, self).set_isolation_level(connection,
level)
level)


dialect = MSDialect_pymssql
48 changes: 17 additions & 31 deletions lib/sqlalchemy/dialects/mssql/pyodbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,34 +64,19 @@
engine = create_engine("mssql+pyodbc:///?odbc_connect=%s" % params)
Unicode Binds
-------------
The current state of PyODBC on a unix backend with FreeTDS and/or
EasySoft is poor regarding unicode; different OS platforms and versions of
UnixODBC versus IODBC versus FreeTDS/EasySoft versus PyODBC itself
dramatically alter how strings are received. The PyODBC dialect attempts to
use all the information it knows to determine whether or not a Python unicode
literal can be passed directly to the PyODBC driver or not; while SQLAlchemy
can encode these to bytestrings first, some users have reported that PyODBC
mis-handles bytestrings for certain encodings and requires a Python unicode
object, while the author has observed widespread cases where a Python unicode
is completely misinterpreted by PyODBC, particularly when dealing with
the information schema tables used in table reflection, and the value
must first be encoded to a bytestring.
It is for this reason that whether or not unicode literals for bound
parameters be sent to PyODBC can be controlled using the
``supports_unicode_binds`` parameter to ``create_engine()``. When
left at its default of ``None``, the PyODBC dialect will use its
best guess as to whether or not the driver deals with unicode literals
well. When ``False``, unicode literals will be encoded first, and when
``True`` unicode literals will be passed straight through. This is an interim
flag that hopefully should not be needed when the unicode situation stabilizes
for unix + PyODBC.
.. versionadded:: 0.7.7
``supports_unicode_binds`` parameter to ``create_engine()``\ .
Driver / Unicode Support
-------------------------
PyODBC works best with Microsoft ODBC drivers, particularly in the area
of Unicode support on both Python 2 and Python 3.
Using the FreeTDS ODBC drivers on Linux or OSX with PyODBC is **not**
recommended; there have been historically many Unicode-related issues
in this area, including before Microsoft offered ODBC drivers for Linux
and OSX. Now that Microsoft offers drivers for all platforms, for
PyODBC support these are recommended. FreeTDS remains relevant for
non-ODBC drivers such as pymssql where it works very well.
Rowcount Support
----------------
Expand Down Expand Up @@ -272,11 +257,12 @@ def __init__(self, description_encoding=None, **params):

def _get_server_version_info(self, connection):
try:
raw = connection.scalar("SELECT SERVERPROPERTY('ProductVersion')")
raw = connection.scalar(
"SELECT CAST(SERVERPROPERTY('ProductVersion') AS VARCHAR)")
except exc.DBAPIError:
# SQL Server docs indicate this function isn't present prior to
# 2008; additionally, unknown combinations of pyodbc aren't
# able to run this query.
# 2008. Before we had the VARCHAR cast above, pyodbc would also
# fail on this query.
return super(MSDialect_pyodbc, self).\
_get_server_version_info(connection)
else:
Expand Down
7 changes: 7 additions & 0 deletions lib/sqlalchemy/dialects/mysql/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1871,6 +1871,13 @@ def initialize(self, connection):
def _is_mariadb(self):
return 'MariaDB' in self.server_version_info

@property
def _mariadb_normalized_version_info(self):
if len(self.server_version_info) > 5:
return self.server_version_info[3:]
else:
return self.server_version_info

@property
def _supports_cast(self):
return self.server_version_info is None or \
Expand Down
Loading

0 comments on commit 2efd89d

Please sign in to comment.