mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-13 04:07:20 -04:00
- 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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
+39
-1
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user