Opened 2 days ago

Last modified 2 hours ago

#36065 new Bug

order_by("pk") not equivalent to order_by(F("pk")) for CompositePrimaryKey

Reported by: Jacob Walls Owned by:
Component: Database layer (models, ORM) Version: dev
Severity: Release blocker Keywords:
Cc: Csirmaz Bendegúz Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

For CompositePrimaryKey, order_by("pk") and order_by(F("pk")) produce different results.

For "pk", direction is distributed to all cols (col1 ASC, col2 ASC), but for F("pk"), it is not: (col1, col2 ASC).

Failing test:

  • tests/composite_pk/test_filter.py

    diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py
    index 7e361c5925..81bbfc65be 100644
    a b  
     1from django.db.models import F
    12from django.test import TestCase
    23
    34from .models import Comment, Tenant, User
    class CompositePKFilterTests(TestCase):  
    7879            ),
    7980        )
    8081
     82    def test_order_by_comments_by_pk_asc_f(self):
     83        self.assertSequenceEqual(
     84            Comment.objects.order_by("pk"),
     85            Comment.objects.order_by(F("pk")),
     86        )
     87
    8188    def test_filter_comments_by_pk_gt(self):
    8289        c11, c12, c13, c24, c15 = (
    8390            self.comment_1,
======================================================================
FAIL: test_order_by_comments_by_pk_asc_f (composite_pk.test_filter.CompositePKFilterTests.test_order_by_comments_by_pk_asc_f)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/user/django/tests/composite_pk/test_filter.py", line 83, in test_order_by_comments_by_pk_asc_f
    self.assertSequenceEqual(
AssertionError: Sequences differ: <Quer[123 chars] Comment object ((1, 5))>, <Comment: Comment object ((2, 4))>]> != <Quer[123 chars] Comment object ((1, 5))>, <Comment: Comment object ((2, 4))>]>

  <QuerySet [<Comment: Comment object ((1, 1))>, <Comment: Comment object ((1, 2))>, <Comment: Comment object ((1, 3))>, <Comment: Comment object ((1, 5))>, <Comment: Comment object ((2, 4))>]>

----------------------------------------------------------------------
(0.000)
SELECT "composite_pk_comment"."tenant_id",
       "composite_pk_comment"."comment_id",
       "composite_pk_comment"."user_id",
       "composite_pk_comment"."text"
FROM "composite_pk_comment"
ORDER BY "composite_pk_comment"."tenant_id" ASC,
         "composite_pk_comment"."comment_id" ASC;

args=();

ALIAS=DEFAULT (0.000)
SELECT "composite_pk_comment"."tenant_id",
       "composite_pk_comment"."comment_id",
       "composite_pk_comment"."user_id",
       "composite_pk_comment"."text"
FROM "composite_pk_comment"
ORDER BY "composite_pk_comment"."tenant_id",
         "composite_pk_comment"."comment_id" ASC;

args=();

ALIAS=DEFAULT
----------------------------------------------------------------------
Ran 103 tests in 0.107s

FAILED (failures=1)

Change History (2)

comment:1 by Sarah Boyce, 10 hours ago

Cc: Csirmaz Bendegúz added
Triage Stage: UnreviewedAccepted

Thank you!

comment:2 by Simon Charette, 2 hours ago

The changes made to SQLCompiler.find_ordering_name to accommodate for order_by(*: str) references to a composite primary key failed to account for the fact that resolvable and direct OrderBy instance can be passed to QuerySet.order_by.

The solution likely lies in adjusting find_ordering_name to create a ColPairs when there are multiple targets (instead of calling composite.unnest) and adjusting OrderBy.as_sql to properly deal with ColPairs. I guess we should test for F("pk").asc(nulls_first=True) and F("pk").asc(nulls_last=True) (even if primary keys cannot contain nulls) while we're at it to ensure the proper NULLS LAST emulation is working correction.

  • django/db/models/expressions.py

    diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
    index 667e9f93c6..2e1828ff9b 100644
    a b def get_source_expressions(self):  
    18501850        return [self.expression]
    18511851
    18521852    def as_sql(self, compiler, connection, template=None, **extra_context):
     1853        if isinstance(self.expression, ColPairs):
     1854            sql_parts = []
     1855            params = []
     1856            for col in self.expression.get_cols():
     1857                copy = self.copy()
     1858                copy.set_source_expressions([col])
     1859                sql, col_params = compiler.compile(copy)
     1860                sql_parts.append(sql)
     1861                params.extend(col_params)
     1862            return ", ".join(sql_parts), params
    18531863        template = template or self.template
    18541864        if connection.features.supports_order_by_nulls_modifier:
    18551865            if self.nulls_last:
  • django/db/models/sql/compiler.py

    diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
    index 5bb491d823..251cc08e51 100644
    a b def find_ordering_name(  
    11171117                )
    11181118            return results
    11191119        targets, alias, _ = self.query.trim_joins(targets, joins, path)
    1120         target_fields = composite.unnest(targets)
    11211120        return [
    11221121            (OrderBy(transform_function(t, alias), descending=descending), False)
    1123             for t in target_fields
     1122            for t in targets
    11241123        ]
    11251124
    11261125    def _setup_joins(self, pieces, opts, alias):
  • tests/composite_pk/test_filter.py

    diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py
    index 7e361c5925..be0e80a518 100644
    a b  
     1from django.db.models import F
    12from django.test import TestCase
    23
    34from .models import Comment, Tenant, User
    def test_order_comments_by_pk_desc(self):  
    7879            ),
    7980        )
    8081
     82    def test_order_comments_by_pk_asc_f(self):
     83        self.assertQuerySetEqual(
     84            Comment.objects.order_by("pk"),
     85            Comment.objects.order_by(F("pk")),
     86        )
     87
    8188    def test_filter_comments_by_pk_gt(self):
    8289        c11, c12, c13, c24, c15 = (
    8390            self.comment_1

Note that we'll need to mark OrderBy.allows_composite_expressions = True per the work on #36042 depending on the order in which the changes are merged.

Note: See TracTickets for help on using tickets.
Back to Top