From: Diana Clarke Date: Sun, 10 Apr 2016 19:19:03 +0000 (-0400) Subject: add autoescape option to startswith, endswith, and contains X-Git-Tag: rel_1_2_0b1~157 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2895c57b29c500fe4388ef23e61f13c5e1e9b4b2;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git add autoescape option to startswith, endswith, and contains Fixes: #2694 Change-Id: I34c0bdcb01c2b76b9ab6cd315dae13e3dd8a502b Pull-request: https://github.com/zzzeek/sqlalchemy/pull/207 --- diff --git a/doc/build/changelog/changelog_12.rst b/doc/build/changelog/changelog_12.rst index 83df9167f9..503c59a268 100644 --- a/doc/build/changelog/changelog_12.rst +++ b/doc/build/changelog/changelog_12.rst @@ -28,4 +28,17 @@ .. seealso:: - :ref:`change_3276` \ No newline at end of file + :ref:`change_3276` + + .. change:: 2694 + :tags: feature, sql + :tickets: 2694 + + Added a new option ``autoescape`` to the "startswith" and + "endswith" classes of comparators; this supplies an escape character + also applies it to all occurrences of the wildcard characters "%" + and "_" automatically. Pull request courtesy Diana Clarke. + + .. seealso:: + + :ref:`change_2694` diff --git a/doc/build/changelog/migration_12.rst b/doc/build/changelog/migration_12.rst index 0950482287..c63d585fb0 100644 --- a/doc/build/changelog/migration_12.rst +++ b/doc/build/changelog/migration_12.rst @@ -37,6 +37,30 @@ New Features and Improvements - ORM New Features and Improvements - Core ==================================== +.. _change_2694: + +New "autoescape" option for startswith(), endswith() +---------------------------------------------------- + +The "autoescape" parameter is added to :meth:`.Operators.startswith`, +:meth:`.Operators.endswith`, :meth:`.Operators.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. + +An expression such as:: + + >>> column('x').startswith('total%score', autoescape='/') + +Renders as:: + + x LIKE :x_1 || '%%' ESCAPE '/' + +Where the value of the parameter "x_1" is ``'total/%score'``. + +:ticket:`2694` + Key Behavioral Changes - ORM ============================ diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index d883392998..1690d546be 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -196,7 +196,7 @@ class Operators(object): class custom_op(object): """Represent a 'custom' operator. - :class:`.custom_op` is normally instantitated when the + :class:`.custom_op` is normally instantiated when the :meth:`.ColumnOperators.op` method is used to create a custom operator callable. The class can also be used directly when programmatically constructing expressions. E.g. @@ -541,6 +541,31 @@ class ColumnOperators(Operators): In a column context, produces the clause ``LIKE '%'`` + E.g.:: + + select([sometable]).where(sometable.c.column.startswith("foobar")) + + :param other: expression to be compared, with SQL wildcard + matching (``%`` and ``_``) enabled, e.g.:: + + somecolumn.startswith("foo%bar") + + :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.:: + + somecolumn.startswith("foo/%bar", escape="/") + + :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="/") + + .. versionadded:: 1.2 + """ return self.operate(startswith_op, other, **kwargs) @@ -549,6 +574,31 @@ class ColumnOperators(Operators): In a column context, produces the clause ``LIKE '%'`` + E.g.:: + + select([sometable]).where(sometable.c.column.endswith("foobar")) + + :param other: expression to be compared, with SQL wildcard + matching (``%`` and ``_``) enabled, e.g.:: + + somecolumn.endswith("foo%bar") + + :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.:: + + somecolumn.endswith("foo/%bar", escape="/") + + :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.endswith("foo%bar", autoescape="/") + + .. versionadded:: 1.2 + """ return self.operate(endswith_op, other, **kwargs) @@ -557,6 +607,31 @@ class ColumnOperators(Operators): In a column context, produces the clause ``LIKE '%%'`` + E.g.:: + + select([sometable]).where(sometable.c.column.contains("foobar")) + + :param other: expression to compare, with SQL wildcard + matching (``%`` and ``_``) enabled, e.g.:: + + somecolumn.contains("foo%bar") + + :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.:: + + somecolumn.contains("foo/%bar", escape="/") + + :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.contains("foo%bar", autoescape="/") + + .. versionadded:: 1.2 + """ return self.operate(contains_op, other, **kwargs) @@ -736,6 +811,10 @@ class ColumnOperators(Operators): return self.reverse_operate(truediv, other) +def _escaped(value, escape): + return value.replace('%', escape + '%').replace('_', escape + '_') + + def from_(): raise NotImplementedError() @@ -824,28 +903,46 @@ def all_op(a): return a.all_() -def startswith_op(a, b, escape=None): - return a.startswith(b, escape=escape) +def startswith_op(a, b, escape=None, autoescape=None): + if autoescape: + return a.startswith(_escaped(b, autoescape), escape=autoescape) + else: + return a.startswith(b, escape=escape) -def notstartswith_op(a, b, escape=None): - return ~a.startswith(b, escape=escape) +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) -def endswith_op(a, b, escape=None): - return a.endswith(b, escape=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) -def notendswith_op(a, b, escape=None): - return ~a.endswith(b, 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 contains_op(a, b, escape=None): - return a.contains(b, escape=escape) +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 notcontains_op(a, b, escape=None): - return ~a.contains(b, escape=escape) +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 match_op(a, b, **kw): diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index 59cae6584c..0bdebab588 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -15,7 +15,7 @@ from sqlalchemy.sql.elements import _literal_as_text from sqlalchemy.schema import Column, Table, MetaData from sqlalchemy.sql import compiler from sqlalchemy.types import TypeEngine, TypeDecorator, UserDefinedType, \ - Boolean, NullType, MatchType, Indexable, Concatenable, ARRAY, JSON, \ + Boolean, MatchType, Indexable, Concatenable, ARRAY, JSON, \ DateTime from sqlalchemy.dialects import mysql, firebird, postgresql, oracle, \ sqlite, mssql @@ -2226,9 +2226,16 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_contains_escape(self): self.assert_compile( - column('x').contains('y', escape='\\'), + column('x').contains('a%b_c', escape='\\'), "x LIKE '%%' || :x_1 || '%%' ESCAPE '\\'", - checkparams={'x_1': 'y'} + checkparams={'x_1': 'a%b_c'} + ) + + 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'} ) def test_contains_literal(self): @@ -2254,9 +2261,16 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_not_contains_escape(self): self.assert_compile( - ~column('x').contains('y', escape='\\'), + ~column('x').contains('a%b_c', escape='\\'), "x NOT LIKE '%%' || :x_1 || '%%' ESCAPE '\\'", - checkparams={'x_1': 'y'} + checkparams={'x_1': 'a%b_c'} + ) + + 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'} ) def test_contains_concat(self): @@ -2291,6 +2305,62 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): dialect=mysql.dialect() ) + def test_like(self): + self.assert_compile( + column('x').like('y'), + "x LIKE :x_1", + checkparams={'x_1': 'y'} + ) + + def test_like_escape(self): + self.assert_compile( + column('x').like('a%b_c', escape='\\'), + "x LIKE :x_1 ESCAPE '\\'", + checkparams={'x_1': 'a%b_c'} + ) + + def test_ilike(self): + self.assert_compile( + column('x').ilike('y'), + "lower(x) LIKE lower(:x_1)", + checkparams={'x_1': 'y'} + ) + + def test_ilike_escape(self): + self.assert_compile( + column('x').ilike('a%b_c', escape='\\'), + "lower(x) LIKE lower(:x_1) ESCAPE '\\'", + checkparams={'x_1': 'a%b_c'} + ) + + def test_notlike(self): + self.assert_compile( + column('x').notlike('y'), + "x NOT LIKE :x_1", + checkparams={'x_1': 'y'} + ) + + def test_notlike_escape(self): + self.assert_compile( + column('x').notlike('a%b_c', escape='\\'), + "x NOT LIKE :x_1 ESCAPE '\\'", + checkparams={'x_1': 'a%b_c'} + ) + + def test_notilike(self): + self.assert_compile( + column('x').notilike('y'), + "lower(x) NOT LIKE lower(:x_1)", + checkparams={'x_1': 'y'} + ) + + def test_notilike_escape(self): + self.assert_compile( + column('x').notilike('a%b_c', escape='\\'), + "lower(x) NOT LIKE lower(:x_1) ESCAPE '\\'", + checkparams={'x_1': 'a%b_c'} + ) + def test_startswith(self): self.assert_compile( column('x').startswith('y'), @@ -2300,9 +2370,16 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_startswith_escape(self): self.assert_compile( - column('x').startswith('y', escape='\\'), + column('x').startswith('a%b_c', escape='\\'), "x LIKE :x_1 || '%%' ESCAPE '\\'", - checkparams={'x_1': 'y'} + checkparams={'x_1': 'a%b_c'} + ) + + 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'} ) def test_not_startswith(self): @@ -2314,9 +2391,16 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_not_startswith_escape(self): self.assert_compile( - ~column('x').startswith('y', escape='\\'), + ~column('x').startswith('a%b_c', escape='\\'), "x NOT LIKE :x_1 || '%%' ESCAPE '\\'", - checkparams={'x_1': 'y'} + checkparams={'x_1': 'a%b_c'} + ) + + 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'} ) def test_startswith_literal(self): @@ -2390,9 +2474,16 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_endswith_escape(self): self.assert_compile( - column('x').endswith('y', escape='\\'), + column('x').endswith('a%b_c', escape='\\'), "x LIKE '%%' || :x_1 ESCAPE '\\'", - checkparams={'x_1': 'y'} + checkparams={'x_1': 'a%b_c'} + ) + + 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'} ) def test_not_endswith(self): @@ -2404,9 +2495,16 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL): def test_not_endswith_escape(self): self.assert_compile( - ~column('x').endswith('y', escape='\\'), + ~column('x').endswith('a%b_c', escape='\\'), "x NOT LIKE '%%' || :x_1 ESCAPE '\\'", - checkparams={'x_1': 'y'} + checkparams={'x_1': 'a%b_c'} + ) + + 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'} ) def test_endswith_literal(self):