From 654ca5463d2045d8dc74d7d790081f58554d796d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 24 Oct 2017 18:08:05 -0400 Subject: [PATCH] Rework autoescape to be a simple boolean; escape the escape character 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 | 5 + doc/build/changelog/migration_12.rst | 29 +- doc/build/changelog/unreleased_12/2694.rst | 15 + lib/sqlalchemy/sql/operators.py | 328 +++++++++++++++----- lib/sqlalchemy/testing/suite/test_select.py | 105 +++++++ test/sql/test_operators.py | 67 ++-- 6 files changed, 443 insertions(+), 106 deletions(-) create mode 100644 doc/build/changelog/unreleased_12/2694.rst diff --git a/doc/build/changelog/changelog_12.rst b/doc/build/changelog/changelog_12.rst index 0535a0ec32..022bb8452b 100644 --- a/doc/build/changelog/changelog_12.rst +++ b/doc/build/changelog/changelog_12.rst @@ -1186,6 +1186,11 @@ 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` diff --git a/doc/build/changelog/migration_12.rst b/doc/build/changelog/migration_12.rst index 35d2e1c769..56f671f188 100644 --- a/doc/build/changelog/migration_12.rst +++ b/doc/build/changelog/migration_12.rst @@ -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 index 0000000000..4386134014 --- /dev/null +++ b/doc/build/changelog/unreleased_12/2694.rst @@ -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` + diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index ef6f78929f..0e8dec2a0d 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -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 '%'`` + Produces a LIKE expression that tests against a match for the start + of a string value:: + + column LIKE || '%' 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 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 '%'`` + column LIKE '%' || 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 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 '%%'`` + column LIKE '%' || || '%' 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 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): diff --git a/lib/sqlalchemy/testing/suite/test_select.py b/lib/sqlalchemy/testing/suite/test_select.py index 77535e1a32..0972148233 100644 --- a/lib/sqlalchemy/testing/suite/test_select.py +++ b/lib/sqlalchemy/testing/suite/test_select.py @@ -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}) diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index 2a11688444..f5446a856d 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -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): -- 2.47.3