]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
fix: allow escape value to be an empty string
authorMartin Caslavsky <mcaslavsky@gmail.com>
Thu, 8 Jun 2023 21:34:38 +0000 (17:34 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 9 Jun 2023 14:08:21 +0000 (10:08 -0400)
Fixed issue where the :paramref:`.ColumnOperators.like.escape` and similar
parameters did not allow an empty string as an argument that would be
passed through as the "escape" character; this is a supported syntax by
PostgreSQL.  Pull requset courtesy Martin Caslavsky.

Fixes: #9907
Closes: #9908
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/9908
Pull-request-sha: d7ecc1778a07b17adfa58050b44f6e2945dd1448

Change-Id: I39adb765a1b9650fe891883ed0973df66adc4e81

doc/build/changelog/unreleased_20/9907.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/sql/compiler.py
test/dialect/postgresql/test_compiler.py
test/sql/test_operators.py

diff --git a/doc/build/changelog/unreleased_20/9907.rst b/doc/build/changelog/unreleased_20/9907.rst
new file mode 100644 (file)
index 0000000..4ce6962
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, postgresql
+    :tickets: 9907
+
+    Fixed issue where the :paramref:`.ColumnOperators.like.escape` and similar
+    parameters did not allow an empty string as an argument that would be
+    passed through as the "escape" character; this is a supported syntax by
+    PostgreSQL.  Pull requset courtesy Martin Caslavsky.
index 00443d79bae8eab96e6f7864c368ad0e59745e96..61aa76db71c13f78f86651b493d4a298acef482f 100644 (file)
@@ -1789,7 +1789,7 @@ class PGCompiler(compiler.SQLCompiler):
             self.process(binary.right, **kw),
         ) + (
             " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
-            if escape
+            if escape is not None
             else ""
         )
 
@@ -1800,7 +1800,7 @@ class PGCompiler(compiler.SQLCompiler):
             self.process(binary.right, **kw),
         ) + (
             " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
-            if escape
+            if escape is not None
             else ""
         )
 
index 198bba3584fe2567b0ac45ed3aa9345eebe410a2..79092ec6619507fbffde4543c709477d006eec29 100644 (file)
@@ -3482,7 +3482,7 @@ class SQLCompiler(Compiled):
             binary.right._compiler_dispatch(self, **kw),
         ) + (
             " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
-            if escape
+            if escape is not None
             else ""
         )
 
@@ -3493,7 +3493,7 @@ class SQLCompiler(Compiled):
             binary.right._compiler_dispatch(self, **kw),
         ) + (
             " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
-            if escape
+            if escape is not None
             else ""
         )
 
index bd944849de07cd1424933d0fac81706178406c64..8f5707f46c2e614ae0f271bf7340f7c9452487e8 100644 (file)
@@ -24,6 +24,7 @@ from sqlalchemy import schema
 from sqlalchemy import select
 from sqlalchemy import Sequence
 from sqlalchemy import SmallInteger
+from sqlalchemy import sql
 from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy import testing
@@ -2583,6 +2584,30 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
             "SELECT c1 # c2 AS anon_1",
         )
 
+    def test_ilike_escaping(self):
+        dialect = postgresql.dialect()
+        self.assert_compile(
+            sql.column("foo").ilike("bar", escape="\\"),
+            "foo ILIKE %(foo_1)s ESCAPE '\\\\'",
+        )
+
+        self.assert_compile(
+            sql.column("foo").ilike("bar", escape=""),
+            "foo ILIKE %(foo_1)s ESCAPE ''",
+            dialect=dialect,
+        )
+
+        self.assert_compile(
+            sql.column("foo").notilike("bar", escape="\\"),
+            "foo NOT ILIKE %(foo_1)s ESCAPE '\\\\'",
+        )
+
+        self.assert_compile(
+            sql.column("foo").notilike("bar", escape=""),
+            "foo NOT ILIKE %(foo_1)s ESCAPE ''",
+            dialect=dialect,
+        )
+
 
 class InsertOnConflictTest(fixtures.TablesTest, AssertsCompiledSQL):
     __dialect__ = postgresql.dialect()
index 8ed8c7d33cad0408e3f414799eef29c976e1db6b..b6d6d95de81cf8f98d2ff9ec24728d3864c91f5c 100644 (file)
@@ -3533,6 +3533,13 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             checkparams={"x_1": "a%b_c"},
         )
 
+    def test_like_escape_empty(self):
+        self.assert_compile(
+            column("x").like("y", escape=""),
+            "x LIKE :x_1 ESCAPE ''",
+            checkparams={"x_1": "y"},
+        )
+
     def test_ilike(self):
         self.assert_compile(
             column("x").ilike("y"),
@@ -3547,6 +3554,13 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             checkparams={"x_1": "a%b_c"},
         )
 
+    def test_ilike_escape_empty(self):
+        self.assert_compile(
+            column("x").ilike("y", escape=""),
+            "lower(x) LIKE lower(:x_1) ESCAPE ''",
+            checkparams={"x_1": "y"},
+        )
+
     def test_not_like(self):
         self.assert_compile(
             column("x").not_like("y"),
@@ -3561,6 +3575,13 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             checkparams={"x_1": "a%b_c"},
         )
 
+    def test_not_like_escape_empty(self):
+        self.assert_compile(
+            column("x").not_like("y", escape=""),
+            "x NOT LIKE :x_1 ESCAPE ''",
+            checkparams={"x_1": "y"},
+        )
+
     def test_not_ilike(self):
         self.assert_compile(
             column("x").not_ilike("y"),
@@ -3575,6 +3596,13 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             checkparams={"x_1": "a%b_c"},
         )
 
+    def test_not_ilike_escape_empty(self):
+        self.assert_compile(
+            column("x").not_ilike("y", escape=""),
+            "lower(x) NOT LIKE lower(:x_1) ESCAPE ''",
+            checkparams={"x_1": "y"},
+        )
+
     def test_startswith(self):
         self.assert_compile(
             column("x").startswith("y"),