--- /dev/null
+.. change::
+ :tags: feature, sql
+ :tickets: 3482
+
+ Added long-requested case-insensitive string operators
+ :meth:`_sql.ColumnOperators.icontains`,
+ :meth:`_sql.ColumnOperators.istartswith`,
+ :meth:`_sql.ColumnOperators.iendswith`, which produce case-insensitive
+ LIKE compositions (using ILIKE on PostgreSQL, and the LOWER() function on
+ all other backends) to complement the existing LIKE composition operators
+ :meth:`_sql.ColumnOperators.contains`,
+ :meth:`_sql.ColumnOperators.startswith`, etc. Huge thanks to Matias
+ Martinez Rebori for their meticulous and complete efforts in implementing
+ these new methods.
\ No newline at end of file
self.process(binary.right, **kw),
)
+ def visit_ilike_case_insensitive_operand(self, element, **kw):
+ return element.element._compiler_dispatch(self, **kw)
+
def visit_ilike_op_binary(self, binary, operator, **kw):
escape = binary.modifiers.get("escape", None)
if not isinstance(
element,
- (elements.ClauseElement, schema.SchemaItem, schema.FetchedValue),
+ (
+ elements.CompilerElement,
+ schema.SchemaItem,
+ schema.FetchedValue,
+ ),
):
resolved = None
from . import elements
from . import functions
from . import operators
+from . import roles
from . import schema
from . import selectable
from . import sqltypes
# this was a Visitable, but to allow accurate detection of
# column elements this is actually a column element
-class _CompileLabel(elements.CompilerColumnElement):
+class _CompileLabel(
+ roles.BinaryElementRole[Any], elements.CompilerColumnElement
+):
"""lightweight label object which acts as an expression.Label."""
return self
+class ilike_case_insensitive(
+ roles.BinaryElementRole[Any], elements.CompilerColumnElement
+):
+ """produce a wrapping element for a case-insensitive portion of
+ an ILIKE construct.
+
+ The construct usually renders the ``lower()`` function, but on
+ PostgreSQL will pass silently with the assumption that "ILIKE"
+ is being used.
+
+ .. versionadded:: 2.0
+
+ """
+
+ __visit_name__ = "ilike_case_insensitive_operand"
+ __slots__ = "element", "comparator"
+
+ def __init__(self, element):
+ self.element = element
+ self.comparator = element.comparator
+
+ @property
+ def proxy_set(self):
+ return self.element.proxy_set
+
+ @property
+ def type(self):
+ return self.element.type
+
+ def self_group(self, **kw):
+ return self
+
+ def _with_binary_element_type(self, type_):
+ return ilike_case_insensitive(
+ self.element._with_binary_element_type(type_)
+ )
+
+
class SQLCompiler(Compiled):
"""Default implementation of :class:`.Compiled`.
def _like_percent_literal(self):
return elements.literal_column("'%'", type_=sqltypes.STRINGTYPE)
+ def visit_ilike_case_insensitive_operand(self, element, **kw):
+ return f"lower({element.element._compiler_dispatch(self, **kw)})"
+
def visit_contains_op_binary(self, binary, operator, **kw):
binary = binary._clone()
percent = self._like_percent_literal
binary.right = percent.concat(binary.right).concat(percent)
return self.visit_not_like_op_binary(binary, operator, **kw)
+ def visit_icontains_op_binary(self, binary, operator, **kw):
+ binary = binary._clone()
+ percent = self._like_percent_literal
+ binary.left = ilike_case_insensitive(binary.left)
+ binary.right = percent.concat(
+ ilike_case_insensitive(binary.right)
+ ).concat(percent)
+ return self.visit_ilike_op_binary(binary, operator, **kw)
+
+ def visit_not_icontains_op_binary(self, binary, operator, **kw):
+ binary = binary._clone()
+ percent = self._like_percent_literal
+ binary.left = ilike_case_insensitive(binary.left)
+ binary.right = percent.concat(
+ ilike_case_insensitive(binary.right)
+ ).concat(percent)
+ return self.visit_not_ilike_op_binary(binary, operator, **kw)
+
def visit_startswith_op_binary(self, binary, operator, **kw):
binary = binary._clone()
percent = self._like_percent_literal
binary.right = percent._rconcat(binary.right)
return self.visit_not_like_op_binary(binary, operator, **kw)
+ def visit_istartswith_op_binary(self, binary, operator, **kw):
+ binary = binary._clone()
+ percent = self._like_percent_literal
+ binary.left = ilike_case_insensitive(binary.left)
+ binary.right = percent._rconcat(ilike_case_insensitive(binary.right))
+ return self.visit_ilike_op_binary(binary, operator, **kw)
+
+ def visit_not_istartswith_op_binary(self, binary, operator, **kw):
+ binary = binary._clone()
+ percent = self._like_percent_literal
+ binary.left = ilike_case_insensitive(binary.left)
+ binary.right = percent._rconcat(ilike_case_insensitive(binary.right))
+ return self.visit_not_ilike_op_binary(binary, operator, **kw)
+
def visit_endswith_op_binary(self, binary, operator, **kw):
binary = binary._clone()
percent = self._like_percent_literal
binary.right = percent.concat(binary.right)
return self.visit_not_like_op_binary(binary, operator, **kw)
+ def visit_iendswith_op_binary(self, binary, operator, **kw):
+ binary = binary._clone()
+ percent = self._like_percent_literal
+ binary.left = ilike_case_insensitive(binary.left)
+ binary.right = percent.concat(ilike_case_insensitive(binary.right))
+ return self.visit_ilike_op_binary(binary, operator, **kw)
+
+ def visit_not_iendswith_op_binary(self, binary, operator, **kw):
+ binary = binary._clone()
+ percent = self._like_percent_literal
+ binary.left = ilike_case_insensitive(binary.left)
+ binary.right = percent.concat(ilike_case_insensitive(binary.right))
+ return self.visit_not_ilike_op_binary(binary, operator, **kw)
+
def visit_like_op_binary(self, binary, operator, **kw):
escape = binary.modifiers.get("escape", None)
- # TODO: use ternary here, not "and"/ "or"
return "%s LIKE %s" % (
binary.left._compiler_dispatch(self, **kw),
binary.right._compiler_dispatch(self, **kw),
)
def visit_ilike_op_binary(self, binary, operator, **kw):
- escape = binary.modifiers.get("escape", None)
- return "lower(%s) LIKE lower(%s)" % (
- binary.left._compiler_dispatch(self, **kw),
- binary.right._compiler_dispatch(self, **kw),
- ) + (
- " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
- if escape
- else ""
- )
+ if operator is operators.ilike_op:
+ binary = binary._clone()
+ binary.left = ilike_case_insensitive(binary.left)
+ binary.right = ilike_case_insensitive(binary.right)
+ # else we assume ilower() has been applied
+
+ return self.visit_like_op_binary(binary, operator, **kw)
def visit_not_ilike_op_binary(self, binary, operator, **kw):
- escape = binary.modifiers.get("escape", None)
- return "lower(%s) NOT LIKE lower(%s)" % (
- binary.left._compiler_dispatch(self, **kw),
- binary.right._compiler_dispatch(self, **kw),
- ) + (
- " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
- if escape
- else ""
- )
+ if operator is operators.not_ilike_op:
+ binary = binary._clone()
+ binary.left = ilike_case_insensitive(binary.left)
+ binary.right = ilike_case_insensitive(binary.right)
+ # else we assume ilower() has been applied
+
+ return self.visit_not_like_op_binary(binary, operator, **kw)
def visit_between_op_binary(self, binary, operator, **kw):
symmetric = binary.modifiers.get("symmetric", False)
_boolean_compare,
util.immutabledict({"negate_op": operators.not_contains_op}),
),
+ "icontains_op": (
+ _boolean_compare,
+ util.immutabledict({"negate_op": operators.not_icontains_op}),
+ ),
"startswith_op": (
_boolean_compare,
util.immutabledict({"negate_op": operators.not_startswith_op}),
),
+ "istartswith_op": (
+ _boolean_compare,
+ util.immutabledict({"negate_op": operators.not_istartswith_op}),
+ ),
"endswith_op": (
_boolean_compare,
util.immutabledict({"negate_op": operators.not_endswith_op}),
),
+ "iendswith_op": (
+ _boolean_compare,
+ util.immutabledict({"negate_op": operators.not_iendswith_op}),
+ ),
"desc_op": (
_scalar,
util.immutabledict({"fn": UnaryExpression._create_desc}),
__slots__ = ()
+ _propagate_attrs = util.EMPTY_DICT
+
# SQLCoreOperations should be suiting the ExpressionElementRole
# and ColumnsClauseRole. however the MRO issues become too elaborate
startswith_op, other, escape=escape, autoescape=autoescape
)
+ def istartswith(
+ self,
+ other: Any,
+ escape: Optional[str] = None,
+ autoescape: bool = False,
+ ) -> ColumnOperators:
+ r"""Implement the ``istartswith`` operator, e.g. case insensitive
+ version of :meth:`.ColumnOperators.startswith`.
+
+ Produces a LIKE expression that tests against an insensitive
+ match for the start of a string value::
+
+ lower(column) LIKE lower(<other>) || '%'
+
+ E.g.::
+
+ stmt = select(sometable).\
+ where(sometable.c.column.istartswith("foobar"))
+
+ 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.istartswith.autoescape` flag
+ may be set to ``True`` to apply escaping to occurrences of these
+ characters within the string value so that they match as themselves
+ and not as wildcard characters. Alternatively, the
+ :paramref:`.ColumnOperators.istartswith.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.
+
+ :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.istartswith.autoescape` flag is
+ set to True.
+
+ :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.istartswith("foo%bar", autoescape=True)
+
+ Will render as::
+
+ lower(somecolumn) LIKE lower(:param) || '%' ESCAPE '/'
+
+ With the value of ``:param`` as ``"foo/%bar"``.
+
+ :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.istartswith("foo/%bar", escape="^")
+
+ Will render as::
+
+ lower(somecolumn) LIKE lower(:param) || '%' ESCAPE '^'
+
+ The parameter may also be combined with
+ :paramref:`.ColumnOperators.istartswith.autoescape`::
+
+ somecolumn.istartswith("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`
+ """
+ return self.operate(
+ istartswith_op, other, escape=escape, autoescape=autoescape
+ )
+
def endswith(
self,
other: Any,
endswith_op, other, escape=escape, autoescape=autoescape
)
+ def iendswith(
+ self,
+ other: Any,
+ escape: Optional[str] = None,
+ autoescape: bool = False,
+ ) -> ColumnOperators:
+ r"""Implement the ``iendswith`` operator, e.g. case insensitive
+ version of :meth:`.ColumnOperators.endswith`.
+
+ Produces a LIKE expression that tests against an insensitive match
+ for the end of a string value::
+
+ lower(column) LIKE '%' || lower(<other>)
+
+ E.g.::
+
+ stmt = select(sometable).\
+ where(sometable.c.column.iendswith("foobar"))
+
+ 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.iendswith.autoescape` flag
+ may be set to ``True`` to apply escaping to occurrences of these
+ characters within the string value so that they match as themselves
+ and not as wildcard characters. Alternatively, the
+ :paramref:`.ColumnOperators.iendswith.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.
+
+ :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.iendswith.autoescape` flag is
+ set to True.
+
+ :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.iendswith("foo%bar", autoescape=True)
+
+ Will render as::
+
+ lower(somecolumn) LIKE '%' || lower(:param) ESCAPE '/'
+
+ With the value of ``:param`` as ``"foo/%bar"``.
+
+ :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.iendswith("foo/%bar", escape="^")
+
+ Will render as::
+
+ lower(somecolumn) LIKE '%' || lower(:param) ESCAPE '^'
+
+ The parameter may also be combined with
+ :paramref:`.ColumnOperators.iendswith.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.endswith`
+ """
+ return self.operate(
+ iendswith_op, other, escape=escape, autoescape=autoescape
+ )
+
def contains(self, other: Any, **kw: Any) -> ColumnOperators:
r"""Implement the 'contains' operator.
"""
return self.operate(contains_op, other, **kw)
+ def icontains(self, other: Any, **kw: Any) -> ColumnOperators:
+ r"""Implement the ``icontains`` operator, e.g. case insensitive
+ version of :meth:`.ColumnOperators.contains`.
+
+ Produces a LIKE expression that tests against an insensitive match
+ for the middle of a string value::
+
+ lower(column) LIKE '%' || lower(<other>) || '%'
+
+ E.g.::
+
+ stmt = select(sometable).\
+ where(sometable.c.column.icontains("foobar"))
+
+ 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.icontains.autoescape` flag
+ may be set to ``True`` to apply escaping to occurrences of these
+ characters within the string value so that they match as themselves
+ and not as wildcard characters. Alternatively, the
+ :paramref:`.ColumnOperators.icontains.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.
+
+ :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.icontains.autoescape` flag is
+ set to True.
+
+ :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.icontains("foo%bar", autoescape=True)
+
+ Will render as::
+
+ lower(somecolumn) LIKE '%' || lower(:param) || '%' ESCAPE '/'
+
+ With the value of ``:param`` as ``"foo/%bar"``.
+
+ :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.icontains("foo/%bar", escape="^")
+
+ Will render as::
+
+ lower(somecolumn) LIKE '%' || lower(:param) || '%' ESCAPE '^'
+
+ The parameter may also be combined with
+ :paramref:`.ColumnOperators.contains.autoescape`::
+
+ somecolumn.icontains("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.contains`
+
+ """
+ return self.operate(icontains_op, other, **kw)
+
def match(self, other: Any, **kwargs: Any) -> ColumnOperators:
"""Implements a database-specific 'match' operator.
notstartswith_op = not_startswith_op
+@comparison_op
+@_operator_fn
+def istartswith_op(
+ a: Any, b: Any, escape: Optional[str] = None, autoescape: bool = False
+) -> Any:
+ return _escaped_like_impl(a.istartswith, b, escape, autoescape)
+
+
+@comparison_op
+@_operator_fn
+def not_istartswith_op(
+ a: Any, b: Any, escape: Optional[str] = None, autoescape: bool = False
+) -> Any:
+ return ~_escaped_like_impl(a.istartswith, b, escape, autoescape)
+
+
@comparison_op
@_operator_fn
def endswith_op(
notendswith_op = not_endswith_op
+@comparison_op
+@_operator_fn
+def iendswith_op(
+ a: Any, b: Any, escape: Optional[str] = None, autoescape: bool = False
+) -> Any:
+ return _escaped_like_impl(a.iendswith, b, escape, autoescape)
+
+
+@comparison_op
+@_operator_fn
+def not_iendswith_op(
+ a: Any, b: Any, escape: Optional[str] = None, autoescape: bool = False
+) -> Any:
+ return ~_escaped_like_impl(a.iendswith, b, escape, autoescape)
+
+
@comparison_op
@_operator_fn
def contains_op(
notcontains_op = not_contains_op
+@comparison_op
+@_operator_fn
+def icontains_op(
+ a: Any, b: Any, escape: Optional[str] = None, autoescape: bool = False
+) -> Any:
+ return _escaped_like_impl(a.icontains, b, escape, autoescape)
+
+
+@comparison_op
+@_operator_fn
+def not_icontains_op(
+ a: Any, b: Any, escape: Optional[str] = None, autoescape: bool = False
+) -> Any:
+ return ~_escaped_like_impl(a.icontains, b, escape, autoescape)
+
+
@comparison_op
@_operator_fn
def match_op(a: Any, b: Any, **kw: Any) -> Any:
checkparams={"x_1": "y"},
)
+ def test_contains_pg(self):
+ self.assert_compile(
+ column("x").contains("y"),
+ "x LIKE '%%' || %(x_1)s || '%%'",
+ checkparams={"x_1": "y"},
+ dialect="postgresql",
+ )
+
def test_contains_encoded(self):
self.assert_compile(
column("x").contains(b"y"),
dialect="mysql",
)
+ def test_not_contains_pg(self):
+ self.assert_compile(
+ ~column("x").contains(b"y"),
+ "x NOT LIKE '%%' || %(x_1)s || '%%'",
+ checkparams={"x_1": b"y"},
+ dialect="postgresql",
+ )
+
def test_contains_escape(self):
self.assert_compile(
column("x").contains("a%b_c", escape="\\"),
dialect=mysql.dialect(),
)
+ def test_icontains(self):
+ """
+ case insensitive contains method
+ """
+ self.assert_compile(
+ column("x").icontains("y"),
+ "lower(x) LIKE '%' || lower(:x_1) || '%'",
+ checkparams={"x_1": "y"},
+ )
+
+ def test_icontains_pg(self):
+ """
+ case insensitive contains method
+ """
+ self.assert_compile(
+ column("x").icontains("y"),
+ "x ILIKE '%%' || %(x_1)s || '%%'",
+ checkparams={"x_1": "y"},
+ dialect="postgresql",
+ )
+
+ def test_icontains_encoded(self):
+ self.assert_compile(
+ column("x").icontains(b"y"),
+ "lower(x) LIKE '%' || lower(:x_1) || '%'",
+ checkparams={"x_1": b"y"},
+ )
+
+ def test_not_icontains_encoded(self):
+ self.assert_compile(
+ ~column("x").icontains(b"y"),
+ "lower(x) NOT LIKE '%' || lower(:x_1) || '%'",
+ checkparams={"x_1": b"y"},
+ )
+
+ def test_icontains_encoded_mysql(self):
+ self.assert_compile(
+ column("x").icontains(b"y"),
+ "lower(x) LIKE concat('%%', lower(%s), '%%')",
+ checkparams={"x_1": b"y"},
+ dialect="mysql",
+ )
+
+ def test_not_icontains_encoded_mysql(self):
+ self.assert_compile(
+ ~column("x").icontains(b"y"),
+ "lower(x) NOT LIKE concat('%%', lower(%s), '%%')",
+ checkparams={"x_1": b"y"},
+ dialect="mysql",
+ )
+
+ def test_not_icontains(self):
+ """
+ same as test_icontains but negate the statement
+ """
+ self.assert_compile(
+ ~column("x").icontains("y"),
+ "lower(x) NOT LIKE '%' || lower(:x_1) || '%'",
+ checkparams={"x_1": "y"},
+ )
+
+ def test_not_icontains_pg(self):
+ """
+ same as test_icontains but negate the statement
+ """
+ self.assert_compile(
+ ~column("x").icontains("y"),
+ "x NOT ILIKE '%%' || %(x_1)s || '%%'",
+ checkparams={"x_1": "y"},
+ dialect="postgresql",
+ )
+
+ def test_icontains_escape(self):
+ """
+ render same SQL statement as icontains but include the ESCAPE
+ keyword to establish that character as the escape character
+ """
+ self.assert_compile(
+ column("x").icontains("a%b_c", escape="\\"),
+ "lower(x) LIKE '%' || lower(:x_1) || '%' ESCAPE '\\'",
+ checkparams={"x_1": "a%b_c"},
+ )
+
+ def test_not_icontains_escape(self):
+ """
+ same as test_icontains_escape but negate the statement
+ """
+ self.assert_compile(
+ ~column("x").icontains("a%b_c", escape="\\"),
+ "lower(x) NOT LIKE '%' || lower(:x_1) || '%' ESCAPE '\\'",
+ checkparams={"x_1": "a%b_c"},
+ )
+
+ def test_icontains_autoescape(self):
+ """
+ apply the escape character to all occurrences of "%", "_" and
+ the escape character itself
+ """
+ self.assert_compile(
+ column("x").icontains("a%b_c/d", autoescape=True),
+ "lower(x) LIKE '%' || lower(:x_1) || '%' ESCAPE '/'",
+ checkparams={"x_1": "a/%b/_c//d"},
+ )
+
+ def test_icontains_autoescape_custom_escape(self):
+ """
+ set an escape character to all occurrences of "%", "_" and
+ the escape character itself
+ """
+ self.assert_compile(
+ column("x").icontains("foo%bar^bat", escape="^", autoescape=True),
+ "lower(x) LIKE '%' || lower(:x_1) || '%' ESCAPE '^'",
+ checkparams={"x_1": "foo^%bar^^bat"},
+ )
+
+ def test_not_icontains_autoescape(self):
+ """
+ same as test_icontains_autoescape but negate the statement
+ """
+ self.assert_compile(
+ ~column("x").icontains("a%b_c/d", autoescape=True),
+ "lower(x) NOT LIKE '%' || lower(:x_1) || '%' ESCAPE '/'",
+ checkparams={"x_1": "a/%b/_c//d"},
+ )
+
+ def test_icontains_literal(self):
+ self.assert_compile(
+ column("x").icontains(literal_column("y")),
+ "lower(x) LIKE '%' || lower(y) || '%'",
+ checkparams={},
+ )
+
+ def test_icontains_text(self):
+ self.assert_compile(
+ column("x").icontains(text("y")),
+ "lower(x) LIKE '%' || lower(y) || '%'",
+ checkparams={},
+ )
+
+ def test_icontains_concat(self):
+ self.assert_compile(
+ column("x").icontains("y"),
+ "lower(x) LIKE concat('%%', lower(%s), '%%')",
+ checkparams={"x_1": "y"},
+ dialect=mysql.dialect(),
+ )
+
+ def test_not_icontains_concat(self):
+ self.assert_compile(
+ ~column("x").icontains("y"),
+ "lower(x) NOT LIKE concat('%%', lower(%s), '%%')",
+ checkparams={"x_1": "y"},
+ dialect=mysql.dialect(),
+ )
+
+ def test_icontains_literal_concat(self):
+ self.assert_compile(
+ column("x").icontains(literal_column("y")),
+ "lower(x) LIKE concat('%%', lower(y), '%%')",
+ checkparams={},
+ dialect=mysql.dialect(),
+ )
+
+ def test_icontains_text_concat(self):
+ self.assert_compile(
+ column("x").icontains(text("y")),
+ "lower(x) LIKE concat('%%', lower(y), '%%')",
+ checkparams={},
+ dialect=mysql.dialect(),
+ )
+
def test_like(self):
self.assert_compile(
column("x").like("y"), "x LIKE :x_1", checkparams={"x_1": "y"}
dialect=mysql.dialect(),
)
+ def test_istartswith(self):
+ """
+ case insensitive startswith method
+ """
+ self.assert_compile(
+ column("x").istartswith("y"),
+ "lower(x) LIKE lower(:x_1) || '%'",
+ checkparams={"x_1": "y"},
+ )
+
+ def test_istartswith_pg(self):
+ """
+ case insensitive startswith method
+ """
+ self.assert_compile(
+ column("x").istartswith("y"),
+ "x ILIKE %(x_1)s || '%%'",
+ checkparams={"x_1": "y"},
+ dialect="postgresql",
+ )
+
+ def test_not_istartswith(self):
+ """
+ same as test_istartswith but negate the statement
+ """
+ self.assert_compile(
+ ~column("x").istartswith("y"),
+ "lower(x) NOT LIKE lower(:x_1) || '%'",
+ checkparams={"x_1": "y"},
+ )
+
+ def test_not_istartswith_pg(self):
+ """
+ same as test_istartswith but negate the statement
+ """
+ self.assert_compile(
+ ~column("x").istartswith("y"),
+ "x NOT ILIKE %(x_1)s || '%%'",
+ checkparams={"x_1": "y"},
+ dialect="postgresql",
+ )
+
+ def test_istartswith_escape(self):
+ """
+ render same SQL statement as istartswith but include the ESCAPE
+ keyword to establish that character as the escape character
+ """
+ self.assert_compile(
+ column("x").istartswith("a%b_c", escape="\\"),
+ "lower(x) LIKE lower(:x_1) || '%' ESCAPE '\\'",
+ checkparams={"x_1": "a%b_c"},
+ )
+
+ def test_not_istartswith_escape(self):
+ self.assert_compile(
+ ~column("x").istartswith("a%b_c", escape="\\"),
+ "lower(x) NOT LIKE lower(:x_1) || '%' ESCAPE '\\'",
+ checkparams={"x_1": "a%b_c"},
+ )
+
+ def test_istartswith_autoescape(self):
+ """
+ apply the escape character to all occurrences of "%", "_" and
+ the escape character itself
+ """
+ self.assert_compile(
+ column("x").istartswith("a%b_c/d", autoescape=True),
+ "lower(x) LIKE lower(:x_1) || '%' ESCAPE '/'",
+ checkparams={"x_1": "a/%b/_c//d"},
+ )
+
+ def test_not_istartswith_autoescape(self):
+ self.assert_compile(
+ ~column("x").istartswith("a%b_c/d", autoescape=True),
+ "lower(x) NOT LIKE lower(:x_1) || '%' ESCAPE '/'",
+ checkparams={"x_1": "a/%b/_c//d"},
+ )
+
+ def test_istartswith_autoescape_custom_escape(self):
+ """
+ set an escape character to all occurrences of "%", "_" and
+ the escape character itself
+ """
+ self.assert_compile(
+ column("x").istartswith("a%b_c/d^e", autoescape=True, escape="^"),
+ "lower(x) LIKE lower(:x_1) || '%' ESCAPE '^'",
+ checkparams={"x_1": "a^%b^_c/d^^e"},
+ )
+
+ def test_istartswith_encoded(self):
+ self.assert_compile(
+ column("x").istartswith(b"y"),
+ "lower(x) LIKE lower(:x_1) || '%'",
+ checkparams={"x_1": b"y"},
+ )
+
+ def test_not_istartswith_encoded(self):
+ self.assert_compile(
+ ~column("x").istartswith(b"y"),
+ "lower(x) NOT LIKE lower(:x_1) || '%'",
+ checkparams={"x_1": b"y"},
+ )
+
+ def test_istartswith_encoded_mysql(self):
+ self.assert_compile(
+ column("x").istartswith(b"y"),
+ "lower(x) LIKE concat(lower(%s), '%%')",
+ checkparams={"x_1": b"y"},
+ dialect="mysql",
+ )
+
+ def test_not_istartswith_encoded_mysql(self):
+ self.assert_compile(
+ ~column("x").istartswith(b"y"),
+ "lower(x) NOT LIKE concat(lower(%s), '%%')",
+ checkparams={"x_1": b"y"},
+ dialect="mysql",
+ )
+
+ def test_istartswith_literal(self):
+ self.assert_compile(
+ column("x").istartswith(literal_column("y")),
+ "lower(x) LIKE lower(y) || '%'",
+ checkparams={},
+ )
+
+ def test_istartswith_text(self):
+ self.assert_compile(
+ column("x").istartswith(text("y")),
+ "lower(x) LIKE lower(y) || '%'",
+ checkparams={},
+ )
+
+ def test_istartswith_concat(self):
+ self.assert_compile(
+ column("x").istartswith("y"),
+ "lower(x) LIKE concat(lower(%s), '%%')",
+ checkparams={"x_1": "y"},
+ dialect=mysql.dialect(),
+ )
+
+ def test_not_istartswith_concat(self):
+ self.assert_compile(
+ ~column("x").istartswith("y"),
+ "lower(x) NOT LIKE concat(lower(%s), '%%')",
+ checkparams={"x_1": "y"},
+ dialect=mysql.dialect(),
+ )
+
+ def test_istartswith_literal_mysql(self):
+ self.assert_compile(
+ column("x").istartswith(literal_column("y")),
+ "lower(x) LIKE concat(lower(y), '%%')",
+ checkparams={},
+ dialect=mysql.dialect(),
+ )
+
+ def test_istartswith_text_mysql(self):
+ self.assert_compile(
+ column("x").istartswith(text("y")),
+ "lower(x) LIKE concat(lower(y), '%%')",
+ checkparams={},
+ dialect=mysql.dialect(),
+ )
+
def test_endswith(self):
self.assert_compile(
column("x").endswith("y"),
checkparams={"x_1": "y"},
)
+ def test_not_endswith_pg(self):
+ self.assert_compile(
+ ~column("x").endswith("y"),
+ "x NOT LIKE '%%' || %(x_1)s",
+ checkparams={"x_1": "y"},
+ dialect="postgresql",
+ )
+
def test_not_endswith_escape(self):
self.assert_compile(
~column("x").endswith("a%b_c", escape="\\"),
dialect=mysql.dialect(),
)
+ def test_iendswith(self):
+ """
+ case insensitive endswith method
+ """
+ self.assert_compile(
+ column("x").iendswith("y"),
+ "lower(x) LIKE '%' || lower(:x_1)",
+ checkparams={"x_1": "y"},
+ )
+
+ def test_iendswith_pg(self):
+ """
+ case insensitive endswith method
+ """
+ self.assert_compile(
+ column("x").iendswith("y"),
+ "x ILIKE '%%' || %(x_1)s",
+ checkparams={"x_1": "y"},
+ dialect="postgresql",
+ )
+
+ def test_not_iendswith(self):
+ """
+ same as test_iendswith but negate the statement
+ """
+ self.assert_compile(
+ ~column("x").iendswith("y"),
+ "lower(x) NOT LIKE '%' || lower(:x_1)",
+ checkparams={"x_1": "y"},
+ )
+
+ def test_not_iendswith_pg(self):
+ """
+ same as test_iendswith but negate the statement
+ """
+ self.assert_compile(
+ ~column("x").iendswith("y"),
+ "x NOT ILIKE '%%' || %(x_1)s",
+ checkparams={"x_1": "y"},
+ dialect="postgresql",
+ )
+
+ def test_iendswith_encoded(self):
+ self.assert_compile(
+ column("x").iendswith(b"y"),
+ "lower(x) LIKE '%' || lower(:x_1)",
+ checkparams={"x_1": b"y"},
+ )
+
+ def test_not_iendswith_encoded(self):
+ self.assert_compile(
+ ~column("x").iendswith(b"y"),
+ "lower(x) NOT LIKE '%' || lower(:x_1)",
+ checkparams={"x_1": b"y"},
+ )
+
+ def test_iendswith_encoded_mysql(self):
+ self.assert_compile(
+ column("x").iendswith(b"y"),
+ "lower(x) LIKE concat('%%', lower(%s))",
+ checkparams={"x_1": b"y"},
+ dialect="mysql",
+ )
+
+ def test_iendswith_escape(self):
+ """
+ render same SQL statement as iendswith but include the ESCAPE
+ keyword to establish that character as the escape character
+ """
+ self.assert_compile(
+ column("x").iendswith("a%b_c", escape="\\"),
+ "lower(x) LIKE '%' || lower(:x_1) ESCAPE '\\'",
+ checkparams={"x_1": "a%b_c"},
+ )
+
+ def test_not_iendswith_escape(self):
+ self.assert_compile(
+ ~column("x").iendswith("a%b_c", escape="\\"),
+ "lower(x) NOT LIKE '%' || lower(:x_1) ESCAPE '\\'",
+ checkparams={"x_1": "a%b_c"},
+ )
+
+ def test_iendswith_autoescape(self):
+ """
+ apply the escape character to all occurrences of "%", "_" and
+ the escape character itself
+ """
+ self.assert_compile(
+ column("x").iendswith("a%b_c/d", autoescape=True),
+ "lower(x) LIKE '%' || lower(:x_1) ESCAPE '/'",
+ checkparams={"x_1": "a/%b/_c//d"},
+ )
+
+ def test_not_iendswith_autoescape(self):
+ self.assert_compile(
+ ~column("x").iendswith("a%b_c/d", autoescape=True),
+ "lower(x) NOT LIKE '%' || lower(:x_1) ESCAPE '/'",
+ checkparams={"x_1": "a/%b/_c//d"},
+ )
+
+ def test_iendswith_autoescape_custom_escape(self):
+ """
+ set an escape character to all occurrences of "%", "_" and
+ the escape character itself
+ """
+ self.assert_compile(
+ column("x").iendswith("a%b_c/d^e", autoescape=True, escape="^"),
+ "lower(x) LIKE '%' || lower(:x_1) ESCAPE '^'",
+ checkparams={"x_1": "a^%b^_c/d^^e"},
+ )
+
+ def test_iendswith_autoescape_warning(self):
+ with expect_warnings("The autoescape parameter is now a simple"):
+ self.assert_compile(
+ column("x").iendswith("a%b_c/d", autoescape="P"),
+ "lower(x) LIKE '%' || lower(:x_1) ESCAPE '/'",
+ checkparams={"x_1": "a/%b/_c//d"},
+ )
+
+ def test_iendswith_autoescape_nosqlexpr(self):
+ assert_raises_message(
+ TypeError,
+ "String value expected when autoescape=True",
+ column("x").iendswith,
+ literal_column("'a%b_c/d'"),
+ autoescape=True,
+ )
+
+ def test_iendswith_literal(self):
+ self.assert_compile(
+ column("x").iendswith(literal_column("y")),
+ "lower(x) LIKE '%' || lower(y)",
+ checkparams={},
+ )
+
+ def test_iendswith_text(self):
+ self.assert_compile(
+ column("x").iendswith(text("y")),
+ "lower(x) LIKE '%' || lower(y)",
+ checkparams={},
+ )
+
+ def test_iendswith_mysql(self):
+ self.assert_compile(
+ column("x").iendswith("y"),
+ "lower(x) LIKE concat('%%', lower(%s))",
+ checkparams={"x_1": "y"},
+ dialect=mysql.dialect(),
+ )
+
+ def test_not_iendswith_mysql(self):
+ self.assert_compile(
+ ~column("x").iendswith("y"),
+ "lower(x) NOT LIKE concat('%%', lower(%s))",
+ checkparams={"x_1": "y"},
+ dialect=mysql.dialect(),
+ )
+
+ def test_iendswith_literal_mysql(self):
+ self.assert_compile(
+ column("x").iendswith(literal_column("y")),
+ "lower(x) LIKE concat('%%', lower(y))",
+ checkparams={},
+ dialect=mysql.dialect(),
+ )
+
+ def test_iendswith_text_mysql(self):
+ self.assert_compile(
+ column("x").iendswith(text("y")),
+ "lower(x) LIKE concat('%%', lower(y))",
+ checkparams={},
+ dialect=mysql.dialect(),
+ )
+
class CustomOpTest(fixtures.TestBase):
def test_is_comparison_legacy(self):