summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql/default_comparator.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2013-08-12 17:50:37 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2013-08-12 17:50:37 -0400
commitf6198d9abf453182f4b111e0579a7a4ef1614e79 (patch)
treee258eafc9db70c4745d98a56b55b439732aebf91 /lib/sqlalchemy/sql/default_comparator.py
parente8c2a2738b6c15cb12e7571b9e12c15cc2f200c9 (diff)
downloadsqlalchemy-f6198d9abf453182f4b111e0579a7a4ef1614e79.tar.gz
- A large refactoring of the ``sqlalchemy.sql`` package has reorganized
the import structure of many core modules. ``sqlalchemy.schema`` and ``sqlalchemy.types`` remain in the top-level package, but are now just lists of names that pull from within ``sqlalchemy.sql``. Their implementations are now broken out among ``sqlalchemy.sql.type_api``, ``sqlalchemy.sql.sqltypes``, ``sqlalchemy.sql.schema`` and ``sqlalchemy.sql.ddl``, the last of which was moved from ``sqlalchemy.engine``. ``sqlalchemy.sql.expression`` is also a namespace now which pulls implementations mostly from ``sqlalchemy.sql.elements``, ``sqlalchemy.sql.selectable``, and ``sqlalchemy.sql.dml``. Most of the "factory" functions used to create SQL expression objects have been moved to classmethods or constructors, which are exposed in ``sqlalchemy.sql.expression`` using a programmatic system. Care has been taken such that all the original import namespaces remain intact and there should be no impact on any existing applications. The rationale here was to break out these very large modules into smaller ones, provide more manageable lists of function names, to greatly reduce "import cycles" and clarify the up-front importing of names, and to remove the need for redundant functions and documentation throughout the expression package.
Diffstat (limited to 'lib/sqlalchemy/sql/default_comparator.py')
-rw-r--r--lib/sqlalchemy/sql/default_comparator.py281
1 files changed, 281 insertions, 0 deletions
diff --git a/lib/sqlalchemy/sql/default_comparator.py b/lib/sqlalchemy/sql/default_comparator.py
new file mode 100644
index 000000000..c9125108a
--- /dev/null
+++ b/lib/sqlalchemy/sql/default_comparator.py
@@ -0,0 +1,281 @@
+# sql/default_comparator.py
+# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"""Default implementation of SQL comparison operations.
+"""
+
+from .. import exc, util
+from . import operators
+from . import type_api
+from .elements import BindParameter, True_, False_, BinaryExpression, \
+ Null, _const_expr, _clause_element_as_expr, \
+ ClauseList, ColumnElement, TextClause, UnaryExpression, \
+ collate, _is_literal
+from .selectable import SelectBase, Alias, Selectable, ScalarSelect
+
+class _DefaultColumnComparator(operators.ColumnOperators):
+ """Defines comparison and math operations.
+
+ See :class:`.ColumnOperators` and :class:`.Operators` for descriptions
+ of all operations.
+
+ """
+
+ @util.memoized_property
+ def type(self):
+ return self.expr.type
+
+ def operate(self, op, *other, **kwargs):
+ o = self.operators[op.__name__]
+ return o[0](self, self.expr, op, *(other + o[1:]), **kwargs)
+
+ def reverse_operate(self, op, other, **kwargs):
+ o = self.operators[op.__name__]
+ return o[0](self, self.expr, op, other, reverse=True, *o[1:], **kwargs)
+
+ def _adapt_expression(self, op, other_comparator):
+ """evaluate the return type of <self> <op> <othertype>,
+ and apply any adaptations to the given operator.
+
+ This method determines the type of a resulting binary expression
+ given two source types and an operator. For example, two
+ :class:`.Column` objects, both of the type :class:`.Integer`, will
+ produce a :class:`.BinaryExpression` that also has the type
+ :class:`.Integer` when compared via the addition (``+``) operator.
+ However, using the addition operator with an :class:`.Integer`
+ and a :class:`.Date` object will produce a :class:`.Date`, assuming
+ "days delta" behavior by the database (in reality, most databases
+ other than Postgresql don't accept this particular operation).
+
+ The method returns a tuple of the form <operator>, <type>.
+ The resulting operator and type will be those applied to the
+ resulting :class:`.BinaryExpression` as the final operator and the
+ right-hand side of the expression.
+
+ Note that only a subset of operators make usage of
+ :meth:`._adapt_expression`,
+ including math operators and user-defined operators, but not
+ boolean comparison or special SQL keywords like MATCH or BETWEEN.
+
+ """
+ return op, other_comparator.type
+
+ def _boolean_compare(self, expr, op, obj, negate=None, reverse=False,
+ _python_is_types=(util.NoneType, bool),
+ **kwargs):
+
+ if isinstance(obj, _python_is_types + (Null, True_, False_)):
+
+ # allow x ==/!= True/False to be treated as a literal.
+ # this comes out to "== / != true/false" or "1/0" if those
+ # constants aren't supported and works on all platforms
+ if op in (operators.eq, operators.ne) and \
+ isinstance(obj, (bool, True_, False_)):
+ return BinaryExpression(expr,
+ obj,
+ op,
+ type_=type_api.BOOLEANTYPE,
+ negate=negate, modifiers=kwargs)
+ else:
+ # all other None/True/False uses IS, IS NOT
+ if op in (operators.eq, operators.is_):
+ return BinaryExpression(expr, _const_expr(obj),
+ operators.is_,
+ negate=operators.isnot)
+ elif op in (operators.ne, operators.isnot):
+ return BinaryExpression(expr, _const_expr(obj),
+ operators.isnot,
+ negate=operators.is_)
+ else:
+ raise exc.ArgumentError(
+ "Only '=', '!=', 'is_()', 'isnot()' operators can "
+ "be used with None/True/False")
+ else:
+ obj = self._check_literal(expr, op, obj)
+
+ if reverse:
+ return BinaryExpression(obj,
+ expr,
+ op,
+ type_=type_api.BOOLEANTYPE,
+ negate=negate, modifiers=kwargs)
+ else:
+ return BinaryExpression(expr,
+ obj,
+ op,
+ type_=type_api.BOOLEANTYPE,
+ negate=negate, modifiers=kwargs)
+
+ def _binary_operate(self, expr, op, obj, reverse=False, result_type=None,
+ **kw):
+ obj = self._check_literal(expr, op, obj)
+
+ if reverse:
+ left, right = obj, expr
+ else:
+ left, right = expr, obj
+
+ if result_type is None:
+ op, result_type = left.comparator._adapt_expression(
+ op, right.comparator)
+
+ return BinaryExpression(left, right, op, type_=result_type)
+
+ def _scalar(self, expr, op, fn, **kw):
+ return fn(expr)
+
+ def _in_impl(self, expr, op, seq_or_selectable, negate_op, **kw):
+ seq_or_selectable = _clause_element_as_expr(seq_or_selectable)
+
+ if isinstance(seq_or_selectable, ScalarSelect):
+ return self._boolean_compare(expr, op, seq_or_selectable,
+ negate=negate_op)
+ elif isinstance(seq_or_selectable, SelectBase):
+
+ # TODO: if we ever want to support (x, y, z) IN (select x,
+ # y, z from table), we would need a multi-column version of
+ # as_scalar() to produce a multi- column selectable that
+ # does not export itself as a FROM clause
+
+ return self._boolean_compare(
+ expr, op, seq_or_selectable.as_scalar(),
+ negate=negate_op, **kw)
+ elif isinstance(seq_or_selectable, (Selectable, TextClause)):
+ return self._boolean_compare(expr, op, seq_or_selectable,
+ negate=negate_op, **kw)
+
+ # Handle non selectable arguments as sequences
+ args = []
+ for o in seq_or_selectable:
+ if not _is_literal(o):
+ if not isinstance(o, operators.ColumnOperators):
+ raise exc.InvalidRequestError('in() function accept'
+ 's either a list of non-selectable values, '
+ 'or a selectable: %r' % o)
+ elif o is None:
+ o = Null()
+ else:
+ o = expr._bind_param(op, o)
+ args.append(o)
+ if len(args) == 0:
+
+ # Special case handling for empty IN's, behave like
+ # comparison against zero row selectable. We use != to
+ # build the contradiction as it handles NULL values
+ # appropriately, i.e. "not (x IN ())" should not return NULL
+ # values for x.
+
+ util.warn('The IN-predicate on "%s" was invoked with an '
+ 'empty sequence. This results in a '
+ 'contradiction, which nonetheless can be '
+ 'expensive to evaluate. Consider alternative '
+ 'strategies for improved performance.' % expr)
+ if op is operators.in_op:
+ return expr != expr
+ else:
+ return expr == expr
+
+ return self._boolean_compare(expr, op,
+ ClauseList(*args).self_group(against=op),
+ negate=negate_op)
+
+ def _unsupported_impl(self, expr, op, *arg, **kw):
+ raise NotImplementedError("Operator '%s' is not supported on "
+ "this expression" % op.__name__)
+
+ def _neg_impl(self, expr, op, **kw):
+ """See :meth:`.ColumnOperators.__neg__`."""
+ return UnaryExpression(expr, operator=operators.neg)
+
+ def _match_impl(self, expr, op, other, **kw):
+ """See :meth:`.ColumnOperators.match`."""
+ return self._boolean_compare(expr, operators.match_op,
+ self._check_literal(expr, operators.match_op,
+ other))
+
+ def _distinct_impl(self, expr, op, **kw):
+ """See :meth:`.ColumnOperators.distinct`."""
+ return UnaryExpression(expr, operator=operators.distinct_op,
+ type_=expr.type)
+
+ def _between_impl(self, expr, op, cleft, cright, **kw):
+ """See :meth:`.ColumnOperators.between`."""
+ return BinaryExpression(
+ expr,
+ ClauseList(
+ self._check_literal(expr, operators.and_, cleft),
+ self._check_literal(expr, operators.and_, cright),
+ operator=operators.and_,
+ group=False),
+ operators.between_op)
+
+ def _collate_impl(self, expr, op, other, **kw):
+ return collate(expr, other)
+
+ # a mapping of operators with the method they use, along with
+ # their negated operator for comparison operators
+ operators = {
+ "add": (_binary_operate,),
+ "mul": (_binary_operate,),
+ "sub": (_binary_operate,),
+ "div": (_binary_operate,),
+ "mod": (_binary_operate,),
+ "truediv": (_binary_operate,),
+ "custom_op": (_binary_operate,),
+ "concat_op": (_binary_operate,),
+ "lt": (_boolean_compare, operators.ge),
+ "le": (_boolean_compare, operators.gt),
+ "ne": (_boolean_compare, operators.eq),
+ "gt": (_boolean_compare, operators.le),
+ "ge": (_boolean_compare, operators.lt),
+ "eq": (_boolean_compare, operators.ne),
+ "like_op": (_boolean_compare, operators.notlike_op),
+ "ilike_op": (_boolean_compare, operators.notilike_op),
+ "notlike_op": (_boolean_compare, operators.like_op),
+ "notilike_op": (_boolean_compare, operators.ilike_op),
+ "contains_op": (_boolean_compare, operators.notcontains_op),
+ "startswith_op": (_boolean_compare, operators.notstartswith_op),
+ "endswith_op": (_boolean_compare, operators.notendswith_op),
+ "desc_op": (_scalar, UnaryExpression._create_desc),
+ "asc_op": (_scalar, UnaryExpression._create_asc),
+ "nullsfirst_op": (_scalar, UnaryExpression._create_nullsfirst),
+ "nullslast_op": (_scalar, UnaryExpression._create_nullslast),
+ "in_op": (_in_impl, operators.notin_op),
+ "notin_op": (_in_impl, operators.in_op),
+ "is_": (_boolean_compare, operators.is_),
+ "isnot": (_boolean_compare, operators.isnot),
+ "collate": (_collate_impl,),
+ "match_op": (_match_impl,),
+ "distinct_op": (_distinct_impl,),
+ "between_op": (_between_impl, ),
+ "neg": (_neg_impl,),
+ "getitem": (_unsupported_impl,),
+ "lshift": (_unsupported_impl,),
+ "rshift": (_unsupported_impl,),
+ }
+
+ def _check_literal(self, expr, operator, other):
+ if isinstance(other, (ColumnElement, TextClause)):
+ if isinstance(other, BindParameter) and \
+ other.type._isnull:
+ # TODO: perhaps we should not mutate the incoming
+ # bindparam() here and instead make a copy of it.
+ # this might be the only place that we're mutating
+ # an incoming construct.
+ other.type = expr.type
+ return other
+ elif hasattr(other, '__clause_element__'):
+ other = other.__clause_element__()
+ elif isinstance(other, type_api.TypeEngine.Comparator):
+ other = other.expr
+
+ if isinstance(other, (SelectBase, Alias)):
+ return other.as_scalar()
+ elif not isinstance(other, (ColumnElement, TextClause)):
+ return expr._bind_param(operator, other)
+ else:
+ return other
+