Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #13312 -- Added a way to customize the order_by() of null fields. #6981

Closed
wants to merge 4 commits into from

Conversation

boblefrag
Copy link
Contributor

template = None
if self.nullslast:
template = 'IF(ISNULL(%(expression)s),1,0),%(expression)s %(ordering)s '
sql, params = self.as_sql(compiler, connection, template=template)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to use the pattern used in other as_* methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, my bad. Fixed

@charettes charettes changed the title Fixed #13312 -- Add a way to customize the order_by() of null fields Fixed #13312 -- Added a way to customize the order_by() of null fields. Jul 27, 2016
@charettes
Copy link
Member

I suppose we could make OrderBy.asc()/desc() chainable in another commit by making them return self.

@boblefrag boblefrag force-pushed the ticket_13312 branch 2 times, most recently from bdee62b to 8d84390 Compare July 27, 2016 15:53
sql, params = self.as_sql(compiler, connection, template=template)
return sql, params

def as_postgresql(self, compiler, connection):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be better to rename this as_sql and to remove the as_oracle method since it is the "default" implementation that all backends follow unless they opt out. As-is, any third party backends must implement their own version even if they support NULLS LAST syntax, such as mssql.

@boblefrag
Copy link
Contributor Author

@charettes now asc and desc are chainable with nulls_last as asked.

@@ -879,6 +883,8 @@ def get_source_expressions(self):
return [self.expression]

def as_sql(self, compiler, connection, template=None, **extra_context):
if not template and self.nullslast:
template = '%(expression)s %(ordering)s NULLS LAST'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use "%s NULLS LAST" % self.template to prevent code duplication.

@charettes
Copy link
Member

Should we also add support for NULLS FIRST?

@charettes
Copy link
Member

I feel like making nulls a parameter of asc()/desc() would make a better API than chaining by exposing both a nulls_first() and nulls_last() method. e.g.:

Event.objects.order_by(F('start_date').asc(nulls='first')) or
Event.objects.order_by(F('start_date').asc(nulls_first=True))

@boblefrag boblefrag force-pushed the ticket_13312 branch 2 times, most recently from 118136b to d7c4e3f Compare July 29, 2016 18:19
@boblefrag
Copy link
Contributor Author

@charette now comply with the api you suggest.

@charettes
Copy link
Member

@jarshwah, @mjtamlyn what you think of the (nulls='first') API? The only thing that bugs me is the usage of a string constant as it not used anywhere else in the ORM. We could also take a tri-valued nulls_first (or nulls_last kwarg) where False maps to NULLS LAST and None does nothing.

@jarshwah
Copy link
Member

jarshwah commented Aug 1, 2016

Don't like the string argument, sorry, and I feel that a tri-value would constantly require referencing the docs. I'd prefer nulls_first=True or nulls_last=True as an argument to asc/desc. Are there any better options?

@charettes
Copy link
Member

Are there any better options?

I can't think of any. Maybe the this should be taken to the developpers mailing list?

@boblefrag
Copy link
Contributor Author

I am ok with nulls_first=True syntax but before implementing it I need a green flag of yours so just let me know the endorsed syntax and I'll implement it.

@mjtamlyn
Copy link
Member

mjtamlyn commented Aug 2, 2016

I like the nulls_first=True option best as well. It's not brilliant, but best of a bad bunhc.

@charettes
Copy link
Member

@boblefrag let's go with nulls_first=True and nulls_last=True.

You might want to delegate asc()/desc()'s **kwargs to OrderBy.__init__() and raise a ValueError if both nulls_first and nulls_last are specified.

@adamchainz
Copy link
Sponsor Member

adamchainz commented Aug 15, 2016

FWIW nulls_first=True and nulls_last=True seems like a nice solution to me too (that said using a set of allowed strings like nulls='first' isn't exactly unpythonic per se, c.f. bytes.decode('ascii', errors='replace')).

@boblefrag
Copy link
Contributor Author

@charettes nulls_first=True and nulls_last=True. What's next step?

@charettes
Copy link
Member

The code is LGTM. Maybe break the nulls_first and nulls_last options assertions in two tests and add an additional test method for the ValueError raised when both options are used together.

@boblefrag
Copy link
Contributor Author

@charettes done.

@@ -90,6 +90,90 @@ def test_order_by_override(self):
attrgetter("headline")
)

def test_order_by_nulls(self):
with self.assertRaises(ValueError):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use assertRaisesMessage.

Article.objects.create(
headline="Article 5",
author=author_1,
pub_date=datetime(2005, 7, 28)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a trailing comma.

@charettes
Copy link
Member

You can reuse the Article objects created in setup instead of creating two new ones per tests by adding an author to one of them. You could also convert the test class to use setupTestData instead of setUp. Please use the title of this PR as commit message.

@boblefrag
Copy link
Contributor Author

boblefrag commented Aug 22, 2016

@charettes is there anything left to be done for making this PR mergeable ?

@charettes
Copy link
Member

Marked the ticket as ready for final review.

@timgraham
Copy link
Member

Documentation is missing. See our patch review checklist for details. Please uncheck "Needs documentation" on the ticket after updating.

def __init__(self, expression, descending=False, nulls_first=False,
nulls_last=False):
if nulls_first and nulls_last:
raise ValueError("nulls_first and nulls_last are mutualy exclusive")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor typo: should be 'mutually'.

@boblefrag
Copy link
Contributor Author

fixed typo and added doc

Copy link
Member

@timgraham timgraham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a test failure on Oracle also:

======================================================================
FAIL: test_order_by_nulls_first (ordering.tests.OrderingTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/ordering/tests.py", line 138, in test_order_by_nulls_first
    attrgetter("headline")
  File "/home/tim/code/django/django/test/testcases.py", line 964, in assertQuerysetEqual
    return self.assertEqual(list(items), values, msg=msg)
AssertionError: Lists differ: ['Article 1', 'Article 4', 'Article 3', 'Article 2'] != ['Article 1', 'Article 2', 'Article 3', 'Article 4']

First differing element 1:
'Article 4'
'Article 2'

- ['Article 1', 'Article 4', 'Article 3', 'Article 2']
?                        ^                         ^

+ ['Article 1', 'Article 2', 'Article 3', 'Article 4']

@@ -551,10 +551,14 @@ calling the appropriate methods on the wrapped expression.
.. method:: asc()

Returns the expression ready to be sorted in ascending order.
`nulls_last` and `nulls_first` define how null values
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be added as kwargs to the method signatures.
Use double backticks rather than single.
Add a mention in the 1.11 release notes.
Add .. versionchanged:: 1.11 annotations as described in https://docs.djangoproject.com/en/dev/internals/contributing/writing-documentation/#documenting-new-features.

@@ -90,6 +96,58 @@ def test_order_by_override(self):
attrgetter("headline")
)

def test_order_by_nulls_first_and_last(self):
with self.assertRaisesMessage(ValueError,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use:

msg =  '...'
with self.assertRaisesMessage(ValueError, msg):

Article.objects.filter(headline="Article 3").update(author=self.author_1)
Article.objects.filter(headline="Article 4").update(author=self.author_2)

# check that asc and desc are chainable with nulls_last
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chop "check that" and just state the expected behavior.


# check that asc and desc are chainable with nulls_last

self.assertQuerysetEqual(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use:

self.assertSequenceEqual(
    Article.objects.order_by(F("author").desc(nulls_last=True)),
    [self.a4, ...]
)

def setUp(self):
self.a1 = Article.objects.create(

@classmethod
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this change and the test changes unrelated to the bug fix in a separate commit for clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this have been made to comply with #6981 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change is fine, I'm just asking that it be a be separate commit similar to 31098e3 so that the refactoring isn't mixed in with the new feature.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense?

@boblefrag boblefrag force-pushed the ticket_13312 branch 3 times, most recently from 1a57da5 to 7f2db39 Compare September 23, 2016 16:18
@boblefrag
Copy link
Contributor Author

@timgraham here is the 2 commits. Let me know if it's ok for you.

@@ -213,14 +265,10 @@ def test_order_by_pk(self):
Ensure that 'pk' works as an ordering option in Meta.
Refs #8291.
"""
Author.objects.create(pk=1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My think was that the setUpTestData changes would be the first commit and changes like this would be included there so that the bug fix commit doesn't have to make changes in unrelated tests.

]
)

def test_order_by_nulls_first(self):
Copy link
Member

@felixxm felixxm Nov 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add authors for article3 and article4 like in test_order_by_nulls_last:

--- a/tests/ordering/tests.py
+++ b/tests/ordering/tests.py
@@ -125,6 +125,8 @@ class OrderingTests(TestCase):
         )

     def test_order_by_nulls_first(self):
+        Article.objects.filter(headline="Article 3").update(author=self.author_1)
+        Article.objects.filter(headline="Article 4").update(author=self.author_2)
         self.assertSequenceEqual(
             Article.objects.order_by(F("author").asc(nulls_first=True)), [
                 self.a1,
@@ -138,8 +140,8 @@ class OrderingTests(TestCase):
             Article.objects.order_by(F("author").desc(nulls_first=True)), [
                 self.a1,
                 self.a2,
-                self.a3,
-                self.a4
+                self.a4,
+                self.a3
             ]
         )

Now we have 4 articles without authors, hence we do not check that asc and desc works with nulls_first flag.

@timgraham
Copy link
Member

Updated in #7563.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
8 participants