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_1_4_49~3 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e9681237daa186b0d3d49e365c0859c5ac844d2b;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 (cherry picked from commit 2d8ff4f9171bcef9fa70dfa27f2c0cab708fd75e) --- 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 9948602d3d..73cb1ac09a 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -1650,7 +1650,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), ) @@ -1668,7 +1668,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 @@ -1683,25 +1683,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 390ea5098c..1b8540b8ef 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -1281,7 +1281,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): @@ -1291,21 +1291,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 61e9645626..a73569b1a7 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -2391,14 +2391,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), ) @@ -2410,21 +2410,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): diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index a8d0674604..0a460b8c09 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -4501,11 +4501,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), ) diff --git a/lib/sqlalchemy/sql/default_comparator.py b/lib/sqlalchemy/sql/default_comparator.py index 73a1c0351b..bb44674808 100644 --- a/lib/sqlalchemy/sql/default_comparator.py +++ b/lib/sqlalchemy/sql/default_comparator.py @@ -264,41 +264,41 @@ def _collate_impl(expr, op, other, **kw): def _regexp_match_impl(expr, op, pattern, flags, **kw): - if flags is not None: - flags = coercions.expect( + return BinaryExpression( + expr, + coercions.expect( roles.BinaryElementRole, - flags, + pattern, expr=expr, - operator=operators.regexp_replace_op, - ) - return _boolean_compare( - expr, + operator=operators.comma_op, + ), op, - pattern, - flags=flags, - negate=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}, ) def _regexp_replace_impl(expr, op, pattern, replacement, flags, **kw): - replacement = coercions.expect( - roles.BinaryElementRole, - replacement, - expr=expr, - operator=operators.regexp_replace_op, - ) - if flags is not None: - flags = coercions.expect( - roles.BinaryElementRole, - flags, - expr=expr, - operator=operators.regexp_replace_op, - ) - return _binary_operate( - expr, op, pattern, replacement=replacement, flags=flags, **kw + return BinaryExpression( + expr, + ClauseList( + coercions.expect( + roles.BinaryElementRole, + pattern, + expr=expr, + operator=operators.comma_op, + ), + coercions.expect( + roles.BinaryElementRole, + replacement, + expr=expr, + operator=operators.comma_op, + ), + 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 2ce1add26f..b6e9e27b8c 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -1037,8 +1037,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 @@ -1046,6 +1046,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` @@ -1080,13 +1088,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/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py index de97b9de94..fd20bbc4cf 100644 --- a/lib/sqlalchemy/sql/traversals.py +++ b/lib/sqlalchemy/sql/traversals.py @@ -383,7 +383,10 @@ class CacheKey(namedtuple("CacheKey", ["key", "bindparams"])): return repr((sql_str, param_tuple)) def __eq__(self, other): - return self.key == other.key + return bool(self.key == other.key) + + def __ne__(self, other): + return not (self.key == other.key) @classmethod def _diff_tuples(cls, left, right): diff --git a/test/dialect/mysql/test_compiler.py b/test/dialect/mysql/test_compiler.py index ba162b4902..bb16099cd8 100644 --- a/test/dialect/mysql/test_compiler.py +++ b/test/dialect/mysql/test_compiler.py @@ -1210,18 +1210,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): @@ -1229,26 +1236,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): @@ -1256,8 +1279,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 08b68f0f03..6c3e0fb706 100644 --- a/test/dialect/oracle/test_compiler.py +++ b/test/dialect/oracle/test_compiler.py @@ -1473,14 +1473,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"}, ) @@ -1505,20 +1505,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): @@ -1554,21 +1545,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 e9de407c8e..a005821cc6 100644 --- a/test/dialect/postgresql/test_compiler.py +++ b/test/dialect/postgresql/test_compiler.py @@ -3182,8 +3182,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): @@ -3193,13 +3193,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"), @@ -3224,8 +3217,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): @@ -3235,15 +3228,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"), @@ -3277,22 +3261,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 c8e1efbf1b..d64deb8677 100644 --- a/test/sql/test_compare.py +++ b/test/sql/test_compare.py @@ -237,6 +237,14 @@ class CoreFixtures(object): 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, @@ -1108,6 +1116,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 62f33c2ec2..a03cb21fb3 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -2486,6 +2486,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="\\"),