]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
11th hour realization that Load() needs to do the _chop_path() thing as
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 7 Oct 2013 00:12:28 +0000 (20:12 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 7 Oct 2013 00:50:51 +0000 (20:50 -0400)
well.  this probably has some bugs

doc/build/orm/mapper_config.rst
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/strategy_options.py
test/orm/test_deferred.py

index 40901520075381c5555c41e2f67206ca9ccfa1e8..64d7d4735f33eb782a7d90e37149568ec29d7612 100644 (file)
@@ -319,13 +319,6 @@ level using options, including :func:`.orm.defer` and :func:`.orm.undefer`::
     query = query.options(undefer('excerpt'))
     query.all()
 
-An arbitrary set of columns can be selected as "load only" columns, which will
-be loaded while deferring all other columns on a given entity, using :func:`.orm.load_only`::
-
-    from sqlalchemy.orm import load_only
-
-    session.query(Book).options(load_only("summary", "excerpt"))
-
 :func:`.orm.deferred` attributes which are marked with a "group" can be undeferred
 using :func:`.orm.undefer_group`, sending in the group name::
 
@@ -334,6 +327,18 @@ using :func:`.orm.undefer_group`, sending in the group name::
     query = session.query(Book)
     query.options(undefer_group('photos')).all()
 
+Load Only Cols
+---------------
+
+An arbitrary set of columns can be selected as "load only" columns, which will
+be loaded while deferring all other columns on a given entity, using :func:`.orm.load_only`::
+
+    from sqlalchemy.orm import load_only
+
+    session.query(Book).options(load_only("summary", "excerpt"))
+
+.. versionadded:: 0.9.0
+
 Deferred Loading with Multiple Entities
 ---------------------------------------
 
@@ -371,6 +376,8 @@ unchanged, use :func:`.orm.defaultload`::
                 defaultload(Book.author).load_only("summary", "excerpt"),
             )
 
+.. versionadded:: 0.9.0 support for :class:`.Load` and other options which
+   allow for better targeting of deferral options.
 
 Column Deferral API
 -------------------
index 18723e4f6efd817236f6bb37d8909905ac4bd1ba..f613967502ed7d05233db49b7231fe35895dce0d 100644 (file)
@@ -430,6 +430,9 @@ class StrategizedProperty(MapperProperty):
         # that the path is stated in terms of our base
         search_path = dict.__getitem__(path, self)
 
+        #if self.key == "email_address":
+        #    import pdb
+        #    pdb.set_trace()
         # search among: exact match, "attr.*", "default" strategy
         # if any.
         for path_key in (
index 5f7eb2c257566512471a01cb19076655bad26900..c066c2dffbe4a0eace215bd0f8e9286b752311ae 100644 (file)
@@ -92,7 +92,14 @@ class Load(Generative, MapperOption):
         self._process(query, False)
 
     def _process(self, query, raiseerr):
-        query._attributes.update(self.context)
+        current_path = query._current_path
+        if current_path:
+            for (token, start_path), loader in self.context.items():
+                chopped_start_path = self._chop_path(start_path, current_path)
+                if chopped_start_path is not None:
+                    query._attributes[(token, chopped_start_path)] = loader
+        else:
+            query._attributes.update(self.context)
 
     def _generate_path(self, path, attr, wildcard_key, raiseerr=True):
         if raiseerr and not path.has_entity:
@@ -205,6 +212,25 @@ class Load(Generative, MapperOption):
         self.__dict__.update(state)
         self.path = PathRegistry.deserialize(self.path)
 
+    def _chop_path(self, to_chop, path):
+        i = -1
+
+        for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)):
+            if isinstance(c_token, util.string_types):
+                # TODO: this is approximated from the _UnboundLoad
+                # version and probably has issues, not fully covered.
+
+                if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN):
+                    return to_chop
+                elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_token.key:
+                    return None
+
+            if c_token is p_token:
+                continue
+            else:
+                return None
+        return to_chop[i+1:]
+
 
 class _UnboundLoad(Load):
     """Represent a loader option that isn't tied to a root entity.
@@ -289,6 +315,23 @@ class _UnboundLoad(Load):
         return opt
 
 
+    def _chop_path(self, to_chop, path):
+        i = -1
+        for i, (c_token, (p_mapper, p_prop)) in enumerate(zip(to_chop, path.pairs())):
+            if isinstance(c_token, util.string_types):
+                if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN):
+                    return to_chop
+                elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_prop.key:
+                    return None
+            elif isinstance(c_token, PropComparator):
+                if c_token.property is not p_prop:
+                    return None
+        else:
+            i += 1
+
+        return to_chop[i:]
+
+
     def _bind_loader(self, query, context, raiseerr):
         start_path = self.path
         # _current_path implies we're in a
@@ -349,22 +392,6 @@ class _UnboundLoad(Load):
         else:
             effective_path.set(context, "loader", loader)
 
-    def _chop_path(self, to_chop, path):
-        i = -1
-        for i, (c_token, (p_mapper, p_prop)) in enumerate(zip(to_chop, path.pairs())):
-            if isinstance(c_token, util.string_types):
-                if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN):
-                    return to_chop
-                elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_prop.key:
-                    return None
-            elif isinstance(c_token, PropComparator):
-                if c_token.property is not p_prop:
-                    return None
-        else:
-            i += 1
-
-        return to_chop[i:]
-
     def _find_entity_prop_comparator(self, query, token, mapper, raiseerr):
         if _is_aliased_class(mapper):
             searchfor = mapper
@@ -749,7 +776,7 @@ def defaultload(*keys):
     return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {})
 
 @loader_option()
-def defer(loadopt, key, *addl_attrs):
+def defer(loadopt, key):
     """Indicate that the given column-oriented attribute should be deferred, e.g.
     not loaded until accessed.
 
@@ -801,17 +828,17 @@ def defer(loadopt, key, *addl_attrs):
 
     """
     return loadopt.set_column_strategy(
-                (key, ) + addl_attrs,
+                (key, ),
                 {"deferred": True, "instrument": True}
             )
 
 
 @defer._add_unbound_fn
-def defer(*key):
-    return _UnboundLoad._from_keys(_UnboundLoad.defer, key, False, {})
+def defer(key, *addl_attrs):
+    return _UnboundLoad._from_keys(_UnboundLoad.defer, (key, ) + addl_attrs, False, {})
 
 @loader_option()
-def undefer(loadopt, key, *addl_attrs):
+def undefer(loadopt, key):
     """Indicate that the given column-oriented attribute should be undeferred, e.g.
     specified within the SELECT statement of the entity as a whole.
 
@@ -845,13 +872,13 @@ def undefer(loadopt, key, *addl_attrs):
 
     """
     return loadopt.set_column_strategy(
-                (key, ) + addl_attrs,
+                (key, ),
                 {"deferred": False, "instrument": True}
             )
 
 @undefer._add_unbound_fn
-def undefer(*key):
-    return _UnboundLoad._from_keys(_UnboundLoad.undefer, key, False, {})
+def undefer(key, *addl_attrs):
+    return _UnboundLoad._from_keys(_UnboundLoad.undefer, (key, ) + addl_attrs, False, {})
 
 @loader_option()
 def undefer_group(loadopt, name):
index 2dcd821dcfa4d2981d8d31662d7cdc1dbc620adf..3336f03b53d159b7ec70d7665591ee55236a324e 100644 (file)
@@ -374,6 +374,43 @@ class DeferredOptionsTest(AssertsCompiledSQL, _fixtures.FixtureTest):
         self.sql_count_(0, go)
         eq_(item.description, 'item 4')
 
+    def test_path_entity(self):
+        """test the legacy *addl_attrs argument."""
+
+        User = self.classes.User
+        Order = self.classes.Order
+        Item = self.classes.Item
+
+        users = self.tables.users
+        orders = self.tables.orders
+        items = self.tables.items
+        order_items = self.tables.order_items
+
+        mapper(User, users, properties={
+                "orders": relationship(Order, lazy="joined")
+            })
+        mapper(Order, orders, properties={
+                "items": relationship(Item, secondary=order_items, lazy="joined")
+            })
+        mapper(Item, items)
+
+        sess = create_session()
+
+        exp = ("SELECT users.id AS users_id, users.name AS users_name, "
+            "items_1.id AS items_1_id, orders_1.id AS orders_1_id, "
+            "orders_1.user_id AS orders_1_user_id, orders_1.address_id "
+            "AS orders_1_address_id, orders_1.description AS "
+            "orders_1_description, orders_1.isopen AS orders_1_isopen "
+            "FROM users LEFT OUTER JOIN orders AS orders_1 "
+            "ON users.id = orders_1.user_id LEFT OUTER JOIN "
+            "(order_items AS order_items_1 JOIN items AS items_1 "
+                "ON items_1.id = order_items_1.item_id) "
+            "ON orders_1.id = order_items_1.order_id")
+
+        q = sess.query(User).options(defer(User.orders, Order.items, Item.description))
+        self.assert_compile(q, exp)
+
+
     def test_chained_multi_col_options(self):
         users, User = self.tables.users, self.classes.User
         orders, Order = self.tables.orders, self.classes.Order
@@ -422,6 +459,48 @@ class DeferredOptionsTest(AssertsCompiledSQL, _fixtures.FixtureTest):
             "orders.user_id AS orders_user_id, "
             "orders.isopen AS orders_isopen FROM orders")
 
+    def test_load_only_propagate_unbound(self):
+        self._test_load_only_propagate(False)
+
+    def test_load_only_propagate_bound(self):
+        self._test_load_only_propagate(True)
+
+    def _test_load_only_propagate(self, use_load):
+        User = self.classes.User
+        Address = self.classes.Address
+
+        users = self.tables.users
+        addresses = self.tables.addresses
+
+        mapper(User, users, properties={
+                "addresses": relationship(Address)
+            })
+        mapper(Address, addresses)
+
+        sess = create_session()
+        expected = [
+            ("SELECT users.id AS users_id, users.name AS users_name "
+                "FROM users WHERE users.id IN (:id_1, :id_2)", {'id_2': 8, 'id_1': 7}),
+            ("SELECT addresses.id AS addresses_id, "
+                "addresses.email_address AS addresses_email_address "
+                "FROM addresses WHERE :param_1 = addresses.user_id", {'param_1': 7}),
+            ("SELECT addresses.id AS addresses_id, "
+                "addresses.email_address AS addresses_email_address "
+                "FROM addresses WHERE :param_1 = addresses.user_id", {'param_1': 8}),
+        ]
+
+        if use_load:
+            opt = Load(User).defaultload(User.addresses).load_only("id", "email_address")
+        else:
+            opt = defaultload(User.addresses).load_only("id", "email_address")
+        q = sess.query(User).options(opt).filter(User.id.in_([7, 8]))
+        def go():
+            for user in q:
+                user.addresses
+
+        self.sql_eq_(go, expected)
+
+
     def test_load_only_parent_specific(self):
         User = self.classes.User
         Address = self.classes.Address