summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorFederico Caselli <cfederico87@gmail.com>2020-01-23 17:51:38 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2020-01-25 18:03:48 -0500
commit1de64504d8e68e2c0d14669c7638cf6f6d74973f (patch)
treebcb2f19fe89efc629408c4576c355f3fe998578b /lib/sqlalchemy
parent411637fbcf679f36448f1b094afef375158df15e (diff)
downloadsqlalchemy-1de64504d8e68e2c0d14669c7638cf6f6d74973f.tar.gz
Deprecate empty or_() and and_()
Creating an :func:`.and_` or :func:`.or_` construct with no arguments or empty ``*args`` will now emit a deprecation warning, as the SQL produced is a no-op (i.e. it renders as a blank string). This behavior is considered to be non-intuitive, so for empty or possibly empty :func:`.and_` or :func:`.or_` constructs, an appropriate default boolean should be included, such as ``and_(True, *args)`` or ``or_(False, *args)``. As has been the case for many major versions of SQLAlchemy, these particular boolean values will not render if the ``*args`` portion is non-empty. As there are some internal cases where an empty and_() construct is used in order to build an optional WHERE expression, a private utility function is added to suit this use case. Co-authored-by: Mike Bayer <mike_mp@zzzcomputing.com> Fixes: #5054 Closes: #5062 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/5062 Pull-request-sha: 5ca2f27281977d74e390148c0fb8deaa0e0e4ad9 Change-Id: I599b9c8befa64d9a59a35ad7dd84ff400e3aa647
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/orm/persistence.py27
-rw-r--r--lib/sqlalchemy/sql/elements.py106
2 files changed, 103 insertions, 30 deletions
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index 06aab1101..31b8b0a20 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -30,8 +30,10 @@ from .. import sql
from .. import util
from ..sql import coercions
from ..sql import expression
+from ..sql import operators
from ..sql import roles
from ..sql.base import _from_objects
+from ..sql.elements import BooleanClauseList
def _bulk_insert(
@@ -864,15 +866,15 @@ def _emit_update_statements(
)
def update_stmt():
- clause = sql.and_()
+ clauses = BooleanClauseList._construct_raw(operators.and_)
for col in mapper._pks_by_table[table]:
- clause.clauses.append(
+ clauses.clauses.append(
col == sql.bindparam(col._label, type_=col.type)
)
if needs_version_id:
- clause.clauses.append(
+ clauses.clauses.append(
mapper.version_id_col
== sql.bindparam(
mapper.version_id_col._label,
@@ -880,7 +882,7 @@ def _emit_update_statements(
)
)
- stmt = table.update(clause)
+ stmt = table.update(clauses)
return stmt
cached_stmt = base_mapper._memo(("update", table), update_stmt)
@@ -1180,15 +1182,15 @@ def _emit_post_update_statements(
)
def update_stmt():
- clause = sql.and_()
+ clauses = BooleanClauseList._construct_raw(operators.and_)
for col in mapper._pks_by_table[table]:
- clause.clauses.append(
+ clauses.clauses.append(
col == sql.bindparam(col._label, type_=col.type)
)
if needs_version_id:
- clause.clauses.append(
+ clauses.clauses.append(
mapper.version_id_col
== sql.bindparam(
mapper.version_id_col._label,
@@ -1196,7 +1198,7 @@ def _emit_post_update_statements(
)
)
- stmt = table.update(clause)
+ stmt = table.update(clauses)
if mapper.version_id_col is not None:
stmt = stmt.return_defaults(mapper.version_id_col)
@@ -1295,21 +1297,22 @@ def _emit_delete_statements(
)
def delete_stmt():
- clause = sql.and_()
+ clauses = BooleanClauseList._construct_raw(operators.and_)
+
for col in mapper._pks_by_table[table]:
- clause.clauses.append(
+ clauses.clauses.append(
col == sql.bindparam(col.key, type_=col.type)
)
if need_version_id:
- clause.clauses.append(
+ clauses.clauses.append(
mapper.version_id_col
== sql.bindparam(
mapper.version_id_col.key, type_=mapper.version_id_col.type
)
)
- return table.delete(clause)
+ return table.delete(clauses)
statement = base_mapper._memo(("delete", table), delete_stmt)
for connection, recs in groupby(delete, lambda rec: rec[1]): # connection
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 648394ed2..a99c6ca35 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -2131,32 +2131,64 @@ class BooleanClauseList(ClauseList, ColumnElement):
@classmethod
def _construct(cls, operator, continue_on, skip_on, *clauses, **kw):
+
+ has_continue_on = None
+ special_elements = (continue_on, skip_on)
convert_clauses = []
- clauses = [
- coercions.expect(roles.WhereHavingRole, clause)
- for clause in util.coerce_generator_arg(clauses)
- ]
- for clause in clauses:
+ for clause in util.coerce_generator_arg(clauses):
+ clause = coercions.expect(roles.WhereHavingRole, clause)
- if isinstance(clause, continue_on):
- continue
+ # elements that are not the continue/skip are the most
+ # common, try to have only one isinstance() call for that case.
+ if not isinstance(clause, special_elements):
+ convert_clauses.append(clause)
elif isinstance(clause, skip_on):
+ # instance of skip_on, e.g. and_(x, y, False, z), cancels
+ # the rest out
return clause.self_group(against=operators._asbool)
-
- convert_clauses.append(clause)
-
- if len(convert_clauses) == 1:
+ elif has_continue_on is None:
+ # instance of continue_on, like and_(x, y, True, z), store it
+ # if we didn't find one already, we will use it if there
+ # are no other expressions here.
+ has_continue_on = clause
+
+ lcc = len(convert_clauses)
+
+ if lcc > 1:
+ # multiple elements. Return regular BooleanClauseList
+ # which will link elements against the operator.
+ return cls._construct_raw(
+ operator,
+ [c.self_group(against=operator) for c in convert_clauses],
+ )
+ elif lcc == 1:
+ # just one element. return it as a single boolean element,
+ # not a list and discard the operator.
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
- ]
+ elif not lcc and has_continue_on is not None:
+ # no elements but we had a "continue", just return the continue
+ # as a boolean element, discard the operator.
+ return has_continue_on.self_group(against=operators._asbool)
+ else:
+ # no elements period. deprecated use case. return an empty
+ # ClauseList construct that generates nothing unless it has
+ # elements added to it.
+ util.warn_deprecated(
+ "Invoking %(name)s() without arguments is deprecated, and "
+ "will be disallowed in a future release. For an empty "
+ "%(name)s() construct, use %(name)s(%(continue_on)s, *args)."
+ % {
+ "name": operator.__name__,
+ "continue_on": "True" if continue_on is True_ else "False",
+ }
+ )
+ return cls._construct_raw(operator)
+ @classmethod
+ def _construct_raw(cls, operator, clauses=None):
self = cls.__new__(cls)
- self.clauses = convert_clauses
+ self.clauses = clauses if clauses else []
self.group = True
self.operator = operator
self.group_contents = True
@@ -2198,6 +2230,25 @@ class BooleanClauseList(ClauseList, ColumnElement):
where(users_table.c.name == 'wendy').\
where(users_table.c.enrolled == True)
+ The :func:`.and_` construct must be given at least one positional
+ argument in order to be valid; a :func:`.and_` construct with no
+ arguments is ambiguous. To produce an "empty" or dynamically
+ generated :func:`.and_` expression, from a given list of expressions,
+ a "default" element of ``True`` should be specified::
+
+ criteria = and_(True, *expressions)
+
+ The above expression will compile to SQL as the expression ``true``
+ or ``1 = 1``, depending on backend, if no other expressions are
+ present. If expressions are present, then the ``True`` value is
+ ignored as it does not affect the outcome of an AND expression that
+ has other elements.
+
+ .. deprecated:: 1.4 The :func:`.and_` element now requires that at
+ least one argument is passed; creating the :func:`.and_` construct
+ with no arguments is deprecated, and will emit a deprecation warning
+ while continuing to produce a blank SQL string.
+
.. seealso::
:func:`.or_`
@@ -2230,6 +2281,25 @@ class BooleanClauseList(ClauseList, ColumnElement):
(users_table.c.name == 'jack')
)
+ The :func:`.or_` construct must be given at least one positional
+ argument in order to be valid; a :func:`.or_` construct with no
+ arguments is ambiguous. To produce an "empty" or dynamically
+ generated :func:`.or_` expression, from a given list of expressions,
+ a "default" element of ``False`` should be specified::
+
+ or_criteria = or_(False, *expressions)
+
+ The above expression will compile to SQL as the expression ``false``
+ or ``0 = 1``, depending on backend, if no other expressions are
+ present. If expressions are present, then the ``False`` value is
+ ignored as it does not affect the outcome of an OR expression which
+ has other elements.
+
+ .. deprecated:: 1.4 The :func:`.or_` element now requires that at
+ least one argument is passed; creating the :func:`.or_` construct
+ with no arguments is deprecated, and will emit a deprecation warning
+ while continuing to produce a blank SQL string.
+
.. seealso::
:func:`.and_`