From: Mike Bayer Date: Sun, 17 Jul 2022 15:32:27 +0000 (-0400) Subject: use concat() directly for contains, startswith, endswith X-Git-Tag: rel_1_4_40~25 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0052827c44e9124e89cb3ba5b922b786fe333c9e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git use concat() directly for contains, startswith, endswith Adjusted the SQL compilation for string containment functions ``.contains()``, ``.startswith()``, ``.endswith()`` to force the use of the string concatenation operator, rather than relying upon the overload of the addition operator, so that non-standard use of these operators with for example bytestrings still produces string concatenation operators. To accommodate this, needed to add a new _rconcat operator function, which is private, as well as a fallback in concat_op() that works similarly to Python builtin ops. Fixes: #8253 Change-Id: I2b7f56492f765742d88cb2a7834ded6a2892bd7e (cherry picked from commit 85a88df13ab8d217331cf98392544a888b4d7df3) --- diff --git a/doc/build/changelog/unreleased_14/8253.rst b/doc/build/changelog/unreleased_14/8253.rst new file mode 100644 index 0000000000..7496ae9fb0 --- /dev/null +++ b/doc/build/changelog/unreleased_14/8253.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, sql + :tickets: 8253 + + Adjusted the SQL compilation for string containment functions + ``.contains()``, ``.startswith()``, ``.endswith()`` to force the use of the + string concatenation operator, rather than relying upon the overload of the + addition operator, so that non-standard use of these operators with for + example bytestrings still produces string concatenation operators. + diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 667dd7d3de..330f3c3bc8 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2295,37 +2295,37 @@ class SQLCompiler(Compiled): def visit_contains_op_binary(self, binary, operator, **kw): binary = binary._clone() percent = self._like_percent_literal - binary.right = percent.__add__(binary.right).__add__(percent) + binary.right = percent.concat(binary.right).concat(percent) return self.visit_like_op_binary(binary, operator, **kw) def visit_not_contains_op_binary(self, binary, operator, **kw): binary = binary._clone() percent = self._like_percent_literal - binary.right = percent.__add__(binary.right).__add__(percent) + binary.right = percent.concat(binary.right).concat(percent) return self.visit_not_like_op_binary(binary, operator, **kw) def visit_startswith_op_binary(self, binary, operator, **kw): binary = binary._clone() percent = self._like_percent_literal - binary.right = percent.__radd__(binary.right) + binary.right = percent._rconcat(binary.right) return self.visit_like_op_binary(binary, operator, **kw) def visit_not_startswith_op_binary(self, binary, operator, **kw): binary = binary._clone() percent = self._like_percent_literal - binary.right = percent.__radd__(binary.right) + binary.right = percent._rconcat(binary.right) return self.visit_not_like_op_binary(binary, operator, **kw) def visit_endswith_op_binary(self, binary, operator, **kw): binary = binary._clone() percent = self._like_percent_literal - binary.right = percent.__add__(binary.right) + binary.right = percent.concat(binary.right) return self.visit_like_op_binary(binary, operator, **kw) def visit_not_endswith_op_binary(self, binary, operator, **kw): binary = binary._clone() percent = self._like_percent_literal - binary.right = percent.__add__(binary.right) + binary.right = percent.concat(binary.right) return self.visit_not_like_op_binary(binary, operator, **kw) def visit_like_op_binary(self, binary, operator, **kw): diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index 826b312938..1da5032296 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -466,6 +466,16 @@ class ColumnOperators(Operators): """ return self.operate(concat_op, other) + def _rconcat(self, other): + """Implement an 'rconcat' operator. + + this is for internal use at the moment + + .. versionadded:: 1.4.40 + + """ + return self.reverse_operate(concat_op, other) + def like(self, other, escape=None): r"""Implement the ``like`` operator. @@ -1512,7 +1522,12 @@ def filter_op(a, b): def concat_op(a, b): - return a.concat(b) + try: + concat = a.concat + except AttributeError: + return b._rconcat(a) + else: + return concat(b) def desc_op(a): diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index 116d6b7923..62f33c2ec2 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -2841,6 +2841,36 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): checkparams={"x_1": "y"}, ) + def test_contains_encoded(self): + self.assert_compile( + column("x").contains(b"y"), + "x LIKE '%' || :x_1 || '%'", + checkparams={"x_1": b"y"}, + ) + + def test_not_contains_encoded(self): + self.assert_compile( + ~column("x").contains(b"y"), + "x NOT LIKE '%' || :x_1 || '%'", + checkparams={"x_1": b"y"}, + ) + + def test_contains_encoded_mysql(self): + self.assert_compile( + column("x").contains(b"y"), + "x LIKE concat(concat('%%', %s), '%%')", + checkparams={"x_1": b"y"}, + dialect="mysql", + ) + + def test_not_contains_encoded_mysql(self): + self.assert_compile( + ~column("x").contains(b"y"), + "x NOT LIKE concat(concat('%%', %s), '%%')", + checkparams={"x_1": b"y"}, + dialect="mysql", + ) + def test_contains_escape(self): self.assert_compile( column("x").contains("a%b_c", escape="\\"), @@ -3004,6 +3034,36 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): checkparams={"x_1": "a^%b^_c/d^^e"}, ) + def test_startswith_encoded(self): + self.assert_compile( + column("x").startswith(b"y"), + "x LIKE :x_1 || '%'", + checkparams={"x_1": b"y"}, + ) + + def test_startswith_encoded_mysql(self): + self.assert_compile( + column("x").startswith(b"y"), + "x LIKE concat(%s, '%%')", + checkparams={"x_1": b"y"}, + dialect="mysql", + ) + + def test_not_startswith_encoded(self): + self.assert_compile( + ~column("x").startswith(b"y"), + "x NOT LIKE :x_1 || '%'", + checkparams={"x_1": b"y"}, + ) + + def test_not_startswith_encoded_mysql(self): + self.assert_compile( + ~column("x").startswith(b"y"), + "x NOT LIKE concat(%s, '%%')", + checkparams={"x_1": b"y"}, + dialect="mysql", + ) + def test_not_startswith(self): self.assert_compile( ~column("x").startswith("y"), @@ -3094,6 +3154,28 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): checkparams={"x_1": "y"}, ) + def test_endswith_encoded(self): + self.assert_compile( + column("x").endswith(b"y"), + "x LIKE '%' || :x_1", + checkparams={"x_1": b"y"}, + ) + + def test_endswith_encoded_mysql(self): + self.assert_compile( + column("x").endswith(b"y"), + "x LIKE concat('%%', %s)", + checkparams={"x_1": b"y"}, + dialect="mysql", + ) + + def test_not_endswith_encoded(self): + self.assert_compile( + ~column("x").endswith(b"y"), + "x NOT LIKE '%' || :x_1", + checkparams={"x_1": b"y"}, + ) + def test_endswith_escape(self): self.assert_compile( column("x").endswith("a%b_c", escape="\\"),