diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-10-23 17:41:55 -0400 | 
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-10-23 17:44:52 -0400 | 
| commit | f035b6e0a41238d092ea2ddd10fdd5de298ff789 (patch) | |
| tree | 76c2c9b9e4b63964847126aba054de19cfc485f7 /lib/sqlalchemy/sql/elements.py | |
| parent | 382cd56772efd92a9fe5ce46623029a04163c8cf (diff) | |
| download | sqlalchemy-f035b6e0a41238d092ea2ddd10fdd5de298ff789.tar.gz | |
An overhaul of expression handling for special symbols particularly
with conjunctions, e.g.
``None`` :func:`.expression.null` :func:`.expression.true`
:func:`.expression.false`, including consistency in rendering NULL
in conjunctions, "short-circuiting" of :func:`.and_` and :func:`.or_`
expressions which contain boolean constants, and rendering of
boolean constants and expressions as compared to "1" or "0" for backends
that don't feature ``true``/``false`` constants. [ticket:2804]
Diffstat (limited to 'lib/sqlalchemy/sql/elements.py')
| -rw-r--r-- | lib/sqlalchemy/sql/elements.py | 296 | 
1 files changed, 230 insertions, 66 deletions
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 251102d59..e9b995eaa 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -149,30 +149,6 @@ def outparam(key, type_=None):                  key, None, type_=type_, unique=False, isoutparam=True) -def and_(*clauses): -    """Join a list of clauses together using the ``AND`` operator. - -    The ``&`` operator is also overloaded on all :class:`.ColumnElement` -    subclasses to produce the -    same result. - -    """ -    if len(clauses) == 1: -        return clauses[0] -    return BooleanClauseList(operator=operators.and_, *clauses) - - -def or_(*clauses): -    """Join a list of clauses together using the ``OR`` operator. - -    The ``|`` operator is also overloaded on all -    :class:`.ColumnElement` subclasses to produce the -    same result. - -    """ -    if len(clauses) == 1: -        return clauses[0] -    return BooleanClauseList(operator=operators.or_, *clauses)  def not_(clause): @@ -465,7 +441,10 @@ class ClauseElement(Visitable):          return or_(self, other)      def __invert__(self): -        return self._negate() +        if hasattr(self, 'negation_clause'): +            return self.negation_clause +        else: +            return self._negate()      def __bool__(self):          raise TypeError("Boolean value of this clause is not defined") @@ -473,13 +452,10 @@ class ClauseElement(Visitable):      __nonzero__ = __bool__      def _negate(self): -        if hasattr(self, 'negation_clause'): -            return self.negation_clause -        else: -            return UnaryExpression( -                        self.self_group(against=operators.inv), -                        operator=operators.inv, -                        negate=None) +        return UnaryExpression( +                    self.self_group(against=operators.inv), +                    operator=operators.inv, +                    negate=None)      def __repr__(self):          friendly = getattr(self, 'description', None) @@ -537,6 +513,19 @@ class ColumnElement(ClauseElement, operators.ColumnOperators):      _key_label = None      _alt_names = () +    def self_group(self, against=None): +        if against in (operators.and_, operators.or_, operators._asbool) and \ +            self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity: +            return AsBoolean(self, operators.istrue, operators.isfalse) +        else: +            return self + +    def _negate(self): +        if self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity: +            return AsBoolean(self, operators.isfalse, operators.istrue) +        else: +            return super(ColumnElement, self)._negate() +      @util.memoized_property      def type(self):          return type_api.NULLTYPE @@ -1062,52 +1051,153 @@ class TextClause(Executable, ClauseElement):  class Null(ColumnElement):      """Represent the NULL keyword in a SQL statement. +    :class:`.Null` is accessed as a constant via the +    :func:`.null` function. +      """      __visit_name__ = 'null' -    def __init__(self): -        """Return a :class:`Null` object, which compiles to ``NULL``. +    @util.memoized_property +    def type(self): +        return type_api.NULLTYPE -        """ -        self.type = type_api.NULLTYPE +    @classmethod +    def _singleton(cls): +        """Return a constant :class:`.Null` construct.""" + +        return NULL      def compare(self, other):          return isinstance(other, Null)  class False_(ColumnElement): -    """Represent the ``false`` keyword in a SQL statement. +    """Represent the ``false`` keyword, or equivalent, in a SQL statement. + +    :class:`.False_` is accessed as a constant via the +    :func:`.false` function.      """      __visit_name__ = 'false' -    def __init__(self): -        """Return a :class:`False_` object. +    @util.memoized_property +    def type(self): +        return type_api.BOOLEANTYPE + +    def _negate(self): +        return TRUE + +    @classmethod +    def _singleton(cls): +        """Return a constant :class:`.False_` construct. + +        E.g.:: + +            >>> from sqlalchemy import false +            >>> print select([t.c.x]).where(false()) +            SELECT x FROM t WHERE false + +        A backend which does not support true/false constants will render as +        an expression against 1 or 0:: + +            >>> print select([t.c.x]).where(false()) +            SELECT x FROM t WHERE 0 = 1 + +        The :func:`.true` and :func:`.false` constants also feature +        "short circuit" operation within an :func:`.and_` or :func:`.or_` +        conjunction:: + +            >>> print select([t.c.x]).where(or_(t.c.x > 5, true())) +            SELECT x FROM t WHERE true + +            >>> print select([t.c.x]).where(and_(t.c.x > 5, false())) +            SELECT x FROM t WHERE false + +        .. versionchanged:: 0.9 :func:`.true` and :func:`.false` feature +           better integrated behavior within conjunctions and on dialects +           that don't support true/false constants. + +        .. seealso:: + +            :func:`.true`          """ -        self.type = type_api.BOOLEANTYPE + +        return FALSE      def compare(self, other):          return isinstance(other, False_)  class True_(ColumnElement): -    """Represent the ``true`` keyword in a SQL statement. +    """Represent the ``true`` keyword, or equivalent, in a SQL statement. + +    :class:`.True_` is accessed as a constant via the +    :func:`.true` function.      """      __visit_name__ = 'true' -    def __init__(self): -        """Return a :class:`True_` object. +    @util.memoized_property +    def type(self): +        return type_api.BOOLEANTYPE + +    def _negate(self): +        return FALSE + +    @classmethod +    def _ifnone(cls, other): +        if other is None: +            return cls._singleton() +        else: +            return other + +    @classmethod +    def _singleton(cls): +        """Return a constant :class:`.True_` construct. + +        E.g.:: + +            >>> from sqlalchemy import true +            >>> print select([t.c.x]).where(true()) +            SELECT x FROM t WHERE true + +        A backend which does not support true/false constants will render as +        an expression against 1 or 0:: + +            >>> print select([t.c.x]).where(true()) +            SELECT x FROM t WHERE 1 = 1 + +        The :func:`.true` and :func:`.false` constants also feature +        "short circuit" operation within an :func:`.and_` or :func:`.or_` +        conjunction:: + +            >>> print select([t.c.x]).where(or_(t.c.x > 5, true())) +            SELECT x FROM t WHERE true + +            >>> print select([t.c.x]).where(and_(t.c.x > 5, false())) +            SELECT x FROM t WHERE false + +        .. versionchanged:: 0.9 :func:`.true` and :func:`.false` feature +           better integrated behavior within conjunctions and on dialects +           that don't support true/false constants. + +        .. seealso:: + +            :func:`.false`          """ -        self.type = type_api.BOOLEANTYPE + +        return TRUE      def compare(self, other):          return isinstance(other, True_) +NULL = Null() +FALSE = False_() +TRUE = True_()  class ClauseList(ClauseElement):      """Describe a list of clauses, separated by an operator. @@ -1124,11 +1214,11 @@ class ClauseList(ClauseElement):          if self.group_contents:              self.clauses = [                  _literal_as_text(clause).self_group(against=self.operator) -                for clause in clauses if clause is not None] +                for clause in clauses]          else:              self.clauses = [                  _literal_as_text(clause) -                for clause in clauses if clause is not None] +                for clause in clauses]      def __iter__(self):          return iter(self.clauses) @@ -1141,10 +1231,6 @@ class ClauseList(ClauseElement):          return iter(self)      def append(self, clause): -        # TODO: not sure if i like the 'group_contents' flag.  need to -        # define the difference between a ClauseList of ClauseLists, -        # and a "flattened" ClauseList of ClauseLists.  flatten() -        # method ?          if self.group_contents:              self.clauses.append(_literal_as_text(clause).\                                  self_group(against=self.operator)) @@ -1185,13 +1271,65 @@ class ClauseList(ClauseElement):              return False +  class BooleanClauseList(ClauseList, ColumnElement):      __visit_name__ = 'clauselist' -    def __init__(self, *clauses, **kwargs): -        super(BooleanClauseList, self).__init__(*clauses, **kwargs) -        self.type = type_api.to_instance(kwargs.get('type_', -                type_api.BOOLEANTYPE)) +    def __init__(self, *arg, **kw): +        raise NotImplementedError( +                "BooleanClauseList has a private constructor") + +    @classmethod +    def _construct(cls, operator, continue_on, skip_on, *clauses, **kw): +        convert_clauses = [] + +        for clause in clauses: +            clause = _literal_as_text(clause) + +            if isinstance(clause, continue_on): +                continue +            elif isinstance(clause, skip_on): +                return clause.self_group(against=operators._asbool) + +            convert_clauses.append(clause) + +        if len(convert_clauses) == 1: +            return convert_clauses[0].self_group(against=operators._asbool) +        elif not convert_clauses and clauses: +            return clauses[0].self_group(against=operators._asbool) + +        convert_clauses = [c.self_group(against=operator) +                                for c in convert_clauses] + +        self = cls.__new__(cls) +        self.clauses = convert_clauses +        self.group = True +        self.operator = operator +        self.group_contents = True +        self.type = type_api.BOOLEANTYPE +        return self + +    @classmethod +    def and_(cls, *clauses): +        """Join a list of clauses together using the ``AND`` operator. + +        The ``&`` operator is also overloaded on all :class:`.ColumnElement` +        subclasses to produce the +        same result. + +        """ +        return cls._construct(operators.and_, True_, False_, *clauses) + +    @classmethod +    def or_(cls, *clauses): +        """Join a list of clauses together using the ``OR`` operator. + +        The ``|`` operator is also overloaded on all +        :class:`.ColumnElement` subclasses to produce the +        same result. + +        """ +        return cls._construct(operators.or_, False_, True_, *clauses)      @property      def _select_iterable(self): @@ -1203,6 +1341,12 @@ class BooleanClauseList(ClauseList, ColumnElement):          else:              return super(BooleanClauseList, self).self_group(against=against) +    def _negate(self): +        return ClauseList._negate(self) + + +and_ = BooleanClauseList.and_ +or_ = BooleanClauseList.or_  class Tuple(ClauseList, ColumnElement):      """Represent a SQL tuple.""" @@ -1465,9 +1609,7 @@ class UnaryExpression(ColumnElement):                              type_=None, negate=None):          self.operator = operator          self.modifier = modifier - -        self.element = _literal_as_text(element).\ -                    self_group(against=self.operator or self.modifier) +        self.element = element.self_group(against=self.operator or self.modifier)          self.type = type_api.to_instance(type_)          self.negate = negate @@ -1484,7 +1626,8 @@ class UnaryExpression(ColumnElement):            ORDER BY mycol DESC NULLS FIRST          """ -        return UnaryExpression(column, modifier=operators.nullsfirst_op) +        return UnaryExpression( +                _literal_as_text(column), modifier=operators.nullsfirst_op)      @classmethod @@ -1500,7 +1643,8 @@ class UnaryExpression(ColumnElement):              ORDER BY mycol DESC NULLS LAST          """ -        return UnaryExpression(column, modifier=operators.nullslast_op) +        return UnaryExpression( +            _literal_as_text(column), modifier=operators.nullslast_op)      @classmethod @@ -1516,7 +1660,8 @@ class UnaryExpression(ColumnElement):              ORDER BY mycol DESC          """ -        return UnaryExpression(column, modifier=operators.desc_op) +        return UnaryExpression( +            _literal_as_text(column), modifier=operators.desc_op)      @classmethod      def _create_asc(cls, column): @@ -1531,7 +1676,8 @@ class UnaryExpression(ColumnElement):            ORDER BY mycol ASC          """ -        return UnaryExpression(column, modifier=operators.asc_op) +        return UnaryExpression( +            _literal_as_text(column), modifier=operators.asc_op)      @classmethod      def _create_distinct(cls, expr): @@ -1587,16 +1733,31 @@ class UnaryExpression(ColumnElement):                  modifier=self.modifier,                  type_=self.type)          else: -            return super(UnaryExpression, self)._negate() +            return ClauseElement._negate(self)      def self_group(self, against=None): -        if self.operator and operators.is_precedent(self.operator, -                against): +        if self.operator and operators.is_precedent(self.operator, against):              return Grouping(self)          else:              return self +class AsBoolean(UnaryExpression): + +    def __init__(self, element, operator, negate): +        self.element = element +        self.type = type_api.BOOLEANTYPE +        self.operator = operator +        self.negate = negate +        self.modifier = None + +    def self_group(self, against=None): +        return self + +    def _negate(self): +        return self.element._negate() + +  class BinaryExpression(ColumnElement):      """Represent an expression that is ``LEFT <operator> RIGHT``. @@ -1620,8 +1781,8 @@ class BinaryExpression(ColumnElement):          if isinstance(operator, util.string_types):              operator = operators.custom_op(operator)          self._orig = (left, right) -        self.left = _literal_as_text(left).self_group(against=operator) -        self.right = _literal_as_text(right).self_group(against=operator) +        self.left = left.self_group(against=operator) +        self.right = right.self_group(against=operator)          self.operator = operator          self.type = type_api.to_instance(type_)          self.negate = negate @@ -1702,6 +1863,9 @@ class Grouping(ColumnElement):          self.element = element          self.type = getattr(element, 'type', type_api.NULLTYPE) +    def self_group(self, against=None): +        return self +      @property      def _label(self):          return getattr(self.element, '_label', None) or self.anon_label  | 
