Fixed #33768 -- Fixed ordering compound queries by nulls_first/nulls_last on MySQL.

Columns of the left outer most select statement in a combined query
can be referenced by alias just like by index.

This removes combined query ordering by column index and avoids an
unnecessary usage of RawSQL which causes issues for backends that
specialize the treatment of null ordering.
This commit is contained in:
Simon Charette 2022-07-04 21:51:07 +01:00 committed by Mariusz Felisiak
parent 344d31c7e9
commit c58a8acd41
4 changed files with 31 additions and 10 deletions

View File

@ -235,6 +235,7 @@ class BaseDatabaseFeatures:
supports_select_difference = True supports_select_difference = True
supports_slicing_ordering_in_compound = False supports_slicing_ordering_in_compound = False
supports_parentheses_in_compound = True supports_parentheses_in_compound = True
requires_compound_order_by_subquery = False
# Does the database support SQL 2003 FILTER (WHERE ...) in aggregate # Does the database support SQL 2003 FILTER (WHERE ...) in aggregate
# expressions? # expressions?

View File

@ -69,6 +69,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_partial_indexes = False supports_partial_indexes = False
can_rename_index = True can_rename_index = True
supports_slicing_ordering_in_compound = True supports_slicing_ordering_in_compound = True
requires_compound_order_by_subquery = True
allows_multiple_constraints_on_same_fields = False allows_multiple_constraints_on_same_fields = False
supports_boolean_expr_in_select_clause = False supports_boolean_expr_in_select_clause = False
supports_comparing_boolean_expr = False supports_comparing_boolean_expr = False

View File

@ -435,21 +435,18 @@ class SQLCompiler:
for expr, is_ref in self._order_by_pairs(): for expr, is_ref in self._order_by_pairs():
resolved = expr.resolve_expression(self.query, allow_joins=True, reuse=None) resolved = expr.resolve_expression(self.query, allow_joins=True, reuse=None)
if self.query.combinator and self.select: if not is_ref and self.query.combinator and self.select:
src = resolved.expression src = resolved.expression
expr_src = expr.expression expr_src = expr.expression
# Relabel order by columns to raw numbers if this is a combined for sel_expr, _, col_alias in self.select:
# query; necessary since the columns can't be referenced by the if col_alias and not (
# fully qualified name and the simple column names may collide.
for idx, (sel_expr, _, col_alias) in enumerate(self.select):
if is_ref and col_alias == src.refs:
src = src.source
elif col_alias and not (
isinstance(expr_src, F) and col_alias == expr_src.name isinstance(expr_src, F) and col_alias == expr_src.name
): ):
continue continue
if src == sel_expr: if src == sel_expr:
resolved.set_source_expressions([RawSQL("%d" % (idx + 1), ())]) resolved.set_source_expressions(
[Ref(col_alias if col_alias else src.target.column, src)]
)
break break
else: else:
if col_alias: if col_alias:
@ -853,7 +850,11 @@ class SQLCompiler:
for _, (o_sql, o_params, _) in order_by: for _, (o_sql, o_params, _) in order_by:
ordering.append(o_sql) ordering.append(o_sql)
params.extend(o_params) params.extend(o_params)
result.append("ORDER BY %s" % ", ".join(ordering)) order_by_sql = "ORDER BY %s" % ", ".join(ordering)
if combinator and features.requires_compound_order_by_subquery:
result = ["SELECT * FROM (", *result, ")", order_by_sql]
else:
result.append(order_by_sql)
if with_limit_offset: if with_limit_offset:
result.append( result.append(

View File

@ -61,6 +61,24 @@ class QuerySetSetOperationTests(TestCase):
self.assertSequenceEqual(qs3.none(), []) self.assertSequenceEqual(qs3.none(), [])
self.assertNumbersEqual(qs3, [0, 1, 8, 9], ordered=False) self.assertNumbersEqual(qs3, [0, 1, 8, 9], ordered=False)
def test_union_order_with_null_first_last(self):
Number.objects.filter(other_num=5).update(other_num=None)
qs1 = Number.objects.filter(num__lte=1)
qs2 = Number.objects.filter(num__gte=2)
qs3 = qs1.union(qs2)
self.assertSequenceEqual(
qs3.order_by(
F("other_num").asc(nulls_first=True),
).values_list("other_num", flat=True),
[None, 1, 2, 3, 4, 6, 7, 8, 9, 10],
)
self.assertSequenceEqual(
qs3.order_by(
F("other_num").asc(nulls_last=True),
).values_list("other_num", flat=True),
[1, 2, 3, 4, 6, 7, 8, 9, 10, None],
)
@skipUnlessDBFeature("supports_select_intersection") @skipUnlessDBFeature("supports_select_intersection")
def test_intersection_with_empty_qs(self): def test_intersection_with_empty_qs(self):
qs1 = Number.objects.all() qs1 = Number.objects.all()