:ticket:`10050`
+
+.. _change_11250:
+
+Potential breaking change to odbc_connect= handling for mssql+pyodbc
+--------------------------------------------------------------------
+
+Fixed a mssql+pyodbc issue where valid plus signs in an already-unquoted
+``odbc_connect=`` (raw DBAPI) connection string were replaced with spaces.
+
+Previously, the pyodbc connector would always pass the odbc_connect value
+to unquote_plus(), even if it was not required. So, if the (unquoted)
+odbc_connect value contained ``PWD=pass+word`` that would get changed to
+``PWD=pass word``, and the login would fail. One workaround was to quote
+just the plus sign — ``PWD=pass%2Bword`` — which would then get unquoted
+to ``PWD=pass+word``.
+
+Implementations using the above workaround with :meth:`_engine.URL.create`
+to specify a plus sign in the ``PWD=`` argument of an odbc_connect string
+will have to remove the workaround and just pass the ``PWD=`` value as it
+would appear in a valid ODBC connection string (i.e., the same as would be
+required if using the connection string directly with ``pyodbc.connect()``).
+
+:ticket:`11250`
--- /dev/null
+.. change::
+ :tags: bug, mssql
+ :tickets: 11250
+
+ Fix mssql+pyodbc issue where valid plus signs in an already-unquoted
+ ``odbc_connect=`` (raw DBAPI) connection string are replaced with spaces.
+
+ The pyodbc connector would unconditionally pass the odbc_connect value
+ to unquote_plus(), even if it was not required. So, if the (unquoted)
+ odbc_connect value contained ``PWD=pass+word`` that would get changed to
+ ``PWD=pass word``, and the login would fail. One workaround was to quote
+ just the plus sign — ``PWD=pass%2Bword`` — which would then get unquoted
+ to ``PWD=pass+word``.
from typing import Optional
from typing import Tuple
from typing import Union
-from urllib.parse import unquote_plus
from . import Connector
from .. import ExecutionContext
connect_args[param] = util.asbool(keys.pop(param))
if "odbc_connect" in keys:
- connectors = [unquote_plus(keys.pop("odbc_connect"))]
+ # (potential breaking change for issue #11250)
+ connectors = [keys.pop("odbc_connect")]
else:
def check_quote(token: str) -> str:
connection,
)
+ @testing.combinations(
+ (
+ "quoted_plus",
+ (
+ "mssql+pyodbc:///?odbc_connect=DSN%3Dmydsn%3B"
+ "UID%3Ded%3BPWD%3Dpass%2Bword"
+ ),
+ "DSN=mydsn;UID=ed;PWD=pass+word",
+ ("DSN=mydsn;UID=ed;PWD=pass+word",),
+ "",
+ ),
+ (
+ "plus_for_space",
+ (
+ "mssql+pyodbc:///?odbc_connect=DSN%3Dmydsn%3B"
+ "UID%3Ded%3BPWD%3Dpass+word"
+ ),
+ "DSN=mydsn;UID=ed;PWD=pass word",
+ ("DSN=mydsn;UID=ed;PWD=pass word",),
+ "",
+ ),
+ (
+ "issue_11250_breaking_change",
+ (
+ "mssql+pyodbc:///?odbc_connect=DSN%3Dmydsn%3B"
+ "UID%3Ded%3BPWD%3Dpass%252Bword"
+ ),
+ "DSN=mydsn;UID=ed;PWD=pass%2Bword",
+ ("DSN=mydsn;UID=ed;PWD=pass%2Bword",),
+ "pre-11250 would unquote_plus() to PWD=pass+word",
+ ),
+ argnames="quoted_url, value_in_url_object, connection_string",
+ id_="iaaai",
+ )
+ def test_pyodbc_odbc_connect_with_pwd_plus(
+ self, quoted_url, value_in_url_object, connection_string
+ ):
+ dialect = pyodbc.dialect()
+ u = url.make_url(quoted_url)
+ eq_(value_in_url_object, u.query["odbc_connect"])
+ connection = dialect.create_connect_args(u)
+ eq_(
+ (
+ (connection_string),
+ {},
+ ),
+ connection,
+ )
+
def test_pyodbc_odbc_connect_ignores_other_values(self):
dialect = pyodbc.dialect()
u = url.make_url(