Opened 53 minutes ago
#37353 new Bug
QuerySet.values("pk").distinct().order_by() on a composite primary key silently truncates the primary key
| Reported by: | Dave Gaeddert | Owned by: | |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 5.2 |
| Severity: | Normal | Keywords: | composite primary key |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Selecting a composite primary key with values() / values_list(), combined with distinct() and order_by() on a field that isn't selected, returns the primary key with a component missing:
class Tenant(models.Model):
pass
class User(models.Model):
pk = models.CompositePrimaryKey("tenant_id", "id")
tenant = models.ForeignKey(Tenant, models.CASCADE)
id = models.SmallIntegerField(unique=True)
email = models.EmailField(unique=True)
User.objects.values("pk").distinct().order_by("email")
# [{'pk': (1,)}, {'pk': (1,)}, {'pk': (2,)}]
# expected [{'pk': (1, 1)}, {'pk': (1, 2)}, {'pk': (2, 3)}]
User.objects.values("pk", "id").distinct().order_by("email")
# IndexError: list index out of range
No exception in the first case — silent data loss. Reproduces on 5.2, 6.0, and main, on both PostgreSQL 16 and SQLite.
Cause: ordering by an unselected field with distinct() appends that field to the select clause via get_extra_select(), and the extra column is then stripped from each row by slicing with SQLCompiler.col_count. col_count is the number of selections, but a CompositePrimaryKey is one selection compiled to N columns, so the slice cuts into the primary key itself.
Found while verifying the fix for #37222. Jacob Walls agreed this is a bug on the PR thread: https://github.com/django/django/pull/21659#discussion_r3899304732
Fix with a regression test on my fork: https://github.com/davegaeddert/django/pull/6 — happy to open it against main if accepted.
(AI assistance: Claude Code was used to find and reproduce this and draft the fix; I verified the reproducer and test results.)