Version 0.9 is a faster-than-usual push from version 0.8,
featuring a more versatile codebase with regards to modern
-Python versions. The upgrade path at the moment requires no changes
-to user code, however this is subject to change.
+Python versions. See :ref:`behavioral_changes_09` for
+potentially backwards-incompatible changes.
Platform Support
================
At the moment, the C extensions are still not fully ported to
Python 3.
+
+
+.. _behavioral_changes_09:
+
+Behavioral Changes
+==================
+
+.. _migration_2736:
+
+:meth:`.Query.select_from` no longer applies the clause to corresponding entities
+---------------------------------------------------------------------------------
+
+The :meth:`.Query.select_from` method has been popularized in recent versions
+as a means of controlling the first thing that a :class:`.Query` object
+"selects from", typically for the purposes of controlling how a JOIN will
+render.
+
+Consider the following example against the usual ``User`` mapping::
+
+ select_stmt = select([User]).where(User.id == 7).alias()
+
+ q = session.query(User).\
+ join(select_stmt, User.id == select_stmt.c.id).\
+ filter(User.name == 'ed')
+
+The above statement predictably renders SQL like the following::
+
+ SELECT "user".id AS user_id, "user".name AS user_name
+ FROM "user" JOIN (SELECT "user".id AS id, "user".name AS name
+ FROM "user"
+ WHERE "user".id = :id_1) AS anon_1 ON "user".id = anon_1.id
+ WHERE "user".name = :name_1
+
+If we wanted to reverse the order of the left and right elements of the
+JOIN, the documentation would lead us to believe we could use
+:meth:`.Query.select_from` to do so::
+
+ q = session.query(User).\
+ select_from(select_stmt).\
+ join(User, User.id == select_stmt.c.id).\
+ filter(User.name == 'ed')
+
+However, in version 0.8 and earlier, the above use of :meth:`.Query.select_from`
+would apply the ``select_stmt`` to **replace** the ``User`` entity, as it
+selects from the ``user`` table which is compatible with ``User``::
+
+ -- SQLAlchemy 0.8 and earlier...
+ SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name
+ FROM (SELECT "user".id AS id, "user".name AS name
+ FROM "user"
+ WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON anon_1.id = anon_1.id
+ WHERE anon_1.name = :name_1
+
+The above statement is a mess, the ON clause refers ``anon_1.id = anon_1.id``,
+our WHERE clause has been replaced with ``anon_1`` as well.
+
+This behavior is quite intentional, but has a different use case from that
+which has become popular for :meth:`.Query.select_from`. The above behavior
+is now available by a new method known as :meth:`.Query.select_entity_from`.
+This is a lesser used behavior that in modern SQLAlchemy is roughly equivalent
+to selecting from a customized :func:`.aliased` construct::
+
+ select_stmt = select([User]).where(User.id == 7)
+ user_from_stmt = aliased(User, select_stmt.alias())
+
+ q = session.query(user_from_stmt).filter(user_from_stmt.name == 'ed')
+
+So with SQLAlchemy 0.9, our query that selects from ``select_stmt`` produces
+the SQL we expect::
+
+ -- SQLAlchemy 0.9
+ SELECT "user".id AS user_id, "user".name AS user_name
+ FROM (SELECT "user".id AS id, "user".name AS name
+ FROM "user"
+ WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON "user".id = id
+ WHERE "user".name = :name_1
+
+The :meth:`.Query.select_entity_from` method will be available in SQLAlchemy
+**0.8.2**, so applications which rely on the old behavior can transition
+to this method first, ensure all tests continue to function, then upgrade
+to 0.9 without issue.
+
+:ticket:`2736`
+
+
+
for m in m2.iterate_to_root():
self._polymorphic_adapters[m.local_table] = adapter
- def _set_select_from(self, *obj):
+ def _set_select_from(self, obj, set_base_alias):
fa = []
select_from_alias = None
+
for from_obj in obj:
info = inspect(from_obj)
if hasattr(info, 'mapper') and \
(info.is_mapper or info.is_aliased_class):
- self._select_from_entity = from_obj
+ if set_base_alias:
+ raise sa_exc.ArgumentError(
+ "A selectable (FromClause) instance is "
+ "expected when the base alias is being set.")
fa.append(info.selectable)
elif not info.is_selectable:
raise sa_exc.ArgumentError(
else:
if isinstance(from_obj, expression.SelectBase):
from_obj = from_obj.alias()
- select_from_alias = from_obj
+ if set_base_alias:
+ select_from_alias = from_obj
fa.append(from_obj)
self._from_obj = tuple(fa)
- if len(self._from_obj) == 1 and \
+ if set_base_alias and \
+ len(self._from_obj) == 1 and \
isinstance(select_from_alias, expression.Alias):
equivs = self.__all_equivs()
self._from_obj_alias = sql_util.ColumnAdapter(
'_prefixes',
):
self.__dict__.pop(attr, None)
- self._set_select_from(fromclause)
+ self._set_select_from([fromclause], True)
# this enables clause adaptation for non-ORM
# expressions.
def select_from(self, *from_obj):
"""Set the FROM clause of this :class:`.Query` explicitly.
- Sending a mapped class or entity here effectively replaces the
+ :meth:`.Query.select_from` is often used in conjunction with
+ :meth:`.Query.join` in order to control which entity is selected
+ from on the "left" side of the join.
+
+ The entity or selectable object here effectively replaces the
"left edge" of any calls to :meth:`~.Query.join`, when no
joinpoint is otherwise established - usually, the default "join
point" is the leftmost entity in the :class:`~.Query` object's
list of entities to be selected.
- Mapped entities or plain :class:`~.Table` or other selectables
- can be sent here which will form the default FROM clause.
+ A typical example::
+
+ q = session.query(Address).select_from(User).\\
+ join(User.addresses).\\
+ filter(User.name == 'ed')
+
+ Which produces SQL equivalent to::
+
+ SELECT address.* FROM user
+ JOIN address ON user.id=address.user_id
+ WHERE user.name = :name_1
+
+ :param \*from_obj: collection of one or more entities to apply
+ to the FROM clause. Entities can be mapped classes,
+ :class:`.AliasedClass` objects, :class:`.Mapper` objects
+ as well as core :class:`.FromClause` elements like subqueries.
+
+ .. versionchanged:: 0.9
+ This method no longer applies the given FROM object
+ to be the selectable from which matching entities
+ select from; the :meth:`.select_entity_from` method
+ now accomplishes this. See that method for a description
+ of this behavior.
+
+ .. seealso::
+
+ :meth:`~.Query.join`
+
+ :meth:`.Query.select_entity_from`
+
+ """
+
+ self._set_select_from(from_obj, False)
+
+ @_generative(_no_clauseelement_condition)
+ def select_entity_from(self, from_obj):
+ """Set the FROM clause of this :class:`.Query` to a
+ core selectable, applying it as a replacement FROM clause
+ for corresponding mapped entities.
+
+ This method is similar to the :meth:`.Query.select_from`
+ method, in that it sets the FROM clause of the query. However,
+ where :meth:`.Query.select_from` only affects what is placed
+ in the FROM, this method also applies the given selectable
+ to replace the FROM which the selected entities would normally
+ select from.
+
+ The given ``from_obj`` must be an instance of a :class:`.FromClause`,
+ e.g. a :func:`.select` or :class:`.Alias` construct.
+
+ An example would be a :class:`.Query` that selects ``User`` entities,
+ but uses :meth:`.Query.select_entity_from` to have the entities
+ selected from a :func:`.select` construct instead of the
+ base ``user`` table::
+
+ select_stmt = select([User]).where(User.id == 7)
+
+ q = session.query(User).\\
+ select_entity_from(select_stmt).\\
+ filter(User.name == 'ed')
+
+ The query generated will select ``User`` entities directly
+ from the given :func:`.select` construct, and will be::
+
+ SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name
+ FROM (SELECT "user".id AS id, "user".name AS name
+ FROM "user"
+ WHERE "user".id = :id_1) AS anon_1
+ WHERE anon_1.name = :name_1
+
+ Notice above that even the WHERE criterion was "adapted" such that
+ the ``anon_1`` subquery effectively replaces all references to the
+ ``user`` table, except for the one that it refers to internally.
+
+ Compare this to :meth:`.Query.select_from`, which as of
+ version 0.9, does not affect existing entities. The
+ statement below::
+
+ q = session.query(User).\\
+ select_from(select_stmt).\\
+ filter(User.name == 'ed')
+
+ Produces SQL where both the ``user`` table as well as the
+ ``select_stmt`` construct are present as separate elements
+ in the FROM clause. No "adaptation" of the ``user`` table
+ is applied::
+
+ SELECT "user".id AS user_id, "user".name AS user_name
+ FROM "user", (SELECT "user".id AS id, "user".name AS name
+ FROM "user"
+ WHERE "user".id = :id_1) AS anon_1
+ WHERE "user".name = :name_1
+
+ :meth:`.Query.select_entity_from` maintains an older
+ behavior of :meth:`.Query.select_from`. In modern usage,
+ similar results can also be achieved using :func:`.aliased`::
+
+ select_stmt = select([User]).where(User.id == 7)
+ user_from_select = aliased(User, select_stmt.alias())
+
+ q = session.query(user_from_select)
+
+ :param from_obj: a :class:`.FromClause` object that will replace
+ the FROM clause of this :class:`.Query`.
+
+ .. seealso::
+
+ :meth:`.Query.select_from`
- See the example in :meth:`~.Query.join` for a typical
- usage of :meth:`~.Query.select_from`.
+ .. versionadded:: 0.8
+ :meth:`.Query.select_entity_from` was added to specify
+ the specific behavior of entity replacement, however
+ the :meth:`.Query.select_from` maintains this behavior
+ as well until 0.9.
"""
- self._set_select_from(*from_obj)
+ self._set_select_from([from_obj], True)
def __getitem__(self, item):
if isinstance(item, slice):
sess = create_session()
- self.assert_compile(sess.query(users).select_from(
+ self.assert_compile(sess.query(users).select_entity_from(
users.select()).with_labels().statement,
"SELECT users.id AS users_id, users.name AS users_name FROM users, "
"(SELECT users.id AS id, users.name AS name FROM users) AS anon_1",
filter(addresses.c.user_id == users.c.id).correlate(users).\
statement.alias()
- self.assert_compile(sess.query(users, s.c.email).select_from(
+ self.assert_compile(sess.query(users, s.c.email).select_entity_from(
users.join(s, s.c.id == users.c.id)
).with_labels().statement,
"SELECT users.id AS users_id, users.name AS users_name, "
"WHERE anon_1.anon_2_users_name = :name_1"
)
- def test_select_from(self):
+ def test_select_entity_from(self):
User = self.classes.User
sess = create_session()
q = sess.query(User)
- q = sess.query(User).select_from(q.statement)
+ q = sess.query(User).select_entity_from(q.statement)
self.assert_compile(
q.filter(User.name=='ed'),
"SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name "
"users) AS anon_1 WHERE anon_1.name = :name_1"
)
+ def test_select_from_no_aliasing(self):
+ User = self.classes.User
+ sess = create_session()
+
+ q = sess.query(User)
+ q = sess.query(User).select_from(q.statement)
+ self.assert_compile(
+ q.filter(User.name=='ed'),
+ "SELECT users.id AS users_id, users.name AS users_name "
+ "FROM users, (SELECT users.id AS id, users.name AS name FROM "
+ "users) AS anon_1 WHERE users.name = :name_1"
+ )
+
def test_anonymous_expression(self):
from sqlalchemy.sql import column
assert self.static.user_address_result == l
self.assert_sql_count(testing.db, go, 1)
- # better way. use select_from()
+ # better way. use select_entity_from()
def go():
- l = sess.query(User).select_from(query).\
+ l = sess.query(User).select_entity_from(query).\
options(contains_eager('addresses')).all()
assert self.static.user_address_result == l
self.assert_sql_count(testing.db, go, 1)
# same thing, but alias addresses, so that the adapter
- # generated by select_from() is wrapped within
+ # generated by select_entity_from() is wrapped within
# the adapter created by contains_eager()
adalias = addresses.alias()
query = users.select(users.c.id==7).\
select(use_labels=True,
order_by=['ulist.id', adalias.c.id])
def go():
- l = sess.query(User).select_from(query).\
+ l = sess.query(User).select_entity_from(query).\
options(contains_eager('addresses', alias=adalias)).all()
assert self.static.user_address_result == l
self.assert_sql_count(testing.db, go, 1)
adalias = addresses.alias()
q = sess.query(User).\
- select_from(users.outerjoin(adalias)).\
+ select_entity_from(users.outerjoin(adalias)).\
options(contains_eager(User.addresses, alias=adalias)).\
order_by(User.id, adalias.c.id)
def go():
sel = users.select(User.id.in_([7, 8])).alias()
q = sess.query(User)
- q2 = q.select_from(sel).values(User.name)
+ q2 = q.select_entity_from(sel).values(User.name)
eq_(list(q2), [('jack',), ('ed',)])
q = sess.query(User)
q2 = q.values(func.count(User.name))
assert next(q2) == (4,)
- q2 = q.select_from(sel).filter(User.id==8).values(User.name, sel.c.name, User.name)
+ q2 = q.select_entity_from(sel).filter(User.id==8).values(User.name, sel.c.name, User.name)
eq_(list(q2), [('ed', 'ed', 'ed')])
# using User.xxx is alised against "sel", so this query returns nothing
- q2 = q.select_from(sel).\
+ q2 = q.select_entity_from(sel).\
filter(User.id==8).\
filter(User.id>sel.c.id).values(User.name, sel.c.name, User.name)
eq_(list(q2), [])
# whereas this uses users.c.xxx, is not aliased and creates a new join
- q2 = q.select_from(sel).\
+ q2 = q.select_entity_from(sel).\
filter(users.c.id==8).\
filter(users.c.id>sel.c.id).values(users.c.name, sel.c.name, User.name)
eq_(list(q2), [('ed', 'jack', 'jack')])
sel = users.select(User.id.in_([7, 8])).alias()
q = sess.query(User)
u2 = aliased(User)
- q2 = q.select_from(sel).\
+ q2 = q.select_entity_from(sel).\
filter(u2.id>1).\
order_by(User.id, sel.c.id, u2.id).\
values(User.name, sel.c.name, u2.name)
sel = users.select(User.id.in_([7, 8])).alias()
q = sess.query(User.name)
- q2 = q.select_from(sel).all()
+ q2 = q.select_entity_from(sel).all()
eq_(list(q2), [('jack',), ('ed',)])
eq_(sess.query(User.name, Address.email_address).filter(User.id==Address.user_id).all(), [
]
)
- # test eager aliasing, with/without select_from aliasing
+ # test eager aliasing, with/without select_entity_from aliasing
for q in [
sess.query(User, adalias.email_address).\
outerjoin(adalias, User.addresses).\
q = sess.query(User)
adalias = addresses.alias('adalias')
- q = q.add_entity(Address, alias=adalias).select_from(users.outerjoin(adalias))
+ q = q.add_entity(Address, alias=adalias).select_entity_from(users.outerjoin(adalias))
l = q.order_by(User.id, adalias.c.id).all()
assert l == expected
sess.expunge_all()
q = sess.query(User).add_entity(Address, alias=adalias)
- l = q.select_from(users.outerjoin(adalias)).filter(adalias.c.email_address=='ed@bettyboop.com').all()
+ l = q.select_entity_from(users.outerjoin(adalias)).filter(adalias.c.email_address=='ed@bettyboop.com').all()
assert l == [(user8, address3)]
def test_with_entities(self):
sess.expunge_all()
- # test with select_from()
+ # test with select_entity_from()
q = create_session().query(User).add_column(func.count(addresses.c.id))\
- .add_column(("Name:" + users.c.name)).select_from(users.outerjoin(addresses))\
+ .add_column(("Name:" + users.c.name)).select_entity_from(users.outerjoin(addresses))\
.group_by(users).order_by(users.c.id)
assert q.all() == expected
sel = users.select(users.c.id.in_([7, 8])).alias()
sess = create_session()
- eq_(sess.query(User).select_from(sel).all(), [User(id=7), User(id=8)])
+ eq_(sess.query(User).select_entity_from(sel).all(), [User(id=7), User(id=8)])
- eq_(sess.query(User).select_from(sel).filter(User.id==8).all(), [User(id=8)])
+ eq_(sess.query(User).select_entity_from(sel).filter(User.id==8).all(), [User(id=8)])
- eq_(sess.query(User).select_from(sel).order_by(desc(User.name)).all(), [
+ eq_(sess.query(User).select_entity_from(sel).order_by(desc(User.name)).all(), [
User(name='jack',id=7), User(name='ed',id=8)
])
- eq_(sess.query(User).select_from(sel).order_by(asc(User.name)).all(), [
+ eq_(sess.query(User).select_entity_from(sel).order_by(asc(User.name)).all(), [
User(name='ed',id=8), User(name='jack',id=7)
])
- eq_(sess.query(User).select_from(sel).options(joinedload('addresses')).first(),
+ eq_(sess.query(User).select_entity_from(sel).options(joinedload('addresses')).first(),
User(name='jack', addresses=[Address(id=1)])
)
sel = users.select(users.c.id.in_([7, 8]))
sess = create_session()
- eq_(sess.query(User).select_from(sel).all(),
+ eq_(sess.query(User).select_entity_from(sel).all(),
[
User(name='jack',id=7), User(name='ed',id=8)
]
)
self.assert_compile(
- sess.query(ualias).select_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_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 "
)
self.assert_compile(
- sess.query(ualias).select_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 "
# this one uses an explicit join(left, right, onclause) so works
self.assert_compile(
- sess.query(ualias).select_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",
# here for comparison
self.assert_compile(
sess.query(User.name).\
- select_from(users.select().where(users.c.id > 5)),
+ select_entity_from(users.select().where(users.c.id > 5)),
"SELECT anon_1.name AS anon_1_name FROM (SELECT users.id AS id, "
"users.name AS name FROM users WHERE users.id > :id_1) AS anon_1"
)
sel = users.select(users.c.id.in_([7, 8]))
sess = create_session()
- eq_(sess.query(User).select_from(sel).all(),
+ eq_(sess.query(User).select_entity_from(sel).all(),
[
User(name='jack',id=7), User(name='ed',id=8)
]
sel = users.select(users.c.id.in_([7, 8]))
sess = create_session()
- eq_(sess.query(User).select_from(sel).join('addresses').
+ eq_(sess.query(User).select_entity_from(sel).join('addresses').
add_entity(Address).order_by(User.id).order_by(Address.id).all(),
[
(User(name='jack',id=7), Address(user_id=7,email_address='jack@bean.com',id=1)),
)
adalias = aliased(Address)
- eq_(sess.query(User).select_from(sel).join(adalias, 'addresses').
+ eq_(sess.query(User).select_entity_from(sel).join(adalias, 'addresses').
add_entity(adalias).order_by(User.id).order_by(adalias.id).all(),
[
(User(name='jack',id=7), Address(user_id=7,email_address='jack@bean.com',id=1)),
sess = create_session()
sel = users.select(users.c.id.in_([7, 8]))
- eq_(sess.query(User).select_from(sel).\
+ eq_(sess.query(User).select_entity_from(sel).\
join('orders', 'items', 'keywords').\
filter(Keyword.name.in_(['red', 'big', 'round'])).\
all(),
User(name='jack',id=7)
])
- eq_(sess.query(User).select_from(sel).\
+ eq_(sess.query(User).select_entity_from(sel).\
join('orders', 'items', 'keywords', aliased=True).\
filter(Keyword.name.in_(['red', 'big', 'round'])).\
all(),
def go():
eq_(
- sess.query(User).select_from(sel).
+ sess.query(User).select_entity_from(sel).
options(joinedload_all('orders.items.keywords')).
join('orders', 'items', 'keywords', aliased=True).
filter(Keyword.name.in_(['red', 'big', 'round'])).\
sess.expunge_all()
sel2 = orders.select(orders.c.id.in_([1,2,3]))
- eq_(sess.query(Order).select_from(sel2).\
+ eq_(sess.query(Order).select_entity_from(sel2).\
join('items', 'keywords').\
filter(Keyword.name == 'red').\
order_by(Order.id).all(), [
Order(description='order 1',id=1),
Order(description='order 2',id=2),
])
- eq_(sess.query(Order).select_from(sel2).\
+ eq_(sess.query(Order).select_entity_from(sel2).\
join('items', 'keywords', aliased=True).\
filter(Keyword.name == 'red').\
order_by(Order.id).all(), [
sess = create_session()
def go():
- eq_(sess.query(User).options(joinedload('addresses')).select_from(sel).order_by(User.id).all(),
+ eq_(sess.query(User).options(
+ joinedload('addresses')
+ ).select_entity_from(sel).order_by(User.id).all(),
[
User(id=7, addresses=[Address(id=1)]),
User(id=8, addresses=[Address(id=2), Address(id=3), Address(id=4)])
sess.expunge_all()
def go():
- eq_(sess.query(User).options(joinedload('addresses')).select_from(sel).filter(User.id==8).order_by(User.id).all(),
+ eq_(sess.query(User).options(
+ joinedload('addresses')
+ ).select_entity_from(sel).filter(User.id==8).order_by(User.id).all(),
[User(id=8, addresses=[Address(id=2), Address(id=3), Address(id=4)])]
)
self.assert_sql_count(testing.db, go, 1)
sess.expunge_all()
def go():
- eq_(sess.query(User).options(joinedload('addresses')).select_from(sel).order_by(User.id)[1], User(id=8, addresses=[Address(id=2), Address(id=3), Address(id=4)]))
+ eq_(sess.query(User).options(
+ joinedload('addresses')
+ ).select_entity_from(sel).order_by(User.id)[1],
+ User(id=8, addresses=[Address(id=2), Address(id=3), Address(id=4)]))
self.assert_sql_count(testing.db, go, 1)
class CustomJoinTest(QueryTest):
ua = aliased(User)
eq_(sess.query(Address, ua.concat, ua.count).
- select_from(join(Address, ua, 'user')).
+ select_entity_from(join(Address, ua, 'user')).
options(joinedload(Address.user)).order_by(Address.id).all(),
[
(Address(id=1, user=User(id=7, concat=14, count=1)), 14, 1),
[(1, 7, 14, 1), (2, 8, 16, 3), (3, 8, 16, 3), (4, 8, 16, 3), (5, 9, 18, 1)]
)
- eq_(list(sess.query(Address, ua).select_from(join(Address,ua, 'user')).values(Address.id, ua.id, ua.concat, ua.count)),
+ eq_(list(sess.query(Address, ua).select_entity_from(join(Address,ua, 'user')).values(Address.id, ua.id, ua.concat, ua.count)),
[(1, 7, 14, 1), (2, 8, 16, 3), (3, 8, 16, 3), (4, 8, 16, 3), (5, 9, 18, 1)]
)