diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-04-26 15:04:54 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-04-26 15:04:54 -0400 |
commit | 34dc02773b6bdb2751b301146f2f3c9a938d4cbb (patch) | |
tree | dabfc3b285816838d45a8cf447b25fa017c8a0e2 | |
parent | 0f26f549191fc74bfebac319b39323cb865f7a50 (diff) | |
parent | 22c4ae0aaf3a00e9020c3950a53d2a3238b2091c (diff) | |
download | sqlalchemy-34dc02773b6bdb2751b301146f2f3c9a938d4cbb.tar.gz |
merge default
-rw-r--r-- | doc/build/changelog/changelog_08.rst | 21 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 84 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 7 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 108 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/expression.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/util.py | 2 | ||||
-rw-r--r-- | test/ext/declarative/test_inheritance.py | 95 | ||||
-rw-r--r-- | test/orm/inheritance/test_relationship.py | 264 | ||||
-rw-r--r-- | test/orm/test_joins.py | 212 | ||||
-rw-r--r-- | test/orm/test_query.py | 15 |
10 files changed, 613 insertions, 197 deletions
diff --git a/doc/build/changelog/changelog_08.rst b/doc/build/changelog/changelog_08.rst index 97b3626af..9d392e778 100644 --- a/doc/build/changelog/changelog_08.rst +++ b/doc/build/changelog/changelog_08.rst @@ -8,6 +8,27 @@ .. change:: :tags: bug, orm + :tickets: 2714 + + A significant improvement to the inner workings of query.join(), + such that the decisionmaking involved on how to join has been + dramatically simplified. New test cases now pass such as + multiple joins extending from the middle of an already complex + series of joins involving inheritance and such. Joining from + deeply nested subquery structures is still complicated and + not without caveats, but with these improvements the edge + cases are hopefully pushed even farther out to the edges. + + .. change:: + :tags: feature, orm + :tickets: 2673 + + Added a convenience method to Query that turns a query into an + EXISTS subquery of the form + ``EXISTS (SELECT 1 FROM ... WHERE ...)``. + + .. change:: + :tags: bug, orm Added a conditional to the unpickling process for ORM mapped objects, such that if the reference to the object diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index b18e28abb..c9f3a2699 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -1812,9 +1812,9 @@ class Query(object): "are the same entity" % (left, right)) - right, right_is_aliased, onclause = self._prepare_right_side( + right, onclause = self._prepare_right_side( right, onclause, - outerjoin, create_aliases, + create_aliases, prop) # if joining on a MapperProperty path, @@ -1825,16 +1825,11 @@ class Query(object): 'prev': ((left, right, prop.key), self._joinpoint) }) else: - self._joinpoint = { - '_joinpoint_entity': right - } + self._joinpoint = {'_joinpoint_entity': right} - self._join_to_left(left, right, - right_is_aliased, - onclause, outerjoin) + self._join_to_left(left, right, onclause, outerjoin) - def _prepare_right_side(self, right, onclause, outerjoin, - create_aliases, prop): + def _prepare_right_side(self, right, onclause, create_aliases, prop): info = inspect(right) right_mapper, right_selectable, right_is_aliased = \ @@ -1911,48 +1906,22 @@ class Query(object): ) ) - return right, right_is_aliased, onclause + return right, onclause - def _join_to_left(self, left, right, right_is_aliased, - onclause, outerjoin): + def _join_to_left(self, left, right, onclause, outerjoin): info = inspect(left) - left_mapper, left_selectable, left_is_aliased = \ - getattr(info, 'mapper', None),\ - info.selectable,\ - getattr(info, 'is_aliased_class', False) + left_mapper = getattr(info, 'mapper', None) + left_selectable = info.selectable - # this is an overly broad assumption here, but there's a - # very wide variety of situations where we rely upon orm.join's - # adaption to glue clauses together, with joined-table inheritance's - # wide array of variables taking up most of the space. - # Setting the flag here is still a guess, so it is a bug - # that we don't have definitive criterion to determine when - # adaption should be enabled (or perhaps that we're even doing the - # whole thing the way we are here). - join_to_left = not right_is_aliased and not left_is_aliased - - if self._from_obj and left_selectable is not None: + if self._from_obj: replace_clause_index, clause = sql_util.find_join_source( self._from_obj, left_selectable) if clause is not None: - # the entire query's FROM clause is an alias of itself (i.e. - # from_self(), similar). if the left clause is that one, - # ensure it adapts to the left side. - if self._from_obj_alias and clause is self._from_obj[0]: - join_to_left = True - - # An exception case where adaption to the left edge is not - # desirable. See above note on join_to_left. - if join_to_left and isinstance(clause, expression.Join) and \ - sql_util.clause_is_present(left_selectable, clause): - join_to_left = False - try: clause = orm_join(clause, right, - onclause, isouter=outerjoin, - join_to_left=join_to_left) + onclause, isouter=outerjoin) except sa_exc.ArgumentError, ae: raise sa_exc.InvalidRequestError( "Could not find a FROM clause to join from. " @@ -1971,18 +1940,13 @@ class Query(object): break else: clause = left - elif left_selectable is not None: - clause = left_selectable else: - clause = None + clause = left_selectable - if clause is None: - raise sa_exc.InvalidRequestError( - "Could not find a FROM clause to join from") + assert clause is not None try: - clause = orm_join(clause, right, onclause, - isouter=outerjoin, join_to_left=join_to_left) + clause = orm_join(clause, right, onclause, isouter=outerjoin) except sa_exc.ArgumentError, ae: raise sa_exc.InvalidRequestError( "Could not find a FROM clause to join from. " @@ -2373,6 +2337,26 @@ class Query(object): kwargs.get('offset') is not None or kwargs.get('distinct', False)) + def exists(self): + """A convenience method that turns a query into an EXISTS subquery + of the form EXISTS (SELECT 1 FROM ... WHERE ...). + + e.g.:: + + q = session.query(User).filter(User.name == 'fred') + session.query(q.exists()) + + Producing SQL similar to:: + + SELECT EXISTS ( + SELECT 1 FROM users WHERE users.name = :name_1 + ) AS anon_1 + + .. versionadded:: 0.8.1 + + """ + return sql.exists(self.with_entities('1').statement) + def count(self): """Return a count of rows this Query would return. diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 0eed50ea4..6660a39ef 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -1152,7 +1152,6 @@ class JoinedLoader(AbstractRelationshipLoader): towrap = context.eager_joins.setdefault(entity_key, default_towrap) - join_to_left = False if adapter: if getattr(adapter, 'aliased_class', None): onclause = getattr( @@ -1168,11 +1167,6 @@ class JoinedLoader(AbstractRelationshipLoader): self.key, self.parent_property ) - if onclause is self.parent_property: - # TODO: this is a temporary hack to - # account for polymorphic eager loads where - # the eagerload is referencing via of_type(). - join_to_left = True else: onclause = self.parent_property @@ -1182,7 +1176,6 @@ class JoinedLoader(AbstractRelationshipLoader): towrap, clauses.aliased_class, onclause, - join_to_left=join_to_left, isouter=not innerjoin ) diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index f3b8e271d..35cb0bdf5 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -870,73 +870,59 @@ class _ORMJoin(expression.Join): __visit_name__ = expression.Join.__visit_name__ - def __init__(self, left, right, onclause=None, - isouter=False, join_to_left=True): - adapt_from = None - - if hasattr(left, '_orm_mappers'): - left_mapper = left._orm_mappers[1] - if join_to_left: - adapt_from = left.right - else: - info = inspection.inspect(left) - left_mapper = getattr(info, 'mapper', None) - left = info.selectable - left_is_aliased = getattr(info, 'is_aliased_class', False) + def __init__(self, left, right, onclause=None, isouter=False): + + left_info = inspection.inspect(left) + left_orm_info = getattr(left, '_joined_from_info', left_info) - if join_to_left and (left_is_aliased or not left_mapper): - adapt_from = left + right_info = inspection.inspect(right) + adapt_to = right_info.selectable - info = inspection.inspect(right) - right_mapper = getattr(info, 'mapper', None) - right = info.selectable - right_is_aliased = getattr(info, 'is_aliased_class', False) + self._joined_from_info = right_info - if right_is_aliased: - adapt_to = right + if isinstance(onclause, basestring): + onclause = getattr(left_orm_info.entity, onclause) + + if isinstance(onclause, attributes.QueryableAttribute): + on_selectable = onclause.comparator._source_selectable() + prop = onclause.property + elif isinstance(onclause, MapperProperty): + prop = onclause + on_selectable = prop.parent.selectable else: - adapt_to = None - - if left_mapper or right_mapper: - self._orm_mappers = (left_mapper, right_mapper) - - if isinstance(onclause, basestring): - prop = left_mapper.get_property(onclause) - elif isinstance(onclause, attributes.QueryableAttribute): - if adapt_from is None: - adapt_from = onclause.comparator._source_selectable() - prop = onclause.property - elif isinstance(onclause, MapperProperty): - prop = onclause + prop = None + + if prop: + if sql_util.clause_is_present(on_selectable, left_info.selectable): + adapt_from = on_selectable else: - prop = None + adapt_from = left_info.selectable - if prop: - pj, sj, source, dest, \ + pj, sj, source, dest, \ secondary, target_adapter = prop._create_joins( - source_selectable=adapt_from, - dest_selectable=adapt_to, - source_polymorphic=True, - dest_polymorphic=True, - of_type=right_mapper) - - if sj is not None: - left = sql.join(left, secondary, pj, isouter) - onclause = sj - else: - onclause = pj - self._target_adapter = target_adapter + source_selectable=adapt_from, + dest_selectable=adapt_to, + source_polymorphic=True, + dest_polymorphic=True, + of_type=right_info.mapper) + + if sj is not None: + left = sql.join(left, secondary, pj, isouter) + onclause = sj + else: + onclause = pj + self._target_adapter = target_adapter expression.Join.__init__(self, left, right, onclause, isouter) - def join(self, right, onclause=None, isouter=False, join_to_left=True): - return _ORMJoin(self, right, onclause, isouter, join_to_left) + def join(self, right, onclause=None, isouter=False, join_to_left=None): + return _ORMJoin(self, right, onclause, isouter) - def outerjoin(self, right, onclause=None, join_to_left=True): - return _ORMJoin(self, right, onclause, True, join_to_left) + def outerjoin(self, right, onclause=None, join_to_left=None): + return _ORMJoin(self, right, onclause, True) -def join(left, right, onclause=None, isouter=False, join_to_left=True): +def join(left, right, onclause=None, isouter=False, join_to_left=None): """Produce an inner join between left and right clauses. :func:`.orm.join` is an extension to the core join interface @@ -947,11 +933,6 @@ def join(left, right, onclause=None, isouter=False, join_to_left=True): be a SQL expression, or an attribute or string name referencing a configured :func:`.relationship`. - ``join_to_left`` indicates to attempt aliasing the ON clause, - in whatever form it is passed, to the selectable - passed as the left side. If False, the onclause - is used as is. - :func:`.orm.join` is not commonly needed in modern usage, as its functionality is encapsulated within that of the :meth:`.Query.join` method, which features a @@ -975,11 +956,14 @@ def join(left, right, onclause=None, isouter=False, join_to_left=True): See :meth:`.Query.join` for information on modern usage of ORM level joins. + .. versionchanged:: 0.8.1 - the ``join_to_left`` parameter + is no longer used, and is deprecated. + """ - return _ORMJoin(left, right, onclause, isouter, join_to_left) + return _ORMJoin(left, right, onclause, isouter) -def outerjoin(left, right, onclause=None, join_to_left=True): +def outerjoin(left, right, onclause=None, join_to_left=None): """Produce a left outer join between left and right clauses. This is the "outer join" version of the :func:`.orm.join` function, @@ -987,7 +971,7 @@ def outerjoin(left, right, onclause=None, join_to_left=True): See that function's documentation for other usage details. """ - return _ORMJoin(left, right, onclause, True, join_to_left) + return _ORMJoin(left, right, onclause, True) def with_parent(instance, prop): diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index d2e644ce2..7846ac3b2 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -3909,7 +3909,7 @@ class Join(FromClause): def is_derived_from(self, fromclause): return fromclause is self or \ - self.left.is_derived_from(fromclause) or\ + self.left.is_derived_from(fromclause) or \ self.right.is_derived_from(fromclause) def self_group(self, against=None): diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 520c90f99..4aa2d7496 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -203,7 +203,7 @@ def clause_is_present(clause, search): stack = [search] while stack: elem = stack.pop() - if clause is elem: + if clause == elem: # use == here so that Annotated's compare return True elif isinstance(elem, expression.Join): stack.extend((elem.left, elem.right)) diff --git a/test/ext/declarative/test_inheritance.py b/test/ext/declarative/test_inheritance.py index f0372e8ee..1a3a4aba5 100644 --- a/test/ext/declarative/test_inheritance.py +++ b/test/ext/declarative/test_inheritance.py @@ -2,18 +2,13 @@ from sqlalchemy.testing import eq_, assert_raises, \ assert_raises_message, is_ from sqlalchemy.ext import declarative as decl -from sqlalchemy import exc import sqlalchemy as sa from sqlalchemy import testing -from sqlalchemy import MetaData, Integer, String, ForeignKey, \ - ForeignKeyConstraint, Index +from sqlalchemy import Integer, String, ForeignKey from sqlalchemy.testing.schema import Table, Column from sqlalchemy.orm import relationship, create_session, class_mapper, \ - joinedload, configure_mappers, backref, clear_mappers, \ - polymorphic_union, deferred, column_property, composite,\ - Session -from sqlalchemy.testing import eq_ -from sqlalchemy.util import classproperty + configure_mappers, clear_mappers, \ + polymorphic_union, deferred, Session from sqlalchemy.ext.declarative import declared_attr, AbstractConcreteBase, \ ConcreteBase, has_inherited_table from sqlalchemy.testing import fixtures @@ -56,10 +51,10 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): __tablename__ = 'people' id = Column(Integer, primary_key=True) - a=Column(Integer) - b=Column(Integer) - c=Column(Integer) - d=Column(Integer) + a = Column(Integer) + b = Column(Integer) + c = Column(Integer) + d = Column(Integer) discriminator = Column('type', String(50)) __mapper_args__ = {'polymorphic_on': discriminator, 'polymorphic_identity': 'person', @@ -149,7 +144,10 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): # is appropriately treated as the "id" column in the "manager" # table (reversed from 0.6's behavior.) - assert Manager.id.property.columns == [Manager.__table__.c.id, Person.__table__.c.id] + eq_( + Manager.id.property.columns, + [Manager.__table__.c.id, Person.__table__.c.id] + ) # assert that the "id" column is available without a second # load. as of 0.7, the ColumnProperty tests all columns @@ -195,8 +193,8 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): sess.add(e1) sess.flush() sess.expunge_all() - eq_(sess.query(Person).first(), Engineer(primary_language='java' - , name='dilbert')) + eq_(sess.query(Person).first(), + Engineer(primary_language='java', name='dilbert')) def test_add_parentcol_after_the_fact(self): @@ -815,8 +813,8 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): String(50)) foo_bar = Column(Integer, primary_key=True) - assert_raises_message(sa.exc.ArgumentError, 'place primary key' - , go) + assert_raises_message(sa.exc.ArgumentError, + 'place primary key', go) def test_single_no_table_args(self): @@ -848,13 +846,12 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): @testing.emits_warning("This declarative") def test_dupe_name_in_hierarchy(self): class A(Base): - __tablename__ = "a" - id = Column( Integer, primary_key=True) + __tablename__ = "a" + id = Column(Integer, primary_key=True) a_1 = A class A(a_1): - __tablename__ = 'b' - - id = Column(Integer(),ForeignKey(a_1.id), primary_key = True) + __tablename__ = 'b' + id = Column(Integer(), ForeignKey(a_1.id), primary_key=True) assert A.__mapper__.inherits is a_1.__mapper__ @@ -897,7 +894,8 @@ class OverlapColPrecedenceTest(DeclarativeTestBase): class Engineer(Person): __tablename__ = 'engineer' - id = Column("eid", Integer, ForeignKey('person.id'), primary_key=True) + id = Column("eid", Integer, ForeignKey('person.id'), + primary_key=True) self._run_test(Engineer, "eid", "id") @@ -908,7 +906,8 @@ class OverlapColPrecedenceTest(DeclarativeTestBase): class Engineer(Person): __tablename__ = 'engineer' - id = Column("eid", Integer, ForeignKey('person.pid'), primary_key=True) + id = Column("eid", Integer, ForeignKey('person.pid'), + primary_key=True) self._run_test(Engineer, "eid", "pid") @@ -959,22 +958,24 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): def test_explicit(self): engineers = Table('engineers', Base.metadata, Column('id', Integer, primary_key=True, - test_needs_autoincrement=True), Column('name' - , String(50)), Column('primary_language', - String(50))) + test_needs_autoincrement=True), + Column('name', String(50)), + Column('primary_language', String(50))) managers = Table('managers', Base.metadata, - Column('id',Integer, primary_key=True, test_needs_autoincrement=True), + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), Column('name', String(50)), Column('golf_swing', String(50)) ) boss = Table('boss', Base.metadata, - Column('id',Integer, primary_key=True, test_needs_autoincrement=True), + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), Column('name', String(50)), Column('golf_swing', String(50)) ) punion = polymorphic_union({ 'engineer': engineers, - 'manager' : managers, + 'manager': managers, 'boss': boss}, 'type', 'punion') class Employee(Base, fixtures.ComparableEntity): @@ -1050,8 +1051,8 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): name = Column(String(50)) golf_swing = Column(String(40)) __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} + 'polymorphic_identity': 'manager', + 'concrete': True} class Boss(Manager): __tablename__ = 'boss' @@ -1060,8 +1061,8 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): name = Column(String(50)) golf_swing = Column(String(40)) __mapper_args__ = { - 'polymorphic_identity':'boss', - 'concrete':True} + 'polymorphic_identity': 'boss', + 'concrete': True} class Engineer(Employee): __tablename__ = 'engineer' @@ -1069,8 +1070,8 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): test_needs_autoincrement=True) name = Column(String(50)) primary_language = Column(String(40)) - __mapper_args__ = {'polymorphic_identity':'engineer', - 'concrete':True} + __mapper_args__ = {'polymorphic_identity': 'engineer', + 'concrete': True} self._roundtrip(Employee, Manager, Engineer, Boss) @@ -1081,8 +1082,8 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): test_needs_autoincrement=True) name = Column(String(50)) __mapper_args__ = { - 'polymorphic_identity':'employee', - 'concrete':True} + 'polymorphic_identity': 'employee', + 'concrete': True} class Manager(Employee): __tablename__ = 'manager' employee_id = Column(Integer, primary_key=True, @@ -1090,8 +1091,8 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): name = Column(String(50)) golf_swing = Column(String(40)) __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} + 'polymorphic_identity': 'manager', + 'concrete': True} class Boss(Manager): __tablename__ = 'boss' @@ -1100,8 +1101,8 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): name = Column(String(50)) golf_swing = Column(String(40)) __mapper_args__ = { - 'polymorphic_identity':'boss', - 'concrete':True} + 'polymorphic_identity': 'boss', + 'concrete': True} class Engineer(Employee): __tablename__ = 'engineer' @@ -1109,8 +1110,8 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): test_needs_autoincrement=True) name = Column(String(50)) primary_language = Column(String(40)) - __mapper_args__ = {'polymorphic_identity':'engineer', - 'concrete':True} + __mapper_args__ = {'polymorphic_identity': 'engineer', + 'concrete': True} self._roundtrip(Employee, Manager, Engineer, Boss) @@ -1170,7 +1171,7 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): __mapper_args__ = { 'polymorphic_identity': "manager", - 'concrete':True} + 'concrete': True} class Boss(Manager): __tablename__ = 'boss' @@ -1185,7 +1186,7 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): __mapper_args__ = { 'polymorphic_identity': "boss", - 'concrete':True} + 'concrete': True} class Engineer(Employee): __tablename__ = 'engineer' @@ -1198,5 +1199,5 @@ class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): def type(self): return "engineer" __mapper_args__ = {'polymorphic_identity': "engineer", - 'concrete':True} + 'concrete': True} self._roundtrip(Employee, Manager, Engineer, Boss, explicit_type=True) diff --git a/test/orm/inheritance/test_relationship.py b/test/orm/inheritance/test_relationship.py index 509d540ef..36dbb7d27 100644 --- a/test/orm/inheritance/test_relationship.py +++ b/test/orm/inheritance/test_relationship.py @@ -1113,3 +1113,267 @@ class SubClassToSubClassFromParentTest(fixtures.MappedTest): a1 = session.query(A).first() eq_(a1.related, []) self.assert_sql_count(testing.db, go, 3) + + +class SubClassToSubClassMultiTest(AssertsCompiledSQL, fixtures.MappedTest): + """ + Two different joined-inh subclasses, led by a + parent, with two distinct endpoints: + + parent -> subcl1 -> subcl2 -> (ep1, ep2) + + the join to ep2 indicates we need to join + from the middle of the joinpoint, skipping ep1 + + """ + + run_create_tables = None + run_deletes = None + __dialect__ = 'default' + + @classmethod + def define_tables(cls, metadata): + Table('parent', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('data', String(30)) + ) + Table('base1', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('data', String(30)) + ) + Table('sub1', metadata, + Column('id', Integer, ForeignKey('base1.id'), primary_key=True), + Column('parent_id', ForeignKey('parent.id')), + Column('subdata', String(30)) + ) + + Table('base2', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('base1_id', ForeignKey('base1.id')), + Column('data', String(30)) + ) + Table('sub2', metadata, + Column('id', Integer, ForeignKey('base2.id'), primary_key=True), + Column('subdata', String(30)) + ) + Table('ep1', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('base2_id', Integer, ForeignKey('base2.id')), + Column('data', String(30)) + ) + Table('ep2', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('base2_id', Integer, ForeignKey('base2.id')), + Column('data', String(30)) + ) + + @classmethod + def setup_classes(cls): + class Parent(cls.Comparable): + pass + class Base1(cls.Comparable): + pass + class Sub1(Base1): + pass + class Base2(cls.Comparable): + pass + class Sub2(Base2): + pass + class EP1(cls.Comparable): + pass + class EP2(cls.Comparable): + pass + + @classmethod + def _classes(cls): + return cls.classes.Parent, cls.classes.Base1,\ + cls.classes.Base2, cls.classes.Sub1,\ + cls.classes.Sub2, cls.classes.EP1,\ + cls.classes.EP2 + + @classmethod + def setup_mappers(cls): + Parent, Base1, Base2, Sub1, Sub2, EP1, EP2 = cls._classes() + + mapper(Parent, cls.tables.parent, properties={ + 'sub1': relationship(Sub1) + }) + mapper(Base1, cls.tables.base1, properties={ + 'sub2': relationship(Sub2) + }) + mapper(Sub1, cls.tables.sub1, inherits=Base1) + mapper(Base2, cls.tables.base2, properties={ + 'ep1': relationship(EP1), + 'ep2': relationship(EP2) + }) + mapper(Sub2, cls.tables.sub2, inherits=Base2) + mapper(EP1, cls.tables.ep1) + mapper(EP2, cls.tables.ep2) + + def test_one(self): + Parent, Base1, Base2, Sub1, Sub2, EP1, EP2 = self._classes() + + s = Session() + self.assert_compile( + s.query(Parent).join(Parent.sub1, Sub1.sub2). + join(Sub2.ep1). + join(Sub2.ep2), + "SELECT parent.id AS parent_id, parent.data AS parent_data " + "FROM parent JOIN (SELECT base1.id AS base1_id, " + "base1.data AS base1_data, sub1.id AS sub1_id, " + "sub1.parent_id AS sub1_parent_id, sub1.subdata AS sub1_subdata " + "FROM base1 JOIN sub1 ON base1.id = sub1.id) AS anon_1 " + "ON parent.id = anon_1.sub1_parent_id JOIN " + "(SELECT base2.id AS base2_id, base2.base1_id AS base2_base1_id, " + "base2.data AS base2_data, sub2.id AS sub2_id, " + "sub2.subdata AS sub2_subdata FROM base2 JOIN sub2 " + "ON base2.id = sub2.id) AS anon_2 " + "ON anon_1.base1_id = anon_2.base2_base1_id " + "JOIN ep1 ON anon_2.base2_id = ep1.base2_id " + "JOIN ep2 ON anon_2.base2_id = ep2.base2_id" + ) + + def test_two(self): + Parent, Base1, Base2, Sub1, Sub2, EP1, EP2 = self._classes() + + s2a = aliased(Sub2) + + s = Session() + self.assert_compile( + s.query(Parent).join(Parent.sub1). + join(s2a, Sub1.sub2), + "SELECT parent.id AS parent_id, parent.data AS parent_data " + "FROM parent JOIN (SELECT base1.id AS base1_id, " + "base1.data AS base1_data, sub1.id AS sub1_id, " + "sub1.parent_id AS sub1_parent_id, sub1.subdata AS sub1_subdata " + "FROM base1 JOIN sub1 ON base1.id = sub1.id) AS anon_1 " + "ON parent.id = anon_1.sub1_parent_id JOIN " + "(SELECT base2.id AS base2_id, base2.base1_id AS base2_base1_id, " + "base2.data AS base2_data, sub2.id AS sub2_id, " + "sub2.subdata AS sub2_subdata FROM base2 JOIN sub2 " + "ON base2.id = sub2.id) AS anon_2 " + "ON anon_1.base1_id = anon_2.base2_base1_id" + ) + + def test_three(self): + Parent, Base1, Base2, Sub1, Sub2, EP1, EP2 = self._classes() + + s = Session() + self.assert_compile( + s.query(Base1).join(Base1.sub2). + join(Sub2.ep1).\ + join(Sub2.ep2), + "SELECT base1.id AS base1_id, base1.data AS base1_data " + "FROM base1 JOIN (SELECT base2.id AS base2_id, base2.base1_id " + "AS base2_base1_id, base2.data AS base2_data, sub2.id AS sub2_id, " + "sub2.subdata AS sub2_subdata FROM base2 JOIN sub2 " + "ON base2.id = sub2.id) AS anon_1 ON base1.id = " + "anon_1.base2_base1_id " + "JOIN ep1 ON anon_1.base2_id = ep1.base2_id " + "JOIN ep2 ON anon_1.base2_id = ep2.base2_id" + ) + + def test_four(self): + Parent, Base1, Base2, Sub1, Sub2, EP1, EP2 = self._classes() + + s = Session() + self.assert_compile( + s.query(Sub2).join(Base1, Base1.id == Sub2.base1_id). + join(Sub2.ep1).\ + join(Sub2.ep2), + "SELECT sub2.id AS sub2_id, base2.id AS base2_id, " + "base2.base1_id AS base2_base1_id, base2.data AS base2_data, " + "sub2.subdata AS sub2_subdata " + "FROM base2 JOIN sub2 ON base2.id = sub2.id " + "JOIN base1 ON base1.id = base2.base1_id " + "JOIN ep1 ON base2.id = ep1.base2_id " + "JOIN ep2 ON base2.id = ep2.base2_id" + ) + + def test_five(self): + Parent, Base1, Base2, Sub1, Sub2, EP1, EP2 = self._classes() + + s = Session() + self.assert_compile( + s.query(Sub2).join(Sub1, Sub1.id == Sub2.base1_id). + join(Sub2.ep1).\ + join(Sub2.ep2), + "SELECT sub2.id AS sub2_id, base2.id AS base2_id, " + "base2.base1_id AS base2_base1_id, base2.data AS base2_data, " + "sub2.subdata AS sub2_subdata " + "FROM base2 JOIN sub2 ON base2.id = sub2.id " + "JOIN " + "(SELECT base1.id AS base1_id, base1.data AS base1_data, " + "sub1.id AS sub1_id, sub1.parent_id AS sub1_parent_id, " + "sub1.subdata AS sub1_subdata " + "FROM base1 JOIN sub1 ON base1.id = sub1.id) AS anon_1 " + "ON anon_1.sub1_id = base2.base1_id " + "JOIN ep1 ON base2.id = ep1.base2_id " + "JOIN ep2 ON base2.id = ep2.base2_id" + ) + + def test_six(self): + Parent, Base1, Base2, Sub1, Sub2, EP1, EP2 = self._classes() + + s = Session() + self.assert_compile( + s.query(Sub2).from_self().\ + join(Sub2.ep1). + join(Sub2.ep2), + "SELECT anon_1.sub2_id AS anon_1_sub2_id, " + "anon_1.base2_id AS anon_1_base2_id, " + "anon_1.base2_base1_id AS anon_1_base2_base1_id, " + "anon_1.base2_data AS anon_1_base2_data, " + "anon_1.sub2_subdata AS anon_1_sub2_subdata " + "FROM (SELECT sub2.id AS sub2_id, base2.id AS base2_id, " + "base2.base1_id AS base2_base1_id, base2.data AS base2_data, " + "sub2.subdata AS sub2_subdata " + "FROM base2 JOIN sub2 ON base2.id = sub2.id) AS anon_1 " + "JOIN ep1 ON anon_1.base2_id = ep1.base2_id " + "JOIN ep2 ON anon_1.base2_id = ep2.base2_id" + ) + + def test_seven(self): + Parent, Base1, Base2, Sub1, Sub2, EP1, EP2 = self._classes() + + s = Session() + self.assert_compile( + # adding Sub2 to the entities list helps it, + # otherwise the joins for Sub2.ep1/ep2 don't have columns + # to latch onto. Can't really make it better than this + s.query(Parent, Sub2).join(Parent.sub1).\ + join(Sub1.sub2).from_self().\ + join(Sub2.ep1). + join(Sub2.ep2), + "SELECT anon_1.parent_id AS anon_1_parent_id, " + "anon_1.parent_data AS anon_1_parent_data, " + "anon_1.anon_2_sub2_id AS anon_1_anon_2_sub2_id, " + "anon_1.anon_2_base2_id AS anon_1_anon_2_base2_id, " + "anon_1.anon_2_base2_base1_id AS anon_1_anon_2_base2_base1_id, " + "anon_1.anon_2_base2_data AS anon_1_anon_2_base2_data, " + "anon_1.anon_2_sub2_subdata AS anon_1_anon_2_sub2_subdata " + "FROM (SELECT parent.id AS parent_id, parent.data AS parent_data, " + "anon_2.sub2_id AS anon_2_sub2_id, " + "anon_2.base2_id AS anon_2_base2_id, " + "anon_2.base2_base1_id AS anon_2_base2_base1_id, " + "anon_2.base2_data AS anon_2_base2_data, " + "anon_2.sub2_subdata AS anon_2_sub2_subdata " + "FROM parent JOIN (SELECT base1.id AS base1_id, " + "base1.data AS base1_data, sub1.id AS sub1_id, " + "sub1.parent_id AS sub1_parent_id, sub1.subdata AS sub1_subdata " + "FROM base1 JOIN sub1 ON base1.id = sub1.id) AS anon_3 " + "ON parent.id = anon_3.sub1_parent_id JOIN " + "(SELECT base2.id AS base2_id, base2.base1_id AS base2_base1_id, " + "base2.data AS base2_data, sub2.id AS sub2_id, " + "sub2.subdata AS sub2_subdata " + "FROM base2 JOIN sub2 ON base2.id = sub2.id) AS anon_2 " + "ON anon_3.base1_id = anon_2.base2_base1_id) AS anon_1 " + "JOIN ep1 ON anon_1.anon_2_base2_id = ep1.base2_id " + "JOIN ep2 ON anon_1.anon_2_base2_id = ep2.base2_id" + ) + diff --git a/test/orm/test_joins.py b/test/orm/test_joins.py index 4c0a193a0..2bf0d8d92 100644 --- a/test/orm/test_joins.py +++ b/test/orm/test_joins.py @@ -215,7 +215,7 @@ class InheritedJoinTest(fixtures.MappedTest, AssertsCompiledSQL): , use_default_dialect = True ) - def test_prop_with_polymorphic(self): + def test_prop_with_polymorphic_1(self): Person, Manager, Paperwork = (self.classes.Person, self.classes.Manager, self.classes.Paperwork) @@ -238,6 +238,13 @@ class InheritedJoinTest(fixtures.MappedTest, AssertsCompiledSQL): , use_default_dialect=True ) + def test_prop_with_polymorphic_2(self): + Person, Manager, Paperwork = (self.classes.Person, + self.classes.Manager, + self.classes.Paperwork) + + sess = create_session() + self.assert_compile( sess.query(Person).with_polymorphic(Manager). join('paperwork', aliased=True). @@ -426,14 +433,14 @@ class JoinTest(QueryTest, AssertsCompiledSQL): #) self.assert_compile( - sess.query(User).join((Order, User.id==Order.user_id)), + sess.query(User).join((Order, User.id == Order.user_id)), "SELECT users.id AS users_id, users.name AS users_name " "FROM users JOIN orders ON users.id = orders.user_id", ) self.assert_compile( sess.query(User).join( - (Order, User.id==Order.user_id), + (Order, User.id == Order.user_id), (Item, Order.items)), "SELECT users.id AS users_id, users.name AS users_name " "FROM users JOIN orders ON users.id = orders.user_id " @@ -449,7 +456,7 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "FROM users JOIN orders ON users.id = orders.user_id", ) - def test_single_prop(self): + def test_single_prop_1(self): Item, Order, User, Address = (self.classes.Item, self.classes.Order, self.classes.User, @@ -462,14 +469,27 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "FROM users JOIN orders ON users.id = orders.user_id" ) + def test_single_prop_2(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() self.assert_compile( sess.query(User).join(Order.user), "SELECT users.id AS users_id, users.name AS users_name " "FROM orders JOIN users ON users.id = orders.user_id" ) + def test_single_prop_3(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() oalias1 = aliased(Order) - oalias2 = aliased(Order) self.assert_compile( sess.query(User).join(oalias1.user), @@ -477,6 +497,15 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "FROM orders AS orders_1 JOIN users ON users.id = orders_1.user_id" ) + def test_single_prop_4(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() + oalias1 = aliased(Order) + oalias2 = aliased(Order) # another nonsensical query. (from [ticket:1537]). # in this case, the contract of "left to right" is honored self.assert_compile( @@ -486,6 +515,13 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "orders AS orders_2 JOIN users ON users.id = orders_2.user_id" ) + def test_single_prop_5(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() self.assert_compile( sess.query(User).join(User.orders, Order.items), "SELECT users.id AS users_id, users.name AS users_name FROM users " @@ -493,6 +529,13 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "ON orders.id = order_items_1.order_id JOIN items ON items.id = order_items_1.item_id" ) + def test_single_prop_6(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() ualias = aliased(User) self.assert_compile( sess.query(ualias).join(ualias.orders), @@ -500,6 +543,13 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "FROM users AS users_1 JOIN orders ON users_1.id = orders.user_id" ) + def test_single_prop_7(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() # this query is somewhat nonsensical. the old system didn't render a correct # query for this. In this case its the most faithful to what was asked - # there's no linkage between User.orders and "oalias", so two FROM elements @@ -513,8 +563,16 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "JOIN items ON items.id = order_items_1.item_id" ) + def test_single_prop_8(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() # same as before using an aliased() for User as well ualias = aliased(User) + oalias = aliased(Order) self.assert_compile( sess.query(ualias).join(ualias.orders, oalias.items), "SELECT users_1.id AS users_1_id, users_1.name AS users_1_name FROM users AS users_1 " @@ -523,23 +581,47 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "JOIN items ON items.id = order_items_1.item_id" ) + def test_single_prop_9(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() self.assert_compile( - sess.query(User).filter(User.name=='ed').from_self().join(User.orders), + sess.query(User).filter(User.name == 'ed').from_self(). + join(User.orders), "SELECT anon_1.users_id AS anon_1_users_id, anon_1.users_name AS anon_1_users_name " "FROM (SELECT users.id AS users_id, users.name AS users_name " "FROM users " "WHERE users.name = :name_1) AS anon_1 JOIN orders ON anon_1.users_id = orders.user_id" ) + def test_single_prop_10(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() self.assert_compile( - sess.query(User).join(User.addresses, aliased=True).filter(Address.email_address=='foo'), + sess.query(User).join(User.addresses, aliased=True). + filter(Address.email_address == 'foo'), "SELECT users.id AS users_id, users.name AS users_name " "FROM users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id " "WHERE addresses_1.email_address = :email_address_1" ) + def test_single_prop_11(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() self.assert_compile( - sess.query(User).join(User.orders, Order.items, aliased=True).filter(Item.id==10), + sess.query(User).join(User.orders, Order.items, aliased=True). + filter(Item.id == 10), "SELECT users.id AS users_id, users.name AS users_name " "FROM users JOIN orders AS orders_1 ON users.id = orders_1.user_id " "JOIN order_items AS order_items_1 ON orders_1.id = order_items_1.order_id " @@ -547,6 +629,14 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "WHERE items_1.id = :id_1" ) + def test_single_prop_12(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() + oalias1 = aliased(Order) # test #1 for [ticket:1706] ualias = aliased(User) self.assert_compile( @@ -559,7 +649,15 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "= addresses.user_id" ) + def test_single_prop_13(self): + Item, Order, User, Address = (self.classes.Item, + self.classes.Order, + self.classes.User, + self.classes.Address) + + sess = create_session() # test #2 for [ticket:1706] + ualias = aliased(User) ualias2 = aliased(User) self.assert_compile( sess.query(ualias). @@ -1837,34 +1935,50 @@ class SelfReferentialTest(fixtures.MappedTest, AssertsCompiledSQL): use_default_dialect=True ) - def test_explicit_join(self): + def test_explicit_join_1(self): Node = self.classes.Node - - sess = create_session() - n1 = aliased(Node) n2 = aliased(Node) self.assert_compile( join(Node, n1, 'children').join(n2, 'children'), - "nodes JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id", + "nodes JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id " + "JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id", use_default_dialect=True ) + def test_explicit_join_2(self): + Node = self.classes.Node + n1 = aliased(Node) + n2 = aliased(Node) + self.assert_compile( join(Node, n1, Node.children).join(n2, n1.children), - "nodes JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id", + "nodes JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id " + "JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id", use_default_dialect=True ) + def test_explicit_join_3(self): + Node = self.classes.Node + n1 = aliased(Node) + n2 = aliased(Node) + # the join_to_left=False here is unfortunate. the default on this flag should # be False. self.assert_compile( join(Node, n1, Node.children).join(n2, Node.children, join_to_left=False), - "nodes JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id JOIN nodes AS nodes_2 ON nodes.id = nodes_2.parent_id", + "nodes JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id " + "JOIN nodes AS nodes_2 ON nodes.id = nodes_2.parent_id", use_default_dialect=True ) + def test_explicit_join_4(self): + Node = self.classes.Node + sess = create_session() + n1 = aliased(Node) + n2 = aliased(Node) + self.assert_compile( sess.query(Node).join(n1, Node.children).join(n2, n1.children), "SELECT nodes.id AS nodes_id, nodes.parent_id AS nodes_parent_id, nodes.data AS " @@ -1873,6 +1987,12 @@ class SelfReferentialTest(fixtures.MappedTest, AssertsCompiledSQL): use_default_dialect=True ) + def test_explicit_join_5(self): + Node = self.classes.Node + sess = create_session() + n1 = aliased(Node) + n2 = aliased(Node) + self.assert_compile( sess.query(Node).join(n1, Node.children).join(n2, Node.children), "SELECT nodes.id AS nodes_id, nodes.parent_id AS nodes_parent_id, nodes.data AS " @@ -1881,25 +2001,59 @@ class SelfReferentialTest(fixtures.MappedTest, AssertsCompiledSQL): use_default_dialect=True ) - node = sess.query(Node).select_from(join(Node, n1, 'children')).filter(n1.data=='n122').first() - assert node.data=='n12' + def test_explicit_join_6(self): + Node = self.classes.Node + sess = create_session() + n1 = aliased(Node) - node = sess.query(Node).select_from(join(Node, n1, 'children').join(n2, 'children')).\ - filter(n2.data=='n122').first() - assert node.data=='n1' + node = sess.query(Node).select_from(join(Node, n1, 'children')).\ + filter(n1.data == 'n122').first() + assert node.data == 'n12' + + def test_explicit_join_7(self): + Node = self.classes.Node + sess = create_session() + n1 = aliased(Node) + n2 = aliased(Node) + + node = sess.query(Node).select_from( + join(Node, n1, 'children').join(n2, 'children')).\ + filter(n2.data == 'n122').first() + assert node.data == 'n1' + + def test_explicit_join_8(self): + Node = self.classes.Node + sess = create_session() + n1 = aliased(Node) + n2 = aliased(Node) # mix explicit and named onclauses - node = sess.query(Node).select_from(join(Node, n1, Node.id==n1.parent_id).join(n2, 'children')).\ - filter(n2.data=='n122').first() - assert node.data=='n1' + node = sess.query(Node).select_from( + join(Node, n1, Node.id == n1.parent_id).join(n2, 'children')).\ + filter(n2.data == 'n122').first() + assert node.data == 'n1' + + def test_explicit_join_9(self): + Node = self.classes.Node + sess = create_session() + n1 = aliased(Node) + n2 = aliased(Node) node = sess.query(Node).select_from(join(Node, n1, 'parent').join(n2, 'parent')).\ - filter(and_(Node.data=='n122', n1.data=='n12', n2.data=='n1')).first() + filter(and_(Node.data == 'n122', n1.data == 'n12', n2.data == 'n1')).first() assert node.data == 'n122' + def test_explicit_join_10(self): + Node = self.classes.Node + sess = create_session() + n1 = aliased(Node) + n2 = aliased(Node) + eq_( list(sess.query(Node).select_from(join(Node, n1, 'parent').join(n2, 'parent')).\ - filter(and_(Node.data=='n122', n1.data=='n12', n2.data=='n1')).values(Node.data, n1.data, n2.data)), + filter(and_(Node.data == 'n122', + n1.data == 'n12', + n2.data == 'n1')).values(Node.data, n1.data, n2.data)), [('n122', 'n12', 'n1')]) def test_join_to_nonaliased(self): @@ -1949,8 +2103,8 @@ class SelfReferentialTest(fixtures.MappedTest, AssertsCompiledSQL): sess.query(Node, parent, grandparent).\ join(parent, Node.parent).\ join(grandparent, parent.parent).\ - filter(Node.data=='n122').filter(parent.data=='n12').\ - filter(grandparent.data=='n1').from_self().first(), + filter(Node.data == 'n122').filter(parent.data == 'n12').\ + filter(grandparent.data == 'n1').from_self().first(), (Node(data='n122'), Node(data='n12'), Node(data='n1')) ) @@ -1966,8 +2120,8 @@ class SelfReferentialTest(fixtures.MappedTest, AssertsCompiledSQL): sess.query(parent, grandparent, Node).\ join(parent, Node.parent).\ join(grandparent, parent.parent).\ - filter(Node.data=='n122').filter(parent.data=='n12').\ - filter(grandparent.data=='n1').from_self().first(), + filter(Node.data == 'n122').filter(parent.data == 'n12').\ + filter(grandparent.data == 'n1').from_self().first(), (Node(data='n12'), Node(data='n1'), Node(data='n122')) ) diff --git a/test/orm/test_query.py b/test/orm/test_query.py index ac9c95f41..3882ec4b5 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -1620,6 +1620,21 @@ class AggregateTest(QueryTest): assert [User(name=u'jack',id=7), User(name=u'fred',id=9)] == sess.query(User).order_by(User.id).group_by(User).join('addresses').having(func.count(Address.id)< 2).all() + +class ExistsTest(QueryTest, AssertsCompiledSQL): + + def test_exists(self): + User = self.classes.User + sess = create_session() + q1 = sess.query(User).filter(User.name == 'fred') + self.assert_compile(sess.query(q1.exists()), + 'SELECT EXISTS (' + 'SELECT 1 FROM users WHERE users.name = :name_1' + ') AS anon_1', + dialect=default.DefaultDialect() + ) + + class CountTest(QueryTest): def test_basic(self): users, User = self.tables.users, self.classes.User |