summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2015-04-27 17:32:05 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2015-04-27 17:40:41 -0400
commit34f98a63b54a17c06e48eab5a29e9c090488b4bd (patch)
treea3e82f802d78a67bea9ee92bec4982800de0b831
parente5c20fce93549d9af972e99a7bf826d7cc99189f (diff)
downloadsqlalchemy-34f98a63b54a17c06e48eab5a29e9c090488b4bd.tar.gz
- altered part of the use contract first set up in #2992; we
now skip textual label references when copying ORDER BY elements to the joined-eager-load subquery, as we can't know that these expressions are compatible with this placement; either because they are meant for text(), or because they refer to label names already stated and aren't bound to a table. fixes #3392
-rw-r--r--doc/build/changelog/changelog_10.rst33
-rw-r--r--lib/sqlalchemy/sql/util.py5
-rw-r--r--test/orm/test_query.py135
3 files changed, 154 insertions, 19 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst
index b313c7cac..5b0ddf64f 100644
--- a/doc/build/changelog/changelog_10.rst
+++ b/doc/build/changelog/changelog_10.rst
@@ -19,6 +19,39 @@
:version: 1.0.3
.. change::
+ :tags: bug, orm
+ :tickets: 3392
+
+ Fixed regression due to :ticket:`2992` where textual elements placed
+ into the :meth:`.Query.order_by` clause in conjunction with joined
+ eager loading would be added to the columns clause of the inner query
+ in such a way that they were assumed to be table-bound column names,
+ in the case where the joined eager load needs to wrap the query
+ in a subquery to accommodate for a limit/offset.
+
+ Originally, the behavior here was intentional, in that a query such
+ as ``query(User).order_by('name').limit(1)``
+ would order by ``user.name`` even if the query was modified by
+ joined eager loading to be within a subquery, as ``'name'`` would
+ be interpreted as a symbol to be located within the FROM clauses,
+ in this case ``User.name``, which would then be copied into the
+ columns clause to ensure it were present for ORDER BY. However, the
+ feature fails to anticipate the case where ``order_by("name")`` refers
+ to a specific label name present in the local columns clause already
+ and not a name bound to a selectable in the FROM clause.
+
+ Beyond that, the feature also fails for deprecated cases such as
+ ``order_by("name desc")``, which, while it emits a
+ warning that :func:`.text` should be used here (note that the issue
+ does not impact cases where :func:`.text` is used explicitly),
+ still produces a different query than previously where the "name desc"
+ expression is copied into the columns clause inappropriately. The
+ resolution is such that the "joined eager loading" aspect of the
+ feature will skip over these so-called "label reference" expressions
+ when augmenting the inner columns clause, as though they were
+ :func:`.text` constructs already.
+
+ .. change::
:tags: bug, sql
:tickets: 3391
diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py
index bec5b5824..8f502fc86 100644
--- a/lib/sqlalchemy/sql/util.py
+++ b/lib/sqlalchemy/sql/util.py
@@ -16,7 +16,8 @@ from itertools import chain
from collections import deque
from .elements import BindParameter, ColumnClause, ColumnElement, \
- Null, UnaryExpression, literal_column, Label, _label_reference
+ Null, UnaryExpression, literal_column, Label, _label_reference, \
+ _textual_label_reference
from .selectable import ScalarSelect, Join, FromClause, FromGrouping
from .schema import Column
@@ -163,6 +164,8 @@ def unwrap_order_by(clause):
):
if isinstance(t, _label_reference):
t = t.element
+ if isinstance(t, (_textual_label_reference)):
+ continue
cols.add(t)
else:
for c in t.get_children():
diff --git a/test/orm/test_query.py b/test/orm/test_query.py
index 4021cdfbb..850014ab4 100644
--- a/test/orm/test_query.py
+++ b/test/orm/test_query.py
@@ -2860,44 +2860,143 @@ class TextTest(QueryTest, AssertsCompiledSQL):
[User(id=7), User(id=8), User(id=9), User(id=10)]
)
- def test_order_by_w_eager(self):
+ def test_order_by_w_eager_one(self):
+ User = self.classes.User
+ s = create_session()
+
+ # from 1.0.0 thru 1.0.2, the "name" symbol here was considered
+ # to be part of the things we need to ORDER BY and it was being
+ # placed into the inner query's columns clause, as part of
+ # query._compound_eager_statement where we add unwrap_order_by()
+ # to the columns clause. However, as #3392 illustrates, unlocatable
+ # string expressions like "name desc" will only fail in this scenario,
+ # so in general the changing of the query structure with string labels
+ # is dangerous.
+ #
+ # the queries here are again "invalid" from a SQL perspective, as the
+ # "name" field isn't matched up to anything.
+ #
+ with expect_warnings("Can't resolve label reference 'name';"):
+ self.assert_compile(
+ s.query(User).options(joinedload("addresses")).
+ order_by(desc("name")).limit(1),
+ "SELECT anon_1.users_id AS anon_1_users_id, "
+ "anon_1.users_name AS anon_1_users_name, "
+ "addresses_1.id AS addresses_1_id, "
+ "addresses_1.user_id AS addresses_1_user_id, "
+ "addresses_1.email_address AS addresses_1_email_address "
+ "FROM (SELECT users.id AS users_id, users.name AS users_name "
+ "FROM users ORDER BY users.name "
+ "DESC LIMIT :param_1) AS anon_1 "
+ "LEFT OUTER JOIN addresses AS addresses_1 "
+ "ON anon_1.users_id = addresses_1.user_id "
+ "ORDER BY name DESC, addresses_1.id"
+ )
+
+ def test_order_by_w_eager_two(self):
+ User = self.classes.User
+ s = create_session()
+
+ with expect_warnings("Can't resolve label reference 'name';"):
+ self.assert_compile(
+ s.query(User).options(joinedload("addresses")).
+ order_by("name").limit(1),
+ "SELECT anon_1.users_id AS anon_1_users_id, "
+ "anon_1.users_name AS anon_1_users_name, "
+ "addresses_1.id AS addresses_1_id, "
+ "addresses_1.user_id AS addresses_1_user_id, "
+ "addresses_1.email_address AS addresses_1_email_address "
+ "FROM (SELECT users.id AS users_id, users.name AS users_name "
+ "FROM users ORDER BY users.name "
+ "LIMIT :param_1) AS anon_1 "
+ "LEFT OUTER JOIN addresses AS addresses_1 "
+ "ON anon_1.users_id = addresses_1.user_id "
+ "ORDER BY name, addresses_1.id"
+ )
+
+ def test_order_by_w_eager_three(self):
+ User = self.classes.User
+ s = create_session()
+
+ self.assert_compile(
+ s.query(User).options(joinedload("addresses")).
+ order_by("users_name").limit(1),
+ "SELECT anon_1.users_id AS anon_1_users_id, "
+ "anon_1.users_name AS anon_1_users_name, "
+ "addresses_1.id AS addresses_1_id, "
+ "addresses_1.user_id AS addresses_1_user_id, "
+ "addresses_1.email_address AS addresses_1_email_address "
+ "FROM (SELECT users.id AS users_id, users.name AS users_name "
+ "FROM users ORDER BY users.name "
+ "LIMIT :param_1) AS anon_1 "
+ "LEFT OUTER JOIN addresses AS addresses_1 "
+ "ON anon_1.users_id = addresses_1.user_id "
+ "ORDER BY anon_1.users_name, addresses_1.id"
+ )
+
+ # however! this works (again?)
+ eq_(
+ s.query(User).options(joinedload("addresses")).
+ order_by("users_name").first(),
+ User(name='chuck', addresses=[])
+ )
+
+ def test_order_by_w_eager_four(self):
User = self.classes.User
Address = self.classes.Address
s = create_session()
- # here, we are seeing how Query has to take the order by expressions
- # of the query and then add them to the columns list, so that the
- # outer subquery can order by that same label. With the anonymous
- # label, our column gets sucked up and restated again in the
- # inner columns list!
- # we could try to play games with making this "smarter" but it
- # would add permanent overhead to Select._columns_plus_names,
- # since that's where references would need to be resolved.
- # so as it is, this query takes the _label_reference and makes a
- # full blown proxy and all the rest of it.
self.assert_compile(
s.query(User).options(joinedload("addresses")).
- order_by(desc("name")).limit(1),
+ order_by(desc("users_name")).limit(1),
"SELECT anon_1.users_id AS anon_1_users_id, "
"anon_1.users_name AS anon_1_users_name, "
- "anon_1.anon_2 AS anon_1_anon_2, "
"addresses_1.id AS addresses_1_id, "
"addresses_1.user_id AS addresses_1_user_id, "
"addresses_1.email_address AS addresses_1_email_address "
- "FROM (SELECT users.id AS users_id, users.name AS users_name, "
- "users.name AS anon_2 FROM users ORDER BY users.name "
- "DESC LIMIT :param_1) AS anon_1 "
+ "FROM (SELECT users.id AS users_id, users.name AS users_name "
+ "FROM users ORDER BY users.name DESC "
+ "LIMIT :param_1) AS anon_1 "
"LEFT OUTER JOIN addresses AS addresses_1 "
"ON anon_1.users_id = addresses_1.user_id "
- "ORDER BY anon_1.anon_2 DESC, addresses_1.id"
+ "ORDER BY anon_1.users_name DESC, addresses_1.id"
)
+ # however! this works (again?)
eq_(
s.query(User).options(joinedload("addresses")).
- order_by(desc("name")).first(),
+ order_by(desc("users_name")).first(),
User(name='jack', addresses=[Address()])
)
+ def test_order_by_w_eager_five(self):
+ """essentially the same as test_eager_relations -> test_limit_3,
+ but test for textual label elements that are freeform.
+ this is again #3392."""
+
+ User = self.classes.User
+ Address = self.classes.Address
+ Order = self.classes.Order
+
+ sess = create_session()
+
+ q = sess.query(User, Address.email_address.label('email_address'))
+
+ l = q.join('addresses').options(joinedload(User.orders)).\
+ order_by(
+ "email_address desc").limit(1).offset(0)
+ with expect_warnings(
+ "Can't resolve label reference 'email_address desc'"):
+ eq_(
+ [
+ (User(
+ id=7,
+ orders=[Order(id=1), Order(id=3), Order(id=5)],
+ addresses=[Address(id=1)]
+ ), 'jack@bean.com')
+ ],
+ l.all())
+
class TextWarningTest(QueryTest, AssertsCompiledSQL):
def _test(self, fn, arg, offending_clause, expected):