diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-10-15 17:21:38 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-10-15 17:21:38 -0400 |
commit | c307df6596dab489109cd216665cf30006b70d13 (patch) | |
tree | 4ce669b36759f8289d959c0e7e92b776b77d1865 | |
parent | 3510e38a772a2e48a8bb4b0a4efc6479034f649e (diff) | |
download | sqlalchemy-c307df6596dab489109cd216665cf30006b70d13.tar.gz |
- [feature] "scalar" selects now have a WHERE method
to help with generative building. Also slight adjustment
regarding how SS "correlates" columns; the new methodology
no longer applies meaning to the underlying
Table column being selected. This improves
some fairly esoteric situations, and the logic
that was there didn't seem to have any purpose.
- [feature] Some support for auto-rendering of a
relationship join condition based on the mapped
attribute, with usage of core SQL constructs.
E.g. select([SomeClass]).where(SomeClass.somerelationship)
would render SELECT from "someclass" and use the
primaryjoin of "somerelationship" as the WHERE
clause. This changes the previous meaning
of "SomeClass.somerelationship" when used in a
core SQL context; previously, it would "resolve"
to the parent selectable, which wasn't generally
useful. Related to [ticket:2245].
-rw-r--r-- | CHANGES | 20 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/properties.py | 23 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/expression.py | 14 | ||||
-rw-r--r-- | test/orm/test_eager_relations.py | 12 | ||||
-rw-r--r-- | test/orm/test_mapper.py | 32 | ||||
-rw-r--r-- | test/orm/test_query.py | 20 | ||||
-rw-r--r-- | test/sql/test_compiler.py | 37 | ||||
-rw-r--r-- | test/sql/test_selectable.py | 4 |
9 files changed, 131 insertions, 33 deletions
@@ -285,6 +285,18 @@ underneath "0.7.xx". methods, where they will be unwrapped into selectables. [ticket:2245] + - [feature] Some support for auto-rendering of a + relationship join condition based on the mapped + attribute, with usage of core SQL constructs. + E.g. select([SomeClass]).where(SomeClass.somerelationship) + would render SELECT from "someclass" and use the + primaryjoin of "somerelationship" as the WHERE + clause. This changes the previous meaning + of "SomeClass.somerelationship" when used in a + core SQL context; previously, it would "resolve" + to the parent selectable, which wasn't generally + useful. Related to [ticket:2245]. + - [feature] The registry of classes in declarative_base() is now a WeakValueDictionary. So subclasses of @@ -654,6 +666,14 @@ underneath "0.7.xx". function against the new schema.CreateColumn construct. [ticket:2463] + - [feature] "scalar" selects now have a WHERE method + to help with generative building. Also slight adjustment + regarding how SS "correlates" columns; the new methodology + no longer applies meaning to the underlying + Table column being selected. This improves + some fairly esoteric situations, and the logic + that was there didn't seem to have any purpose. + - [bug] Fixes to the interpretation of the Column "default" parameter as a callable to not pass ExecutionContext into a keyword diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index f8288f5fb..048b4fad3 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -350,6 +350,8 @@ class RelationshipProperty(StrategizedProperty): """ + _of_type = None + def __init__(self, prop, mapper, of_type=None, adapter=None): """Construction of :class:`.RelationshipProperty.Comparator` is internal to the ORM's attribute mechanics. @@ -376,13 +378,30 @@ class RelationshipProperty(StrategizedProperty): def parententity(self): return self.property.parent - def __clause_element__(self): + def _source_selectable(self): elem = self.property.parent._with_polymorphic_selectable if self.adapter: return self.adapter(elem) else: return elem + def __clause_element__(self): + adapt_from = self._source_selectable() + if self._of_type: + of_type = inspect(self._of_type).mapper + else: + of_type = None + + pj, sj, source, dest, \ + secondary, target_adapter = self.property._create_joins( + source_selectable=adapt_from, + source_polymorphic=True, + of_type=of_type) + if sj is not None: + return pj & sj + else: + return pj + def of_type(self, cls): """Produce a construct that represents a particular 'subtype' of attribute for the parent class. @@ -477,7 +496,7 @@ class RelationshipProperty(StrategizedProperty): to_selectable = None if self.adapter: - source_selectable = self.__clause_element__() + source_selectable = self._source_selectable() else: source_selectable = None diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 750c3b298..2e38e0ce3 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -796,7 +796,7 @@ class _ORMJoin(expression.Join): prop = left_mapper.get_property(onclause) elif isinstance(onclause, attributes.QueryableAttribute): if adapt_from is None: - adapt_from = onclause.__clause_element__() + adapt_from = onclause.comparator._source_selectable() prop = onclause.property elif isinstance(onclause, MapperProperty): prop = onclause diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 63b1a4037..5b6e4d82d 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -4839,7 +4839,7 @@ class SelectBase(Executable, FromClause): return [self] -class ScalarSelect(Grouping): +class ScalarSelect(Generative, Grouping): _from_objects = [] def __init__(self, element): @@ -4853,13 +4853,17 @@ class ScalarSelect(Grouping): 'column-level expression.') c = columns + @_generative + def where(self, crit): + """Apply a WHERE clause to the SELECT statement referred to + by this :class:`.ScalarSelect`. + + """ + self.element = self.element.where(crit) + def self_group(self, **kwargs): return self - def _make_proxy(self, selectable, name=None, **kw): - return list(self.inner_columns)[0]._make_proxy( - selectable, name=name) - class CompoundSelect(SelectBase): """Forms the basis of ``UNION``, ``UNION ALL``, and other SELECT-based set operations.""" diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py index 664ddb8e3..39a7a7301 100644 --- a/test/orm/test_eager_relations.py +++ b/test/orm/test_eager_relations.py @@ -1442,11 +1442,12 @@ class SubqueryAliasingTest(fixtures.MappedTest, testing.AssertsCompiledSQL): b_table, a_table = self.tables.b, self.tables.a self._fixture({}) cp = select([func.sum(b_table.c.value)]).\ - where(b_table.c.a_id==a_table.c.id).\ + where(b_table.c.a_id == a_table.c.id).\ correlate(a_table).as_scalar() - # note its re-rendering the subquery in the - # outermost order by. usually we want it to address - # the column within the subquery. labelling fixes that. + + # up until 0.8, this was ordering by a new subquery. + # the removal of a separate _make_proxy() from ScalarSelect + # fixed that. self.assert_compile( create_session().query(A).options(joinedload_all('bs')). order_by(cp). @@ -1458,8 +1459,7 @@ class SubqueryAliasingTest(fixtures.MappedTest, testing.AssertsCompiledSQL): "b.a_id = a.id) AS anon_2 FROM a ORDER BY (SELECT " "sum(b.value) AS sum_1 FROM b WHERE b.a_id = a.id) " "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN b AS b_1 " - "ON anon_1.a_id = b_1.a_id ORDER BY " - "(SELECT anon_1.anon_2 FROM b WHERE b.a_id = anon_1.a_id)" + "ON anon_1.a_id = b_1.a_id ORDER BY anon_1.anon_2" ) def test_standalone_subquery_labeled(self): diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index b2e36b273..f41843455 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -2190,28 +2190,46 @@ class ComparatorFactoryTest(_fixtures.FixtureTest, AssertsCompiledSQL): from sqlalchemy.orm.properties import PropertyLoader + # NOTE: this API changed in 0.8, previously __clause_element__() + # gave the parent selecatable, now it gives the + # primaryjoin/secondaryjoin class MyFactory(PropertyLoader.Comparator): __hash__ = None def __eq__(self, other): - return func.foobar(self.__clause_element__().c.user_id) == func.foobar(other.id) + return func.foobar(self._source_selectable().c.user_id) == \ + func.foobar(other.id) class MyFactory2(PropertyLoader.Comparator): __hash__ = None def __eq__(self, other): - return func.foobar(self.__clause_element__().c.id) == func.foobar(other.user_id) + return func.foobar(self._source_selectable().c.id) == \ + func.foobar(other.user_id) mapper(User, users) mapper(Address, addresses, properties={ - 'user':relationship(User, comparator_factory=MyFactory, + 'user': relationship(User, comparator_factory=MyFactory, backref=backref("addresses", comparator_factory=MyFactory2) ) } ) - self.assert_compile(Address.user == User(id=5), "foobar(addresses.user_id) = foobar(:foobar_1)", dialect=default.DefaultDialect()) - self.assert_compile(User.addresses == Address(id=5, user_id=7), "foobar(users.id) = foobar(:foobar_1)", dialect=default.DefaultDialect()) - self.assert_compile(aliased(Address).user == User(id=5), "foobar(addresses_1.user_id) = foobar(:foobar_1)", dialect=default.DefaultDialect()) - self.assert_compile(aliased(User).addresses == Address(id=5, user_id=7), "foobar(users_1.id) = foobar(:foobar_1)", dialect=default.DefaultDialect()) + # these are kind of nonsensical tests. + self.assert_compile(Address.user == User(id=5), + "foobar(addresses.user_id) = foobar(:foobar_1)", + dialect=default.DefaultDialect()) + self.assert_compile(User.addresses == Address(id=5, user_id=7), + "foobar(users.id) = foobar(:foobar_1)", + dialect=default.DefaultDialect()) + + self.assert_compile( + aliased(Address).user == User(id=5), + "foobar(addresses_1.user_id) = foobar(:foobar_1)", + dialect=default.DefaultDialect()) + + self.assert_compile( + aliased(User).addresses == Address(id=5, user_id=7), + "foobar(users_1.id) = foobar(:foobar_1)", + dialect=default.DefaultDialect()) class DeferredTest(_fixtures.FixtureTest): diff --git a/test/orm/test_query.py b/test/orm/test_query.py index 56275a735..52f83ba32 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -129,6 +129,26 @@ class RawSelectTest(QueryTest, AssertsCompiledSQL): "SELECT * FROM users" ) + def test_where_relationship(self): + User = self.classes.User + + self.assert_compile( + select([User]).where(User.addresses), + "SELECT users.id, users.name FROM users, addresses " + "WHERE users.id = addresses.user_id" + ) + + def test_where_m2m_relationship(self): + Item = self.classes.Item + + self.assert_compile( + select([Item]).where(Item.keywords), + "SELECT items.id, items.description FROM items, " + "item_keywords AS item_keywords_1, keywords " + "WHERE items.id = item_keywords_1.item_id " + "AND keywords.id = item_keywords_1.keyword_id" + ) + def test_inline_select_from_entity(self): User = self.classes.User diff --git a/test/sql/test_compiler.py b/test/sql/test_compiler.py index b09ae1ab0..53e626010 100644 --- a/test/sql/test_compiler.py +++ b/test/sql/test_compiler.py @@ -620,6 +620,14 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL): 'mytable.description, (SELECT mytable.myid ' 'FROM mytable) AS anon_1 FROM mytable') + s = select([table1.c.myid]).as_scalar() + s2 = s.where(table1.c.myid == 5) + self.assert_compile( + s2, "(SELECT mytable.myid FROM mytable WHERE mytable.myid = :myid_1)" + ) + self.assert_compile( + s, "(SELECT mytable.myid FROM mytable)" + ) # test that aliases use as_scalar() when used in an explicitly # scalar context @@ -2018,7 +2026,7 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL): self.assert_compile( expr, "x = :key", - {'x':12} + {'x': 12} ) def test_bind_params_missing(self): @@ -2114,7 +2122,7 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL): self.assert_compile(table1.c.myid.in_([literal('a'), table1.c.myid]), "mytable.myid IN (:param_1, mytable.myid)") - self.assert_compile(table1.c.myid.in_([literal('a'), table1.c.myid +'a']), + self.assert_compile(table1.c.myid.in_([literal('a'), table1.c.myid + 'a']), "mytable.myid IN (:param_1, mytable.myid + :myid_1)") self.assert_compile(table1.c.myid.in_([literal(1), 'a' + table1.c.myid]), @@ -2144,11 +2152,13 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL): self.assert_compile( select([table1.c.myid.in_(select([table2.c.otherid]))]), - "SELECT mytable.myid IN (SELECT myothertable.otherid FROM myothertable) AS anon_1 FROM mytable" + "SELECT mytable.myid IN (SELECT myothertable.otherid " + "FROM myothertable) AS anon_1 FROM mytable" ) self.assert_compile( select([table1.c.myid.in_(select([table2.c.otherid]).as_scalar())]), - "SELECT mytable.myid IN (SELECT myothertable.otherid FROM myothertable) AS anon_1 FROM mytable" + "SELECT mytable.myid IN (SELECT myothertable.otherid " + "FROM myothertable) AS anon_1 FROM mytable" ) self.assert_compile(table1.c.myid.in_( @@ -2160,17 +2170,24 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL): "SELECT mytable.myid FROM mytable WHERE mytable.myid = :myid_1 "\ "UNION SELECT mytable.myid FROM mytable WHERE mytable.myid = :myid_2)") - # test that putting a select in an IN clause does not blow away its ORDER BY clause + # test that putting a select in an IN clause does not + # blow away its ORDER BY clause self.assert_compile( select([table1, table2], table2.c.otherid.in_( - select([table2.c.otherid], order_by=[table2.c.othername], limit=10, correlate=False) + select([table2.c.otherid], order_by=[table2.c.othername], + limit=10, correlate=False) ), - from_obj=[table1.join(table2, table1.c.myid==table2.c.otherid)], order_by=[table1.c.myid] + from_obj=[table1.join(table2, + table1.c.myid == table2.c.otherid)], + order_by=[table1.c.myid] ), - "SELECT mytable.myid, mytable.name, mytable.description, myothertable.otherid, myothertable.othername FROM mytable "\ - "JOIN myothertable ON mytable.myid = myothertable.otherid WHERE myothertable.otherid IN (SELECT myothertable.otherid "\ - "FROM myothertable ORDER BY myothertable.othername LIMIT :param_1) ORDER BY mytable.myid", + "SELECT mytable.myid, mytable.name, mytable.description, " + "myothertable.otherid, myothertable.othername FROM mytable "\ + "JOIN myothertable ON mytable.myid = myothertable.otherid " + "WHERE myothertable.otherid IN (SELECT myothertable.otherid "\ + "FROM myothertable ORDER BY myothertable.othername " + "LIMIT :param_1) ORDER BY mytable.myid", {'param_1':10} ) diff --git a/test/sql/test_selectable.py b/test/sql/test_selectable.py index 374147a1b..bbf7eeab1 100644 --- a/test/sql/test_selectable.py +++ b/test/sql/test_selectable.py @@ -62,11 +62,11 @@ class SelectableTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled eq_( s1.c.foo.proxy_set, - set([s1.c.foo, scalar_select, scalar_select.element, table1.c.col1]) + set([s1.c.foo, scalar_select, scalar_select.element]) ) eq_( s2.c.foo.proxy_set, - set([s2.c.foo, scalar_select, scalar_select.element, table1.c.col1]) + set([s2.c.foo, scalar_select, scalar_select.element]) ) assert s1.corresponding_column(scalar_select) is s1.c.foo |