diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-09-03 10:30:51 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-09-03 10:31:29 -0400 |
commit | 4399431b53e5d132672431205c654d7d6b32dd77 (patch) | |
tree | e0d6baef528c1f6dae8331bf1e86fa7f84d90aa8 | |
parent | 613d8ca0f84d3e92b35403eaba21824e72b8ada8 (diff) | |
download | sqlalchemy-4399431b53e5d132672431205c654d7d6b32dd77.tar.gz |
- The hostname-based connection format for SQL Server when using
pyodbc will no longer specify a default "driver name", and a warning
is emitted if this is missing. The optimal driver name for SQL Server
changes frequently and is per-platform, so hostname based connections
need to specify this. DSN-based connections are preferred.
fixes #3182
-rw-r--r-- | doc/build/changelog/changelog_10.rst | 14 | ||||
-rw-r--r-- | doc/build/changelog/migration_10.rst | 18 | ||||
-rw-r--r-- | lib/sqlalchemy/connectors/pyodbc.py | 21 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/mssql/pyodbc.py | 87 | ||||
-rw-r--r-- | test/dialect/mssql/test_engine.py | 26 |
5 files changed, 102 insertions, 64 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index 643035477..715936068 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -22,6 +22,20 @@ on compatibility concerns, see :doc:`/changelog/migration_10`. .. change:: + :tags: changed, mssql + :tickets: 3182 + + The hostname-based connection format for SQL Server when using + pyodbc will no longer specify a default "driver name", and a warning + is emitted if this is missing. The optimal driver name for SQL Server + changes frequently and is per-platform, so hostname based connections + need to specify this. DSN-based connections are preferred. + + .. seealso:: + + :ref:`change_3182` + + .. change:: :tags: changed, sql The :func:`~.expression.column` and :func:`~.expression.table` diff --git a/doc/build/changelog/migration_10.rst b/doc/build/changelog/migration_10.rst index 8acaa0445..58aa42df0 100644 --- a/doc/build/changelog/migration_10.rst +++ b/doc/build/changelog/migration_10.rst @@ -808,6 +808,24 @@ a cursor to be closed unless all results are fully fetched. :ticket:`2515` +.. _change_3182: + +PyODBC driver name is required with hostname-based SQL Server connections +------------------------------------------------------------------------- + +Connecting to SQL Server with PyODBC using a DSN-less connection, e.g. +with an explicit hostname, now requires a driver name - SQLAlchemy will no +longer attempt to guess a default:: + + engine = create_engine("mssql+pyodbc://scott:tiger@myhost:port/databasename?driver=SQL+Server+Native+Client+10.0") + +SQLAlchemy's previously hardcoded default of "SQL Server" is obsolete on +Windows, and SQLAlchemy cannot be tasked with guessing the best driver +based on operation system/driver detection. Using a DSN is always preferred +when using ODBC to avoid this issue entirely. + +:ticket:`3182` + .. _change_2984: Drizzle Dialect is now an External Dialect diff --git a/lib/sqlalchemy/connectors/pyodbc.py b/lib/sqlalchemy/connectors/pyodbc.py index ef72c8049..907e4d353 100644 --- a/lib/sqlalchemy/connectors/pyodbc.py +++ b/lib/sqlalchemy/connectors/pyodbc.py @@ -26,7 +26,7 @@ class PyODBCConnector(Connector): supports_native_decimal = True default_paramstyle = 'named' - # for non-DSN connections, this should + # for non-DSN connections, this *may* be used to # hold the desired driver name pyodbc_driver_name = None @@ -75,10 +75,21 @@ class PyODBCConnector(Connector): if 'port' in keys and 'port' not in query: port = ',%d' % int(keys.pop('port')) - connectors = ["DRIVER={%s}" % - keys.pop('driver', self.pyodbc_driver_name), - 'Server=%s%s' % (keys.pop('host', ''), port), - 'Database=%s' % keys.pop('database', '')] + connectors = [] + driver = keys.pop('driver', self.pyodbc_driver_name) + if driver is None: + util.warn( + "No driver name specified; " + "this is expected by PyODBC when using " + "DSN-less connections") + else: + connectors.append("DRIVER={%s}" % driver) + + connectors.extend( + [ + 'Server=%s%s' % (keys.pop('host', ''), port), + 'Database=%s' % keys.pop('database', '') + ]) user = keys.pop("user", None) if user: diff --git a/lib/sqlalchemy/dialects/mssql/pyodbc.py b/lib/sqlalchemy/dialects/mssql/pyodbc.py index 1c75fe1ff..445584d24 100644 --- a/lib/sqlalchemy/dialects/mssql/pyodbc.py +++ b/lib/sqlalchemy/dialects/mssql/pyodbc.py @@ -12,74 +12,57 @@ :connectstring: mssql+pyodbc://<username>:<password>@<dsnname> :url: http://pypi.python.org/pypi/pyodbc/ -Additional Connection Examples -------------------------------- +Connecting to PyODBC +-------------------- -Examples of pyodbc connection string URLs: +The URL here is to be translated to PyODBC connection strings, as +detailed in `ConnectionStrings <https://code.google.com/p/pyodbc/wiki/ConnectionStrings>`_. -* ``mssql+pyodbc://mydsn`` - connects using the specified DSN named ``mydsn``. - The connection string that is created will appear like:: +DSN Connections +^^^^^^^^^^^^^^^ - dsn=mydsn;Trusted_Connection=Yes +A DSN-based connection is **preferred** overall when using ODBC. A +basic DSN-based connection looks like:: -* ``mssql+pyodbc://user:pass@mydsn`` - connects using the DSN named - ``mydsn`` passing in the ``UID`` and ``PWD`` information. The - connection string that is created will appear like:: + engine = create_engine("mssql+pyodbc://scott:tiger@some_dsn") - dsn=mydsn;UID=user;PWD=pass - -* ``mssql+pyodbc://user:pass@mydsn/?LANGUAGE=us_english`` - connects - using the DSN named ``mydsn`` passing in the ``UID`` and ``PWD`` - information, plus the additional connection configuration option - ``LANGUAGE``. The connection string that is created will appear - like:: - - dsn=mydsn;UID=user;PWD=pass;LANGUAGE=us_english - -* ``mssql+pyodbc://user:pass@host/db`` - connects using a connection - that would appear like:: +Which above, will pass the following connection string to PyODBC:: - DRIVER={SQL Server};Server=host;Database=db;UID=user;PWD=pass - -* ``mssql+pyodbc://user:pass@host:123/db`` - connects using a connection - string which includes the port - information using the comma syntax. This will create the following - connection string:: - - DRIVER={SQL Server};Server=host,123;Database=db;UID=user;PWD=pass + dsn=mydsn;UID=user;PWD=pass -* ``mssql+pyodbc://user:pass@host/db?port=123`` - connects using a connection - string that includes the port - information as a separate ``port`` keyword. This will create the - following connection string:: +If the username and password are omitted, the DSN form will also add +the ``Trusted_Connection=yes`` directive to the ODBC string. - DRIVER={SQL Server};Server=host;Database=db;UID=user;PWD=pass;port=123 +Hostname Connections +^^^^^^^^^^^^^^^^^^^^ -* ``mssql+pyodbc://user:pass@host/db?driver=MyDriver`` - connects using a - connection string that includes a custom ODBC driver name. This will create - the following connection string:: +Hostname-based connections are **not preferred**, however are supported. +The ODBC driver name must be explicitly specified:: - DRIVER={MyDriver};Server=host;Database=db;UID=user;PWD=pass + engine = create_engine("mssql+pyodbc://scott:tiger@myhost:port/databasename?driver=SQL+Server+Native+Client+10.0") -If you require a connection string that is outside the options -presented above, use the ``odbc_connect`` keyword to pass in a -urlencoded connection string. What gets passed in will be urldecoded -and passed directly. +.. versionchanged:: 1.0.0 Hostname-based PyODBC connections now require the + SQL Server driver name specified explicitly. SQLAlchemy cannot + choose an optimal default here as it varies based on platform + and installed drivers. -For example:: +Other keywords interpreted by the Pyodbc dialect to be passed to +``pyodbc.connect()`` in both the DSN and hostname cases include: +``odbc_autotranslate``, ``ansi``, ``unicode_results``, ``autocommit``. - mssql+pyodbc:///?odbc_connect=dsn%3Dmydsn%3BDatabase%3Ddb +Pass through exact Pyodbc string +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -would create the following connection string:: +A PyODBC connection string can also be sent exactly as specified in +`ConnectionStrings <https://code.google.com/p/pyodbc/wiki/ConnectionStrings>`_ +into the driver using the parameter ``odbc_connect``. The delimeters must be URL escaped, however, +as illustrated below using ``urllib.quote_plus``:: - dsn=mydsn;Database=db + import urllib + params = urllib.quote_plus("DRIVER={SQL Server Native Client 10.0};SERVER=dagger;DATABASE=test;UID=user;PWD=password") -Encoding your connection string can be easily accomplished through -the python shell. For example:: + engine = create_engine("mssql+pyodbc:///?odbc_connect=%s" % params) - >>> import urllib - >>> urllib.quote_plus('dsn=mydsn;Database=db') - 'dsn%3Dmydsn%3BDatabase%3Ddb' Unicode Binds ------------- @@ -243,8 +226,6 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect): execution_ctx_cls = MSExecutionContext_pyodbc - pyodbc_driver_name = 'SQL Server' - colspecs = util.update_copy( MSDialect.colspecs, { diff --git a/test/dialect/mssql/test_engine.py b/test/dialect/mssql/test_engine.py index c07f30040..8ac9c6c16 100644 --- a/test/dialect/mssql/test_engine.py +++ b/test/dialect/mssql/test_engine.py @@ -6,7 +6,7 @@ from sqlalchemy.dialects.mssql import pyodbc, pymssql from sqlalchemy.engine import url from sqlalchemy.testing import fixtures from sqlalchemy import testing -from sqlalchemy.testing import assert_raises_message +from sqlalchemy.testing import assert_raises_message, assert_warnings class ParseConnectTest(fixtures.TestBase): @@ -38,18 +38,32 @@ class ParseConnectTest(fixtures.TestBase): assert ";LANGUAGE=us_english" in dsn_string assert ";foo=bar" in dsn_string - def test_pyodbc_connect(self): + def test_pyodbc_hostname(self): dialect = pyodbc.dialect() - u = url.make_url('mssql://username:password@hostspec/database') + u = url.make_url('mssql://username:password@hostspec/database?driver=SQL+Server') connection = dialect.create_connect_args(u) eq_([['DRIVER={SQL Server};Server=hostspec;Database=database;UI' 'D=username;PWD=password'], {}], connection) + def test_pyodbc_host_no_driver(self): + dialect = pyodbc.dialect() + u = url.make_url('mssql://username:password@hostspec/database') + + def go(): + return dialect.create_connect_args(u) + connection = assert_warnings( + go, + ["No driver name specified; this is expected by " + "PyODBC when using DSN-less connections"]) + + eq_([['Server=hostspec;Database=database;UI' + 'D=username;PWD=password'], {}], connection) + def test_pyodbc_connect_comma_port(self): dialect = pyodbc.dialect() u = \ url.make_url('mssql://username:password@hostspec:12345/data' - 'base') + 'base?driver=SQL Server') connection = dialect.create_connect_args(u) eq_([['DRIVER={SQL Server};Server=hostspec,12345;Database=datab' 'ase;UID=username;PWD=password'], {}], connection) @@ -58,7 +72,7 @@ class ParseConnectTest(fixtures.TestBase): dialect = pyodbc.dialect() u = \ url.make_url('mssql://username:password@hostspec/database?p' - 'ort=12345') + 'ort=12345&driver=SQL+Server') connection = dialect.create_connect_args(u) eq_([['DRIVER={SQL Server};Server=hostspec;Database=database;UI' 'D=username;PWD=password;port=12345'], {}], connection) @@ -67,7 +81,7 @@ class ParseConnectTest(fixtures.TestBase): dialect = pyodbc.dialect() u = \ url.make_url('mssql://username:password@hostspec/database?L' - 'ANGUAGE=us_english&foo=bar') + 'ANGUAGE=us_english&foo=bar&driver=SQL+Server') connection = dialect.create_connect_args(u) eq_(connection[1], {}) eq_(connection[0][0] |