From: Mike Bayer Date: Fri, 30 Jun 2023 14:14:55 +0000 (-0400) Subject: remove use of SQL expressions in "modifiers" for regexp X-Git-Tag: rel_2_0_18~14^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=2d8ff4f9171bcef9fa70dfa27f2c0cab708fd75e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git remove use of SQL expressions in "modifiers" for regexp Fixed issue where the :meth:`_sql.ColumnOperators.regexp_match` when using "flags" would not produce a "stable" cache key, that is, the cache key would keep changing each time causing cache pollution. The same issue existed for :meth:`_sql.ColumnOperators.regexp_replace` with both the flags and the actual replacement expression. The flags are now represented as fixed modifier strings rendered as safestrings rather than bound parameters, and the replacement expression is established within the primary portion of the "binary" element so that it generates an appropriate cache key. Note that as part of this change, the :paramref:`_sql.ColumnOperators.regexp_match.flags` and :paramref:`_sql.ColumnOperators.regexp_replace.flags` have been modified to render as literal strings only, whereas previously they were rendered as full SQL expressions, typically bound parameters. These parameters should always be passed as plain Python strings and not as SQL expression constructs; it's not expected that SQL expression constructs were used in practice for this parameter, so this is a backwards-incompatible change. The change also modifies the internal structure of the expression generated, for :meth:`_sql.ColumnOperators.regexp_replace` with or without flags, and for :meth:`_sql.ColumnOperators.regexp_match` with flags. Third party dialects which may have implemented regexp implementations of their own (no such dialects could be located in a search, so impact is expected to be low) would need to adjust the traversal of the structure to accommodate. Fixed issue in mostly-internal :class:`.CacheKey` construct where the ``__ne__()`` operator were not properly implemented, leading to nonsensical results when comparing :class:`.CacheKey` instances to each other. Fixes: #10042 Change-Id: I2e245f81d7ee7136ad04cf77be35f9745c5da5e5 --- diff --git a/doc/build/changelog/unreleased_14/10042.rst b/doc/build/changelog/unreleased_14/10042.rst new file mode 100644 index 0000000000..2248701403 --- /dev/null +++ b/doc/build/changelog/unreleased_14/10042.rst @@ -0,0 +1,43 @@ +.. change:: + :tags: bug, sql + :tickets: 10042 + :versions: 2.0.18 + + Fixed issue where the :meth:`_sql.ColumnOperators.regexp_match` + when using "flags" would not produce a "stable" cache key, that + is, the cache key would keep changing each time causing cache pollution. + The same issue existed for :meth:`_sql.ColumnOperators.regexp_replace` + with both the flags and the actual replacement expression. + The flags are now represented as fixed modifier strings rendered as + safestrings rather than bound parameters, and the replacement + expression is established within the primary portion of the "binary" + element so that it generates an appropriate cache key. + + Note that as part of this change, the + :paramref:`_sql.ColumnOperators.regexp_match.flags` and + :paramref:`_sql.ColumnOperators.regexp_replace.flags` have been modified to + render as literal strings only, whereas previously they were rendered as + full SQL expressions, typically bound parameters. These parameters should + always be passed as plain Python strings and not as SQL expression + constructs; it's not expected that SQL expression constructs were used in + practice for this parameter, so this is a backwards-incompatible change. + + The change also modifies the internal structure of the expression + generated, for :meth:`_sql.ColumnOperators.regexp_replace` with or without + flags, and for :meth:`_sql.ColumnOperators.regexp_match` with flags. Third + party dialects which may have implemented regexp implementations of their + own (no such dialects could be located in a search, so impact is expected + to be low) would need to adjust the traversal of the structure to + accommodate. + + +.. change:: + :tags: bug, sql + :versions: 2.0.18 + + Fixed issue in mostly-internal :class:`.CacheKey` construct where the + ``__ne__()`` operator were not properly implemented, leading to nonsensical + results when comparing :class:`.CacheKey` instances to each other. + + + diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 18e64f1b48..4be22a7271 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -1703,7 +1703,7 @@ class MySQLCompiler(compiler.SQLCompiler): def _mariadb_regexp_flags(self, flags, pattern, **kw): return "CONCAT('(?', %s, ')', %s)" % ( - self.process(flags, **kw), + self.render_literal_value(flags, sqltypes.STRINGTYPE), self.process(pattern, **kw), ) @@ -1721,7 +1721,7 @@ class MySQLCompiler(compiler.SQLCompiler): text = "REGEXP_LIKE(%s, %s, %s)" % ( self.process(binary.left, **kw), self.process(binary.right, **kw), - self.process(flags, **kw), + self.render_literal_value(flags, sqltypes.STRINGTYPE), ) if op_string == " NOT REGEXP ": return "NOT %s" % text @@ -1736,25 +1736,22 @@ class MySQLCompiler(compiler.SQLCompiler): def visit_regexp_replace_op_binary(self, binary, operator, **kw): flags = binary.modifiers["flags"] - replacement = binary.modifiers["replacement"] if flags is None: - return "REGEXP_REPLACE(%s, %s, %s)" % ( + return "REGEXP_REPLACE(%s, %s)" % ( self.process(binary.left, **kw), self.process(binary.right, **kw), - self.process(replacement, **kw), ) elif self.dialect.is_mariadb: return "REGEXP_REPLACE(%s, %s, %s)" % ( self.process(binary.left, **kw), - self._mariadb_regexp_flags(flags, binary.right), - self.process(replacement, **kw), + self._mariadb_regexp_flags(flags, binary.right.clauses[0]), + self.process(binary.right.clauses[1], **kw), ) else: - return "REGEXP_REPLACE(%s, %s, %s, %s)" % ( + return "REGEXP_REPLACE(%s, %s, %s)" % ( self.process(binary.left, **kw), self.process(binary.right, **kw), - self.process(replacement, **kw), - self.process(flags, **kw), + self.render_literal_value(flags, sqltypes.STRINGTYPE), ) diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index 194b5a56df..4e876eed3a 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -1217,7 +1217,7 @@ class OracleCompiler(compiler.SQLCompiler): return "REGEXP_LIKE(%s, %s, %s)" % ( string, pattern, - self.process(flags, **kw), + self.render_literal_value(flags, sqltypes.STRINGTYPE), ) def visit_not_regexp_match_op_binary(self, binary, operator, **kw): @@ -1227,21 +1227,18 @@ class OracleCompiler(compiler.SQLCompiler): def visit_regexp_replace_op_binary(self, binary, operator, **kw): string = self.process(binary.left, **kw) - pattern = self.process(binary.right, **kw) - replacement = self.process(binary.modifiers["replacement"], **kw) + pattern_replace = self.process(binary.right, **kw) flags = binary.modifiers["flags"] if flags is None: - return "REGEXP_REPLACE(%s, %s, %s)" % ( + return "REGEXP_REPLACE(%s, %s)" % ( string, - pattern, - replacement, + pattern_replace, ) else: - return "REGEXP_REPLACE(%s, %s, %s, %s)" % ( + return "REGEXP_REPLACE(%s, %s, %s)" % ( string, - pattern, - replacement, - self.process(flags, **kw), + pattern_replace, + self.render_literal_value(flags, sqltypes.STRINGTYPE), ) diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 835ff5b2a4..1dcf58cecb 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1812,14 +1812,14 @@ class PGCompiler(compiler.SQLCompiler): return self._generate_generic_binary( binary, " %s " % base_op, **kw ) - if isinstance(flags, elements.BindParameter) and flags.value == "i": + if flags == "i": return self._generate_generic_binary( binary, " %s* " % base_op, **kw ) return "%s %s CONCAT('(?', %s, ')', %s)" % ( self.process(binary.left, **kw), base_op, - self.process(flags, **kw), + self.render_literal_value(flags, sqltypes.STRINGTYPE), self.process(binary.right, **kw), ) @@ -1831,21 +1831,18 @@ class PGCompiler(compiler.SQLCompiler): def visit_regexp_replace_op_binary(self, binary, operator, **kw): string = self.process(binary.left, **kw) - pattern = self.process(binary.right, **kw) + pattern_replace = self.process(binary.right, **kw) flags = binary.modifiers["flags"] - replacement = self.process(binary.modifiers["replacement"], **kw) if flags is None: - return "REGEXP_REPLACE(%s, %s, %s)" % ( + return "REGEXP_REPLACE(%s, %s)" % ( string, - pattern, - replacement, + pattern_replace, ) else: - return "REGEXP_REPLACE(%s, %s, %s, %s)" % ( + return "REGEXP_REPLACE(%s, %s, %s)" % ( string, - pattern, - replacement, - self.process(flags, **kw), + pattern_replace, + self.render_literal_value(flags, sqltypes.STRINGTYPE), ) def visit_empty_set_expr(self, element_types, **kw): diff --git a/lib/sqlalchemy/sql/cache_key.py b/lib/sqlalchemy/sql/cache_key.py index e92167cef4..8c21be1b41 100644 --- a/lib/sqlalchemy/sql/cache_key.py +++ b/lib/sqlalchemy/sql/cache_key.py @@ -465,6 +465,9 @@ class CacheKey(NamedTuple): def __eq__(self, other: Any) -> bool: return bool(self.key == other.key) + def __ne__(self, other: Any) -> bool: + return not (self.key == other.key) + @classmethod def _diff_tuples(cls, left: CacheKey, right: CacheKey) -> str: ck1 = CacheKey(left, []) diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 6e84f8de40..df93161461 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -6366,11 +6366,9 @@ class StrSQLCompiler(SQLCompiler): return self._generate_generic_binary(binary, " ", **kw) def visit_regexp_replace_op_binary(self, binary, operator, **kw): - replacement = binary.modifiers["replacement"] - return "(%s, %s, %s)" % ( + return "(%s, %s)" % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw), - replacement._compiler_dispatch(self, **kw), ) def visit_try_cast(self, cast, **kwargs): diff --git a/lib/sqlalchemy/sql/default_comparator.py b/lib/sqlalchemy/sql/default_comparator.py index 645114aedf..5dbf3e3573 100644 --- a/lib/sqlalchemy/sql/default_comparator.py +++ b/lib/sqlalchemy/sql/default_comparator.py @@ -361,24 +361,17 @@ def _regexp_match_impl( flags: Optional[str], **kw: Any, ) -> ColumnElement[Any]: - if flags is not None: - flags_expr = coercions.expect( + return BinaryExpression( + expr, + coercions.expect( roles.BinaryElementRole, - flags, + pattern, expr=expr, - operator=operators.regexp_replace_op, - ) - else: - flags_expr = None - return _boolean_compare( - expr, + operator=operators.comma_op, + ), op, - pattern, - flags=flags_expr, - negate_op=operators.not_regexp_match_op - if op is operators.regexp_match_op - else operators.regexp_match_op, - **kw, + negate=operators.not_regexp_match_op, + modifiers={"flags": flags}, ) @@ -390,23 +383,27 @@ def _regexp_replace_impl( flags: Optional[str], **kw: Any, ) -> ColumnElement[Any]: - replacement = coercions.expect( - roles.BinaryElementRole, - replacement, - expr=expr, - operator=operators.regexp_replace_op, - ) - if flags is not None: - flags_expr = coercions.expect( - roles.BinaryElementRole, - flags, - expr=expr, - operator=operators.regexp_replace_op, - ) - else: - flags_expr = None - return _binary_operate( - expr, op, pattern, replacement=replacement, flags=flags_expr, **kw + return BinaryExpression( + expr, + ExpressionClauseList._construct_for_list( + operators.comma_op, + type_api.NULLTYPE, + coercions.expect( + roles.BinaryElementRole, + pattern, + expr=expr, + operator=operators.comma_op, + ), + coercions.expect( + roles.BinaryElementRole, + replacement, + expr=expr, + operator=operators.comma_op, + ), + group=False, + ), + op, + modifiers={"flags": flags}, ) diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index 352e5b62df..d35ee9d6a4 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -1573,8 +1573,8 @@ class ColumnOperators(Operators): :param pattern: The regular expression pattern string or column clause. - :param flags: Any regular expression string flags to apply. Flags - tend to be backend specific. It can be a string or a column clause. + :param flags: Any regular expression string flags to apply, passed as + plain Python string only. These flags are backend specific. Some backends, like PostgreSQL and MariaDB, may alternatively specify the flags as part of the pattern. When using the ignore case flag 'i' in PostgreSQL, the ignore case @@ -1582,6 +1582,14 @@ class ColumnOperators(Operators): .. versionadded:: 1.4 + .. versionchanged:: 1.4.48, 2.0.18 Note that due to an implementation + error, the "flags" parameter previously accepted SQL expression + objects such as column expressions in addition to plain Python + strings. This implementation did not work correctly with caching + and was removed; strings only should be passed for the "flags" + parameter, as these flags are rendered as literal inline values + within SQL expressions. + .. seealso:: :meth:`_sql.ColumnOperators.regexp_replace` @@ -1618,13 +1626,22 @@ class ColumnOperators(Operators): :param pattern: The regular expression pattern string or column clause. :param pattern: The replacement string or column clause. - :param flags: Any regular expression string flags to apply. Flags - tend to be backend specific. It can be a string or a column clause. + :param flags: Any regular expression string flags to apply, passed as + plain Python string only. These flags are backend specific. Some backends, like PostgreSQL and MariaDB, may alternatively specify the flags as part of the pattern. .. versionadded:: 1.4 + .. versionchanged:: 1.4.48, 2.0.18 Note that due to an implementation + error, the "flags" parameter previously accepted SQL expression + objects such as column expressions in addition to plain Python + strings. This implementation did not work correctly with caching + and was removed; strings only should be passed for the "flags" + parameter, as these flags are rendered as literal inline values + within SQL expressions. + + .. seealso:: :meth:`_sql.ColumnOperators.regexp_match` diff --git a/test/dialect/mysql/test_compiler.py b/test/dialect/mysql/test_compiler.py index 15650f3c77..b2e05d951d 100644 --- a/test/dialect/mysql/test_compiler.py +++ b/test/dialect/mysql/test_compiler.py @@ -1306,18 +1306,25 @@ class RegexpCommon(testing.AssertsCompiledSQL): class RegexpTestMySql(fixtures.TestBase, RegexpCommon): __dialect__ = "mysql" + def test_regexp_match_flags_safestring(self): + self.assert_compile( + self.table.c.myid.regexp_match("pattern", flags="i'g"), + "REGEXP_LIKE(mytable.myid, %s, 'i''g')", + checkpositional=("pattern",), + ) + def test_regexp_match_flags(self): self.assert_compile( self.table.c.myid.regexp_match("pattern", flags="ig"), - "REGEXP_LIKE(mytable.myid, %s, %s)", - checkpositional=("pattern", "ig"), + "REGEXP_LIKE(mytable.myid, %s, 'ig')", + checkpositional=("pattern",), ) def test_not_regexp_match_flags(self): self.assert_compile( ~self.table.c.myid.regexp_match("pattern", flags="ig"), - "NOT REGEXP_LIKE(mytable.myid, %s, %s)", - checkpositional=("pattern", "ig"), + "NOT REGEXP_LIKE(mytable.myid, %s, 'ig')", + checkpositional=("pattern",), ) def test_regexp_replace_flags(self): @@ -1325,26 +1332,42 @@ class RegexpTestMySql(fixtures.TestBase, RegexpCommon): self.table.c.myid.regexp_replace( "pattern", "replacement", flags="ig" ), - "REGEXP_REPLACE(mytable.myid, %s, %s, %s)", - checkpositional=("pattern", "replacement", "ig"), + "REGEXP_REPLACE(mytable.myid, %s, %s, 'ig')", + checkpositional=("pattern", "replacement"), + ) + + def test_regexp_replace_flags_safestring(self): + self.assert_compile( + self.table.c.myid.regexp_replace( + "pattern", "replacement", flags="i'g" + ), + "REGEXP_REPLACE(mytable.myid, %s, %s, 'i''g')", + checkpositional=("pattern", "replacement"), ) class RegexpTestMariaDb(fixtures.TestBase, RegexpCommon): __dialect__ = "mariadb" + def test_regexp_match_flags_safestring(self): + self.assert_compile( + self.table.c.myid.regexp_match("pattern", flags="i'g"), + "mytable.myid REGEXP CONCAT('(?', 'i''g', ')', %s)", + checkpositional=("pattern",), + ) + def test_regexp_match_flags(self): self.assert_compile( self.table.c.myid.regexp_match("pattern", flags="ig"), - "mytable.myid REGEXP CONCAT('(?', %s, ')', %s)", - checkpositional=("ig", "pattern"), + "mytable.myid REGEXP CONCAT('(?', 'ig', ')', %s)", + checkpositional=("pattern",), ) def test_not_regexp_match_flags(self): self.assert_compile( ~self.table.c.myid.regexp_match("pattern", flags="ig"), - "mytable.myid NOT REGEXP CONCAT('(?', %s, ')', %s)", - checkpositional=("ig", "pattern"), + "mytable.myid NOT REGEXP CONCAT('(?', 'ig', ')', %s)", + checkpositional=("pattern",), ) def test_regexp_replace_flags(self): @@ -1352,8 +1375,8 @@ class RegexpTestMariaDb(fixtures.TestBase, RegexpCommon): self.table.c.myid.regexp_replace( "pattern", "replacement", flags="ig" ), - "REGEXP_REPLACE(mytable.myid, CONCAT('(?', %s, ')', %s), %s)", - checkpositional=("ig", "pattern", "replacement"), + "REGEXP_REPLACE(mytable.myid, CONCAT('(?', 'ig', ')', %s), %s)", + checkpositional=("pattern", "replacement"), ) diff --git a/test/dialect/oracle/test_compiler.py b/test/dialect/oracle/test_compiler.py index 603d54e4b6..9b858c125e 100644 --- a/test/dialect/oracle/test_compiler.py +++ b/test/dialect/oracle/test_compiler.py @@ -1689,14 +1689,14 @@ class RegexpTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_regexp_match_flags(self): self.assert_compile( self.table.c.myid.regexp_match("pattern", flags="ig"), - "REGEXP_LIKE(mytable.myid, :myid_1, :myid_2)", - checkparams={"myid_1": "pattern", "myid_2": "ig"}, + "REGEXP_LIKE(mytable.myid, :myid_1, 'ig')", + checkparams={"myid_1": "pattern"}, ) - def test_regexp_match_flags_col(self): + def test_regexp_match_flags_safestring(self): self.assert_compile( - self.table.c.myid.regexp_match("pattern", flags=self.table.c.name), - "REGEXP_LIKE(mytable.myid, :myid_1, mytable.name)", + self.table.c.myid.regexp_match("pattern", flags="i'g"), + "REGEXP_LIKE(mytable.myid, :myid_1, 'i''g')", checkparams={"myid_1": "pattern"}, ) @@ -1721,20 +1721,11 @@ class RegexpTest(fixtures.TestBase, testing.AssertsCompiledSQL): checkparams={"param_1": "string"}, ) - def test_not_regexp_match_flags_col(self): - self.assert_compile( - ~self.table.c.myid.regexp_match( - "pattern", flags=self.table.c.name - ), - "NOT REGEXP_LIKE(mytable.myid, :myid_1, mytable.name)", - checkparams={"myid_1": "pattern"}, - ) - def test_not_regexp_match_flags(self): self.assert_compile( ~self.table.c.myid.regexp_match("pattern", flags="ig"), - "NOT REGEXP_LIKE(mytable.myid, :myid_1, :myid_2)", - checkparams={"myid_1": "pattern", "myid_2": "ig"}, + "NOT REGEXP_LIKE(mytable.myid, :myid_1, 'ig')", + checkparams={"myid_1": "pattern"}, ) def test_regexp_replace(self): @@ -1770,21 +1761,23 @@ class RegexpTest(fixtures.TestBase, testing.AssertsCompiledSQL): self.table.c.myid.regexp_replace( "pattern", "replacement", flags="ig" ), - "REGEXP_REPLACE(mytable.myid, :myid_1, :myid_2, :myid_3)", + "REGEXP_REPLACE(mytable.myid, :myid_1, :myid_2, 'ig')", checkparams={ "myid_1": "pattern", "myid_2": "replacement", - "myid_3": "ig", }, ) - def test_regexp_replace_flags_col(self): + def test_regexp_replace_flags_safestring(self): self.assert_compile( self.table.c.myid.regexp_replace( - "pattern", "replacement", flags=self.table.c.name + "pattern", "replacement", flags="i'g" ), - "REGEXP_REPLACE(mytable.myid, :myid_1, :myid_2, mytable.name)", - checkparams={"myid_1": "pattern", "myid_2": "replacement"}, + "REGEXP_REPLACE(mytable.myid, :myid_1, :myid_2, 'i''g')", + checkparams={ + "myid_1": "pattern", + "myid_2": "replacement", + }, ) diff --git a/test/dialect/postgresql/test_compiler.py b/test/dialect/postgresql/test_compiler.py index 8f5707f46c..6e2907039c 100644 --- a/test/dialect/postgresql/test_compiler.py +++ b/test/dialect/postgresql/test_compiler.py @@ -3679,8 +3679,8 @@ class RegexpTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_regexp_match_flags(self): self.assert_compile( self.table.c.myid.regexp_match("pattern", flags="ig"), - "mytable.myid ~ CONCAT('(?', %(myid_1)s, ')', %(myid_2)s)", - checkparams={"myid_2": "pattern", "myid_1": "ig"}, + "mytable.myid ~ CONCAT('(?', 'ig', ')', %(myid_1)s)", + checkparams={"myid_1": "pattern"}, ) def test_regexp_match_flags_ignorecase(self): @@ -3690,13 +3690,6 @@ class RegexpTest(fixtures.TestBase, testing.AssertsCompiledSQL): checkparams={"myid_1": "pattern"}, ) - def test_regexp_match_flags_col(self): - self.assert_compile( - self.table.c.myid.regexp_match("pattern", flags=self.table.c.name), - "mytable.myid ~ CONCAT('(?', mytable.name, ')', %(myid_1)s)", - checkparams={"myid_1": "pattern"}, - ) - def test_not_regexp_match(self): self.assert_compile( ~self.table.c.myid.regexp_match("pattern"), @@ -3721,8 +3714,8 @@ class RegexpTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_not_regexp_match_flags(self): self.assert_compile( ~self.table.c.myid.regexp_match("pattern", flags="ig"), - "mytable.myid !~ CONCAT('(?', %(myid_1)s, ')', %(myid_2)s)", - checkparams={"myid_2": "pattern", "myid_1": "ig"}, + "mytable.myid !~ CONCAT('(?', 'ig', ')', %(myid_1)s)", + checkparams={"myid_1": "pattern"}, ) def test_not_regexp_match_flags_ignorecase(self): @@ -3732,15 +3725,6 @@ class RegexpTest(fixtures.TestBase, testing.AssertsCompiledSQL): checkparams={"myid_1": "pattern"}, ) - def test_not_regexp_match_flags_col(self): - self.assert_compile( - ~self.table.c.myid.regexp_match( - "pattern", flags=self.table.c.name - ), - "mytable.myid !~ CONCAT('(?', mytable.name, ')', %(myid_1)s)", - checkparams={"myid_1": "pattern"}, - ) - def test_regexp_replace(self): self.assert_compile( self.table.c.myid.regexp_replace("pattern", "replacement"), @@ -3774,22 +3758,23 @@ class RegexpTest(fixtures.TestBase, testing.AssertsCompiledSQL): self.table.c.myid.regexp_replace( "pattern", "replacement", flags="ig" ), - "REGEXP_REPLACE(mytable.myid, %(myid_1)s, %(myid_2)s, %(myid_3)s)", + "REGEXP_REPLACE(mytable.myid, %(myid_1)s, %(myid_2)s, 'ig')", checkparams={ "myid_1": "pattern", "myid_2": "replacement", - "myid_3": "ig", }, ) - def test_regexp_replace_flags_col(self): + def test_regexp_replace_flags_safestring(self): self.assert_compile( self.table.c.myid.regexp_replace( - "pattern", "replacement", flags=self.table.c.name + "pattern", "replacement", flags="i'g" ), - "REGEXP_REPLACE(mytable.myid, %(myid_1)s," - " %(myid_2)s, mytable.name)", - checkparams={"myid_1": "pattern", "myid_2": "replacement"}, + "REGEXP_REPLACE(mytable.myid, %(myid_1)s, %(myid_2)s, 'i''g')", + checkparams={ + "myid_1": "pattern", + "myid_2": "replacement", + }, ) @testing.combinations( diff --git a/test/sql/test_compare.py b/test/sql/test_compare.py index 8aeaf9b961..353537ad3e 100644 --- a/test/sql/test_compare.py +++ b/test/sql/test_compare.py @@ -238,6 +238,14 @@ class CoreFixtures: column("q").like("somstr", escape="\\"), column("q").like("somstr", escape="X"), ), + lambda: ( + column("q").regexp_match("y", flags="ig"), + column("q").regexp_match("y", flags="q"), + column("q").regexp_match("y"), + column("q").regexp_replace("y", "z", flags="ig"), + column("q").regexp_replace("y", "z", flags="q"), + column("q").regexp_replace("y", "z"), + ), lambda: ( column("q", ARRAY(Integer))[3] == 5, column("q", ARRAY(Integer))[3:5] == 5, @@ -1161,6 +1169,27 @@ class CacheKeyTest(fixtures.CacheKeyFixture, CoreFixtures, fixtures.TestBase): is_(large_v1._generate_cache_key(), None) + @testing.combinations( + (lambda: column("x"), lambda: column("x"), lambda: column("y")), + ( + lambda: func.foo_bar(1, 2, 3), + lambda: func.foo_bar(4, 5, 6), + lambda: func.foo_bar_bat(1, 2, 3), + ), + ) + def test_cache_key_object_comparators(self, lc1, lc2, lc3): + """test ne issue detected as part of #10042""" + c1 = lc1() + c2 = lc2() + c3 = lc3() + + eq_(c1._generate_cache_key(), c2._generate_cache_key()) + ne_(c1._generate_cache_key(), c3._generate_cache_key()) + is_true(c1._generate_cache_key() == c2._generate_cache_key()) + is_false(c1._generate_cache_key() != c2._generate_cache_key()) + is_true(c1._generate_cache_key() != c3._generate_cache_key()) + is_false(c1._generate_cache_key() == c3._generate_cache_key()) + def test_cache_key(self): for fixtures_, compare_values in [ (self.fixtures, True), diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index b6d6d95de8..eafc2c3a76 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -2868,6 +2868,12 @@ class LikeTest(fixtures.TestBase, testing.AssertsCompiledSQL): "mytable.myid LIKE :myid_1 ESCAPE '\\'", ) + def test_like_quote_escape(self): + self.assert_compile( + self.table1.c.myid.like("somstr", escape="'"), + "mytable.myid LIKE :myid_1 ESCAPE ''''", + ) + def test_like_4(self): self.assert_compile( ~self.table1.c.myid.like("somstr", escape="\\"),