]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Dynamic attributes don't support collection
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 5 Aug 2010 16:28:01 +0000 (12:28 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 5 Aug 2010 16:28:01 +0000 (12:28 -0400)
population - added an assertion for when
set_committed_value() is called, as well as
when joinedload() or subqueryload() options
are applied to a dynamic attribute, instead
of failure / silent failure.  [ticket:1864]

CHANGES
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/dynamic.py
lib/sqlalchemy/orm/strategies.py
test/orm/test_dynamic.py
test/orm/test_eager_relations.py
test/orm/test_subquery_relations.py

diff --git a/CHANGES b/CHANGES
index c0b814a4569f5f63fb8dd47c2cde678851f3a002..9baf9048740ae9ca29cc3b18b1653df8be172b8b 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -54,6 +54,13 @@ CHANGES
     specifically asks for a column element, no longer
     misleads with incorrect information about
     text() or literal().  [ticket:1863]
+
+  - Dynamic attributes don't support collection 
+    population - added an assertion for when
+    set_committed_value() is called, as well as
+    when joinedload() or subqueryload() options
+    are applied to a dynamic attribute, instead
+    of failure / silent failure.  [ticket:1864]
     
 - sql
   - Changed the scheme used to generate truncated
index ab31736ed19522be3f83e91ad779f00195f5271e..f91ff51f2f7b489e34020dd87f02d6e0dcdc1b43 100644 (file)
@@ -427,6 +427,7 @@ class ScalarAttributeImpl(AttributeImpl):
 
     accepts_scalar_loader = True
     uses_objects = False
+    supports_population = True
 
     def delete(self, state, dict_):
 
@@ -481,6 +482,7 @@ class MutableScalarAttributeImpl(ScalarAttributeImpl):
     """
 
     uses_objects = False
+    supports_population = True
 
     def __init__(self, class_, key, callable_,
                     class_manager, copy_function=None,
@@ -547,6 +549,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl):
 
     accepts_scalar_loader = False
     uses_objects = True
+    supports_population = True
 
     def __init__(self, class_, key, callable_, 
                     trackparent=False, extension=None, copy_function=None,
@@ -637,6 +640,7 @@ class CollectionAttributeImpl(AttributeImpl):
     """
     accepts_scalar_loader = False
     uses_objects = True
+    supports_population = True
 
     def __init__(self, class_, key, callable_, 
                     typecallable=None, trackparent=False, extension=None,
index d0aa2c20b09367592309323359f3268470b02b12..d558380114fd4cee7230c1c7b7ccfe0a55084974 100644 (file)
@@ -43,10 +43,12 @@ log.class_logger(DynaLoader)
 class DynamicAttributeImpl(attributes.AttributeImpl):
     uses_objects = True
     accepts_scalar_loader = False
-
+    supports_population = False
+    
     def __init__(self, class_, key, typecallable,
                      target_mapper, order_by, query_class=None, **kwargs):
-        super(DynamicAttributeImpl, self).__init__(class_, key, typecallable, **kwargs)
+        super(DynamicAttributeImpl, self).\
+                    __init__(class_, key, typecallable, **kwargs)
         self.target_mapper = target_mapper
         self.order_by = order_by
         if not query_class:
@@ -58,15 +60,18 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
 
     def get(self, state, dict_, passive=False):
         if passive:
-            return self._get_collection_history(state, passive=True).added_items
+            return self._get_collection_history(state,
+                    passive=True).added_items
         else:
             return self.query_class(self, state)
 
     def get_collection(self, state, dict_, user_data=None, passive=True):
         if passive:
-            return self._get_collection_history(state, passive=passive).added_items
+            return self._get_collection_history(state,
+                    passive=passive).added_items
         else:
-            history = self._get_collection_history(state, passive=passive)
+            history = self._get_collection_history(state,
+                    passive=passive)
             return history.added_items + history.unchanged_items
 
     def fire_append_event(self, state, dict_, value, initiator):
@@ -105,30 +110,36 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
         dict_[self.key] = True
         return state.committed_state[self.key]
 
-    def set(self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF):
+    def set(self, state, dict_, value, initiator,
+                        passive=attributes.PASSIVE_OFF):
         if initiator is self:
             return
 
         self._set_iterable(state, dict_, value)
 
     def _set_iterable(self, state, dict_, iterable, adapter=None):
-
         collection_history = self._modified_event(state, dict_)
         new_values = list(iterable)
-
         if state.has_identity:
             old_collection = list(self.get(state, dict_))
         else:
             old_collection = []
-
-        collections.bulk_replace(new_values, DynCollectionAdapter(self, state, old_collection), DynCollectionAdapter(self, state, new_values))
+        collections.bulk_replace(new_values, DynCollectionAdapter(self,
+                                 state, old_collection),
+                                 DynCollectionAdapter(self, state,
+                                 new_values))
 
     def delete(self, *args, **kwargs):
         raise NotImplementedError()
 
+    def set_committed_value(self, state, dict_, value):
+        raise NotImplementedError("Dynamic attributes don't support "
+                                  "collection population.")
+    
     def get_history(self, state, dict_, passive=False):
         c = self._get_collection_history(state, passive)
-        return attributes.History(c.added_items, c.unchanged_items, c.deleted_items)
+        return attributes.History(c.added_items, c.unchanged_items,
+                                  c.deleted_items)
 
     def _get_collection_history(self, state, passive=False):
         if self.key in state.committed_state:
@@ -193,7 +204,8 @@ class AppenderMixin(object):
 
     def __session(self):
         sess = object_session(self.instance)
-        if sess is not None and self.autoflush and sess.autoflush and self.instance in sess:
+        if sess is not None and self.autoflush and sess.autoflush \
+            and self.instance in sess:
             sess.flush()
         if not has_identity(self.instance):
             return None
@@ -283,7 +295,8 @@ class CollectionHistory(object):
             deleted = util.IdentitySet(apply_to.deleted_items)
             added = apply_to.added_items
             coll = AppenderQuery(attr, state).autoflush(False)
-            self.unchanged_items = [o for o in util.IdentitySet(coll) if o not in deleted]
+            self.unchanged_items = [o for o in util.IdentitySet(coll)
+                                    if o not in deleted]
             self.added_items = apply_to.added_items
             self.deleted_items = apply_to.deleted_items
         else:
index 62602ff37bd03a61884d2e57f8fedfb98f1c60df..1c4571aed8257c5a13a2ea82a39050c7110c51dd 100644 (file)
@@ -806,6 +806,12 @@ class SubqueryLoader(AbstractRelationshipLoader):
                 ]
         
     def create_row_processor(self, context, path, mapper, row, adapter):
+        if not self.parent.class_manager[self.key].impl.supports_population:
+            raise sa_exc.InvalidRequestError(
+                        "'%s' does not support object "
+                        "population - eager loading cannot be applied." % 
+                        self)
+        
         path = path + (self.key,)
 
         path = interfaces._reduce_path(path)
@@ -876,6 +882,7 @@ class EagerLoader(AbstractRelationshipLoader):
                                 **kwargs):
         """Add a left outer join to the statement thats being constructed."""
 
+        
         if not context.query._enable_eagerloads:
             return
             
@@ -1069,7 +1076,14 @@ class EagerLoader(AbstractRelationshipLoader):
             return False
 
     def create_row_processor(self, context, path, mapper, row, adapter):
+        if not self.parent.class_manager[self.key].impl.supports_population:
+            raise sa_exc.InvalidRequestError(
+                        "'%s' does not support object "
+                        "population - eager loading cannot be applied." % 
+                        self)
+
         path = path + (self.key,)
+
             
         eager_adapter = self._create_eager_adapter(
                                                 context, 
index b2fac60052dcf699d53b46039adc16cb6eb5ccff..5d822fa3d39457d24a188c9d550a6edaf1895d34 100644 (file)
@@ -6,7 +6,7 @@ from sqlalchemy import Integer, String, ForeignKey, desc, select, func
 from sqlalchemy.test.schema import Table, Column
 from sqlalchemy.orm import mapper, relationship, create_session, Query, attributes
 from sqlalchemy.orm.dynamic import AppenderMixin
-from sqlalchemy.test.testing import eq_, AssertsCompiledSQL
+from sqlalchemy.test.testing import eq_, AssertsCompiledSQL, assert_raises_message
 from sqlalchemy.util import function_named
 from test.orm import _base, _fixtures
 
@@ -122,6 +122,18 @@ class DynamicTest(_fixtures.FixtureTest, AssertsCompiledSQL):
                 q.filter(User.id==7).all())
         self.assert_sql_count(testing.db, go, 2)
 
+    @testing.resolve_artifact_names
+    def test_no_populate(self):
+        mapper(User, users, properties={
+            'addresses':dynamic_loader(mapper(Address, addresses))
+        })
+        u1 = User()
+        assert_raises_message(
+            NotImplementedError,
+            "Dynamic attributes don't support collection population.",
+            attributes.set_committed_value, u1, 'addresses', []
+        )
+        
     @testing.resolve_artifact_names
     def test_m2m(self):
         mapper(Order, orders, properties={
index de397f47b1cc5f2dc6c9074cf974a37aace28212..b96014a575257c9289261e2081fe2a2e1f766bca 100644 (file)
@@ -1,13 +1,17 @@
-"""basic tests of eager loaded attributes"""
+"""tests of joined-eager loaded attributes"""
 
 from sqlalchemy.test.testing import eq_, is_, is_not_
 import sqlalchemy as sa
 from sqlalchemy.test import testing
-from sqlalchemy.orm import joinedload, deferred, undefer, joinedload_all, backref
-from sqlalchemy import Integer, String, Date, ForeignKey, and_, select, func
+from sqlalchemy.orm import joinedload, deferred, undefer, \
+    joinedload_all, backref
+from sqlalchemy import Integer, String, Date, ForeignKey, and_, select, \
+    func
 from sqlalchemy.test.schema import Table, Column
-from sqlalchemy.orm import mapper, relationship, create_session, lazyload, aliased
-from sqlalchemy.test.testing import eq_, assert_raises
+from sqlalchemy.orm import mapper, relationship, create_session, \
+    lazyload, aliased
+from sqlalchemy.test.testing import eq_, assert_raises, \
+    assert_raises_message
 from sqlalchemy.test.assertsql import CompiledSQL
 from test.orm import _base, _fixtures
 from sqlalchemy.util import OrderedDict as odict
@@ -283,7 +287,20 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
                 )
             self.assert_sql_count(testing.db, go, count)
 
-
+    @testing.resolve_artifact_names
+    def test_disable_dynamic(self):
+        """test no joined option on a dynamic."""
+        
+        mapper(User, users, properties={
+            'addresses':relationship(Address, lazy="dynamic")
+        })
+        mapper(Address, addresses)
+        sess = create_session()
+        assert_raises_message(
+            sa.exc.InvalidRequestError,
+            "User.addresses' does not support object population - eager loading cannot be applied.",
+            sess.query(User).options(joinedload(User.addresses)).first,
+        )
 
     @testing.resolve_artifact_names
     def test_many_to_many(self):
index 827630c3d0e60109ef6b29cc0242292cbf0688e0..71f87a7267b1812e82d0e081362b880fc808f3e9 100644 (file)
@@ -3,10 +3,10 @@ from sqlalchemy.test import testing
 from sqlalchemy.test.schema import Table, Column
 from sqlalchemy import Integer, String, ForeignKey, bindparam
 from sqlalchemy.orm import backref, subqueryload, subqueryload_all, \
-                mapper, relationship, clear_mappers,\
-                create_session, lazyload, aliased, joinedload,\
-                deferred, undefer
-from sqlalchemy.test.testing import eq_, assert_raises
+    mapper, relationship, clear_mappers, create_session, lazyload, \
+    aliased, joinedload, deferred, undefer
+from sqlalchemy.test.testing import eq_, assert_raises, \
+    assert_raises_message
 from sqlalchemy.test.assertsql import CompiledSQL
 from test.orm import _base, _fixtures
 import sqlalchemy as sa
@@ -80,6 +80,23 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
 
         self.assert_sql_count(testing.db, go, 2)
         
+    @testing.resolve_artifact_names
+    def test_disable_dynamic(self):
+        """test no subquery option on a dynamic."""
+
+        mapper(User, users, properties={
+            'addresses':relationship(Address, lazy="dynamic")
+        })
+        mapper(Address, addresses)
+        sess = create_session()
+        
+        # previously this would not raise, but would emit
+        # the query needlessly and put the result nowhere.
+        assert_raises_message(
+            sa.exc.InvalidRequestError,
+            "User.addresses' does not support object population - eager loading cannot be applied.",
+            sess.query(User).options(subqueryload(User.addresses)).first,
+        )
         
     @testing.resolve_artifact_names
     def test_many_to_many(self):