]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Added a new method :meth:`.Query.select_entity_from` which
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 31 May 2013 00:24:48 +0000 (20:24 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 31 May 2013 00:24:48 +0000 (20:24 -0400)
will in 0.9 replace part of the functionality of
:meth:`.Query.select_from`.  In 0.8, the two methods perform
the same function, so that code can be migrated to use the
:meth:`.Query.select_entity_from` method as appropriate.
See the 0.9 migration guide for details.
[ticket:2736]

doc/build/changelog/changelog_08.rst
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/strategies.py
test/orm/test_froms.py

index acf4ea670cd952f3a056e86014870bd4cdc913d4..d10a3da28510c2f523c41b142cb54926dee2badd 100644 (file)
@@ -6,6 +6,17 @@
 .. changelog::
     :version: 0.8.2
 
+    .. change::
+      :tags: feature, orm
+      :tickets: 2736
+
+      Added a new method :meth:`.Query.select_entity_from` which
+      will in 0.9 replace part of the functionality of
+      :meth:`.Query.select_from`.  In 0.8, the two methods perform
+      the same function, so that code can be migrated to use the
+      :meth:`.Query.select_entity_from` method as appropriate.
+      See the 0.9 migration guide for details.
+
     .. change::
       :tags: bug, postgresql
       :tickets: 2735
index c9f3a2699ca8c69df7eac9b2ad72f4bec06e8f7f..cb6348f6f1d37411cc54a2131a39019c5c42f028 100644 (file)
@@ -162,15 +162,19 @@ class Query(object):
             for m in m2.iterate_to_root():
                 self._polymorphic_adapters[m.local_table] = adapter
 
-    def _set_select_from(self, *obj):
+    def _set_select_from(self, obj, set_base_alias):
         fa = []
         select_from_alias = None
+
         for from_obj in obj:
             info = inspect(from_obj)
 
             if hasattr(info, 'mapper') and \
                 (info.is_mapper or info.is_aliased_class):
-                self._select_from_entity = from_obj
+                if set_base_alias:
+                    raise sa_exc.ArgumentError(
+                            "A selectable (FromClause) instance is "
+                            "expected when the base alias is being set.")
                 fa.append(info.selectable)
             elif not info.is_selectable:
                 raise sa_exc.ArgumentError(
@@ -190,6 +194,7 @@ class Query(object):
             self._from_obj_alias = sql_util.ColumnAdapter(
                                                 self._from_obj[0], equivs)
 
+
     def _reset_polymorphic_adapter(self, mapper):
         for m2 in mapper._with_polymorphic_mappers:
             self._polymorphic_adapters.pop(m2, None)
@@ -953,7 +958,7 @@ class Query(object):
                 '_prefixes',
         ):
             self.__dict__.pop(attr, None)
-        self._set_select_from(fromclause)
+        self._set_select_from([fromclause], True)
 
         # this enables clause adaptation for non-ORM
         # expressions.
@@ -1970,25 +1975,147 @@ class Query(object):
         """
         self._reset_joinpoint()
 
+
     @_generative(_no_clauseelement_condition)
     def select_from(self, *from_obj):
         """Set the FROM clause of this :class:`.Query` explicitly.
 
-        Sending a mapped class or entity here effectively replaces the
+        :meth:`.Query.select_from` is often used in conjunction with
+        :meth:`.Query.join` in order to control which entity is selected
+        from on the "left" side of the join.
+
+        The entity or selectable object here effectively replaces the
         "left edge" of any calls to :meth:`~.Query.join`, when no
         joinpoint is otherwise established - usually, the default "join
         point" is the leftmost entity in the :class:`~.Query` object's
         list of entities to be selected.
 
-        Mapped entities or plain :class:`~.Table` or other selectables
-        can be sent here which will form the default FROM clause.
+        A typical example::
+
+            q = session.query(Address).select_from(User).\\
+                join(User.addresses).\\
+                filter(User.name == 'ed')
+
+        Which produces SQL equivalent to::
+
+            SELECT address.* FROM user
+            JOIN address ON user.id=address.user_id
+            WHERE user.name = :name_1
+
+        :param \*from_obj: collection of one or more entities to apply
+         to the FROM clause.  Entities can be mapped classes,
+         :class:`.AliasedClass` objects, :class:`.Mapper` objects
+         as well as core :class:`.FromClause` elements like subqueries.
+
+        .. note::
+
+            :meth:`.Query.select_from` features a deprecated behavior
+            whereby when passed a :class:`.FromClause` element,
+            such as a select construct, it will apply that select
+            construct to *replace* the FROM clause that an existing
+            entity is joined from.  This behavior is being removed
+            in SQLAlchemy 0.9, to be replaced with the
+            :meth:`.Query.select_entity_from` method.  Applications
+            which rely on this behavior to re-base query entities to
+            an arbitrary selectable should transition to this
+            method before upgrading to 0.9.
+
+        .. seealso::
+
+            :meth:`~.Query.join`
+
+            :meth:`.Query.select_entity_from`
+
+        """
+
+        self._set_select_from(from_obj, False)
+
+    @_generative(_no_clauseelement_condition)
+    def select_entity_from(self, from_obj):
+        """Set the FROM clause of this :class:`.Query` to a
+        core selectable, applying it as a replacement FROM clause
+        for corresponding mapped entities.
+
+        This method is currently equivalent to the
+        :meth:`.Query.select_from` method, but in 0.9 these two
+        methods will diverge in functionality.
+
+        In addition to changing the FROM list, the method will
+        also apply the given selectable
+        to replace the FROM which the selected entities would normally
+        select from.
+
+        The given ``from_obj`` must be an instance of a :class:`.FromClause`,
+        e.g. a :func:`.select` or :class:`.Alias` construct.
+
+        An example would be a :class:`.Query` that selects ``User`` entities,
+        but uses :meth:`.Query.select_entity_from` to have the entities
+        selected from a :func:`.select` construct instead of the
+        base ``user`` table::
+
+            select_stmt = select([User]).where(User.id == 7)
+
+            q = session.query(User).\\
+                    select_entity_from(select_stmt).\\
+                    filter(User.name == 'ed')
+
+        The query generated will select ``User`` entities directly
+        from the given :func:`.select` construct, and will be::
+
+            SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name
+            FROM (SELECT "user".id AS id, "user".name AS name
+            FROM "user"
+            WHERE "user".id = :id_1) AS anon_1
+            WHERE anon_1.name = :name_1
+
+        Notice above that even the WHERE criterion was "adapted" such that
+        the ``anon_1`` subquery effectively replaces all references to the
+        ``user`` table, except for the one that it refers to internally.
+
+        Compare this to :meth:`.Query.select_from`, which as of
+        version 0.9, does not affect existing entities.  The
+        statement below::
+
+            q = session.query(User).\\
+                    select_from(select_stmt).\\
+                    filter(User.name == 'ed')
+
+        Produces SQL where both the ``user`` table as well as the
+        ``select_stmt`` construct are present as separate elements
+        in the FROM clause.  No "adaptation" of the ``user`` table
+        is applied::
+
+            SELECT "user".id AS user_id, "user".name AS user_name
+            FROM "user", (SELECT "user".id AS id, "user".name AS name
+            FROM "user"
+            WHERE "user".id = :id_1) AS anon_1
+            WHERE "user".name = :name_1
+
+        :meth:`.Query.select_entity_from` maintains an older
+        behavior of :meth:`.Query.select_from`.  In modern usage,
+        similar results can also be achieved using :func:`.aliased`::
+
+            select_stmt = select([User]).where(User.id == 7)
+            user_from_select = aliased(User, select_stmt.alias())
+
+            q = session.query(user_from_select)
+
+        :param from_obj: a :class:`.FromClause` object that will replace
+         the FROM clause of this :class:`.Query`.
+
+        .. seealso::
+
+            :meth:`.Query.select_from`
 
-        See the example in :meth:`~.Query.join` for a typical
-        usage of :meth:`~.Query.select_from`.
+        .. versionadded:: 0.8.2
+            :meth:`.Query.select_entity_from` was added to specify
+            the specific behavior of entity replacement, however
+            the :meth:`.Query.select_from` maintains this behavior
+            as well until 0.9.
 
         """
 
-        self._set_select_from(*from_obj)
+        self._set_select_from([from_obj], True)
 
     def __getitem__(self, item):
         if isinstance(item, slice):
index 6660a39ef0881a9bf6a0fb42e6186cba81e1f6d7..6984f4127abca5ab97058b5525ab139a5dc46661 100644 (file)
@@ -781,7 +781,7 @@ class SubqueryLoader(AbstractRelationshipLoader):
         # set a real "from" if not present, as this is more
         # accurate than just going off of the column expression
         if not q._from_obj and entity_mapper.isa(leftmost_mapper):
-            q._set_select_from(entity_mapper)
+            q._set_select_from([entity_mapper], False)
 
         # select from the identity columns of the outer
         q._set_entities(q._adapt_col_list(leftmost_attr))
index c701a70765820d258f776b1f8a0b7f46724f3e76..8c332fb30d7fb2496359b5005044eebcacf8706b 100644 (file)
@@ -425,6 +425,32 @@ class ColumnAccessTest(QueryTest, AssertsCompiledSQL):
             "WHERE anon_1.anon_2_users_name = :name_1"
         )
 
+    def test_select_entity_from(self):
+        User = self.classes.User
+        sess = create_session()
+
+        q = sess.query(User)
+        q = sess.query(User).select_entity_from(q.statement)
+        self.assert_compile(
+            q.filter(User.name=='ed'),
+            "SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name "
+            "FROM (SELECT users.id AS id, users.name AS name FROM "
+            "users) AS anon_1 WHERE anon_1.name = :name_1"
+        )
+
+    def test_select_entity_from_no_entities(self):
+        User = self.classes.User
+        sess = create_session()
+
+        q = sess.query(User)
+        assert_raises_message(
+            sa.exc.ArgumentError,
+            r"A selectable \(FromClause\) instance is "
+            "expected when the base alias is being set.",
+            sess.query(User).select_entity_from, User
+        )
+
+
     def test_select_from(self):
         User = self.classes.User
         sess = create_session()