summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/changelog_09.rst17
-rw-r--r--doc/build/changelog/migration_09.rst60
-rw-r--r--doc/build/core/sqlelement.rst12
-rw-r--r--lib/sqlalchemy/__init__.py2
-rw-r--r--lib/sqlalchemy/engine/default.py4
-rw-r--r--lib/sqlalchemy/orm/query.py5
-rw-r--r--lib/sqlalchemy/orm/relationships.py2
-rw-r--r--lib/sqlalchemy/sql/__init__.py2
-rw-r--r--lib/sqlalchemy/sql/compiler.py23
-rw-r--r--lib/sqlalchemy/sql/default_comparator.py6
-rw-r--r--lib/sqlalchemy/sql/elements.py296
-rw-r--r--lib/sqlalchemy/sql/expression.py11
-rw-r--r--lib/sqlalchemy/sql/operators.py10
-rw-r--r--lib/sqlalchemy/sql/selectable.py16
-rw-r--r--test/orm/inheritance/test_relationship.py1
-rw-r--r--test/sql/test_compiler.py68
-rw-r--r--test/sql/test_operators.py210
-rw-r--r--test/sql/test_types.py9
18 files changed, 636 insertions, 118 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst
index 6ac4d1726..24bf6eb09 100644
--- a/doc/build/changelog/changelog_09.rst
+++ b/doc/build/changelog/changelog_09.rst
@@ -34,6 +34,23 @@
.. change::
:tags: feature, sql
+ :tickets: 2804, 2823, 2734
+
+ 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.
+
+ .. seealso::
+
+ :ref:`migration_2804`
+
+ .. change::
+ :tags: feature, sql
:tickets: 2838
The typing system now handles the task of rendering "literal bind" values,
diff --git a/doc/build/changelog/migration_09.rst b/doc/build/changelog/migration_09.rst
index f0254c789..1f3f834c9 100644
--- a/doc/build/changelog/migration_09.rst
+++ b/doc/build/changelog/migration_09.rst
@@ -399,6 +399,66 @@ publically.
:ticket:`2812`
+.. _migration_2804:
+
+Improved rendering of Boolean constants, NULL constants, conjunctions
+----------------------------------------------------------------------
+
+New capabilities have been added to the :func:`.true` and :func:`.false`
+constants, in particular in conjunction with :func:`.and_` and :func:`.or_`
+functions as well as the behavior of the WHERE/HAVING clauses in conjunction
+with these types, boolean types overall, and the :func:`.null` constant.
+
+Starting with a table such as this::
+
+ from sqlalchemy import Table, Boolean, Integer, Column, MetaData
+
+ t1 = Table('t', MetaData(), Column('x', Boolean()), Column('y', Integer))
+
+A select construct will now render the boolean column as a binary expression
+on backends that don't feature ``true``/``false`` constant beahvior::
+
+ >>> from sqlalchemy import select, and_, false, true
+ >>> from sqlalchemy.dialects import mysql, postgresql
+
+ >>> print select([t1]).where(t1.c.x).compile(dialect=mysql.dialect())
+ SELECT t.x, t.y FROM t WHERE t.x = 1
+
+The :func:`.and_` and :func:`.or_` constructs will now exhibit quasi
+"short circuit" behavior, that is truncating a rendered expression, when a
+:func:`.true` or :func:`.false` constant is present::
+
+ >>> print select([t1]).where(and_(t1.c.y > 5, false())).compile(
+ ... dialect=postgresql.dialect())
+ SELECT t.x, t.y FROM t WHERE false
+
+:func:`.true` can be used as the base to build up an expression::
+
+ >>> expr = true()
+ >>> expr = expr & (t1.c.y > 5)
+ >>> print select([t1]).where(expr)
+ SELECT t.x, t.y FROM t WHERE t.y > :y_1
+
+The boolean constants :func:`.true` and :func:`.false` themselves render as
+``0 = 1`` and ``1 = 1`` for a backend with no boolean constants::
+
+ >>> print select([t1]).where(and_(t1.c.y > 5, false())).compile(
+ ... dialect=mysql.dialect())
+ SELECT t.x, t.y FROM t WHERE 0 = 1
+
+Interpretation of ``None``, while not particularly valid SQL, is at least
+now consistent::
+
+ >>> print select([t1.c.x]).where(None)
+ SELECT t.x FROM t WHERE NULL
+
+ >>> print select([t1.c.x]).where(None).where(None)
+ SELECT t.x FROM t WHERE NULL AND NULL
+
+ >>> print select([t1.c.x]).where(and_(None, None))
+ SELECT t.x FROM t WHERE NULL AND NULL
+
+:ticket:`2804`
New Features
============
diff --git a/doc/build/core/sqlelement.rst b/doc/build/core/sqlelement.rst
index 0676f18d0..e39c988d4 100644
--- a/doc/build/core/sqlelement.rst
+++ b/doc/build/core/sqlelement.rst
@@ -66,11 +66,8 @@ used to construct any kind of typed SQL expression.
.. autoclass:: BinaryExpression
:members:
- :inherited-members:
-
.. autoclass:: BindParameter
:members:
- :inherited-members:
.. autoclass:: ClauseElement
:members:
@@ -82,7 +79,6 @@ used to construct any kind of typed SQL expression.
.. autoclass:: ColumnClause
:members:
- :inherited-members:
.. autoclass:: ColumnCollection
:members:
@@ -97,6 +93,14 @@ used to construct any kind of typed SQL expression.
:special-members:
:inherited-members:
+.. autoclass:: sqlalchemy.sql.elements.False_
+ :members:
+
+.. autoclass:: sqlalchemy.sql.elements.Null
+ :members:
+
+.. autoclass:: sqlalchemy.sql.elements.True_
+ :members:
.. autoclass:: sqlalchemy.sql.operators.custom_op
:members:
diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py
index 98156cdc9..c794d949a 100644
--- a/lib/sqlalchemy/__init__.py
+++ b/lib/sqlalchemy/__init__.py
@@ -21,6 +21,7 @@ from .sql import (
except_all,
exists,
extract,
+ false,
func,
insert,
intersect,
@@ -38,6 +39,7 @@ from .sql import (
select,
subquery,
text,
+ true,
tuple_,
type_coerce,
union,
diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py
index b2dbccb17..8fb7c3bb8 100644
--- a/lib/sqlalchemy/engine/default.py
+++ b/lib/sqlalchemy/engine/default.py
@@ -113,6 +113,7 @@ class DefaultDialect(interfaces.Dialect):
implicit_returning=None,
supports_right_nested_joins=None,
case_sensitive=True,
+ supports_native_boolean=None,
label_length=None, **kwargs):
if not getattr(self, 'ported_sqla_06', True):
@@ -138,7 +139,8 @@ class DefaultDialect(interfaces.Dialect):
self.type_compiler = self.type_compiler(self)
if supports_right_nested_joins is not None:
self.supports_right_nested_joins = supports_right_nested_joins
-
+ if supports_native_boolean is not None:
+ self.supports_native_boolean = supports_native_boolean
self.case_sensitive = case_sensitive
if label_length and label_length > self.max_identifier_length:
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 17fb32663..3df12c0a7 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -2865,8 +2865,9 @@ class Query(object):
if adapter:
single_crit = adapter.traverse(single_crit)
single_crit = self._adapt_clause(single_crit, False, False)
- context.whereclause = sql.and_(context.whereclause,
- single_crit)
+ context.whereclause = sql.and_(
+ sql.True_._ifnone(context.whereclause),
+ single_crit)
def __str__(self):
return str(self._compile_context().statement)
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 9fc67cd38..f0b23111c 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -900,7 +900,7 @@ class RelationshipProperty(StrategizedProperty):
criterion = criterion._annotate(
{'no_replacement_traverse': True})
- crit = j & criterion
+ crit = j & sql.True_._ifnone(criterion)
ex = sql.exists([1], crit, from_obj=dest).correlate_except(dest)
if secondary is not None:
diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py
index e1497e9fa..c6ecb8afd 100644
--- a/lib/sqlalchemy/sql/__init__.py
+++ b/lib/sqlalchemy/sql/__init__.py
@@ -35,6 +35,7 @@ from .expression import (
exists,
extract,
false,
+ False_,
func,
insert,
intersect,
@@ -55,6 +56,7 @@ from .expression import (
table,
text,
true,
+ True_,
tuple_,
type_coerce,
union,
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py
index f526203ac..2bf7d3f4a 100644
--- a/lib/sqlalchemy/sql/compiler.py
+++ b/lib/sqlalchemy/sql/compiler.py
@@ -113,6 +113,7 @@ OPERATORS = {
operators.asc_op: ' ASC',
operators.nullsfirst_op: ' NULLS FIRST',
operators.nullslast_op: ' NULLS LAST',
+
}
FUNCTIONS = {
@@ -608,10 +609,16 @@ class SQLCompiler(Compiled):
return 'NULL'
def visit_true(self, expr, **kw):
- return 'true'
+ if self.dialect.supports_native_boolean:
+ return 'true'
+ else:
+ return "1"
def visit_false(self, expr, **kw):
- return 'false'
+ if self.dialect.supports_native_boolean:
+ return 'false'
+ else:
+ return "0"
def visit_clauselist(self, clauselist, order_by_select=None, **kw):
if order_by_select is not None:
@@ -783,6 +790,18 @@ class SQLCompiler(Compiled):
raise exc.CompileError(
"Unary expression has no operator or modifier")
+ def visit_istrue_unary_operator(self, element, operator, **kw):
+ if self.dialect.supports_native_boolean:
+ return self.process(element.element, **kw)
+ else:
+ return "%s = 1" % self.process(element.element, **kw)
+
+ def visit_isfalse_unary_operator(self, element, operator, **kw):
+ if self.dialect.supports_native_boolean:
+ return "NOT %s" % self.process(element.element, **kw)
+ else:
+ return "%s = 0" % self.process(element.element, **kw)
+
def visit_binary(self, binary, **kw):
# don't allow "? = ?" to render
if self.ansi_bind_rules and \
diff --git a/lib/sqlalchemy/sql/default_comparator.py b/lib/sqlalchemy/sql/default_comparator.py
index 7c803ac4c..423a5b56d 100644
--- a/lib/sqlalchemy/sql/default_comparator.py
+++ b/lib/sqlalchemy/sql/default_comparator.py
@@ -13,7 +13,7 @@ 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
+ collate, _is_literal, _literal_as_text
from .selectable import SelectBase, Alias, Selectable, ScalarSelect
class _DefaultColumnComparator(operators.ColumnOperators):
@@ -75,7 +75,7 @@ class _DefaultColumnComparator(operators.ColumnOperators):
if op in (operators.eq, operators.ne) and \
isinstance(obj, (bool, True_, False_)):
return BinaryExpression(expr,
- obj,
+ _literal_as_text(obj),
op,
type_=type_api.BOOLEANTYPE,
negate=negate, modifiers=kwargs)
@@ -209,7 +209,7 @@ class _DefaultColumnComparator(operators.ColumnOperators):
self._check_literal(expr, operators.and_, cleft),
self._check_literal(expr, operators.and_, cright),
operator=operators.and_,
- group=False),
+ group=False, group_contents=False),
operators.between_op)
def _collate_impl(self, expr, op, other, **kw):
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
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 01091bc0a..6be32f454 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -33,7 +33,7 @@ from .elements import ClauseElement, ColumnElement,\
BindParameter, UnaryExpression, BooleanClauseList, \
Label, Cast, Case, ColumnClause, TextClause, Over, Null, \
True_, False_, BinaryExpression, Tuple, TypeClause, Extract, \
- Grouping, and_, or_, not_, \
+ Grouping, not_, \
collate, literal_column, between,\
literal, outparam, type_coerce, ClauseList
@@ -56,6 +56,8 @@ from .dml import Insert, Update, Delete, UpdateBase, ValuesBase
# the functions to be available in the sqlalchemy.sql.* namespace and
# to be auto-cross-documenting from the function to the class itself.
+and_ = public_factory(BooleanClauseList.and_, ".expression.and_")
+or_ = public_factory(BooleanClauseList.or_, ".expression.or_")
bindparam = public_factory(BindParameter, ".expression.bindparam")
select = public_factory(Select, ".expression.select")
text = public_factory(TextClause, ".expression.tet")
@@ -79,9 +81,9 @@ nullslast = public_factory(UnaryExpression._create_nullslast, ".expression.nulls
asc = public_factory(UnaryExpression._create_asc, ".expression.asc")
desc = public_factory(UnaryExpression._create_desc, ".expression.desc")
distinct = public_factory(UnaryExpression._create_distinct, ".expression.distinct")
-true = public_factory(True_, ".expression.true")
-false = public_factory(False_, ".expression.false")
-null = public_factory(Null, ".expression.null")
+true = public_factory(True_._singleton, ".expression.true")
+false = public_factory(False_._singleton, ".expression.false")
+null = public_factory(Null._singleton, ".expression.null")
join = public_factory(Join._create_join, ".expression.join")
outerjoin = public_factory(Join._create_outerjoin, ".expression.outerjoin")
insert = public_factory(Insert, ".expression.insert")
@@ -89,7 +91,6 @@ update = public_factory(Update, ".expression.update")
delete = public_factory(Delete, ".expression.delete")
-
# internal functions still being called from tests and the ORM,
# these might be better off in some other namespace
from .base import _from_objects
diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py
index 128442158..e9b904d7c 100644
--- a/lib/sqlalchemy/sql/operators.py
+++ b/lib/sqlalchemy/sql/operators.py
@@ -654,6 +654,12 @@ def exists():
raise NotImplementedError()
+def istrue(a):
+ raise NotImplementedError()
+
+def isfalse(a):
+ raise NotImplementedError()
+
def is_(a, b):
return a.is_(b)
@@ -779,6 +785,7 @@ parenthesize (a op b).
"""
+_asbool = util.symbol('_asbool', canonical=-10)
_smallest = util.symbol('_smallest', canonical=-100)
_largest = util.symbol('_largest', canonical=100)
@@ -816,12 +823,15 @@ _PRECEDENCE = {
between_op: 5,
distinct_op: 5,
inv: 5,
+ istrue: 5,
+ isfalse: 5,
and_: 3,
or_: 2,
comma_op: -1,
collate: 7,
as_: -1,
exists: 0,
+ _asbool: -10,
_smallest: _smallest,
_largest: _largest
}
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index 43d5a084c..550e250f1 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -14,7 +14,7 @@ from .elements import ClauseElement, TextClause, ClauseList, \
from .elements import _clone, \
_literal_as_text, _interpret_as_column_or_from, _expand_cloned,\
_select_iterables, _anonymous_label, _clause_element_as_expr,\
- _cloned_intersection, _cloned_difference
+ _cloned_intersection, _cloned_difference, True_
from .base import Immutable, Executable, _generative, \
ColumnCollection, ColumnSet, _from_objects, Generative
from . import type_api
@@ -2519,13 +2519,9 @@ class Select(HasPrefixes, SelectBase):
:term:`method chaining`.
"""
- self._reset_exported()
- whereclause = _literal_as_text(whereclause)
- if self._whereclause is not None:
- self._whereclause = and_(self._whereclause, whereclause)
- else:
- self._whereclause = whereclause
+ self._reset_exported()
+ self._whereclause = and_(True_._ifnone(self._whereclause), whereclause)
def append_having(self, having):
"""append the given expression to this select() construct's HAVING
@@ -2538,10 +2534,8 @@ class Select(HasPrefixes, SelectBase):
:term:`method chaining`.
"""
- if self._having is not None:
- self._having = and_(self._having, _literal_as_text(having))
- else:
- self._having = _literal_as_text(having)
+ self._reset_exported()
+ self._having = and_(True_._ifnone(self._having), having)
def append_from(self, fromclause):
"""append the given FromClause expression to this select() construct's
diff --git a/test/orm/inheritance/test_relationship.py b/test/orm/inheritance/test_relationship.py
index ecb4bf407..a436ca5fc 100644
--- a/test/orm/inheritance/test_relationship.py
+++ b/test/orm/inheritance/test_relationship.py
@@ -415,7 +415,6 @@ class M2MFilterTest(fixtures.MappedTest):
sess = create_session()
e1 = sess.query(Person).filter(Engineer.name == 'e1').one()
- # this works
eq_(sess.query(Organization)
.filter(~Organization.engineers
.of_type(Engineer)
diff --git a/test/sql/test_compiler.py b/test/sql/test_compiler.py
index cd8ac2aef..fbb88924d 100644
--- a/test/sql/test_compiler.py
+++ b/test/sql/test_compiler.py
@@ -18,7 +18,7 @@ from sqlalchemy import Integer, String, MetaData, Table, Column, select, \
literal, and_, null, type_coerce, alias, or_, literal_column,\
Float, TIMESTAMP, Numeric, Date, Text, collate, union, except_,\
intersect, union_all, Boolean, distinct, join, outerjoin, asc, desc,\
- over, subquery, case
+ over, subquery, case, true
import decimal
from sqlalchemy.util import u
from sqlalchemy import exc, sql, util, types, schema
@@ -272,9 +272,10 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL):
"SELECT foo() AS foo_1"
)
+ # this is native_boolean=False for default dialect
self.assert_compile(
select([not_(True)], use_labels=True),
- "SELECT NOT :param_1"
+ "SELECT :param_1 = 0"
)
self.assert_compile(
@@ -874,6 +875,26 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL):
"SELECT t.x FROM t WHERE t.x = :x_1 OR t.x = :x_2"
)
+ def test_true_short_circuit(self):
+ t = table('t', column('x'))
+
+ self.assert_compile(
+ select([t]).where(true()),
+ "SELECT t.x FROM t WHERE 1 = 1",
+ dialect=default.DefaultDialect(supports_native_boolean=False)
+ )
+ self.assert_compile(
+ select([t]).where(true()),
+ "SELECT t.x FROM t WHERE true",
+ dialect=default.DefaultDialect(supports_native_boolean=True)
+ )
+
+ self.assert_compile(
+ select([t]),
+ "SELECT t.x FROM t",
+ dialect=default.DefaultDialect(supports_native_boolean=True)
+ )
+
def test_distinct(self):
self.assert_compile(
select([table1.c.myid.distinct()]),
@@ -2921,6 +2942,7 @@ class SchemaTest(fixtures.TestBase, AssertsCompiledSQL):
"(:rem_id, :datatype_id, :value)")
+
class CorrelateTest(fixtures.TestBase, AssertsCompiledSQL):
__dialect__ = 'default'
@@ -3250,13 +3272,34 @@ class CorrelateTest(fixtures.TestBase, AssertsCompiledSQL):
)
class CoercionTest(fixtures.TestBase, AssertsCompiledSQL):
- __dialect__ = 'default'
+ __dialect__ = default.DefaultDialect(supports_native_boolean=True)
def _fixture(self):
m = MetaData()
return Table('foo', m,
Column('id', Integer))
+ bool_table = table('t', column('x', Boolean))
+
+ def test_coerce_bool_where(self):
+ self.assert_compile(
+ select([self.bool_table]).where(self.bool_table.c.x),
+ "SELECT t.x FROM t WHERE t.x"
+ )
+
+ def test_coerce_bool_where_non_native(self):
+ self.assert_compile(
+ select([self.bool_table]).where(self.bool_table.c.x),
+ "SELECT t.x FROM t WHERE t.x = 1",
+ dialect=default.DefaultDialect(supports_native_boolean=False)
+ )
+
+ self.assert_compile(
+ select([self.bool_table]).where(~self.bool_table.c.x),
+ "SELECT t.x FROM t WHERE t.x = 0",
+ dialect=default.DefaultDialect(supports_native_boolean=False)
+ )
+
def test_null_constant(self):
self.assert_compile(_literal_as_text(None), "NULL")
@@ -3269,12 +3312,12 @@ class CoercionTest(fixtures.TestBase, AssertsCompiledSQL):
def test_val_and_false(self):
t = self._fixture()
self.assert_compile(and_(t.c.id == 1, False),
- "foo.id = :id_1 AND false")
+ "false")
def test_val_and_true_coerced(self):
t = self._fixture()
self.assert_compile(and_(t.c.id == 1, True),
- "foo.id = :id_1 AND true")
+ "foo.id = :id_1")
def test_val_is_null_coerced(self):
t = self._fixture()
@@ -3282,26 +3325,21 @@ class CoercionTest(fixtures.TestBase, AssertsCompiledSQL):
"foo.id IS NULL")
def test_val_and_None(self):
- # current convention is None in and_() or
- # other clauselist is ignored. May want
- # to revise this at some point.
t = self._fixture()
self.assert_compile(and_(t.c.id == 1, None),
- "foo.id = :id_1")
+ "foo.id = :id_1 AND NULL")
def test_None_and_val(self):
- # current convention is None in and_() or
- # other clauselist is ignored. May want
- # to revise this at some point.
t = self._fixture()
- self.assert_compile(and_(t.c.id == 1, None),
- "foo.id = :id_1")
+ self.assert_compile(and_(None, t.c.id == 1),
+ "NULL AND foo.id = :id_1")
def test_None_and_nothing(self):
# current convention is None in and_()
# returns None May want
# to revise this at some point.
- assert and_(None) is None
+ self.assert_compile(
+ and_(None), "NULL")
def test_val_and_null(self):
t = self._fixture()
diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py
index 97ce3d3dd..0124d85fa 100644
--- a/test/sql/test_operators.py
+++ b/test/sql/test_operators.py
@@ -9,14 +9,18 @@ from sqlalchemy.sql import operators, table
import operator
from sqlalchemy import String, Integer
from sqlalchemy import exc
+from sqlalchemy.engine import default
+from sqlalchemy.sql.elements import _literal_as_text
from sqlalchemy.schema import Column, Table, MetaData
-from sqlalchemy.types import TypeEngine, TypeDecorator, UserDefinedType
+from sqlalchemy.types import TypeEngine, TypeDecorator, UserDefinedType, Boolean
from sqlalchemy.dialects import mysql, firebird, postgresql, oracle, \
sqlite, mssql
from sqlalchemy import util
import datetime
import collections
from sqlalchemy import text, literal_column
+from sqlalchemy import and_, not_, between, or_
+from sqlalchemy.sql import true, false, null
class LoopOperate(operators.ColumnOperators):
def operate(self, op, *other, **kwargs):
@@ -35,11 +39,11 @@ class DefaultColumnComparatorTest(fixtures.TestBase):
left = column('left')
assert left.comparator.operate(operator, right).compare(
- BinaryExpression(left, right, operator)
+ BinaryExpression(_literal_as_text(left), _literal_as_text(right), operator)
)
assert operator(left, right).compare(
- BinaryExpression(left, right, operator)
+ BinaryExpression(_literal_as_text(left), _literal_as_text(right), operator)
)
self._loop_test(operator, right)
@@ -384,7 +388,205 @@ class ExtensionOperatorTest(fixtures.TestBase, testing.AssertsCompiledSQL):
"x -> :x_1"
)
-from sqlalchemy import and_, not_, between
+
+class BooleanEvalTest(fixtures.TestBase, testing.AssertsCompiledSQL):
+ """test standalone booleans being wrapped in an AsBoolean, as well
+ as true/false compilation."""
+
+ def _dialect(self, native_boolean):
+ d = default.DefaultDialect()
+ d.supports_native_boolean = native_boolean
+ return d
+
+ def test_one(self):
+ c = column('x', Boolean)
+ self.assert_compile(
+ select([c]).where(c),
+ "SELECT x WHERE x",
+ dialect=self._dialect(True)
+ )
+
+ def test_two(self):
+ c = column('x', Boolean)
+ self.assert_compile(
+ select([c]).where(c),
+ "SELECT x WHERE x = 1",
+ dialect=self._dialect(False)
+ )
+
+ def test_three(self):
+ c = column('x', Boolean)
+ self.assert_compile(
+ select([c]).where(~c),
+ "SELECT x WHERE x = 0",
+ dialect=self._dialect(False)
+ )
+
+ def test_four(self):
+ c = column('x', Boolean)
+ self.assert_compile(
+ select([c]).where(~c),
+ "SELECT x WHERE NOT x",
+ dialect=self._dialect(True)
+ )
+
+ def test_five(self):
+ c = column('x', Boolean)
+ self.assert_compile(
+ select([c]).having(c),
+ "SELECT x HAVING x = 1",
+ dialect=self._dialect(False)
+ )
+
+ def test_six(self):
+ self.assert_compile(
+ or_(false(), true()),
+ "1 = 1",
+ dialect=self._dialect(False)
+ )
+
+ def test_eight(self):
+ self.assert_compile(
+ and_(false(), true()),
+ "false",
+ dialect=self._dialect(True)
+ )
+
+ def test_nine(self):
+ self.assert_compile(
+ and_(false(), true()),
+ "0 = 1",
+ dialect=self._dialect(False)
+ )
+
+ def test_ten(self):
+ c = column('x', Boolean)
+ self.assert_compile(
+ c == 1,
+ "x = :x_1",
+ dialect=self._dialect(False)
+ )
+
+ def test_eleven(self):
+ c = column('x', Boolean)
+ self.assert_compile(
+ c.is_(true()),
+ "x IS true",
+ dialect=self._dialect(True)
+ )
+
+ def test_twelve(self):
+ c = column('x', Boolean)
+ # I don't have a solution for this one yet,
+ # other than adding some heavy-handed conditionals
+ # into compiler
+ self.assert_compile(
+ c.is_(true()),
+ "x IS 1",
+ dialect=self._dialect(False)
+ )
+
+
+class ConjunctionTest(fixtures.TestBase, testing.AssertsCompiledSQL):
+ """test interaction of and_()/or_() with boolean , null constants
+ """
+ __dialect__ = default.DefaultDialect(supports_native_boolean=True)
+
+ def test_one(self):
+ self.assert_compile(~and_(true()), "false")
+
+ def test_two(self):
+ self.assert_compile(or_(~and_(true())), "false")
+
+ def test_three(self):
+ self.assert_compile(or_(and_()), "")
+
+ def test_four(self):
+ x = column('x')
+ self.assert_compile(
+ and_(or_(x == 5), or_(x == 7)),
+ "x = :x_1 AND x = :x_2")
+
+
+ def test_five(self):
+ x = column("x")
+ self.assert_compile(
+ and_(true()._ifnone(None), x == 7),
+ "x = :x_1"
+ )
+
+ def test_six(self):
+ x = column("x")
+ self.assert_compile(or_(true(), x == 7), "true")
+ self.assert_compile(or_(x == 7, true()), "true")
+ self.assert_compile(~or_(x == 7, true()), "false")
+
+ def test_six_pt_five(self):
+ x = column("x")
+ self.assert_compile(select([x]).where(or_(x == 7, true())),
+ "SELECT x WHERE true")
+
+ self.assert_compile(select([x]).where(or_(x == 7, true())),
+ "SELECT x WHERE 1 = 1",
+ dialect=default.DefaultDialect(supports_native_boolean=False))
+
+ def test_seven(self):
+ x = column("x")
+ self.assert_compile(
+ and_(true(), x == 7, true(), x == 9),
+ "x = :x_1 AND x = :x_2")
+
+ def test_eight(self):
+ x = column("x")
+ self.assert_compile(
+ or_(false(), x == 7, false(), x == 9),
+ "x = :x_1 OR x = :x_2")
+
+ def test_nine(self):
+ x = column("x")
+ self.assert_compile(
+ and_(x == 7, x == 9, false(), x == 5),
+ "false"
+ )
+ self.assert_compile(
+ ~and_(x == 7, x == 9, false(), x == 5),
+ "true"
+ )
+
+ def test_ten(self):
+ self.assert_compile(
+ and_(None, None),
+ "NULL AND NULL"
+ )
+
+ def test_eleven(self):
+ x = column("x")
+ self.assert_compile(
+ select([x]).where(None).where(None),
+ "SELECT x WHERE NULL AND NULL"
+ )
+
+ def test_twelve(self):
+ x = column("x")
+ self.assert_compile(
+ select([x]).where(and_(None, None)),
+ "SELECT x WHERE NULL AND NULL"
+ )
+
+ def test_thirteen(self):
+ x = column("x")
+ self.assert_compile(
+ select([x]).where(~and_(None, None)),
+ "SELECT x WHERE NOT (NULL AND NULL)"
+ )
+
+ def test_fourteen(self):
+ x = column("x")
+ self.assert_compile(
+ select([x]).where(~null()),
+ "SELECT x WHERE NOT NULL"
+ )
+
class OperatorPrecedenceTest(fixtures.TestBase, testing.AssertsCompiledSQL):
__dialect__ = 'default'
diff --git a/test/sql/test_types.py b/test/sql/test_types.py
index 13d4e378e..30a00ca56 100644
--- a/test/sql/test_types.py
+++ b/test/sql/test_types.py
@@ -1234,15 +1234,18 @@ class ExpressionTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled
)
self.assert_compile(
and_(c1 == True, c2 == True, c3 == True),
- "x = :x_1 AND x = true AND x = :x_2"
+ "x = :x_1 AND x = true AND x = :x_2",
+ dialect=default.DefaultDialect(supports_native_boolean=True)
)
self.assert_compile(
and_(c1 == 3, c2 == 3, c3 == 3),
- "x = :x_1 AND x = :x_2 AND x = :x_3"
+ "x = :x_1 AND x = :x_2 AND x = :x_3",
+ dialect=default.DefaultDialect(supports_native_boolean=True)
)
self.assert_compile(
and_(c1.is_(True), c2.is_(True), c3.is_(True)),
- "x IS :x_1 AND x IS true AND x IS :x_2"
+ "x IS :x_1 AND x IS true AND x IS :x_2",
+ dialect=default.DefaultDialect(supports_native_boolean=True)
)