]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Rework autoescape to be a simple boolean; escape the escape character
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 24 Oct 2017 22:08:05 +0000 (18:08 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 25 Oct 2017 03:11:13 +0000 (23:11 -0400)
Reworked the new "autoescape" feature introduced in
:ref:`change_2694` in 1.2.0b2 to be fully automatic; the escape
character now defaults to a forwards slash ``"/"`` and
is applied to percent, underscore, as well as the escape
character itself, for fully automatic escaping.  The
character can also be changed using the "escape" parameter.

Change-Id: I74894a2576983c0f6eb89480c9e5727f49fa9c25
Fixes: #2694
doc/build/changelog/changelog_12.rst
doc/build/changelog/migration_12.rst
doc/build/changelog/unreleased_12/2694.rst [new file with mode: 0644]
lib/sqlalchemy/sql/operators.py
lib/sqlalchemy/testing/suite/test_select.py
test/sql/test_operators.py

index 0535a0ec32d04b75abc3d924ba02faae8a7f0848..022bb8452b15babfd7feac3909c211148815ea30 100644 (file)
         also applies it to all occurrences of the wildcard characters "%"
         and "_" automatically.  Pull request courtesy Diana Clarke.
 
+        .. note::  This feature has been changed as of 1.2.0b4 from its initial
+           implementation in 1.2.0b2 such that autoescape is now passed as a
+           boolean value, rather than a specific character to use as the escape
+           character.
+
         .. seealso::
 
             :ref:`change_2694`
index 35d2e1c76976be66dcaca8e8334371b818694331..56f671f1880421c8e3773a83e09eb55c2441a406 100644 (file)
@@ -783,22 +783,37 @@ New "autoescape" option for startswith(), endswith()
 ----------------------------------------------------
 
 The "autoescape" parameter is added to :meth:`.ColumnOperators.startswith`,
-:meth:`.ColumnOperators.endswith`, :meth:`.ColumnOperators.contains`.  This parameter
-does what "escape" does, except that it also automatically performs a search-
-and-replace of any wildcard characters to be escaped by that character, as
-these operators already add the wildcard expression on the outside of the
-given value.
+:meth:`.ColumnOperators.endswith`, :meth:`.ColumnOperators.contains`.
+This parameter when set to ``True`` will automatically escape all occurrences
+of ``%``, ``_`` with an escape character, which defaults to a forwards slash ``/``;
+occurrences of the escape character itself are also escaped.  The forwards slash
+is used to avoid conflicts with settings like Postgresql's
+``standard_confirming_strings``, whose default value changed as of Postgresql
+9.1, and MySQL's ``NO_BACKSLASH_ESCAPES`` settings.  The existing "escape" parameter
+can now be used to change the autoescape character, if desired.
+
+.. note::  This feature has been changed as of 1.2.0b4 from its initial
+   implementation in 1.2.0b2 such that autoescape is now passed as a boolean
+   value, rather than a specific character to use as the escape character.
 
 An expression such as::
 
-    >>> column('x').startswith('total%score', autoescape='/')
+    >>> column('x').startswith('total%score', autoescape=True)
 
 Renders as::
 
-    x LIKE :x_1 || '%%' ESCAPE '/'
+    x LIKE :x_1 || '%' ESCAPE '/'
 
 Where the value of the parameter "x_1" is ``'total/%score'``.
 
+Similarly, an expression that has backslashes::
+
+    >>> column('x').startswith('total/score', autoescape=True)
+
+Will render the same way, with the value of the parameter "x_1" as
+``'total//score'``.
+
+
 :ticket:`2694`
 
 .. _change_floats_12:
diff --git a/doc/build/changelog/unreleased_12/2694.rst b/doc/build/changelog/unreleased_12/2694.rst
new file mode 100644 (file)
index 0000000..4386134
--- /dev/null
@@ -0,0 +1,15 @@
+.. change::
+   :tags: bug, sql
+   :tickets: 2694
+
+   Reworked the new "autoescape" feature introduced in
+   :ref:`change_2694` in 1.2.0b2 to be fully automatic; the escape
+   character now defaults to a forwards slash ``"/"`` and
+   is applied to percent, underscore, as well as the escape
+   character itself, for fully automatic escaping.  The
+   character can also be changed using the "escape" parameter.
+
+   .. seealso::
+
+        :ref:`change_2694`
+
index ef6f78929fbfb9e6db024f5b7cbbdfddb6ce933b..0e8dec2a0d75db92f74bdf21abd1770240b16388 100644 (file)
@@ -597,101 +597,267 @@ class ColumnOperators(Operators):
         return self.operate(isnot, other)
 
     def startswith(self, other, **kwargs):
-        """Implement the ``startwith`` operator.
+        r"""Implement the ``startswith`` operator.
 
-        In a column context, produces the clause ``LIKE '<other>%'``
+        Produces a LIKE expression that tests against a match for the start
+        of a string value::
+
+            column LIKE <other> || '%'
 
         E.g.::
 
-            select([sometable]).where(sometable.c.column.startswith("foobar"))
+            stmt = select([sometable]).\
+                where(sometable.c.column.startswith("foobar"))
 
-        :param other: expression to be compared, with SQL wildcard
-          matching (``%`` and ``_``) enabled, e.g.::
+        Since the operator uses ``LIKE``, wildcard characters
+        ``"%"`` and ``"_"`` that are present inside the <other> expression
+        will behave like wildcards as well.   For literal string
+        values, the :paramref:`.ColumnOperators.startswith.autoescape` flag
+        may be set to ``True`` to apply escaping to occurences of these
+        characters within the string value so that they match as themselves
+        and not as wildcard characters.  Alternatively, the
+        :paramref:`.ColumnOperators.startswith.escape` parameter will establish
+        a given character as an escape character which can be of use when
+        the target expression is not a literal string.
 
-            somecolumn.startswith("foo%bar")
+        :param other: expression to be compared.   This is usually a plain
+          string value, but can also be an arbitrary SQL expression.  LIKE
+          wildcard characters ``%`` and ``_`` are not escaped by default unless
+          the :paramref:`.ColumnOperators.startswith.autoescape` flag is
+          set to True.
 
-        :param escape: optional escape character, renders the ``ESCAPE``
-          keyword allowing that escape character to be used to manually
-          disable SQL wildcard matching (``%`` and ``_``) in the expression,
-          e.g.::
+        :param autoescape: boolean; when True, establishes an escape character
+          within the LIKE expression, then applies it to all occurrences of
+          ``"%"``, ``"_"`` and the escape character itself within the
+          comparison value, which is assumed to be a literal string and not a
+          SQL expression.
 
-            somecolumn.startswith("foo/%bar", escape="/")
+          An expression such as::
 
-        :param autoescape: optional escape character, renders the ``ESCAPE``
-          keyword and uses that escape character to auto escape the
-          expression, disabling all SQL wildcard matching (``%`` and ``_``),
-          e.g.::
+            somecolumn.startswith("foo%bar", autoescape=True)
 
-            somecolumn.startswith("foo%bar", autoescape="/")
+          Will render as::
+
+            somecolumn LIKE :param || '%' ESCAPE '/'
+
+          With the value of :param as ``"foo/%bar"``.
 
           .. versionadded:: 1.2
 
+          .. versionchanged:: 1.2.0b4 The
+            :paramref:`.ColumnOperators.startswith.autoescape` parameter is
+             now a simple boolean rather than a character; the escape
+             character itself is also escaped, and defaults to a forwards
+             slash, which itself can be customized using the
+             :paramref:`.ColumnOperators.startswith.escape` parameter.
+
+        :param escape: a character which when given will render with the
+          ``ESCAPE`` keyword to establish that character as the escape
+          character.  This character can then be placed preceding occurrences
+          of ``%`` and ``_`` to allow them to act as themselves and not
+          wildcard characters.
+
+          An expression such as::
+
+            somecolumn.startswith("foo/%bar", escape="^")
+
+          Will render as::
+
+            somecolumn LIKE :param || '%' ESCAPE '^'
+
+          The parameter may also be combined with
+          :paramref:`.ColumnOperators.startswith.autoescape`::
+
+            somecolumn.startswith("foo%bar^bat", escape="^", autoescape=True)
+
+          Where above, the given literal parameter will be converted to
+          ``"foo^%bar^^bat"`` before being passed to the database.
+
+        .. seealso::
+
+            :meth:`.ColumnOperators.endswith`
+
+            :meth:`.ColumnOperators.contains`
+
+            :meth:`.ColumnOperators.like`
+
         """
         return self.operate(startswith_op, other, **kwargs)
 
     def endswith(self, other, **kwargs):
-        """Implement the 'endswith' operator.
+        r"""Implement the 'endswith' operator.
+
+        Produces a LIKE expression that tests against a match for the end
+        of a string value::
 
-        In a column context, produces the clause ``LIKE '%<other>'``
+            column LIKE '%' || <other>
 
         E.g.::
 
-            select([sometable]).where(sometable.c.column.endswith("foobar"))
+            stmt = select([sometable]).\
+                where(sometable.c.column.endswith("foobar"))
 
-        :param other: expression to be compared, with SQL wildcard
-          matching (``%`` and ``_``) enabled, e.g.::
+        Since the operator uses ``LIKE``, wildcard characters
+        ``"%"`` and ``"_"`` that are present inside the <other> expression
+        will behave like wildcards as well.   For literal string
+        values, the :paramref:`.ColumnOperators.endswith.autoescape` flag
+        may be set to ``True`` to apply escaping to occurences of these
+        characters within the string value so that they match as themselves
+        and not as wildcard characters.  Alternatively, the
+        :paramref:`.ColumnOperators.endswith.escape` parameter will establish
+        a given character as an escape character which can be of use when
+        the target expression is not a literal string.
 
-            somecolumn.endswith("foo%bar")
+        :param other: expression to be compared.   This is usually a plain
+          string value, but can also be an arbitrary SQL expression.  LIKE
+          wildcard characters ``%`` and ``_`` are not escaped by default unless
+          the :paramref:`.ColumnOperators.endswith.autoescape` flag is
+          set to True.
 
-        :param escape: optional escape character, renders the ``ESCAPE``
-          keyword allowing that escape character to be used to manually
-          disable SQL wildcard matching (``%`` and ``_``) in the expression,
-          e.g.::
+        :param autoescape: boolean; when True, establishes an escape character
+          within the LIKE expression, then applies it to all occurrences of
+          ``"%"``, ``"_"`` and the escape character itself within the
+          comparison value, which is assumed to be a literal string and not a
+          SQL expression.
+
+          An expression such as::
 
-            somecolumn.endswith("foo/%bar", escape="/")
+            somecolumn.endswith("foo%bar", autoescape=True)
 
-        :param autoescape: optional escape character, renders the ``ESCAPE``
-          keyword and uses that escape character to auto escape the
-          expression, disabling all SQL wildcard matching (``%`` and ``_``),
-          e.g.::
+          Will render as::
 
-            somecolumn.endswith("foo%bar", autoescape="/")
+            somecolumn LIKE '%' || :param ESCAPE '/'
+
+          With the value of :param as ``"foo/%bar"``.
 
           .. versionadded:: 1.2
 
+          .. versionchanged:: 1.2.0b4 The
+            :paramref:`.ColumnOperators.endswith.autoescape` parameter is
+             now a simple boolean rather than a character; the escape
+             character itself is also escaped, and defaults to a forwards
+             slash, which itself can be customized using the
+             :paramref:`.ColumnOperators.endswith.escape` parameter.
+
+        :param escape: a character which when given will render with the
+          ``ESCAPE`` keyword to establish that character as the escape
+          character.  This character can then be placed preceding occurrences
+          of ``%`` and ``_`` to allow them to act as themselves and not
+          wildcard characters.
+
+          An expression such as::
+
+            somecolumn.endswith("foo/%bar", escape="^")
+
+          Will render as::
+
+            somecolumn LIKE '%' || :param ESCAPE '^'
+
+          The parameter may also be combined with
+          :paramref:`.ColumnOperators.endswith.autoescape`::
+
+            somecolumn.endswith("foo%bar^bat", escape="^", autoescape=True)
+
+          Where above, the given literal parameter will be converted to
+          ``"foo^%bar^^bat"`` before being passed to the database.
+
+        .. seealso::
+
+            :meth:`.ColumnOperators.startswith`
+
+            :meth:`.ColumnOperators.contains`
+
+            :meth:`.ColumnOperators.like`
+
         """
         return self.operate(endswith_op, other, **kwargs)
 
     def contains(self, other, **kwargs):
-        """Implement the 'contains' operator.
+        r"""Implement the 'contains' operator.
+
+        Produces a LIKE expression that tests against a match for the middle
+        of a string value::
 
-        In a column context, produces the clause ``LIKE '%<other>%'``
+            column LIKE '%' || <other> || '%'
 
         E.g.::
 
-            select([sometable]).where(sometable.c.column.contains("foobar"))
+            stmt = select([sometable]).\
+                where(sometable.c.column.contains("foobar"))
 
-        :param other: expression to compare, with SQL wildcard
-          matching (``%`` and ``_``) enabled, e.g.::
+        Since the operator uses ``LIKE``, wildcard characters
+        ``"%"`` and ``"_"`` that are present inside the <other> expression
+        will behave like wildcards as well.   For literal string
+        values, the :paramref:`.ColumnOperators.contains.autoescape` flag
+        may be set to ``True`` to apply escaping to occurences of these
+        characters within the string value so that they match as themselves
+        and not as wildcard characters.  Alternatively, the
+        :paramref:`.ColumnOperators.contains.escape` parameter will establish
+        a given character as an escape character which can be of use when
+        the target expression is not a literal string.
 
-            somecolumn.contains("foo%bar")
+        :param other: expression to be compared.   This is usually a plain
+          string value, but can also be an arbitrary SQL expression.  LIKE
+          wildcard characters ``%`` and ``_`` are not escaped by default unless
+          the :paramref:`.ColumnOperators.contains.autoescape` flag is
+          set to True.
 
-        :param escape: optional escape character, renders the ``ESCAPE``
-          keyword allowing that escape character to be used to manually
-          disable SQL wildcard matching (``%`` and ``_``) in the expression,
-          e.g.::
+        :param autoescape: boolean; when True, establishes an escape character
+          within the LIKE expression, then applies it to all occurrences of
+          ``"%"``, ``"_"`` and the escape character itself within the
+          comparison value, which is assumed to be a literal string and not a
+          SQL expression.
+
+          An expression such as::
 
-            somecolumn.contains("foo/%bar", escape="/")
+            somecolumn.contains("foo%bar", autoescape=True)
 
-        :param autoescape: optional escape character, renders the ``ESCAPE``
-          keyword and uses that escape character to auto escape the
-          expression, disabling all SQL wildcard matching (``%`` and ``_``),
-          e.g.::
+          Will render as::
 
-            somecolumn.contains("foo%bar", autoescape="/")
+            somecolumn LIKE '%' || :param || '%' ESCAPE '/'
+
+          With the value of :param as ``"foo/%bar"``.
 
           .. versionadded:: 1.2
 
+          .. versionchanged:: 1.2.0b4 The
+            :paramref:`.ColumnOperators.contains.autoescape` parameter is
+             now a simple boolean rather than a character; the escape
+             character itself is also escaped, and defaults to a forwards
+             slash, which itself can be customized using the
+             :paramref:`.ColumnOperators.contains.escape` parameter.
+
+        :param escape: a character which when given will render with the
+          ``ESCAPE`` keyword to establish that character as the escape
+          character.  This character can then be placed preceding occurrences
+          of ``%`` and ``_`` to allow them to act as themselves and not
+          wildcard characters.
+
+          An expression such as::
+
+            somecolumn.contains("foo/%bar", escape="^")
+
+          Will render as::
+
+            somecolumn LIKE '%' || :param || '%' ESCAPE '^'
+
+          The parameter may also be combined with
+          :paramref:`.ColumnOperators.contains.autoescape`::
+
+            somecolumn.contains("foo%bar^bat", escape="^", autoescape=True)
+
+          Where above, the given literal parameter will be converted to
+          ``"foo^%bar^^bat"`` before being passed to the database.
+
+        .. seealso::
+
+            :meth:`.ColumnOperators.startswith`
+
+            :meth:`.ColumnOperators.endswith`
+
+            :meth:`.ColumnOperators.like`
+
+
         """
         return self.operate(contains_op, other, **kwargs)
 
@@ -909,10 +1075,6 @@ class ColumnOperators(Operators):
         return self.reverse_operate(truediv, other)
 
 
-def _escaped(value, escape):
-    return value.replace('%', escape + '%').replace('_', escape + '_')
-
-
 def from_():
     raise NotImplementedError()
 
@@ -1001,46 +1163,50 @@ def all_op(a):
     return a.all_()
 
 
-def startswith_op(a, b, escape=None, autoescape=None):
+def _escaped_like_impl(fn, other, escape, autoescape):
     if autoescape:
-        return a.startswith(_escaped(b, autoescape), escape=autoescape)
-    else:
-        return a.startswith(b, escape=escape)
+        if autoescape is not True:
+            util.warn(
+                "The autoescape parameter is now a simple boolean True/False")
+        if escape is None:
+            escape = '/'
 
+        if not isinstance(other, util.compat.string_types):
+            raise TypeError("String value expected when autoescape=True")
 
-def notstartswith_op(a, b, escape=None, autoescape=None):
-    if autoescape:
-        return ~a.startswith(_escaped(b, autoescape), escape=autoescape)
-    else:
-        return ~a.startswith(b, escape=escape)
+        if escape not in ('%', '_'):
+            other = other.replace(escape, escape + escape)
 
+        other = (
+            other.replace('%', escape + '%').
+            replace('_', escape + '_')
+        )
 
-def endswith_op(a, b, escape=None, autoescape=None):
-    if autoescape:
-        return a.endswith(_escaped(b, autoescape), escape=autoescape)
-    else:
-        return a.endswith(b, escape=escape)
+    return fn(other, escape=escape)
 
 
-def notendswith_op(a, b, escape=None, autoescape=None):
-    if autoescape:
-        return ~a.endswith(_escaped(b, autoescape), escape=autoescape)
-    else:
-        return ~a.endswith(b, escape=escape)
+def startswith_op(a, b, escape=None, autoescape=False):
+    return _escaped_like_impl(a.startswith, b, escape, autoescape)
 
 
-def contains_op(a, b, escape=None, autoescape=None):
-    if autoescape:
-        return a.contains(_escaped(b, autoescape), escape=autoescape)
-    else:
-        return a.contains(b, escape=escape)
+def notstartswith_op(a, b, escape=None, autoescape=False):
+    return ~_escaped_like_impl(a.startswith, b, escape, autoescape)
 
 
-def notcontains_op(a, b, escape=None, autoescape=None):
-    if autoescape:
-        return ~a.contains(_escaped(b, autoescape), escape=autoescape)
-    else:
-        return ~a.contains(b, escape=escape)
+def endswith_op(a, b, escape=None, autoescape=False):
+    return _escaped_like_impl(a.endswith, b, escape, autoescape)
+
+
+def notendswith_op(a, b, escape=None, autoescape=False):
+    return ~_escaped_like_impl(a.endswith, b, escape, autoescape)
+
+
+def contains_op(a, b, escape=None, autoescape=False):
+    return _escaped_like_impl(a.contains, b, escape, autoescape)
+
+
+def notcontains_op(a, b, escape=None, autoescape=False):
+    return ~_escaped_like_impl(a.contains, b, escape, autoescape)
 
 
 def match_op(a, b, **kw):
index 77535e1a326ce3093c0ff12e4d56b965a952a976..0972148233a80a4ebc9a05002216e17d187b6014 100644 (file)
@@ -4,6 +4,7 @@ from ..assertions import eq_
 from sqlalchemy import util
 from sqlalchemy import Integer, String, select, func, bindparam, union, tuple_
 from sqlalchemy import testing
+from sqlalchemy import literal_column
 
 from ..schema import Table, Column
 
@@ -365,3 +366,107 @@ class ExpandingBoundInTest(fixtures.TablesTest):
             [(2, ), (3, ), (4, )],
             params={"q": [(2, 3), (3, 4), (4, 5)]},
         )
+
+
+class LikeFunctionsTest(fixtures.TablesTest):
+    __backend__ = True
+
+    run_inserts = 'once'
+    run_deletes = None
+
+    @classmethod
+    def define_tables(cls, metadata):
+        Table("some_table", metadata,
+              Column('id', Integer, primary_key=True),
+              Column('data', String(50)))
+
+    @classmethod
+    def insert_data(cls):
+        config.db.execute(
+            cls.tables.some_table.insert(),
+            [
+                {"id": 1, "data": "abcdefg"},
+                {"id": 2, "data": "ab/cdefg"},
+                {"id": 3, "data": "ab%cdefg"},
+                {"id": 4, "data": "ab_cdefg"},
+                {"id": 5, "data": "abcde/fg"},
+                {"id": 6, "data": "abcde%fg"},
+                {"id": 7, "data": "ab#cdefg"},
+                {"id": 8, "data": "ab9cdefg"},
+                {"id": 9, "data": "abcde#fg"},
+                {"id": 10, "data": "abcd9fg"},
+            ]
+        )
+
+    def _test(self, expr, expected):
+        some_table = self.tables.some_table
+
+        with config.db.connect() as conn:
+            rows = {
+                value for value, in
+                conn.execute(select([some_table.c.id]).where(expr))
+            }
+
+        eq_(rows, expected)
+
+    def test_startswith_unescaped(self):
+        col = self.tables.some_table.c.data
+        self._test(col.startswith("ab%c"), {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
+
+    def test_startswith_autoescape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.startswith("ab%c", autoescape=True), {3})
+
+    def test_startswith_sqlexpr(self):
+        col = self.tables.some_table.c.data
+        self._test(
+            col.startswith(literal_column("'ab%c'")),
+            {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
+
+    def test_startswith_escape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.startswith("ab##c", escape="#"), {7})
+
+    def test_startswith_autoescape_escape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.startswith("ab%c", autoescape=True, escape="#"), {3})
+        self._test(col.startswith("ab#c", autoescape=True, escape="#"), {7})
+
+    def test_endswith_unescaped(self):
+        col = self.tables.some_table.c.data
+        self._test(col.endswith("e%fg"), {1, 2, 3, 4, 5, 6, 7, 8, 9})
+
+    def test_endswith_sqlexpr(self):
+        col = self.tables.some_table.c.data
+        self._test(col.endswith(literal_column("'e%fg'")),
+                   {1, 2, 3, 4, 5, 6, 7, 8, 9})
+
+    def test_endswith_autoescape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.endswith("e%fg", autoescape=True), {6})
+
+    def test_endswith_escape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.endswith("e##fg", escape="#"), {9})
+
+    def test_endswith_autoescape_escape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.endswith("e%fg", autoescape=True, escape="#"), {6})
+        self._test(col.endswith("e#fg", autoescape=True, escape="#"), {9})
+
+    def test_contains_unescaped(self):
+        col = self.tables.some_table.c.data
+        self._test(col.contains("b%cde"), {1, 2, 3, 4, 5, 6, 7, 8, 9})
+
+    def test_contains_autoescape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.contains("b%cde", autoescape=True), {3})
+
+    def test_contains_escape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.contains("b##cde", escape="#"), {7})
+
+    def test_contains_autoescape_escape(self):
+        col = self.tables.some_table.c.data
+        self._test(col.contains("b%cd", autoescape=True, escape="#"), {3})
+        self._test(col.contains("b#cd", autoescape=True, escape="#"), {7})
index 2a11688444eeb855a71df743f5f1f22e24551377..f5446a856d4a2aa9134fe245c51d1474c68ea8ff 100644 (file)
@@ -1,6 +1,7 @@
 from sqlalchemy.testing import fixtures, eq_, is_, is_not_
 from sqlalchemy import testing
 from sqlalchemy.testing import assert_raises_message
+from sqlalchemy.testing import expect_warnings
 from sqlalchemy.sql import column, desc, asc, literal, collate, null, \
     true, false, any_, all_
 from sqlalchemy.sql import sqltypes
@@ -2299,9 +2300,9 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_contains_autoescape(self):
         self.assert_compile(
-            column('x').contains('a%b_c', autoescape='\\'),
-            "x LIKE '%' || :x_1 || '%' ESCAPE '\\'",
-            checkparams={'x_1': 'a\\%b\\_c'}
+            column('x').contains('a%b_c/d', autoescape=True),
+            "x LIKE '%' || :x_1 || '%' ESCAPE '/'",
+            checkparams={'x_1': 'a/%b/_c//d'}
         )
 
     def test_contains_literal(self):
@@ -2334,9 +2335,9 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_not_contains_autoescape(self):
         self.assert_compile(
-            ~column('x').contains('a%b_c', autoescape='\\'),
-            "x NOT LIKE '%' || :x_1 || '%' ESCAPE '\\'",
-            checkparams={'x_1': 'a\\%b\\_c'}
+            ~column('x').contains('a%b_c/d', autoescape=True),
+            "x NOT LIKE '%' || :x_1 || '%' ESCAPE '/'",
+            checkparams={'x_1': 'a/%b/_c//d'}
         )
 
     def test_contains_concat(self):
@@ -2443,9 +2444,16 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_startswith_autoescape(self):
         self.assert_compile(
-            column('x').startswith('a%b_c', autoescape='\\'),
-            "x LIKE :x_1 || '%' ESCAPE '\\'",
-            checkparams={'x_1': 'a\\%b\\_c'}
+            column('x').startswith('a%b_c/d', autoescape=True),
+            "x LIKE :x_1 || '%' ESCAPE '/'",
+            checkparams={'x_1': 'a/%b/_c//d'}
+        )
+
+    def test_startswith_autoescape_custom_escape(self):
+        self.assert_compile(
+            column('x').startswith('a%b_c/d^e', autoescape=True, escape='^'),
+            "x LIKE :x_1 || '%' ESCAPE '^'",
+            checkparams={'x_1': 'a^%b^_c/d^^e'}
         )
 
     def test_not_startswith(self):
@@ -2464,9 +2472,9 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_not_startswith_autoescape(self):
         self.assert_compile(
-            ~column('x').startswith('a%b_c', autoescape='\\'),
-            "x NOT LIKE :x_1 || '%' ESCAPE '\\'",
-            checkparams={'x_1': 'a\\%b\\_c'}
+            ~column('x').startswith('a%b_c/d', autoescape=True),
+            "x NOT LIKE :x_1 || '%' ESCAPE '/'",
+            checkparams={'x_1': 'a/%b/_c//d'}
         )
 
     def test_startswith_literal(self):
@@ -2547,9 +2555,32 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_endswith_autoescape(self):
         self.assert_compile(
-            column('x').endswith('a%b_c', autoescape='\\'),
-            "x LIKE '%' || :x_1 ESCAPE '\\'",
-            checkparams={'x_1': 'a\\%b\\_c'}
+            column('x').endswith('a%b_c/d', autoescape=True),
+            "x LIKE '%' || :x_1 ESCAPE '/'",
+            checkparams={'x_1': 'a/%b/_c//d'}
+        )
+
+    def test_endswith_autoescape_custom_escape(self):
+        self.assert_compile(
+            column('x').endswith('a%b_c/d^e', autoescape=True, escape="^"),
+            "x LIKE '%' || :x_1 ESCAPE '^'",
+            checkparams={'x_1': 'a^%b^_c/d^^e'}
+        )
+
+    def test_endswith_autoescape_warning(self):
+        with expect_warnings("The autoescape parameter is now a simple"):
+            self.assert_compile(
+                column('x').endswith('a%b_c/d', autoescape='P'),
+                "x LIKE '%' || :x_1 ESCAPE '/'",
+                checkparams={'x_1': 'a/%b/_c//d'}
+            )
+
+    def test_endswith_autoescape_nosqlexpr(self):
+        assert_raises_message(
+            TypeError,
+            "String value expected when autoescape=True",
+            column('x').endswith,
+            literal_column("'a%b_c/d'"), autoescape=True
         )
 
     def test_not_endswith(self):
@@ -2568,9 +2599,9 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_not_endswith_autoescape(self):
         self.assert_compile(
-            ~column('x').endswith('a%b_c', autoescape='\\'),
-            "x NOT LIKE '%' || :x_1 ESCAPE '\\'",
-            checkparams={'x_1': 'a\\%b\\_c'}
+            ~column('x').endswith('a%b_c/d', autoescape=True),
+            "x NOT LIKE '%' || :x_1 ESCAPE '/'",
+            checkparams={'x_1': 'a/%b/_c//d'}
         )
 
     def test_endswith_literal(self):