Mike Bayer avatar Mike Bayer committed 20b6ce0

- query.with_parent() now accepts transient objects
and will use the non-persistent values of their pk/fk
attributes in order to formulate the criterion.
Docs are also clarified as to the purpose of with_parent().
- fix for PG test executing an alias()

Comments (0)

Files changed (7)

     where a one-step Session constructor is desired. Most
     users should stick with sessionmaker() for general use,
     however.
-
+  
+  - query.with_parent() now accepts transient objects
+    and will use the non-persistent values of their pk/fk
+    attributes in order to formulate the criterion.  
+    Docs are also clarified as to the purpose of with_parent().
+    
   - The include_properties and exclude_properties arguments
     to mapper() now accept Column objects as members in 
     addition to strings.  This so that same-named Column

lib/sqlalchemy/orm/properties.py

             self.prop.parent.compile()
             return self.prop
 
-    def compare(self, op, value, value_is_parent=False, alias_secondary=True):
+    def compare(self, op, value, 
+                            value_is_parent=False, 
+                            alias_secondary=True,
+                            detect_transient_pending=False):
         if op == operators.eq:
             if value is None:
                 if self.uselist:
                 else:
                     return self._optimized_compare(None, 
                                     value_is_parent=value_is_parent,
+                                    detect_transient_pending=detect_transient_pending,
                                     alias_secondary=alias_secondary)
             else:
-                    return self._optimized_compare(value, 
-                                    value_is_parent=value_is_parent,
-                                    alias_secondary=alias_secondary)
+                return self._optimized_compare(value, 
+                                value_is_parent=value_is_parent,
+                                detect_transient_pending=detect_transient_pending,
+                                alias_secondary=alias_secondary)
         else:
             return op(self.comparator, value)
 
     def _optimized_compare(self, value, value_is_parent=False, 
-                                    adapt_source=None, alias_secondary=True):
+                                    adapt_source=None, 
+                                    detect_transient_pending=False,
+                                    alias_secondary=True):
         if value is not None:
             value = attributes.instance_state(value)
         return self._get_strategy(strategies.LazyLoader).lazy_clause(value,
                 reverse_direction=not value_is_parent,
                 alias_secondary=alias_secondary,
+                detect_transient_pending=detect_transient_pending,
                 adapt_source=adapt_source)
 
     def __str__(self):

lib/sqlalchemy/orm/query.py

         self._populate_existing = True
 
     def with_parent(self, instance, property=None):
-        """Add a join criterion corresponding to a relationship to the given
-        parent instance.
-
-        instance
-          a persistent or detached instance which is related to class
-          represented by this query.
-
-        property
-          string name of the property which relates this query's class to the
-          instance.  if None, the method will attempt to find a suitable
-          property.
-
-        Currently, this method only works with immediate parent relationships,
-        but in the future may be enhanced to work across a chain of parent
-        mappers.
+        """Add filtering criterion that relates this query's primary entity
+        to the given related instance, using established :func:`.relationship()`
+        configuration.
+        
+        The SQL rendered is the same as that rendered when a lazy loader
+        would fire off from the given parent on that attribute, meaning
+        that the appropriate state is taken from the parent object in 
+        Python without the need to render joins to the parent table
+        in the rendered statement.
+        
+        As of 0.6.4, this method accepts parent instances in all 
+        persistence states, including transient, persistent, and detached.
+        Only the requisite primary key/foreign key attributes need to
+        be populated.  Previous versions didn't work with transient
+        instances.
+        
+        :param instance:
+          An instance which is related to the class represented by 
+          this query via some :func:`.relationship`, that also 
+          contains the appropriate attribute state that identifies
+          the child object or collection.
+
+        :param property:
+          String property name, or class-bound attribute, which indicates
+          what relationship should be used to reconcile the parent/child
+          relationship.  If None, the method will use the first relationship
+          that links them together - note that this is not deterministic
+          in the case of multiple relationships linking parent/child,
+          so using None is not recommended.
 
         """
         from sqlalchemy.orm import properties
             prop = mapper.get_property(property, resolve_synonyms=True)
         return self.filter(prop.compare(
                                 operators.eq, 
-                                instance, value_is_parent=True))
+                                instance, value_is_parent=True,
+                                detect_transient_pending=True))
 
     @_generative()
     def add_entity(self, entity, alias=None):

lib/sqlalchemy/orm/strategies.py

                 )
 
     def lazy_clause(self, state, reverse_direction=False, 
-                                alias_secondary=False, adapt_source=None):
+                                alias_secondary=False, 
+                                adapt_source=None,
+                                detect_transient_pending=False):
         if state is None:
             return self._lazy_none_clause(
                                         reverse_direction, 
         else:
             mapper = self.parent_property.parent
 
+        o = state.obj() # strong ref
+        dict_ = attributes.instance_dict(o)
+        
         def visit_bindparam(bindparam):
             if bindparam.key in bind_to_col:
-                # use the "committed" (database) version to get 
-                # query column values
-                # also its a deferred value; so that when used 
-                # by Query, the committed value is used
-                # after an autoflush occurs
-                o = state.obj() # strong ref
-                bindparam.value = \
-                                lambda: mapper._get_committed_attr_by_column(
-                                        o, bind_to_col[bindparam.key])
+                # using a flag to enable "detect transient pending" so that
+                # the slightly different usage paradigm of "dynamic" loaders
+                # continue to work as expected, i.e. that all pending objects
+                # should use the "post flush" attributes, and to limit this 
+                # newer behavior to the query.with_parent() method.  
+                # It would be nice to do away with this flag.
+                
+                if detect_transient_pending and \
+                    (not state.key or not state.session_id):
+                    bindparam.value = mapper._get_state_attr_by_column(
+                                        state, dict_, bind_to_col[bindparam.key])
+                else:
+                    # send value as a lambda so that the value is
+                    # acquired after any autoflush occurs.
+                    bindparam.value = \
+                                lambda: mapper._get_committed_state_attr_by_column(
+                                        state, dict_, bind_to_col[bindparam.key])
+                    
 
         if self.parent_property.secondary is not None and alias_secondary:
             criterion = sql_util.ClauseAdapter(

test/dialect/test_postgresql.py

             result.close()
             result = \
                 sess.query(Foo).execution_options(stream_results=True).\
-                    subquery().execute()
+                    statement.execute()
             assert result.cursor.name
             result.close()
         finally:

test/orm/test_dynamic.py

         q = sess.query(User)
 
         u = q.filter(User.id==7).first()
+        
         eq_([User(id=7,
                   addresses=[Address(id=1, email_address='jack@bean.com')])],
             q.filter(User.id==7).all())

test/orm/test_query.py

             q = sess.query(Item).with_parent(u1)
             assert False
         except sa_exc.InvalidRequestError, e:
-            assert str(e) == "Could not locate a property which relates instances of class 'Item' to instances of class 'User'"
+            assert str(e) \
+                == "Could not locate a property which relates "\
+                "instances of class 'Item' to instances of class 'User'"
 
     def test_m2m(self):
         sess = create_session()
         k = sess.query(Keyword).with_parent(i1).all()
         assert [Keyword(name='red'), Keyword(name='small'), Keyword(name='square')] == k
 
+    def test_with_transient(self):
+        sess = Session()
+        
+        q = sess.query(User)
+        u1 = q.filter_by(name='jack').one()
+        utrans = User(id=u1.id)
+        o = sess.query(Order).with_parent(utrans, 'orders')
+        eq_(
+            [Order(description="order 1"), Order(description="order 3"), Order(description="order 5")],
+            o.all()
+        )
+        
+    def test_with_pending_autoflush(self):
+        sess = Session()
+
+        o1 = sess.query(Order).first()
+        opending = Order(id=20, user_id=o1.user_id)
+        sess.add(opending)
+        eq_(
+            sess.query(User).with_parent(opending, 'user').one(),
+            User(id=o1.user_id)
+        )
+
+    def test_with_pending_no_autoflush(self):
+        sess = Session(autoflush=False)
+
+        o1 = sess.query(Order).first()
+        opending = Order(user_id=o1.user_id)
+        sess.add(opending)
+        eq_(
+            sess.query(User).with_parent(opending, 'user').one(),
+            User(id=o1.user_id)
+        )
+        
 class InheritedJoinTest(_base.MappedTest, AssertsCompiledSQL):
     run_setup_mappers = 'once'
     
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.