From: Mike Bayer Date: Tue, 8 Dec 2009 01:54:08 +0000 (+0000) Subject: - backport of r6540 X-Git-Tag: rel_0_5_7~12 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=2305e22d6bde2161f5bee25514f0a8444cce8416;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - backport of r6540 - 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] - 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]. --- diff --git a/CHANGES b/CHANGES index e9ddf26ae2..b97828ff05 100644 --- a/CHANGES +++ b/CHANGES @@ -45,6 +45,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 @@ -65,6 +74,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] diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 68669a1502..01af1de920 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -372,7 +372,6 @@ class LazyLoader(AbstractRelationLoader): # determine if our "lazywhere" clause is the same as the mapper's # get() clause. then we can just use mapper.get() - #from sqlalchemy.orm import query self.use_get = not self.uselist and self.mapper._get_clause[0].compare(self.__lazywhere) if self.use_get: self.logger.info("%s will use query.get() to optimize instance loads" % self) diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 83897ef051..cf08ec1951 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -2003,15 +2003,12 @@ class _BindParamClause(ColumnElement): else: return obj.type - def compare(self, other): - """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. - - """ - return isinstance(other, _BindParamClause) and other.type.__class__ == self.type.__class__ and self.value == other.value + def compare(self, other, **kw): + """Compare this ``_BindParamClause`` to the given clause.""" + + return isinstance(other, _BindParamClause) and \ + self.type._compare_type_affinity(other.type) and \ + self.value == other.value def __getstate__(self): """execute a deferred value for serialization purposes.""" diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index a03d6137df..8e6accdbf5 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -81,6 +81,19 @@ class AbstractType(object): By default, returns the operator unchanged. """ 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: + 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)" % ( @@ -236,6 +249,10 @@ class TypeDecorator(AbstractType): self._impl_dict[dialect] = tt return tt + @util.memoized_property + def _type_affinity(self): + return self.impl._type_affinity + def load_dialect_impl(self, dialect): """Loads the dialect-specific implementation of this type. diff --git a/test/orm/test_lazy_relations.py b/test/orm/test_lazy_relations.py index 8867232b24..ea084e0458 100644 --- a/test/orm/test_lazy_relations.py +++ b/test/orm/test_lazy_relations.py @@ -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 @@ -285,6 +286,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( diff --git a/test/sql/test_types.py b/test/sql/test_types.py index 15799358a7..67096755d1 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -95,6 +95,21 @@ class AdaptTest(TestBase): ]: assert isinstance(start.dialect_impl(dialect), test), "wanted %r got %r" % (test, start.dialect_impl(dialect)) +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):