summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2013-11-23 17:03:48 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2013-11-23 17:03:48 -0500
commitae4629e6a0ff442a819b80f418dee76c25c50938 (patch)
treec2fccb7619c5ee3bf2a2433f489b54b8ebf2d251
parent6b79d2ea7951abc2bb6083b541db0fbf71590dd3 (diff)
downloadsqlalchemy-ae4629e6a0ff442a819b80f418dee76c25c50938.tar.gz
- Some refinements to the :class:`.AliasedClass` construct with regards
to descriptors, like hybrids, synonyms, composites, user-defined descriptors, etc. The attribute adaptation which goes on has been made more robust, such that if a descriptor returns another instrumented attribute, rather than a compound SQL expression element, the operation will still proceed. Addtionally, the "adapted" operator will retain its class; previously, a change in class from ``InstrumentedAttribute`` to ``QueryableAttribute`` (a superclass) would interact with Python's operator system such that an expression like ``aliased(MyClass.x) > MyClass.x`` would reverse itself to read ``myclass.x < myclass_1.x``. The adapted attribute will also refer to the new :class:`.AliasedClass` as its parent which was not always the case before. [ticket:2872]
-rw-r--r--doc/build/changelog/changelog_09.rst18
-rw-r--r--lib/sqlalchemy/orm/attributes.py8
-rw-r--r--lib/sqlalchemy/orm/util.py20
-rw-r--r--test/orm/test_froms.py34
-rw-r--r--test/orm/test_of_type.py4
-rw-r--r--test/orm/test_query.py9
-rw-r--r--test/orm/test_utils.py182
7 files changed, 167 insertions, 108 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst
index cb37cb990..f0d58f205 100644
--- a/doc/build/changelog/changelog_09.rst
+++ b/doc/build/changelog/changelog_09.rst
@@ -15,6 +15,24 @@
:version: 0.9.0b2
.. change::
+ :tags: bug, orm
+ :tickets: 2872
+
+ Some refinements to the :class:`.AliasedClass` construct with regards
+ to descriptors, like hybrids, synonyms, composites, user-defined
+ descriptors, etc. The attribute
+ adaptation which goes on has been made more robust, such that if a descriptor
+ returns another instrumented attribute, rather than a compound SQL
+ expression element, the operation will still proceed.
+ Addtionally, the "adapted" operator will retain its class; previously,
+ a change in class from ``InstrumentedAttribute`` to ``QueryableAttribute``
+ (a superclass) would interact with Python's operator system such that
+ an expression like ``aliased(MyClass.x) > MyClass.x`` would reverse itself
+ to read ``myclass.x < myclass_1.x``. The adapted attribute will also
+ refer to the new :class:`.AliasedClass` as its parent which was not
+ always the case before.
+
+ .. change::
:tags: feature, sql
:tickets: 2867
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 6071b565d..e3c6a3512 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -149,6 +149,12 @@ class QueryableAttribute(interfaces._MappedAttribute,
return self.comparator._query_clause_element()
+ def adapt_to_entity(self, adapt_to_entity):
+ assert not self._of_type
+ return self.__class__(adapt_to_entity.entity, self.key, impl=self.impl,
+ comparator=self.comparator.adapt_to_entity(adapt_to_entity),
+ parententity=adapt_to_entity)
+
def of_type(self, cls):
return QueryableAttribute(
self.class_,
@@ -270,7 +276,7 @@ def create_proxied_attribute(descriptor):
return self._comparator
def adapt_to_entity(self, adapt_to_entity):
- return self.__class__(self.class_, self.key, self.descriptor,
+ return self.__class__(adapt_to_entity.entity, self.key, self.descriptor,
self._comparator,
adapt_to_entity)
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 973707249..1b8f53c9d 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -331,8 +331,10 @@ class AliasedClass(object):
else:
raise AttributeError(key)
- if isinstance(attr, attributes.QueryableAttribute):
- return _aliased_insp._adapt_prop(attr, key)
+ if isinstance(attr, PropComparator):
+ ret = attr.adapt_to_entity(_aliased_insp)
+ setattr(self, key, ret)
+ return ret
elif hasattr(attr, 'func_code'):
is_method = getattr(_aliased_insp._target, key, None)
if is_method and is_method.__self__ is not None:
@@ -343,7 +345,8 @@ class AliasedClass(object):
ret = attr.__get__(None, self)
if isinstance(ret, PropComparator):
return ret.adapt_to_entity(_aliased_insp)
- return ret
+ else:
+ return ret
else:
return attr
@@ -465,17 +468,6 @@ class AliasedInsp(_InspectionAttr):
'parentmapper': self.mapper}
)
- def _adapt_prop(self, existing, key):
- comparator = existing.comparator.adapt_to_entity(self)
- queryattr = attributes.QueryableAttribute(
- self.entity, key,
- impl=existing.impl,
- parententity=self,
- comparator=comparator)
- setattr(self.entity, key, queryattr)
- return queryattr
-
-
def _entity_for_mapper(self, mapper):
self_poly = self.with_polymorphic_mappers
if mapper in self_poly:
diff --git a/test/orm/test_froms.py b/test/orm/test_froms.py
index 8dc06a630..fd4bef71a 100644
--- a/test/orm/test_froms.py
+++ b/test/orm/test_froms.py
@@ -1817,7 +1817,6 @@ class SelectFromTest(QueryTest, AssertsCompiledSQL):
users, User = self.tables.users, self.classes.User
-
mapper(User, users)
sess = create_session()
@@ -1826,21 +1825,21 @@ class SelectFromTest(QueryTest, AssertsCompiledSQL):
ualias = aliased(User)
self.assert_compile(
- sess.query(User).join(sel, User.id>sel.c.id),
+ sess.query(User).join(sel, User.id > sel.c.id),
"SELECT users.id AS users_id, users.name AS users_name FROM "
"users JOIN (SELECT users.id AS id, users.name AS name FROM "
"users WHERE users.id IN (:id_1, :id_2)) AS anon_1 ON users.id > anon_1.id",
)
self.assert_compile(
- sess.query(ualias).select_entity_from(sel).filter(ualias.id>sel.c.id),
+ sess.query(ualias).select_entity_from(sel).filter(ualias.id > sel.c.id),
"SELECT users_1.id AS users_1_id, users_1.name AS users_1_name FROM "
"users AS users_1, (SELECT users.id AS id, users.name AS name FROM "
"users WHERE users.id IN (:id_1, :id_2)) AS anon_1 WHERE users_1.id > anon_1.id",
)
self.assert_compile(
- sess.query(ualias).select_entity_from(sel).join(ualias, ualias.id>sel.c.id),
+ sess.query(ualias).select_entity_from(sel).join(ualias, ualias.id > sel.c.id),
"SELECT users_1.id AS users_1_id, users_1.name AS users_1_name "
"FROM (SELECT users.id AS id, users.name AS name "
"FROM users WHERE users.id IN (:id_1, :id_2)) AS anon_1 "
@@ -1848,29 +1847,26 @@ class SelectFromTest(QueryTest, AssertsCompiledSQL):
)
self.assert_compile(
- sess.query(ualias).select_entity_from(sel).join(ualias, ualias.id>User.id),
+ sess.query(ualias).select_entity_from(sel).join(ualias, ualias.id > User.id),
"SELECT users_1.id AS users_1_id, users_1.name AS users_1_name "
"FROM (SELECT users.id AS id, users.name AS name FROM "
"users WHERE users.id IN (:id_1, :id_2)) AS anon_1 "
- "JOIN users AS users_1 ON anon_1.id < users_1.id"
+ "JOIN users AS users_1 ON users_1.id > anon_1.id"
)
salias = aliased(User, sel)
self.assert_compile(
- sess.query(salias).join(ualias, ualias.id>salias.id),
+ sess.query(salias).join(ualias, ualias.id > salias.id),
"SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name FROM "
"(SELECT users.id AS id, users.name AS name FROM users WHERE users.id "
"IN (:id_1, :id_2)) AS anon_1 JOIN users AS users_1 ON users_1.id > anon_1.id",
)
-
- # this one uses an explicit join(left, right, onclause) so works
self.assert_compile(
- sess.query(ualias).select_entity_from(join(sel, ualias, ualias.id>sel.c.id)),
+ sess.query(ualias).select_entity_from(join(sel, ualias, ualias.id > sel.c.id)),
"SELECT users_1.id AS users_1_id, users_1.name AS users_1_name FROM "
"(SELECT users.id AS id, users.name AS name FROM users WHERE users.id "
- "IN (:id_1, :id_2)) AS anon_1 JOIN users AS users_1 ON users_1.id > anon_1.id",
- use_default_dialect=True
+ "IN (:id_1, :id_2)) AS anon_1 JOIN users AS users_1 ON users_1.id > anon_1.id"
)
@@ -1884,25 +1880,31 @@ class SelectFromTest(QueryTest, AssertsCompiledSQL):
self.assert_compile(
sess.query(User).select_from(ua).join(User, ua.name > User.name),
"SELECT users.id AS users_id, users.name AS users_name "
- "FROM users AS users_1 JOIN users ON users.name < users_1.name"
+ "FROM users AS users_1 JOIN users ON users_1.name > users.name"
)
self.assert_compile(
sess.query(User.name).select_from(ua).join(User, ua.name > User.name),
"SELECT users.name AS users_name FROM users AS users_1 "
- "JOIN users ON users.name < users_1.name"
+ "JOIN users ON users_1.name > users.name"
)
self.assert_compile(
sess.query(ua.name).select_from(ua).join(User, ua.name > User.name),
"SELECT users_1.name AS users_1_name FROM users AS users_1 "
- "JOIN users ON users.name < users_1.name"
+ "JOIN users ON users_1.name > users.name"
)
self.assert_compile(
sess.query(ua).select_from(User).join(ua, ua.name > User.name),
"SELECT users_1.id AS users_1_id, users_1.name AS users_1_name "
- "FROM users JOIN users AS users_1 ON users.name < users_1.name"
+ "FROM users JOIN users AS users_1 ON users_1.name > users.name"
+ )
+
+ self.assert_compile(
+ sess.query(ua).select_from(User).join(ua, User.name > ua.name),
+ "SELECT users_1.id AS users_1_id, users_1.name AS users_1_name "
+ "FROM users JOIN users AS users_1 ON users.name > users_1.name"
)
# this is tested in many other places here, just adding it
diff --git a/test/orm/test_of_type.py b/test/orm/test_of_type.py
index 67baddb52..836d85cc7 100644
--- a/test/orm/test_of_type.py
+++ b/test/orm/test_of_type.py
@@ -506,7 +506,7 @@ class SubclassRelationshipTest(testing.AssertsCompiledSQL, fixtures.DeclarativeM
"FROM job AS job_1 LEFT OUTER JOIN subjob AS subjob_1 "
"ON job_1.id = subjob_1.id "
"WHERE data_container.id = job_1.container_id "
- "AND job.id > job_1.id)"
+ "AND job_1.id < job.id)"
)
def test_any_walias(self):
@@ -531,7 +531,7 @@ class SubclassRelationshipTest(testing.AssertsCompiledSQL, fixtures.DeclarativeM
"WHERE EXISTS (SELECT 1 "
"FROM job AS job_1 "
"WHERE data_container.id = job_1.container_id "
- "AND job.id > job_1.id AND job_1.type = :type_1)"
+ "AND job_1.id < job.id AND job_1.type = :type_1)"
)
def test_join_wpoly(self):
diff --git a/test/orm/test_query.py b/test/orm/test_query.py
index 619836ae4..1b6c1fc3a 100644
--- a/test/orm/test_query.py
+++ b/test/orm/test_query.py
@@ -227,10 +227,13 @@ class RawSelectTest(QueryTest, AssertsCompiledSQL):
where(uu.id == Address.user_id).\
correlate(uu).as_scalar()
]),
- # curious, "address.user_id = uu.id" is reversed here
+ # for a long time, "uu.id = address.user_id" was reversed;
+ # this was resolved as of #2872 and had to do with
+ # InstrumentedAttribute.__eq__() taking precedence over
+ # QueryableAttribute.__eq__()
"SELECT uu.name, addresses.id, "
"(SELECT count(addresses.id) AS count_1 "
- "FROM addresses WHERE addresses.user_id = uu.id) AS anon_1 "
+ "FROM addresses WHERE uu.id = addresses.user_id) AS anon_1 "
"FROM users AS uu, addresses"
)
@@ -1986,7 +1989,7 @@ class HintsTest(QueryTest, AssertsCompiledSQL):
"SELECT users.id AS users_id, users.name AS users_name, "
"users_1.id AS users_1_id, users_1.name AS users_1_name "
"FROM users INNER JOIN users AS users_1 USE INDEX (col1_index,col2_index) "
- "ON users.id < users_1.id",
+ "ON users_1.id > users.id",
dialect=dialect
)
diff --git a/test/orm/test_utils.py b/test/orm/test_utils.py
index 96878424f..ae225ad92 100644
--- a/test/orm/test_utils.py
+++ b/test/orm/test_utils.py
@@ -5,27 +5,31 @@ from sqlalchemy import util
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import Table
-from sqlalchemy.orm import aliased, with_polymorphic
-from sqlalchemy.orm import mapper, create_session
+from sqlalchemy.orm import aliased, with_polymorphic, synonym
+from sqlalchemy.orm import mapper, create_session, Session
from sqlalchemy.testing import fixtures
from test.orm import _fixtures
from sqlalchemy.testing import eq_, is_
from sqlalchemy.orm.path_registry import PathRegistry, RootRegistry
from sqlalchemy import inspect
+from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
+from sqlalchemy.testing import AssertsCompiledSQL
-class AliasedClassTest(fixtures.TestBase):
- def point_map(self, cls):
+class AliasedClassTest(fixtures.TestBase, AssertsCompiledSQL):
+ __dialect__ = 'default'
+
+ def _fixture(self, cls, properties={}):
table = Table('point', MetaData(),
Column('id', Integer(), primary_key=True),
Column('x', Integer),
Column('y', Integer))
- mapper(cls, table)
+ mapper(cls, table, properties=properties)
return table
def test_simple(self):
class Point(object):
pass
- table = self.point_map(Point)
+ table = self._fixture(Point)
alias = aliased(Point)
@@ -36,48 +40,51 @@ class AliasedClassTest(fixtures.TestBase):
assert Point.id.__clause_element__().table is table
assert alias.id.__clause_element__().table is not table
- def test_notcallable(self):
+ def test_not_instantiatable(self):
class Point(object):
pass
- table = self.point_map(Point)
+ table = self._fixture(Point)
alias = aliased(Point)
assert_raises(TypeError, alias)
- def test_instancemethods(self):
+ def test_instancemethod(self):
class Point(object):
def zero(self):
self.x, self.y = 0, 0
- table = self.point_map(Point)
+ table = self._fixture(Point)
alias = aliased(Point)
assert Point.zero
+ # TODO: I don't quite understand this
+ # still
if util.py2k:
- # TODO: what is this testing ??
assert not getattr(alias, 'zero')
+ else:
+ assert getattr(alias, 'zero')
- def test_classmethods(self):
+ def test_classmethod(self):
class Point(object):
@classmethod
def max_x(cls):
return 100
- table = self.point_map(Point)
+ table = self._fixture(Point)
alias = aliased(Point)
assert Point.max_x
assert alias.max_x
- assert Point.max_x() == alias.max_x()
+ assert Point.max_x() == alias.max_x() == 100
- def test_simpleproperties(self):
+ def test_simple_property(self):
class Point(object):
@property
def max_x(self):
return 100
- table = self.point_map(Point)
+ table = self._fixture(Point)
alias = aliased(Point)
assert Point.max_x
@@ -86,7 +93,6 @@ class AliasedClassTest(fixtures.TestBase):
assert Point.max_x is alias.max_x
def test_descriptors(self):
- """Tortured..."""
class descriptor(object):
def __init__(self, fn):
@@ -105,7 +111,7 @@ class AliasedClassTest(fixtures.TestBase):
def thing(self, arg):
return arg.center
- table = self.point_map(Point)
+ table = self._fixture(Point)
alias = aliased(Point)
assert Point.thing != (0, 0)
@@ -115,74 +121,106 @@ class AliasedClassTest(fixtures.TestBase):
assert alias.thing != (0, 0)
assert alias.thing.method() == 'method'
- def test_hybrid_descriptors(self):
+ def _assert_has_table(self, expr, table):
from sqlalchemy import Column # override testlib's override
- import types
-
- class MethodDescriptor(object):
- def __init__(self, func):
- self.func = func
- def __get__(self, instance, owner):
- if instance is None:
- if util.py2k:
- args = (self.func, owner, owner.__class__)
- else:
- args = (self.func, owner)
- else:
- if util.py2k:
- args = (self.func, instance, owner)
- else:
- args = (self.func, instance)
- return types.MethodType(*args)
-
- class PropertyDescriptor(object):
- def __init__(self, fget, fset, fdel):
- self.fget = fget
- self.fset = fset
- self.fdel = fdel
- def __get__(self, instance, owner):
- if instance is None:
- return self.fget(owner)
- else:
- return self.fget(instance)
- def __set__(self, instance, value):
- self.fset(instance, value)
- def __delete__(self, instance):
- self.fdel(instance)
- hybrid = MethodDescriptor
- def hybrid_property(fget, fset=None, fdel=None):
- return PropertyDescriptor(fget, fset, fdel)
-
- def assert_table(expr, table):
- for child in expr.get_children():
- if isinstance(child, Column):
- assert child.table is table
+ for child in expr.get_children():
+ if isinstance(child, Column):
+ assert child.table is table
+ def test_hybrid_descriptor_one(self):
class Point(object):
def __init__(self, x, y):
self.x, self.y = x, y
- @hybrid
+
+ @hybrid_method
def left_of(self, other):
return self.x < other.x
- double_x = hybrid_property(lambda self: self.x * 2)
+ self._fixture(Point)
+ alias = aliased(Point)
+ sess = Session()
+
+ self.assert_compile(
+ sess.query(alias).filter(alias.left_of(Point)),
+ "SELECT point_1.id AS point_1_id, point_1.x AS point_1_x, "
+ "point_1.y AS point_1_y FROM point AS point_1, point "
+ "WHERE point_1.x < point.x"
+ )
+
+ def test_hybrid_descriptor_two(self):
+ class Point(object):
+ def __init__(self, x, y):
+ self.x, self.y = x, y
+
+ @hybrid_property
+ def double_x(self):
+ return self.x * 2
- table = self.point_map(Point)
+ self._fixture(Point)
alias = aliased(Point)
- alias_table = alias.x.__clause_element__().table
- assert table is not alias_table
- p1 = Point(-10, -10)
- p2 = Point(20, 20)
+ eq_(str(Point.double_x), "point.x * :x_1")
+ eq_(str(alias.double_x), "point_1.x * :x_1")
- assert p1.left_of(p2)
- assert p1.double_x == -20
+ sess = Session()
+
+ self.assert_compile(
+ sess.query(alias).filter(alias.double_x > Point.x),
+ "SELECT point_1.id AS point_1_id, point_1.x AS point_1_x, "
+ "point_1.y AS point_1_y FROM point AS point_1, point "
+ "WHERE point_1.x * :x_1 > point.x"
+ )
+
+ def test_hybrid_descriptor_three(self):
+ class Point(object):
+ def __init__(self, x, y):
+ self.x, self.y = x, y
- assert_table(Point.double_x, table)
- assert_table(alias.double_x, alias_table)
+ @hybrid_property
+ def x_alone(self):
+ return self.x
- assert_table(Point.left_of(p2), table)
- assert_table(alias.left_of(p2), alias_table)
+ self._fixture(Point)
+ alias = aliased(Point)
+
+ eq_(str(Point.x_alone), "Point.x")
+ eq_(str(alias.x_alone), "AliasedClass_Point.x")
+
+ assert Point.x_alone is Point.x
+
+ eq_(str(alias.x_alone == alias.x), "point_1.x = point_1.x")
+
+ a2 = aliased(Point)
+ eq_(str(a2.x_alone == alias.x), "point_1.x = point_2.x")
+
+ sess = Session()
+
+ self.assert_compile(
+ sess.query(alias).filter(alias.x_alone > Point.x),
+ "SELECT point_1.id AS point_1_id, point_1.x AS point_1_x, "
+ "point_1.y AS point_1_y FROM point AS point_1, point "
+ "WHERE point_1.x > point.x"
+ )
+
+ def test_proxy_descriptor_one(self):
+ class Point(object):
+ def __init__(self, x, y):
+ self.x, self.y = x, y
+
+ self._fixture(Point, properties={
+ 'x_syn': synonym("x")
+ })
+ alias = aliased(Point)
+
+ eq_(str(Point.x_syn), "Point.x_syn")
+ eq_(str(alias.x_syn), "AliasedClass_Point.x_syn")
+
+ sess = Session()
+ self.assert_compile(
+ sess.query(alias.x_syn).filter(alias.x_syn > Point.x_syn),
+ "SELECT point_1.x AS point_1_x FROM point AS point_1, point "
+ "WHERE point_1.x > point.x"
+ )
class IdentityKeyTest(_fixtures.FixtureTest):
run_inserts = None