]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The "use get" behavior of many-to-one relations, i.e. that a
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 8 Dec 2009 01:53:21 +0000 (01:53 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 8 Dec 2009 01:53:21 +0000 (01:53 +0000)
lazy load will fallback to the possibly cached query.get()
value, now works across join conditions where the two compared
types are not exactly the same class, but share the same
"affinity" - i.e. Integer and SmallInteger.  Also allows
combinations of reflected and non-reflected types to work
with 0.5 style type reflection, such as PGText/Text (note 0.6
reflects types as their generic versions).   [ticket:1556]
- types now support an "affinity comparison" operation, i.e.
that an Integer/SmallInteger are "compatible", or
a Text/String, PickleType/Binary, etc.  Part of
[ticket:1556].

CHANGES
lib/sqlalchemy/sql/expression.py
lib/sqlalchemy/types.py
test/orm/test_lazy_relations.py
test/sql/test_types.py

diff --git a/CHANGES b/CHANGES
index d7c30b55a30fb15498a0085356fa281eccd07584..1e7d759ff8718dcb50a284b38acd3a9edef241cd 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -773,6 +773,15 @@ CHANGES
       when configured on a joined-table subclass, introduced in
       version 0.5.6 as a result of the fix for [ticket:1480].
       [ticket:1616] thx to Scott Torborg.
+
+    - The "use get" behavior of many-to-one relations, i.e. that a 
+      lazy load will fallback to the possibly cached query.get() 
+      value, now works across join conditions where the two compared 
+      types are not exactly the same class, but share the same 
+      "affinity" - i.e. Integer and SmallInteger.  Also allows
+      combinations of reflected and non-reflected types to work
+      with 0.5 style type reflection, such as PGText/Text (note 0.6 
+      reflects types as their generic versions).   [ticket:1556]
       
 - sql
     - Fixed bug in two-phase transaction whereby commit() method
@@ -793,6 +802,11 @@ CHANGES
       (i.e. _CursorFairy) now proxies `__iter__()` to the 
       underlying cursor correctly. [ticket:1632]
 
+    - types now support an "affinity comparison" operation, i.e.
+      that an Integer/SmallInteger are "compatible", or 
+      a Text/String, PickleType/Binary, etc.  Part of 
+      [ticket:1556].
+
 - sqlite
     - sqlite dialect properly generates CREATE INDEX for a table
       that is in an alternate schema.  [ticket:1439]
index a7cba8161bd6c826fa5b512e75606cf9a5e224b3..eb36558ce8d7eb08d3b2bedc450a69cee8a5ec84 100644 (file)
@@ -2131,15 +2131,10 @@ class _BindParamClause(ColumnElement):
             return obj.type
 
     def compare(self, other, **kw):
-        """Compare this ``_BindParamClause`` to the given clause.
-
-        Since ``compare()`` is meant to compare statement syntax, this
-        method returns True if the two ``_BindParamClauses`` have just
-        the same type.
-
-        """
+        """Compare this ``_BindParamClause`` to the given clause."""
+        
         return isinstance(other, _BindParamClause) and \
-                    other.type.__class__ == self.type.__class__ and \
+                    self.type._compare_type_affinity(other.type) and \
                     self.value == other.value
 
     def __getstate__(self):
index 66b90ce0465b5491b0536dd813833a5ffc8a2fab..3c42de2b8087ea06228cd30794ba854b9c056334 100644 (file)
@@ -37,7 +37,7 @@ if util.jython:
     import array
 
 class AbstractType(Visitable):
-
+    
     def __init__(self, *args, **kwargs):
         pass
 
@@ -103,6 +103,19 @@ class AbstractType(Visitable):
 
         """
         return op
+        
+    @util.memoized_property
+    def _type_affinity(self):
+        """Return a rudimental 'affinity' value expressing the general class of type."""
+        
+        for i, t in enumerate(self.__class__.__mro__):
+            if t is TypeEngine or t is UserDefinedType:
+                return self.__class__.__mro__[i - 1]
+        else:
+            return self.__class__
+        
+    def _compare_type_affinity(self, other):
+        return self._type_affinity is other._type_affinity
 
     def __repr__(self):
         return "%s(%s)" % (
@@ -270,6 +283,10 @@ class TypeDecorator(AbstractType):
         self._impl_dict[dialect] = tt
         return tt
 
+    @util.memoized_property
+    def _type_affinity(self):
+        return self.impl._type_affinity
+
     def type_engine(self, dialect):
         impl = self.dialect_impl(dialect)
         if not isinstance(impl, TypeDecorator):
index 8c196cfcfbf3d257a0058554ddd1991212db6018..1cdd8213005308dc813ed76ec7ed5ec2023436aa 100644 (file)
@@ -6,7 +6,8 @@ from sqlalchemy import exc as sa_exc
 from sqlalchemy.orm import attributes
 import sqlalchemy as sa
 from sqlalchemy.test import testing
-from sqlalchemy import Integer, String, ForeignKey
+from sqlalchemy import Integer, String, ForeignKey, SmallInteger
+from sqlalchemy.types import TypeDecorator
 from sqlalchemy.test.schema import Table
 from sqlalchemy.test.schema import Column
 from sqlalchemy.orm import mapper, relation, create_session
@@ -284,6 +285,55 @@ class LazyTest(_fixtures.FixtureTest):
             self.assert_sql_count(testing.db, go, 0)
             sa.orm.clear_mappers()
 
+    @testing.resolve_artifact_names
+    def test_uses_get_compatible_types(self):
+        """test the use_get optimization with compatible but non-identical types"""
+
+        class IntDecorator(TypeDecorator):
+            impl = Integer
+
+        class SmallintDecorator(TypeDecorator):
+            impl = SmallInteger
+        
+        class SomeDBInteger(sa.Integer):
+            pass
+            
+        for tt in [
+            Integer,
+            SmallInteger,
+            IntDecorator,
+            SmallintDecorator,
+            SomeDBInteger,
+        ]:
+            m = sa.MetaData()
+            users = Table('users', m, 
+                Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+                Column('name', String(30), nullable=False),
+            )
+            addresses = Table('addresses', m,
+                  Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+                  Column('user_id', tt, ForeignKey('users.id')),
+                  Column('email_address', String(50), nullable=False),
+            )
+
+            mapper(Address, addresses, properties = dict(
+                user = relation(mapper(User, users))
+            ))
+
+            sess = create_session(bind=testing.db)
+
+            # load address
+            a1 = sess.query(Address).filter_by(email_address="ed@wood.com").one()
+
+            # load user that is attached to the address
+            u1 = sess.query(User).get(8)
+
+            def go():
+                # lazy load of a1.user should get it from the session
+                assert a1.user is u1
+            self.assert_sql_count(testing.db, go, 0)
+            sa.orm.clear_mappers()
+
     @testing.resolve_artifact_names
     def test_many_to_one(self):
         mapper(Address, addresses, properties = dict(
index f9de6dc622db589979db8f64a08d8da423a9f1b0..a332dc1bd31b8d7d90dd06e6e5972d4882d66552 100644 (file)
@@ -55,7 +55,22 @@ class AdaptTest(TestBase):
                 else:
                     assert False, "%r matches none of %r for dialect %s" % (compiled, expected, dialect.name)
             
-
+class TypeAffinityTest(TestBase):
+    def test_type_affinity(self):
+        for t1, t2, comp in [
+            (Integer(), SmallInteger(), True),
+            (Integer(), String(), False),
+            (Integer(), Integer(), True),
+            (Text(), String(), True),
+            (Text(), Unicode(), True),
+            (Binary(), Integer(), False),
+            (Binary(), PickleType(), True),
+            (PickleType(), Binary(), True),
+            (PickleType(), PickleType(), True),
+        ]:
+            eq_(t1._compare_type_affinity(t2), comp, "%s %s" % (t1, t2))
+        
+    
 class UserDefinedTest(TestBase):
     """tests user-defined types."""