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)
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, 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):
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
[(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})
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
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):
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):
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):
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):
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):
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):