From: Mike Bayer Date: Wed, 13 Nov 2024 15:46:17 +0000 (-0500) Subject: apply quote to url.database portion X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=feb17832f17e45a81675f7104dac82f34c078d63;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git apply quote to url.database portion Adjusted URL parsing and stringification to apply url quoting to the "database" portion of the URL. This allows a URL where the "database" portion includes special characters such as question marks to be accommodated. Fixes: #11234 Change-Id: If868c96969b70f1090f0b474403d22fd3a2cc529 --- diff --git a/doc/build/changelog/migration_21.rst b/doc/build/changelog/migration_21.rst index 45a152c7b3..304f9a5d24 100644 --- a/doc/build/changelog/migration_21.rst +++ b/doc/build/changelog/migration_21.rst @@ -134,6 +134,55 @@ lambdas which do the same:: :ticket:`10050` +.. _change_11234: + +URL stringify and parse now supports URL escaping for the "database" portion +---------------------------------------------------------------------------- + +A URL that includes URL-escaped characters in the database portion will +now parse with conversion of those escaped characters:: + + >>> from sqlalchemy import make_url + >>> u = make_url("driver://user:pass@host/database%3Fname") + >>> u.database + 'database?name' + +Previously, such characters would not be unescaped:: + + >>> # pre-2.1 behavior + >>> from sqlalchemy import make_url + >>> u = make_url("driver://user:pass@host/database%3Fname") + >>> u.database + 'database%3Fname' + +This change also applies to the stringify side; most special characters in +the database name will be URL escaped, omitting a few such as plus signs and +slashes:: + + >>> from sqlalchemy import URL + >>> u = URL.create("driver", database="a?b=c") + >>> str(u) + 'driver:///a%3Fb%3Dc' + +Where the above URL correctly round-trips to itself:: + + >>> make_url(str(u)) + driver:///a%3Fb%3Dc + >>> make_url(str(u)).database == u.database + True + + +Whereas previously, special characters applied programmatically would not +be escaped in the result, leading to a URL that does not represent the +original database portion. Below, `b=c` is part of the query string and +not the database portion:: + + >>> from sqlalchemy import URL + >>> u = URL.create("driver", database="a?b=c") + >>> str(u) + 'driver:///a?b=c' + +:ticket:`11234` .. _change_11250: diff --git a/doc/build/changelog/unreleased_21/11234.rst b/doc/build/changelog/unreleased_21/11234.rst new file mode 100644 index 0000000000..f168714e89 --- /dev/null +++ b/doc/build/changelog/unreleased_21/11234.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: bug, engine + :tickets: 11234 + + Adjusted URL parsing and stringification to apply url quoting to the + "database" portion of the URL. This allows a URL where the "database" + portion includes special characters such as question marks to be + accommodated. + + .. seealso:: + + :ref:`change_11234` diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 1eeb73a236..7775a2ed88 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -641,7 +641,7 @@ class URL(NamedTuple): if self.port is not None: s += ":" + str(self.port) if self.database is not None: - s += "/" + self.database + s += "/" + quote(self.database, safe=" +/") if self.query: keys = list(self.query) keys.sort() @@ -888,11 +888,9 @@ def _parse_url(name: str) -> URL: query = None components["query"] = query - if components["username"] is not None: - components["username"] = unquote(components["username"]) - - if components["password"] is not None: - components["password"] = unquote(components["password"]) + for comp in "username", "password", "database": + if components[comp] is not None: + components[comp] = unquote(components[comp]) ipv4host = components.pop("ipv4host") ipv6host = components.pop("ipv6host") diff --git a/test/dialect/mssql/test_engine.py b/test/dialect/mssql/test_engine.py index 0e9d2fdcf0..8703cae765 100644 --- a/test/dialect/mssql/test_engine.py +++ b/test/dialect/mssql/test_engine.py @@ -296,7 +296,7 @@ class ParseConnectTest(fixtures.TestBase): ), ( "DRIVER={foob};Server=somehost%3BPORT%3D50001;" - "Database=somedb%3BPORT%3D50001;UID={someuser;PORT=50001};" + "Database={somedb;PORT=50001};UID={someuser;PORT=50001};" "PWD={some{strange}}pw;PORT=50001}", ), ), diff --git a/test/engine/test_parseconnect.py b/test/engine/test_parseconnect.py index 16b129fd8a..254d9c00fe 100644 --- a/test/engine/test_parseconnect.py +++ b/test/engine/test_parseconnect.py @@ -59,7 +59,9 @@ class URLTest(fixtures.TestBase): "/database?foo=bar", "dbtype://username:password@[2001:da8:2004:1000:202:116:160:90]:80" "/database?foo=bar", - "dbtype://username:password@hostspec/test database with@atsign", + "dbtype://username:password@hostspec/test+database with%40atsign", + "dbtype://username:password@hostspec/db%3Fwith%3Dqmark", + "dbtype://username:password@hostspec/test database with spaces", "dbtype://username:password@hostspec?query=but_no_db", "dbtype://username:password@hostspec:450?query=but_no_db", "dbtype://username:password with spaces@hostspec:450?query=but_no_db", @@ -98,18 +100,29 @@ class URLTest(fixtures.TestBase): ), u.host assert u.database in ( "database", - "test database with@atsign", + "test+database with@atsign", + "test database with spaces", "/usr/local/_xtest@example.com/members.db", "/usr/db_file.db", ":memory:", "", "foo/bar/im/a/file", "E:/work/src/LEM/db/hello.db", + "db?with=qmark", None, ), u.database eq_(url.make_url(u.render_as_string(hide_password=False)), u) + def test_dont_urlescape_slashes(self): + """supplemental test for #11234 where we want to not escape slashes + as this causes problems for alembic tests that deliver paths into + configparser format""" + + u = url.make_url("dbtype:///path/with/slashes") + eq_(str(u), "dbtype:///path/with/slashes") + eq_(u.database, "path/with/slashes") + def test_rfc1738_password(self): u = url.make_url("dbtype://user:pass word + other%3Awords@host/dbname") eq_(u.password, "pass word + other:words")