]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
apply quote to url.database portion
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 13 Nov 2024 15:46:17 +0000 (10:46 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 15 Nov 2024 13:11:40 +0000 (08:11 -0500)
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

doc/build/changelog/migration_21.rst
doc/build/changelog/unreleased_21/11234.rst [new file with mode: 0644]
lib/sqlalchemy/engine/url.py
test/dialect/mssql/test_engine.py
test/engine/test_parseconnect.py

index 45a152c7b3cecfefbed9cd56ab2dbaef682bb788..304f9a5d24944073a33fb8c8b42e08cb9aeb3831 100644 (file)
@@ -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 (file)
index 0000000..f168714
--- /dev/null
@@ -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`
index 1eeb73a2368bfd601e9d69c684b2d89afa75eb91..7775a2ed88d9d9d5a4100a9881d478ca02e81808 100644 (file)
@@ -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")
index 0e9d2fdcf03a07b11f0e55286411c78ade6f8716..8703cae765e15e2d695b7857f5efd701f508021c 100644 (file)
@@ -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}",
             ),
         ),
index 16b129fd8a38af38e8199230450fe9ed7c8a39af..254d9c00fe759b345e36b72d67ebbdf5555c2c42 100644 (file)
@@ -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")