Opened 55 minutes ago

Last modified 19 minutes ago

#37474 new Bug

Combined queries with values() ordered by a non-selected field crash after a clone is evaluated

Reported by: cheek-walnut Owned by:
Component: Database layer (models, ORM) Version: 5.2
Severity: Low Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by cheek-walnut)

On a combined query (e.g. union()) using values() and ordered by a field that isn't in values(), evaluating one queryset breaks other querysets cloned from the same query.

class Author(models.Model):
    name = models.CharField(max_length=20)
    age = models.IntegerField()

class Book(models.Model):
    title = models.CharField(max_length=20)
    pages = models.IntegerField()
    author = models.ForeignKey(Author, models.CASCADE, related_name="books")

With two books (pages 10 and 20):

qs = Book.objects.filter(pages__gt=0).union(Book.objects.filter(pages__gt=0))
qs = qs.values("title").order_by("pages")
qs2 = qs.all()
list(qs)   # [{'title': 'b1'}, {'title': 'b2'}]
list(qs2)  # IndexError
  File "django/db/models/sql/compiler.py", line 77, in setup_query
    self.select, self.klass_info, self.annotation_col_map = self.get_select(
  File "django/db/models/sql/compiler.py", line 295, in get_select
    expression = cols[expression]
IndexError: tuple index out of range

Evaluating qs2 first breaks qs instead. list(qs[:1]) followed by list(qs), or evaluating qs.all() twice, crash the same way. Ordering by a selected field (order_by("title")) works.

Expected: both evaluations return [{'title': 'b1'}, {'title': 'b2'}], as on Django 5.1.

Regression in 65ad4ade74dc9208b9d686a451cd6045df0c9c3a (Refs #28900): the queries work on its parent and fail on it. Reproduced on 5.2.19, 6.0.10, 6.1.3 and main with SQLite, and on main with PostgreSQL 16; 5.1.15 is not affected.

The cause appears to be that Query.clone() (django/db/models/sql/query.py:387-395 at 5bd95c57a2) doesn't copy the selected dict, so clones share it, while Query.add_select_col() (:2265, called from SQLCompiler.get_order_by() at compiler.py:544 for combined queries) mutates it in place and each clone keeps its own select tuple. This is different from #37466, which is about composite primary keys.

AI disclosure: found during a code audit with Claude Code 2.1.317 (Claude Opus 5.5), which also reduced the test case and ran it on the versions above and on 65ad4ade74 and its parent. I reviewed this report and re-ran the reproduction on main and stable/5.1.x.

Change History (4)

comment:1 by cheek-walnut, 52 minutes ago

Summary: Combined queries with values() ordered by a non-selected field crash with IndexError after a clone is → Combined queries with values() ordered by a non-selected field crash after a clone is evaluated
Version: 6.1 → 5.2

comment:2 by cheek-walnut, 38 minutes ago

This is easier to hit than the description suggests, because common operations evaluate a clone of the queryset with its ordering, e.g. Paginator, first() or slicing. With five books:

qs = Book.objects.filter(pages__gt=0).union(Book.objects.filter(pages__lt=1000))
qs = qs.values("title").order_by("-pages")
paginator = Paginator(qs, 2)
list(paginator.page(1))  # [{'title': 'b4'}, {'title': 'b3'}]
list(paginator.page(2))  # IndexError: tuple index out of range

qs.first() followed by list(qs), and values_list("pk", flat=True) evaluated in slices (ids[0:2], then ids[2:4]), crash the same way.

All of these work on 5.1.15 and on the parent of 65ad4ade74, and fail on 65ad4ade74, 5.2.19, 6.0.10, 6.1.3 and main.

AI disclosure: these variants were also found and run with Claude Code 2.1.317 (Claude Opus 5.5); I re-ran them on main and stable/5.1.x.

Last edited 19 minutes ago by cheek-walnut (previous) (diff)

comment:3 by cheek-walnut, 35 minutes ago

Description: modified (diff)

comment:4 by cheek-walnut, 19 minutes ago

Description: modified (diff)
Note: See TracTickets for help on using tickets.
Back to Top