]> 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>
Mon, 3 Jul 2023 12:38:26 +0000 (08:38 -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
(cherry picked from commit 2d8ff4f9171bcef9fa70dfa27f2c0cab708fd75e)

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/compiler.py
lib/sqlalchemy/sql/default_comparator.py
lib/sqlalchemy/sql/operators.py
lib/sqlalchemy/sql/traversals.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 9948602d3dbf1e8ba0c16adcb1a9cec0447c32a8..73cb1ac09a6851b753d009739488049b722d9541 100644 (file)
@@ -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),
             )
 
 
index 390ea5098c8ddad265d6257a08c9c7bf2ba5cea4..1b8540b8ef4b9a7afb9169bec78efb9bcfda5bfe 100644 (file)
@@ -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),
             )
 
 
index 61e9645626b183bf3f3f7fee79a195b0195a0ec9..a73569b1a7f3f55e57b85d18bb5512b009a48a64 100644 (file)
@@ -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):
index a8d0674604c892bc2087419aaaedd3b3a3450dcf..0a460b8c091321e1153f3ae7ebd052327279e74a 100644 (file)
@@ -4501,11 +4501,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),
         )
 
 
index 73a1c0351b45aed380846c5a56ffac69af7cde41..bb4467480869530f48d10efac7c27640969f2c7f 100644 (file)
@@ -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},
     )
 
 
index 2ce1add26f81791acc2da0436cfdea27d66b21e3..b6e9e27b8ccbaa1829199138bd0044b1dd5497be 100644 (file)
@@ -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`
index de97b9de94c5a69a13c3788bf5efae96e317107b..fd20bbc4cf57da90dfbbdd10a48336f5a08c2e22 100644 (file)
@@ -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):
index ba162b49020686aad019ec99f30d7ca6cafc8ed6..bb16099cd8266a3864b94cebd694c811ae285e4c 100644 (file)
@@ -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"),
         )
 
 
index 08b68f0f0301cd51b76ae308edc71f7722352b38..6c3e0fb706b69e1b6808534cfa95f47cb5cdb30b 100644 (file)
@@ -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",
+            },
         )
 
 
index e9de407c8e71e958612700e259ea2871f633bd62..a005821cc6e8f1feb2b41346e853d0d52cbfcc74 100644 (file)
@@ -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(
index c8e1efbf1b7ca8e5e8f00acf354679aa6af51fe5..d64deb86777905bcc3d4cd875cff8b1d135742e4 100644 (file)
@@ -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),
index 62f33c2ec24d04ba63f0d81d4ee3eeffaed51eb8..a03cb21fb30ca6af77d9fc0c850244df6f4d2466 100644 (file)
@@ -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="\\"),