From c8b50c1ffb4ab104144ef9058fa4c653b10ad915 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 23 Jan 2008 19:20:49 +0000 Subject: [PATCH] - query.join() can also accept tuples of attribute name/some selectable as arguments. This allows construction of joins *from* subclasses of a polymorphic relation, i.e.: query(Company).\ join( [('employees', people.join(engineer)), Engineer.name] ) --- CHANGES | 14 ++++++-- lib/sqlalchemy/orm/query.py | 60 +++++++++++++++++++++++++------- lib/sqlalchemy/orm/util.py | 4 +-- lib/sqlalchemy/sql/expression.py | 3 ++ test/orm/inheritance/query.py | 28 +++++++-------- 5 files changed, 78 insertions(+), 31 deletions(-) diff --git a/CHANGES b/CHANGES index 937bae302c..947ee36035 100644 --- a/CHANGES +++ b/CHANGES @@ -54,8 +54,18 @@ CHANGES relation, i.e.: query(Company).join(['employees', Engineer.name]) - - - General improvements to the behavior of join() in + + - query.join() can also accept tuples of attribute + name/some selectable as arguments. This allows + construction of joins *from* subclasses of a + polymorphic relation, i.e.: + + query(Company).\ + join( + [('employees', people.join(engineer)), Engineer.name] + ) + + - General improvements to the behavior of join() in conjunction with polymorphic mappers, i.e. joining from/to polymorphic mappers and properly applying aliases. diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 201d0e2e30..bcf0f3dd33 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -422,18 +422,39 @@ class Query(object): mapper = start alias = self._aliases - for key in util.to_list(keys): + if not isinstance(keys, list): + keys = [keys] + for key in keys: + use_selectable = None + if isinstance(key, tuple): + key, use_selectable = key + if isinstance(key, interfaces.PropComparator): prop = key.property else: prop = mapper.get_property(key, resolve_synonyms=True) + + if use_selectable: + if not use_selectable.is_derived_from(prop.mapper.mapped_table): + raise exceptions.InvalidRequestError("Selectable '%s' is not derived from '%s'" % (use_selectable.description, prop.mapper.mapped_table.description)) + if not isinstance(use_selectable, expression.Alias): + use_selectable = use_selectable.alias() - if prop._is_self_referential() and not create_aliases: + if prop._is_self_referential() and not create_aliases and not use_selectable: raise exceptions.InvalidRequestError("Self-referential query on '%s' property requires create_aliases=True argument." % str(prop)) - if prop.select_table not in currenttables or create_aliases: + if prop.select_table not in currenttables or create_aliases or use_selectable: if prop.secondary: - if create_aliases: + if use_selectable: + alias = mapperutil.PropertyAliasedClauses(prop, + prop.primary_join_against(mapper, adapt_against), + prop.secondary_join_against(mapper), + alias, + alias=use_selectable + ) + crit = alias.primaryjoin + clause = clause.join(alias.secondary, crit, isouter=outerjoin).join(alias.alias, alias.secondaryjoin, isouter=outerjoin) + elif create_aliases: alias = mapperutil.PropertyAliasedClauses(prop, prop.primary_join_against(mapper, adapt_against), prop.secondary_join_against(mapper), @@ -446,7 +467,16 @@ class Query(object): clause = clause.join(prop.secondary, crit, isouter=outerjoin) clause = clause.join(prop.select_table, prop.secondary_join_against(mapper), isouter=outerjoin) else: - if create_aliases: + if use_selectable: + alias = mapperutil.PropertyAliasedClauses(prop, + prop.primary_join_against(mapper, adapt_against), + None, + alias, + alias=use_selectable + ) + crit = alias.primaryjoin + clause = clause.join(alias.alias, crit, isouter=outerjoin) + elif create_aliases: alias = mapperutil.PropertyAliasedClauses(prop, prop.primary_join_against(mapper, adapt_against), None, @@ -464,13 +494,12 @@ class Query(object): mapper = prop.mapper - if mapper.select_table is not mapper.mapped_table: + if use_selectable: + adapt_against = use_selectable + elif mapper.select_table is not mapper.mapped_table: adapt_against = mapper.select_table - if create_aliases: - return (clause, mapper, alias) - else: - return (clause, mapper, None) + return (clause, mapper, alias) def _generative_col_aggregate(self, col, func): """apply the given aggregate function to the query and return the newly @@ -594,14 +623,16 @@ class Query(object): 'prop' may be one of: * a string property name, i.e. "rooms" * a class-mapped attribute, i.e. Houses.rooms - * a list containing a combination of any of the above. + * a 2-tuple containing one of the above, combined with a selectable + which derives from the properties' mapped table + * a list (not a tuple) containing a combination of any of the above. e.g.:: session.query(Company).join('employees') session.query(Company).join(['employees', 'tasks']) session.query(Houses).join([Colonials.rooms, Room.closets]) - + session.query(Company).join([('employees', people.join(engineers)), Engineer.computers]) """ return self._join(prop, id=id, outerjoin=False, aliased=aliased, from_joinpoint=from_joinpoint) @@ -613,13 +644,16 @@ class Query(object): 'prop' may be one of: * a string property name, i.e. "rooms" * a class-mapped attribute, i.e. Houses.rooms - * a list containing a combination of any of the above. + * a 2-tuple containing one of the above, combined with a selectable + which derives from the properties' mapped table + * a list (not a tuple) containing a combination of any of the above. e.g.:: session.query(Company).outerjoin('employees') session.query(Company).outerjoin(['employees', 'tasks']) session.query(Houses).outerjoin([Colonials.rooms, Room.closets]) + session.query(Company).join([('employees', people.join(engineers)), Engineer.computers]) """ diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index d2f1d2491a..a801210f94 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -193,8 +193,8 @@ class AliasedClauses(object): class PropertyAliasedClauses(AliasedClauses): """extends AliasedClauses to add support for primary/secondary joins on a relation().""" - def __init__(self, prop, primaryjoin, secondaryjoin, parentclauses=None): - super(PropertyAliasedClauses, self).__init__(prop.select_table) + def __init__(self, prop, primaryjoin, secondaryjoin, parentclauses=None, alias=None): + super(PropertyAliasedClauses, self).__init__(prop.select_table, alias=alias) self.parentclauses = parentclauses diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index c603418028..aff8654f25 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -2202,6 +2202,9 @@ class Join(FromClause): return "Join object on %s(%d) and %s(%d)" % (self.left.description, id(self.left), self.right.description, id(self.right)) description = property(description) + def is_derived_from(self, fromclause): + return fromclause is self or self.left.is_derived_from(fromclause) or self.right.is_derived_from(fromclause) + def self_group(self, against=None): return _FromGrouping(self) diff --git a/test/orm/inheritance/query.py b/test/orm/inheritance/query.py index b9f11faa7c..5033647872 100644 --- a/test/orm/inheritance/query.py +++ b/test/orm/inheritance/query.py @@ -202,22 +202,22 @@ def make_test(select_type): self.assertEquals(sess.query(Company).join('employees', aliased=True).filter(Person.name=='vlad').one(), c2) def test_join_to_subclass(self): - if select_type == '': - return - sess = create_session() - self.assertEquals(sess.query(Company).select_from(companies.join(people).join(engineers)).filter(Engineer.primary_language=='java').all(), [c1]) - - self.assertEquals(sess.query(Company).join(['employees']).filter(Engineer.primary_language=='java').all(), [c1]) - - self.assertEquals(sess.query(Person).join(Engineer.machines).all(), [e1, e2, e3]) - - self.assertEquals(sess.query(Person).join(Engineer.machines).filter(Machine.name.ilike("%ibm%")).all(), [e1, e3]) - - self.assertEquals(sess.query(Company).join(['employees', Engineer.machines]).all(), [c1, c2]) - - self.assertEquals(sess.query(Company).join(['employees', Engineer.machines]).filter(Machine.name.ilike("%thinkpad%")).all(), [c1]) + if select_type == '': + self.assertEquals(sess.query(Company).select_from(companies.join(people).join(engineers)).filter(Engineer.primary_language=='java').all(), [c1]) + self.assertEquals(sess.query(Company).join(('employees', people.join(engineers))).filter(Engineer.primary_language=='java').all(), [c1]) + self.assertEquals(sess.query(Person).select_from(people.join(engineers)).join(Engineer.machines).all(), [e1, e2, e3]) + self.assertEquals(sess.query(Person).select_from(people.join(engineers)).join(Engineer.machines).filter(Machine.name.ilike("%ibm%")).all(), [e1, e3]) + self.assertEquals(sess.query(Company).join([('employees', people.join(engineers)), Engineer.machines]).all(), [c1, c2]) + self.assertEquals(sess.query(Company).join([('employees', people.join(engineers)), Engineer.machines]).filter(Machine.name.ilike("%thinkpad%")).all(), [c1]) + else: + self.assertEquals(sess.query(Company).select_from(companies.join(people).join(engineers)).filter(Engineer.primary_language=='java').all(), [c1]) + self.assertEquals(sess.query(Company).join(['employees']).filter(Engineer.primary_language=='java').all(), [c1]) + self.assertEquals(sess.query(Person).join(Engineer.machines).all(), [e1, e2, e3]) + self.assertEquals(sess.query(Person).join(Engineer.machines).filter(Machine.name.ilike("%ibm%")).all(), [e1, e3]) + self.assertEquals(sess.query(Company).join(['employees', Engineer.machines]).all(), [c1, c2]) + self.assertEquals(sess.query(Company).join(['employees', Engineer.machines]).filter(Machine.name.ilike("%thinkpad%")).all(), [c1]) def test_join_through_polymorphic(self): -- 2.47.3