From 75ac0abc7d5653d10006769a881374a46b706db5 Mon Sep 17 00:00:00 2001 From: Gord Thompson Date: Sun, 13 Sep 2020 12:37:40 -0600 Subject: Add deprecation warning for .join().alias() The :meth:`_sql.Join.alias` method is deprecated and will be removed in SQLAlchemy 2.0. An explicit select + subquery, or aliasing of the inner tables, should be used instead. Fixes: #5010 Change-Id: Ic913afc31f0d70b0605f9a7af2742a0de1f9ad19 --- lib/sqlalchemy/orm/relationships.py | 5 +- lib/sqlalchemy/orm/strategies.py | 4 +- lib/sqlalchemy/orm/util.py | 47 +++++++------------ lib/sqlalchemy/sql/coercions.py | 6 ++- lib/sqlalchemy/sql/roles.py | 14 +----- lib/sqlalchemy/sql/selectable.py | 88 ++++++++++++++++++++---------------- lib/sqlalchemy/testing/assertions.py | 6 +-- 7 files changed, 77 insertions(+), 93 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 1c95b6e06..cd1502073 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -3636,11 +3636,12 @@ class JoinCondition(object): class _ColInAnnotations(object): - """Seralizable equivalent to: + """Seralizable object that tests for a name in c._annotations. - lambda c: "name" in c._annotations """ + __slots__ = ("name",) + def __init__(self, name): self.name = name diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index fbf153dc5..325bd4dc1 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -1929,7 +1929,7 @@ class JoinedLoader(AbstractRelationshipLoader): if idx >= len(self._aliased_class_pool): to_adapt = orm_util.AliasedClass( self.mapper, - alias=alt_selectable.alias(flat=True) + alias=alt_selectable._anonymous_fromclause(flat=True) if alt_selectable is not None else None, flat=True, @@ -2690,7 +2690,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): ).apply_labels(), lambda_cache=self._query_cache, global_track_bound_values=False, - track_on=(self, effective_entity,) + tuple(pk_cols), + track_on=(self, effective_entity) + tuple(pk_cols), ) if not self.parent_property.bake_queries: diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index c8d639e8c..170e4487e 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -487,7 +487,7 @@ class AliasedClass(object): if alias is None: alias = mapper._with_polymorphic_selectable._anonymous_fromclause( - name=name, flat=flat + name=name, flat=flat, ) self._aliased_insp = AliasedInsp( @@ -1089,17 +1089,15 @@ def aliased(element, alias=None, name=None, flat=False, adapt_on_names=False): :param name: optional string name to use for the alias, if not specified by the ``alias`` parameter. The name, among other things, forms the attribute name that will be accessible via tuples returned by a - :class:`_query.Query` object. + :class:`_query.Query` object. Not supported when creating aliases + of :class:`_sql.Join` objects. :param flat: Boolean, will be passed through to the :meth:`_expression.FromClause.alias` call so that aliases of - :class:`_expression.Join` objects - don't include an enclosing SELECT. This can lead to more efficient - queries in many circumstances. A JOIN against a nested JOIN will be - rewritten as a JOIN against an aliased SELECT subquery on backends that - don't support this syntax. - - .. seealso:: :meth:`_expression.Join.alias` + :class:`_expression.Join` objects will alias the individual tables + inside the join, rather than creating a subquery. This is generally + supported by all modern databases with regards to right-nested joins + and generally produces more efficient queries. :param adapt_on_names: if True, more liberal "matching" will be used when mapping the mapped columns of the ORM entity to those of the @@ -1180,31 +1178,18 @@ def with_polymorphic( Alternatively, it may also be the string ``'*'``, in which case all descending mapped classes will be added to the FROM clause. - :param aliased: when True, the selectable will be wrapped in an - alias, that is ``(SELECT * FROM ) AS anon_1``. - This can be important when using the with_polymorphic() - to create the target of a JOIN on a backend that does not - support parenthesized joins, such as SQLite and older - versions of MySQL. However if the - :paramref:`.with_polymorphic.selectable` parameter is in use - with an existing :class:`_expression.Alias` construct, - then you should not - set this flag. + :param aliased: when True, the selectable will be aliased. For a + JOIN, this means the JOIN will be SELECTed from inside of a subquery + unless the :paramref:`_orm.with_polymorphic.flat` flag is set to + True, which is recommended for simpler use cases. :param flat: Boolean, will be passed through to the :meth:`_expression.FromClause.alias` call so that aliases of - :class:`_expression.Join` - objects don't include an enclosing SELECT. This can lead to more - efficient queries in many circumstances. A JOIN against a nested JOIN - will be rewritten as a JOIN against an aliased SELECT subquery on - backends that don't support this syntax. - - Setting ``flat`` to ``True`` implies the ``aliased`` flag is - also ``True``. - - .. versionadded:: 0.9.0 - - .. seealso:: :meth:`_expression.Join.alias` + :class:`_expression.Join` objects will alias the individual tables + inside the join, rather than creating a subquery. This is generally + supported by all modern databases with regards to right-nested joins + and generally produces more efficient queries. Setting this flag is + recommended as long as the resulting SQL is functional. :param selectable: a table or subquery that will be used in place of the generated FROM clause. This argument is diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index b8525925b..154564a08 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -55,7 +55,7 @@ def _deep_is_literal(element): return ( not isinstance( element, - (Visitable, schema.SchemaEventTarget, HasCacheKey, Options,), + (Visitable, schema.SchemaEventTarget, HasCacheKey, Options), ) and not hasattr(element, "__clause_element__") and ( @@ -881,7 +881,9 @@ class AnonymizedFromClauseImpl(StrictFromClauseImpl): __slots__ = () def _post_coercion(self, element, flat=False, name=None, **kw): - return element.alias(name=name, flat=flat) + assert name is None + + return element._anonymous_fromclause(flat=flat) class DMLTableImpl(_SelectIsNotFrom, _NoTextCoercion, RoleImpl): diff --git a/lib/sqlalchemy/sql/roles.py b/lib/sqlalchemy/sql/roles.py index 4205d9f0d..b88625b88 100644 --- a/lib/sqlalchemy/sql/roles.py +++ b/lib/sqlalchemy/sql/roles.py @@ -144,19 +144,7 @@ class AnonymizedFromClauseRole(StrictFromClauseRole): # calls .alias() as a post processor def _anonymous_fromclause(self, name=None, flat=False): - """A synonym for ``.alias()`` that is only present on objects of this - role. - - This is an implicit assurance of the target object being part of the - role where anonymous aliasing without any warnings is allowed, - as opposed to other kinds of SELECT objects that may or may not have - an ``.alias()`` method. - - The method is used by the ORM but is currently semi-private to - preserve forwards-compatibility. - - """ - return self.alias(name=name, flat=flat) + raise NotImplementedError() class CoerceTextStatementRole(SQLRole): diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 9440fc48b..2e8f41cc8 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -155,9 +155,7 @@ class ReturnsRows(roles.ReturnsRowsRole, ClauseElement): class Selectable(ReturnsRows): - """Mark a class as being selectable. - - """ + """Mark a class as being selectable.""" __visit_name__ = "selectable" @@ -825,6 +823,9 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): """ self._reset_column_collection() + def _anonymous_fromclause(self, name=None, flat=False): + return self.alias(name=name) + class Join(roles.DMLTableRole, FromClause): """Represent a ``JOIN`` construct between two @@ -1224,6 +1225,33 @@ class Join(roles.DMLTableRole, FromClause): return self.left.bind or self.right.bind @util.preload_module("sqlalchemy.sql.util") + def _anonymous_fromclause(self, name=None, flat=False): + sqlutil = util.preloaded.sql_util + if flat: + if name is not None: + raise exc.ArgumentError("Can't send name argument with flat") + left_a, right_a = ( + self.left._anonymous_fromclause(flat=True), + self.right._anonymous_fromclause(flat=True), + ) + adapter = sqlutil.ClauseAdapter(left_a).chain( + sqlutil.ClauseAdapter(right_a) + ) + + return left_a.join( + right_a, + adapter.traverse(self.onclause), + isouter=self.isouter, + full=self.full, + ) + else: + return self.select().apply_labels().correlate(None).alias(name) + + @util.deprecated_20( + ":meth:`_sql.Join.alias`", + alternative="Create a select + subquery, or alias the " + "individual tables inside the join, instead.", + ) def alias(self, name=None, flat=False): r"""Return an alias of this :class:`_expression.Join`. @@ -1246,8 +1274,7 @@ class Join(roles.DMLTableRole, FromClause): JOIN table_b ON table_a.id = table_b.a_id) AS anon_1 The equivalent long-hand form, given a :class:`_expression.Join` - object - ``j``, is:: + object ``j``, is:: from sqlalchemy import select, alias j = alias( @@ -1322,25 +1349,7 @@ class Join(roles.DMLTableRole, FromClause): :func:`_expression.alias` """ - sqlutil = util.preloaded.sql_util - if flat: - assert name is None, "Can't send name argument with flat" - left_a, right_a = ( - self.left.alias(flat=True), - self.right.alias(flat=True), - ) - adapter = sqlutil.ClauseAdapter(left_a).chain( - sqlutil.ClauseAdapter(right_a) - ) - - return left_a.join( - right_a, - adapter.traverse(self.onclause), - isouter=self.isouter, - full=self.full, - ) - else: - return self.select().apply_labels().correlate(None).alias(name) + return self._anonymous_fromclause(flat=flat, name=name) @property def _hide_froms(self): @@ -1983,9 +1992,7 @@ class Subquery(AliasedReturnsRows): @classmethod def _factory(cls, selectable, name=None): - """Return a :class:`.Subquery` object. - - """ + """Return a :class:`.Subquery` object.""" return coercions.expect( roles.SelectStatementRole, selectable ).subquery(name=name) @@ -2035,6 +2042,9 @@ class FromGrouping(GroupedElement, FromClause): def alias(self, **kw): return FromGrouping(self.element.alias(**kw)) + def _anonymous_fromclause(self, **kw): + return FromGrouping(self.element._anonymous_fromclause(**kw)) + @property def _hide_froms(self): return self.element._hide_froms @@ -2294,7 +2304,7 @@ class Values(Generative, FromClause): _data = () _traverse_internals = [ - ("_column_args", InternalTraversal.dp_clauseelement_list,), + ("_column_args", InternalTraversal.dp_clauseelement_list), ("_data", InternalTraversal.dp_dml_multi_values), ("name", InternalTraversal.dp_string), ("literal_binds", InternalTraversal.dp_boolean), @@ -3741,7 +3751,7 @@ class SelectState(util.MemoizedSlots, CompileState): else: self.from_clauses = self.from_clauses + ( - Join(left, right, onclause, isouter=isouter, full=full,), + Join(left, right, onclause, isouter=isouter, full=full), ) @util.preload_module("sqlalchemy.sql.util") @@ -3908,12 +3918,12 @@ class Select( ("_from_obj", InternalTraversal.dp_clauseelement_list), ("_where_criteria", InternalTraversal.dp_clauseelement_tuple), ("_having_criteria", InternalTraversal.dp_clauseelement_tuple), - ("_order_by_clauses", InternalTraversal.dp_clauseelement_tuple,), - ("_group_by_clauses", InternalTraversal.dp_clauseelement_tuple,), - ("_setup_joins", InternalTraversal.dp_setup_join_tuple,), - ("_legacy_setup_joins", InternalTraversal.dp_setup_join_tuple,), + ("_order_by_clauses", InternalTraversal.dp_clauseelement_tuple), + ("_group_by_clauses", InternalTraversal.dp_clauseelement_tuple), + ("_setup_joins", InternalTraversal.dp_setup_join_tuple), + ("_legacy_setup_joins", InternalTraversal.dp_setup_join_tuple), ("_correlate", InternalTraversal.dp_clauseelement_tuple), - ("_correlate_except", InternalTraversal.dp_clauseelement_tuple,), + ("_correlate_except", InternalTraversal.dp_clauseelement_tuple), ("_limit_clause", InternalTraversal.dp_clauseelement), ("_offset_clause", InternalTraversal.dp_clauseelement), ("_for_update_arg", InternalTraversal.dp_clauseelement), @@ -4312,7 +4322,7 @@ class Select( else: return cls._create_future_select(*args) - def __init__(self,): + def __init__(self): raise NotImplementedError() def _scalar_type(self): @@ -4537,7 +4547,7 @@ class Select( :meth:`_expression.Select.join` """ - return self.join(target, onclause=onclause, isouter=True, full=full,) + return self.join(target, onclause=onclause, isouter=True, full=full) @property def froms(self): @@ -4762,7 +4772,7 @@ class Select( for c in coercions._expression_collection_was_a_list( "columns", "Select.with_only_columns", columns ): - c = coercions.expect(roles.ColumnsClauseRole, c,) + c = coercions.expect(roles.ColumnsClauseRole, c) # TODO: why are we doing this here? if isinstance(c, ScalarSelect): c = c.self_group(against=operators.comma_op) @@ -5312,9 +5322,7 @@ class ScalarSelect(roles.InElementRole, Generative, Grouping): class Exists(UnaryExpression): - """Represent an ``EXISTS`` clause. - - """ + """Represent an ``EXISTS`` clause.""" _from_objects = [] inherit_cache = True diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 52a04f9b0..c32b2749b 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -43,7 +43,7 @@ def expect_warnings(*messages, **kw): """ # noqa return _expect_warnings( - (sa_exc.SAWarning, sa_exc.RemovedIn20Warning), messages, **kw + (sa_exc.RemovedIn20Warning, sa_exc.SAWarning), messages, **kw ) @@ -305,7 +305,7 @@ def assert_raises(except_cls, callable_, *args, **kw): def assert_raises_context_ok(except_cls, callable_, *args, **kw): - return _assert_raises(except_cls, callable_, args, kw,) + return _assert_raises(except_cls, callable_, args, kw) def assert_raises_message(except_cls, msg, callable_, *args, **kwargs): @@ -347,7 +347,7 @@ def _expect_raises(except_cls, msg=None, check_context=False): if msg is not None: assert re.search( msg, util.text_type(err), re.UNICODE - ), "%r !~ %s" % (msg, err,) + ), "%r !~ %s" % (msg, err) if check_context and not are_we_already_in_a_traceback: _assert_proper_exception_context(err) print(util.text_type(err).encode("utf-8")) -- cgit v1.2.1