]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The hostname-based connection format for SQL Server when using
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 3 Sep 2014 14:30:51 +0000 (10:30 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 3 Sep 2014 14:31:29 +0000 (10:31 -0400)
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

doc/build/changelog/changelog_10.rst
doc/build/changelog/migration_10.rst
lib/sqlalchemy/connectors/pyodbc.py
lib/sqlalchemy/dialects/mssql/pyodbc.py
test/dialect/mssql/test_engine.py

index 64303547759aac6176edde96d95ce13675adbb8e..715936068c117a9b4d2b0fce68359a0a686049f7 100644 (file)
     series as well.  For changes that are specific to 1.0 with an emphasis
     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
 
index 8acaa04458a93b098c29d5f581e12b1ed215e5ca..58aa42df064861097a0c494c83b1c96c36eb82ab 100644 (file)
@@ -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
index ef72c80496837b5cb8a7b411b025660e8705e465..907e4d35348cb12991c2a17aa3999631d7e9bbcf 100644 (file)
@@ -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:
index 1c75fe1ffa5ba6dc1842d88242231ba89069e144..445584d2497c03fcf90f40293d7006799b4b70fe 100644 (file)
     :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,
         {
index c07f30040515941e21cfd3cf0b26db827722b085..8ac9c6c16c6a00460189a23db141c1c2168ec1b5 100644 (file)
@@ -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]