.. 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`
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
============================
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.
In a column context, produces the clause ``LIKE '<other>%'``
+ 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)
In a column context, produces the clause ``LIKE '%<other>'``
+ 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)
In a column context, produces the clause ``LIKE '%<other>%'``
+ 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)
return self.reverse_operate(truediv, other)
+def _escaped(value, escape):
+ return value.replace('%', escape + '%').replace('_', escape + '_')
+
+
def from_():
raise NotImplementedError()
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):
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
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):
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):
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'),
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):
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):
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):
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):