]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Using a "dynamic" loader with a "secondary" table now produces
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 25 Oct 2009 16:31:54 +0000 (16:31 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 25 Oct 2009 16:31:54 +0000 (16:31 +0000)
a query where the "secondary" table is *not* aliased.  This
allows the secondary Table object to be used in the "order_by"
attribute of the relation(), and also allows it to be used
in filter criterion against the dynamic relation.
[ticket:1531]

- a "dynamic" loader sets up its query criterion at construction
time so that the actual query is returned from non-cloning
accessors like "statement".

CHANGES
lib/sqlalchemy/orm/dynamic.py
lib/sqlalchemy/orm/properties.py
test/orm/test_dynamic.py

diff --git a/CHANGES b/CHANGES
index 5a3ba9c45c28c7a785342d0048603a959c46a8cd..7f217ef1db10c4f49b74dfdee8bb69de6c3a8b35 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -61,7 +61,18 @@ CHANGES
         against the parent table directly along with the
         limit/offset without the extra overhead of a subquery,
         since a many-to-one join does not add rows to the result.
-      
+
+  - Using a "dynamic" loader with a "secondary" table now produces
+    a query where the "secondary" table is *not* aliased.  This
+    allows the secondary Table object to be used in the "order_by"
+    attribute of the relation(), and also allows it to be used
+    in filter criterion against the dynamic relation.
+    [ticket:1531]
+
+  - a "dynamic" loader sets up its query criterion at construction
+    time so that the actual query is returned from non-cloning
+    accessors like "statement".
+    
   - the "named tuple" objects returned when iterating a
     Query() are now pickleable.
 
index 3cd0bf8c0a8aece9268f1a3b9d9877a431543e30..456bcd34e67bae9a4cbc403c0f761a1b064fbaa1 100644 (file)
@@ -13,9 +13,9 @@ basic add/delete mutation.
 
 from sqlalchemy import log, util
 import sqlalchemy.exceptions as sa_exc
-
+from sqlalchemy.sql import operators
 from sqlalchemy.orm import (
-    attributes, object_session, util as mapperutil, strategies,
+    attributes, object_session, util as mapperutil, strategies, object_mapper
     )
 from sqlalchemy.orm.query import Query
 from sqlalchemy.orm.util import _state_has_identity, has_identity
@@ -172,9 +172,20 @@ class AppenderMixin(object):
 
     def __init__(self, attr, state):
         Query.__init__(self, attr.target_mapper, None)
-        self.instance = state.obj()
+        self.instance = instance = state.obj()
         self.attr = attr
 
+        mapper = object_mapper(instance)
+        prop = mapper.get_property(self.attr.key, resolve_synonyms=True)
+        self._criterion = prop.compare(
+                            operators.eq, 
+                            instance, 
+                            value_is_parent=True, 
+                            alias_secondary=False)
+
+        if self.attr.order_by:
+            self._order_by = self.attr.order_by
+
     def __session(self):
         sess = object_session(self.instance)
         if sess is not None and self.autoflush and sess.autoflush and self.instance in sess:
@@ -233,17 +244,21 @@ class AppenderMixin(object):
             query = self.query_class(self.attr.target_mapper, session=sess)
         else:
             query = sess.query(self.attr.target_mapper)
-        query = query.with_parent(instance, self.attr.key)
-
-        if self.attr.order_by:
-            query = query.order_by(*self.attr.order_by)
+        
+        query._criterion = self._criterion
+        query._order_by = self._order_by
+        
         return query
 
     def append(self, item):
-        self.attr.append(attributes.instance_state(self.instance), attributes.instance_dict(self.instance), item, None)
+        self.attr.append(
+            attributes.instance_state(self.instance), 
+            attributes.instance_dict(self.instance), item, None)
 
     def remove(self, item):
-        self.attr.remove(attributes.instance_state(self.instance), attributes.instance_dict(self.instance), item, None)
+        self.attr.remove(
+            attributes.instance_state(self.instance), 
+            attributes.instance_dict(self.instance), item, None)
 
 
 class AppenderQuery(AppenderMixin, Query):
index 0ad0ef5a0382bd83ce508af19e584444b853350c..1ca71390c4677ae165b89e51140b782843a885a3 100644 (file)
@@ -595,23 +595,30 @@ class RelationProperty(StrategizedProperty):
             self.prop.parent.compile()
             return self.prop
 
-    def compare(self, op, value, value_is_parent=False):
+    def compare(self, op, value, value_is_parent=False, alias_secondary=True):
         if op == operators.eq:
             if value is None:
                 if self.uselist:
                     return ~sql.exists([1], self.primaryjoin)
                 else:
-                    return self._optimized_compare(None, value_is_parent=value_is_parent)
+                    return self._optimized_compare(None, 
+                                    value_is_parent=value_is_parent,
+                                    alias_secondary=alias_secondary)
             else:
-                return self._optimized_compare(value, value_is_parent=value_is_parent)
+                    return self._optimized_compare(value, 
+                                    value_is_parent=value_is_parent,
+                                    alias_secondary=alias_secondary)
         else:
             return op(self.comparator, value)
 
-    def _optimized_compare(self, value, value_is_parent=False, adapt_source=None):
+    def _optimized_compare(self, value, value_is_parent=False, 
+                                    adapt_source=None, 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=True, adapt_source=adapt_source)
+                lazy_clause(value, 
+                            reverse_direction=not value_is_parent, 
+                            alias_secondary=alias_secondary, adapt_source=adapt_source)
 
     def __str__(self):
         return str(self.parent.class_.__name__) + "." + self.key
index 5fe7293218bd26131842f5803ff51ceb7c4d1c4c..cc48bfde1f5988006f33d0dcd75d83a74e0156f4 100644 (file)
@@ -6,12 +6,12 @@ from sqlalchemy import Integer, String, ForeignKey, desc, select, func
 from sqlalchemy.test.schema import Table, Column
 from sqlalchemy.orm import mapper, relation, create_session, Query, attributes
 from sqlalchemy.orm.dynamic import AppenderMixin
-from sqlalchemy.test.testing import eq_
+from sqlalchemy.test.testing import eq_, AssertsCompiledSQL
 from sqlalchemy.util import function_named
 from test.orm import _base, _fixtures
 
 
-class DynamicTest(_fixtures.FixtureTest):
+class DynamicTest(_fixtures.FixtureTest, AssertsCompiledSQL):
     @testing.resolve_artifact_names
     def test_basic(self):
         mapper(User, users, properties={
@@ -26,6 +26,25 @@ class DynamicTest(_fixtures.FixtureTest):
             q.filter(User.id==7).all())
         eq_(self.static.user_address_result, q.all())
 
+    @testing.resolve_artifact_names
+    def test_statement(self):
+        """test that the .statement accessor returns the actual statement that
+        would render, without any _clones called."""
+        
+        mapper(User, users, properties={
+            'addresses':dynamic_loader(mapper(Address, addresses))
+        })
+        sess = create_session()
+        q = sess.query(User)
+
+        u = q.filter(User.id==7).first()
+        self.assert_compile(
+            u.addresses.statement, 
+            "SELECT addresses.id, addresses.user_id, addresses.email_address FROM "
+            "addresses WHERE :param_1 = addresses.user_id",
+            use_default_dialect=True
+        )
+        
     @testing.resolve_artifact_names
     def test_order_by(self):
         mapper(User, users, properties={
@@ -33,7 +52,11 @@ class DynamicTest(_fixtures.FixtureTest):
         })
         sess = create_session()
         u = sess.query(User).get(8)
-        eq_(list(u.addresses.order_by(desc(Address.email_address))), [Address(email_address=u'ed@wood.com'), Address(email_address=u'ed@lala.com'), Address(email_address=u'ed@bettyboop.com')])
+        eq_(
+            list(u.addresses.order_by(desc(Address.email_address))),
+             [Address(email_address=u'ed@wood.com'), Address(email_address=u'ed@lala.com'), 
+              Address(email_address=u'ed@bettyboop.com')]
+            )
 
     @testing.resolve_artifact_names
     def test_configured_order_by(self):
@@ -117,6 +140,34 @@ class DynamicTest(_fixtures.FixtureTest):
         assert o1 in i1.orders.all()
         assert i1 in o1.items.all()
 
+    @testing.resolve_artifact_names
+    def test_association_nonaliased(self):
+        mapper(Order, orders, properties={
+            'items':relation(Item, secondary=order_items, 
+                                lazy="dynamic", 
+                                order_by=order_items.c.item_id)
+        })
+        mapper(Item, items)
+
+        sess = create_session()
+        o = sess.query(Order).first()
+
+        self.assert_compile(
+            o.items,
+            "SELECT items.id AS items_id, items.description AS items_description FROM items,"
+            " order_items WHERE :param_1 = order_items.order_id AND items.id = order_items.item_id"
+            " ORDER BY order_items.item_id",
+            use_default_dialect=True
+        )
+
+        # filter criterion against the secondary table 
+        # works
+        eq_(
+            o.items.filter(order_items.c.item_id==2).all(),
+            [Item(id=2)]
+        )
+        
+        
     @testing.resolve_artifact_names
     def test_transient_detached(self):
         mapper(User, users, properties={
@@ -458,11 +509,8 @@ class SessionTest(_fixtures.FixtureTest):
         sess.delete(u)
         sess.close()
 
-
-def _create_backref_test(autoflush, saveuser):
-
     @testing.resolve_artifact_names
-    def test_backref(self):
+    def _backref_test(self, autoflush, saveuser):
         mapper(User, users, properties={
             'addresses':dynamic_loader(mapper(Address, addresses), backref='user')
         })
@@ -494,14 +542,17 @@ def _create_backref_test(autoflush, saveuser):
             sess.flush()
         eq_(list(u.addresses), [])
 
-    test_backref = function_named(
-        test_backref, "test%s%s" % ((autoflush and "_autoflush" or ""),
-                                    (saveuser and "_saveuser" or "_savead")))
-    setattr(SessionTest, test_backref.__name__, test_backref)
+    def test_backref_autoflush_saveuser(self):
+        self._backref_test(True, True)
+
+    def test_backref_autoflush_savead(self):
+        self._backref_test(True, False)
+
+    def test_backref_saveuser(self):
+        self._backref_test(False, True)
 
-for autoflush in (False, True):
-    for saveuser in (False, True):
-        _create_backref_test(autoflush, saveuser)
+    def test_backref_savead(self):
+        self._backref_test(False, False)
 
 class DontDereferenceTest(_base.MappedTest):
     @classmethod