]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- session.refresh() raises an informative error message if
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 5 Jul 2008 20:37:44 +0000 (20:37 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 5 Jul 2008 20:37:44 +0000 (20:37 +0000)
the list of attributes does not include any column-based
attributes.

- query() raises an informative error message if no columns
or mappers are specified.

- lazy loaders now trigger autoflush before proceeding.  This
allows expire() of a collection or scalar relation to
function properly in the context of autoflush.

- whitespace fix to new Table prefixes option

CHANGES
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/sql/compiler.py
test/orm/expire.py
test/orm/transaction.py

diff --git a/CHANGES b/CHANGES
index 191a648e214912a6bbc67cb13c61ff2edc150384..00611fac4792648c345bd2323510003bfe1c0b56 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -12,6 +12,17 @@ CHANGES
     - In addition to expired attributes, deferred attributes
       also load if their data is present in the result set.
       [ticket:870]
+    
+    - session.refresh() raises an informative error message if
+      the list of attributes does not include any column-based
+      attributes.
+    
+    - query() raises an informative error message if no columns
+      or mappers are specified.
+      
+    - lazy loaders now trigger autoflush before proceeding.  This
+      allows expire() of a collection or scalar relation to 
+      function properly in the context of autoflush.
       
     - column_property() attributes which represent SQL expressions
       or columns that are not present in the mapped tables
index 1ce71fdb64e89e307c4203297365046e1d074903..b0eb54301f00b13a710a9034706c68da14622f6b 100644 (file)
@@ -480,7 +480,8 @@ class PropertyLoader(StrategizedProperty):
     def cascade_iterator(self, type_, state, visited_instances, halt_on=None):
         if not type_ in self.cascade:
             return
-        passive = type_ != 'delete' or self.passive_deletes
+        # only actively lazy load on the 'delete' cascade
+        passive = type_ != 'delete' or self.passive_deletes  
         mapper = self.mapper.primary_mapper()
         instances = state.value_as_iterable(self.key, passive=passive)
         if instances:
index 07caae07af555c467ad835340e1d8573ce960fc9..2fba04ee0c84c63e5ed7f120a1c320fd3d481a17 100644 (file)
@@ -1044,7 +1044,7 @@ class Query(object):
     def _execute_and_instances(self, querycontext):
         result = self.session.execute(querycontext.statement, params=self._params, mapper=self._mapper_zero_or_none(), _state=self._refresh_state)
         return self.instances(result, querycontext)
-
+    
     def instances(self, cursor, __context=None):
         """Given a ResultProxy cursor as returned by connection.execute(), return an ORM result as an iterator.
 
@@ -1081,7 +1081,7 @@ class Query(object):
             labels = dict([(label, property(util.itemgetter(i))) for i, label in enumerate(labels) if label])
             rowtuple = type.__new__(type, "RowTuple", (tuple,), labels)
             rowtuple.keys = labels.keys
-            
+        
         while True:
             context.progress = util.Set()
             context.partials = {}
@@ -1165,7 +1165,11 @@ class Query(object):
 
         if lockmode is not None:
             q._lockmode = lockmode
-        q.__get_options(populate_existing=bool(refresh_state), version_check=(lockmode is not None), only_load_props=only_load_props, refresh_state=refresh_state)
+        q.__get_options(
+            populate_existing=bool(refresh_state), 
+            version_check=(lockmode is not None), 
+            only_load_props=only_load_props, 
+            refresh_state=refresh_state)
         q._order_by = None
         try:
             # call using all() to avoid LIMIT compilation complexity
@@ -1174,7 +1178,13 @@ class Query(object):
             return None
 
     def _select_args(self):
-        return {'limit':self._limit, 'offset':self._offset, 'distinct':self._distinct, 'group_by':self._group_by or None, 'having':self._having or None}
+        return {
+            'limit':self._limit, 
+            'offset':self._offset, 
+            'distinct':self._distinct, 
+            'group_by':self._group_by or None, 
+            'having':self._having or None
+        }
     _select_args = property(_select_args)
 
     def _should_nest_selectable(self):
@@ -1343,6 +1353,13 @@ class Query(object):
         
         self._adjust_for_single_inheritance(context)
         
+        if not context.primary_columns:
+            if self._only_load_props:
+                raise sa_exc.InvalidRequestError("No column-based properties specified for refresh operation."
+                " Use session.expire() to reload collections and related items.")
+            else:
+                raise sa_exc.InvalidRequestError("Query contains no columns with which to SELECT from.")
+            
         if eager_joins and self._should_nest_selectable:
             # for eager joins present and LIMIT/OFFSET/DISTINCT, wrap the query inside a select,
             # then append eager joins onto that
index 2110f8826fbcc19bdb369048bb92008313990a8e..b9f1707330eac5ba20ddc7627d1acd0e7b8aa194 100644 (file)
@@ -960,7 +960,7 @@ class Session(object):
             # remove associations
             cascaded = list(_cascade_state_iterator('refresh-expire', state))
             _expire_state(state, None)
-            for (state, m) in cascaded:
+            for (state, m, o) in cascaded:
                 _expire_state(state, None)
 
     def prune(self):
@@ -991,7 +991,7 @@ class Session(object):
             raise sa_exc.InvalidRequestError(
                 "Instance %s is not present in this Session" %
                 mapperutil.state_str(state))
-        for s, m in [(state, None)] + list(_cascade_state_iterator('expunge', state)):
+        for s, m, o in [(state, None, None)] + list(_cascade_state_iterator('expunge', state)):
             self._expunge_state(s)
 
     def _expunge_state(self, state):
@@ -1120,8 +1120,12 @@ class Session(object):
             state = attributes.instance_state(instance)
         except exc.NO_STATE:
             raise exc.UnmappedInstanceError(instance)
-        self._delete_impl(state)
-        for state, m in _cascade_state_iterator('delete', state):
+        
+        # grab the full cascade list first, since lazyloads/autoflush
+        # may be triggered by this operation (delete cascade lazyloads by default)
+        cascade_states = list(_cascade_state_iterator('delete', state))
+        self._delete_impl(state)    
+        for state, m, o in cascade_states:
             self._delete_impl(state, ignore_transient=True)
 
     def merge(self, instance, entity_name=None, dont_load=False,
@@ -1508,8 +1512,11 @@ _sessions = weakref.WeakValueDictionary()
 
 def _cascade_state_iterator(cascade, state, **kwargs):
     mapper = _state_mapper(state)
+    # yield the state, object, mapper.  yielding the object
+    # allows the iterator's results to be held in a list without
+    # states being garbage collected
     for (o, m) in mapper.cascade_iterator(cascade, state, **kwargs):
-        yield attributes.instance_state(o), m
+        yield attributes.instance_state(o), o, m
 
 def _cascade_unknown_state_iterator(cascade, state, **kwargs):
     mapper = _state_mapper(state)
index fcb56865b76ff49c4c6f1cf2c8c79d0fa8979995..3073e17dba05425006d5e552e3616213fbbcb36e 100644 (file)
@@ -524,8 +524,8 @@ class LoadLazyAttribute(object):
                 "lazy load operation of attribute '%s' cannot proceed" % 
                 (mapperutil.state_str(state), self.key)
             )
-
-        q = session.query(prop.mapper).autoflush(False)._adapt_all_clauses()
+        
+        q = session.query(prop.mapper)._adapt_all_clauses()
         
         if self.path:
             q = q._with_current_path(self.path)
index 51ad1dfb0981c686e75899927bd042822a2b3e32..3b82fbdd07982267ebc2f30500245cf3c61b6f1e 100644 (file)
@@ -788,7 +788,7 @@ class SchemaGenerator(DDLBase):
             if column.default is not None:
                 self.traverse_single(column.default)
 
-        self.append("\nCREATE " + " ".join(table._prefixes) + " TABLE " + self.preparer.format_table(table) + " (")
+        self.append("\nCREATE" + " ".join(table._prefixes) + " TABLE " + self.preparer.format_table(table) + " (")
 
         separator = "\n"
 
index d7ed0419ebfd2f91189d78f443cc0c182cfb800d..df456fb58346ba28197cdc310ae5b48169441f9c 100644 (file)
@@ -3,7 +3,7 @@
 import testenv; testenv.configure_for_tests()
 import gc
 from testlib import sa, testing
-from testlib.sa import Table, Column, Integer, String, ForeignKey
+from testlib.sa import Table, Column, Integer, String, ForeignKey, exc as sa_exc
 from testlib.sa.orm import mapper, relation, create_session, attributes
 from orm import _base, _fixtures
 
@@ -98,6 +98,44 @@ class ExpireTest(_fixtures.FixtureTest):
         # but now its back, rollback has occured, the _remove_newly_deleted
         # is reverted
         self.assertEquals(u.name, 'chuck')
+    
+    @testing.resolve_artifact_names
+    def test_lazyload_autoflushes(self):
+        mapper(User, users, properties={
+            'addresses':relation(Address, order_by=addresses.c.email_address)
+        })
+        mapper(Address, addresses)
+        s = create_session(autoflush=True, autocommit=False)
+        u = s.query(User).get(8)
+        adlist = u.addresses
+        self.assertEquals(adlist, [
+            Address(email_address='ed@bettyboop.com'), 
+            Address(email_address='ed@lala.com'),
+            Address(email_address='ed@wood.com'), 
+        ])
+        a1 = u.addresses[2]
+        a1.email_address = 'aaaaa'
+        s.expire(u, ['addresses'])
+        self.assertEquals(u.addresses, [
+            Address(email_address='aaaaa'), 
+            Address(email_address='ed@bettyboop.com'), 
+            Address(email_address='ed@lala.com'),
+        ])
+
+    @testing.resolve_artifact_names
+    def test_refresh_collection_exception(self):
+        """test graceful failure for currently unsupported immediate refresh of a collection"""
+        
+        mapper(User, users, properties={
+            'addresses':relation(Address, order_by=addresses.c.email_address)
+        })
+        mapper(Address, addresses)
+        s = create_session(autoflush=True, autocommit=False)
+        u = s.query(User).get(8)
+        self.assertRaisesMessage(sa_exc.InvalidRequestError, "properties specified for refresh", s.refresh, u, ['addresses'])
+        
+        # in contrast to a regular query with no columns
+        self.assertRaisesMessage(sa_exc.InvalidRequestError, "no columns with which to SELECT", s.query().all)
         
     @testing.resolve_artifact_names
     def test_refresh_cancels_expire(self):
index a4f5a0592d0fb4a673f3097966d678d4a4e6b235..ce487a5f6ca7870c270298a8ca0176e6806aa729 100644 (file)
@@ -81,6 +81,9 @@ class AutoExpireTest(TransactionTest):
         s.add(u1)
         s.commit()
 
+        # this actually tests that the delete() operation,
+        # when cascaded to the "addresses" collection, does not
+        # trigger a flush (via lazyload) before the cascade is complete.
         s.delete(u1)
         assert u1 in s.deleted
         s.rollback()