]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
remove use of SQL expressions in "modifiers" for regexp
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 30 Jun 2023 14:14:55 +0000 (10:14 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 30 Jun 2023 16:18:51 +0000 (12:18 -0400)
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

13 files changed:
doc/build/changelog/unreleased_14/10042.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/oracle/base.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/sql/cache_key.py
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/default_comparator.py
lib/sqlalchemy/sql/operators.py
test/dialect/mysql/test_compiler.py
test/dialect/oracle/test_compiler.py
test/dialect/postgresql/test_compiler.py
test/sql/test_compare.py
test/sql/test_operators.py

diff --git a/doc/build/changelog/unreleased_14/10042.rst b/doc/build/changelog/unreleased_14/10042.rst
new file mode 100644 (file)
index 0000000..2248701
--- /dev/null
@@ -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.
+
+
+
index 18e64f1b484655120268a4fddaeaeb4db733bc34..4be22a7271667b99fa84e673417a3ea8fdc054c0 100644 (file)
@@ -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),
             )
 
 
index 194b5a56dfb35630b39cf25f5800e4e47a774fad..4e876eed3a1f1db40f19a929e0466baf365ff942 100644 (file)
@@ -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),
             )
 
 
index 835ff5b2a4d1111d134149979e3459d0f84fc421..1dcf58cecba57ed36a71781b0eaec2887c5ca30b 100644 (file)
@@ -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):
index e92167cef4ecb6a42f9106702b87229dfc4fc0be..8c21be1b414f6e7c3bb9545507200bad34847fe8 100644 (file)
@@ -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, [])
index 6e84f8de4065789919bd489aa9cc8a72b10ea3b0..df93161461a52332da87fe7727dbc150e7d536f1 100644 (file)
@@ -6366,11 +6366,9 @@ class StrSQLCompiler(SQLCompiler):
         return self._generate_generic_binary(binary, " <not regexp> ", **kw)
 
     def visit_regexp_replace_op_binary(self, binary, operator, **kw):
-        replacement = binary.modifiers["replacement"]
-        return "<regexp replace>(%s, %s, %s)" % (
+        return "<regexp replace>(%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):
index 645114aedf03971b292c26361595334f65a38b67..5dbf3e3573fb65ed4f35eec4bc996cfb9e745995 100644 (file)
@@ -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},
     )
 
 
index 352e5b62df0b07c5d8fa749e24a49b4cfe012baa..d35ee9d6a482e3f2ddb43020bac8d7e175e77d6f 100644 (file)
@@ -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`
index 15650f3c77329663e5f5adbc2e24b887717e85c0..b2e05d951d00e06d3741fa5ce4868724ad0a4e51 100644 (file)
@@ -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"),
         )
 
 
index 603d54e4b64fac6423d3d5b92f99b523db7184a4..9b858c125ee898e76d8f268cf3450ce2700eed5e 100644 (file)
@@ -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",
+            },
         )
 
 
index 8f5707f46c2e614ae0f271bf7340f7c9452487e8..6e2907039cad8d2dac7da6d16bcc830353ff78bb 100644 (file)
@@ -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(
index 8aeaf9b96142df0b7c47c48bb6f2ea819ca83bff..353537ad3ea60bf46b6d0125d43304c0c0d29d10 100644 (file)
@@ -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),
index b6d6d95de81cf8f98d2ff9ec24728d3864c91f5c..eafc2c3a76f40065688daa1ddf4b8ffb16358e14 100644 (file)
@@ -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="\\"),