From: Martin Caslavsky Date: Thu, 8 Jun 2023 21:34:38 +0000 (-0400) Subject: fix: allow escape value to be an empty string X-Git-Tag: rel_2_0_16~4^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=675bf57006b29166fa31cba6889e18e793fbc144;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git fix: allow escape value to be an empty string 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 --- diff --git a/doc/build/changelog/unreleased_20/9907.rst b/doc/build/changelog/unreleased_20/9907.rst new file mode 100644 index 0000000000..4ce6962c8d --- /dev/null +++ b/doc/build/changelog/unreleased_20/9907.rst @@ -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. diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 00443d79ba..61aa76db71 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -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 "" ) diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 198bba3584..79092ec661 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -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 "" ) diff --git a/test/dialect/postgresql/test_compiler.py b/test/dialect/postgresql/test_compiler.py index bd944849de..8f5707f46c 100644 --- a/test/dialect/postgresql/test_compiler.py +++ b/test/dialect/postgresql/test_compiler.py @@ -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() diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index 8ed8c7d33c..b6d6d95de8 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -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"),