diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2008-07-05 20:37:44 +0000 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2008-07-05 20:37:44 +0000 |
commit | 0f42004deeab823398571986ff4a75eb536267ea (patch) | |
tree | 2400ec34559d9cf229e77d45c14f3ef6dc35aae5 | |
parent | cf9edea20362ee97d3bd8887676dfc174e3721bb (diff) | |
download | sqlalchemy-0f42004deeab823398571986ff4a75eb536267ea.tar.gz |
- 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.
- whitespace fix to new Table prefixes option
-rw-r--r-- | CHANGES | 11 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/properties.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 25 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 17 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 2 | ||||
-rw-r--r-- | test/orm/expire.py | 40 | ||||
-rw-r--r-- | test/orm/transaction.py | 3 |
8 files changed, 91 insertions, 14 deletions
@@ -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 diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 1ce71fdb6..b0eb54301 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -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: diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 07caae07a..2fba04ee0 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -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 diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 2110f8826..b9f170733 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -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) diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index fcb56865b..3073e17db 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -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) diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 51ad1dfb0..3b82fbdd0 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -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" diff --git a/test/orm/expire.py b/test/orm/expire.py index d7ed0419e..df456fb58 100644 --- a/test/orm/expire.py +++ b/test/orm/expire.py @@ -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): diff --git a/test/orm/transaction.py b/test/orm/transaction.py index a4f5a0592..ce487a5f6 100644 --- a/test/orm/transaction.py +++ b/test/orm/transaction.py @@ -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() |