Opened 15 years ago

Closed 9 years ago

Last modified 9 years ago

#9596 closed New feature (fixed)

Add a __date lookup for DateTimeField

Reported by: django@… Owned by: Tim Graham <timograham@…>
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords: lookup_type date datetimefield compare comparison query_term field lookup
Cc: jarek.zgoda@…, flosch@…, hv@…, JMagnusson, curtis@…, jon.dufresne@… Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Many times dates are stored in datetime fields even though the majority of lookup operations are going to be based on dates without times.

I am proposing a __date field lookup to make it easier to compare a DateTimeField to a date.

In plain SQL this is very easy; in Mysql/Sqlite it is just "date(fieldname)". The reason I mention this is not to indicate that it would be possible to make Django do it (though it is, and I will), but because it struck me that the operation is much more obscure using the Django DB layer than it is writing in plain SQL. For example, this is how it is done in django.views.date_based:

{'date_field__range': (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max))}

And the way I am proposing:

{'date_field__date': date} # date is a datetime.date object

I have attatched a patch which alters three files and makes this work under Sqlite, and should also make it work under MySQL, however it implementation is by no means thorough. If this is deemed a good idea I will look into improving it.

Another idea is to allow comparing DateTimeFields to arbitrary length tuples which are in the format (YYYY, MM, DD, HH, MM, SS), but are between 1 and 6 elements long... So that for example I could do:

{'date_field': date.timetuple()[:2]}

And that would be true if the date was the same month.

Attachments (9)

dj_date_lookup.patch (2.2 KB ) - added by Scott <django@…> 15 years ago.
Incomplete patch for date lookup type
dj_date_lookup_b.patch (6.6 KB ) - added by Scott <django@…> 15 years ago.
patch b
dj_date_lookup_c.patch (2.6 KB ) - added by Scott <django@…> 15 years ago.
patch c
dj_date_lookup_c.2.patch (2.6 KB ) - added by Scott <django@…> 15 years ago.
patch c_2
dj_date_lookup_c_3.patch (3.7 KB ) - added by Scott <django@…> 15 years ago.
patch c_3 with tests
t9596-r9441-date_lookup.diff (3.8 KB ) - added by Scott <django@…> 15 years ago.
patch with test fixed
t9596-r9441-date_lookup.2.diff (3.9 KB ) - added by Scott 15 years ago.
Updated patch
t9596-r10032-date_lookup.diff (3.9 KB ) - added by Scott 15 years ago.
Silly me patch with corrected name.
9596.diff (5.7 KB ) - added by Chris Beaven 15 years ago.
new patch (with failing test, showing backwards incompatibility)

Download all attachments as: .zip

Change History (45)

by Scott <django@…>, 15 years ago

Attachment: dj_date_lookup.patch added

Incomplete patch for date lookup type

comment:1 by Collin Grady, 15 years ago

I like date better than the tuple idea, which wouldnt be clear

comment:2 by Collin Grady, 15 years ago

woops, i mean __date :)

comment:3 by Scott <django@…>, 15 years ago

milestone: post-1.0
Needs tests: set
Patch needs improvement: set
Version: 1.0SVN

Patch B is an implementation which adds code to all the backends. It works by truncating the date a la "DATE(datetimefield)", like the first patch. The downside is that the datetime column must be truncated, which adds a large overhead to the query especially if the column is indexed.

by Scott <django@…>, 15 years ago

Attachment: dj_date_lookup_b.patch added

patch b

comment:4 by Scott <django@…>, 15 years ago

Patch needs improvement: unset

Patch C is a better implementation which essentially turns the date lookup into a range lookup. It doesn't add or modify any backend specific code and there is much less code overall.

Documentation is also included in the patch. I'm a bit lost as to how to add the tests, so if someone can give me an indication of where to start I'll have a go.

by Scott <django@…>, 15 years ago

Attachment: dj_date_lookup_c.patch added

patch c

by Scott <django@…>, 15 years ago

Attachment: dj_date_lookup_c.2.patch added

patch c_2

comment:5 by Erin Kelly, 15 years ago

I would support adding this. But I would suggest implementing this using the existing date_trunc_sql operations method, which is used for .dates() queries.

comment:6 by Erin Kelly, 15 years ago

For tests: take a look at the structure of tests/modeltests/lookup/models.py, and add them to the tests there.

in reply to:  5 comment:7 by Scott <django@…>, 15 years ago

Replying to ikelly:

I would support adding this. But I would suggest implementing this using the existing date_trunc_sql operations method, which is used for .dates() queries.

In patch B I chose the date_extract_sql function because date_trunc_sql is always returning a full datetime value, wheras the return format of date_extract_sql depends on the parameters. In patch C none of this applies, because it is turning the date lookup into a range lookup and so doesn't need any new backend code.

by Scott <django@…>, 15 years ago

Attachment: dj_date_lookup_c_3.patch added

patch c_3 with tests

comment:8 by Scott <django@…>, 15 years ago

Needs tests: unset

comment:9 by Scott <django@…>, 15 years ago

Patch needs improvement: set

Test is broken right now

by Scott <django@…>, 15 years ago

patch with test fixed

comment:10 by Scott <django@…>, 15 years ago

Patch needs improvement: unset

comment:11 by Jarek Zgoda, 15 years ago

Cc: jarek.zgoda@… added

comment:12 by (none), 15 years ago

milestone: post-1.0

Milestone post-1.0 deleted

comment:13 by Jacob, 15 years ago

Triage Stage: UnreviewedAccepted

Good idea.

by Scott, 15 years ago

Updated patch

by Scott, 15 years ago

Silly me patch with corrected name.

comment:14 by Alex Gaynor, 15 years ago

Any particular reason this patch is implemented as an range as opposed to doing year, month, and day (genuinely curious).

comment:15 by anonymous, 15 years ago

Cc: flosch@… added

comment:16 by Thomas Güttler, 15 years ago

Cc: hv@… added

in reply to:  14 comment:17 by Scott, 15 years ago

Replying to Alex:

Any particular reason this patch is implemented as an range as opposed to doing year, month, and day (genuinely curious).

It didn't occur to me at the time, but I don't think it would be better. I'm not sure how the year, month and day lookups are implemented in Django, but if they aren't also done as ranges then the SQL engine has to convert column for each row, which means that it can't use any indexes that may exist on that column. If they are done as ranges it would produce three range lookups instead of one.

comment:18 by Chris Beaven, 15 years ago

Patch needs improvement: set
Triage Stage: AcceptedDesign decision needed

This looks good, the only one problem here is that it's a backwards incompatible change.

I'm attaching a patch with modified tests (which fail) based on the most recent patch in this ticket to show the incompatibility. We need a design decision about where to go from here... either we document the backwards incompatibility or the internal lookup logic has to change somehow so it doesn't swallow the related field lookup.

by Chris Beaven, 15 years ago

Attachment: 9596.diff added

new patch (with failing test, showing backwards incompatibility)

comment:19 by Luke Plant, 13 years ago

Severity: Normal
Type: New feature

comment:20 by JMagnusson, 13 years ago

Cc: JMagnusson added
Easy pickings: unset
UI/UX: unset

comment:21 by Ulrich Petri, 13 years ago

Related: #16187 Refactor of the lookup system

comment:22 by curtis@…, 13 years ago

Cc: curtis@… added

comment:23 by Aymeric Augustin, 12 years ago

Triage Stage: Design decision neededAccepted

This ticket is in DDN because the current technique fails if the __date lookup is used on a DateField.

There is exactly the same problem with the year lookup; its implementation depends on the field it's applied to:

# django/db/models/fields/__init__.py
        elif lookup_type == 'year':
            if self.get_internal_type() == 'DateField':
                return connection.ops.year_lookup_bounds_for_date_field(value)
            else:
                return connection.ops.year_lookup_bounds(value)

The implementation of year_lookup_bounds and year_lookup_bounds_for_date_field is database dependent. This allows setting the upper bound to YYYY-12-31 instead of YYYY-12-31 23:59:59.999999 for DateFields under Oracle.

I think we could mirror this solution for the __date lookup. This means implementing connection.ops.date_lookup_bounds(value) connection.ops.date_lookup_bounds_for_date_field(value) (in a backend dependent fashion if necessary).

It would be even better to convert the lookup to an exact match for DateField, but I don't believe that's doable with the current implementation.

Note that the tests in the patch need to be rewritten as unittests.

comment:24 by Anssi Kääriäinen, 10 years ago

Now that #16187 is fixed this should be relatively easy to implement:

class DateTransform(Transform):
    lookup_name = 'as_date'  # or just date, implementer's choice
    output_type = DateField()

    def as_sql(self, qn, connection):
        lhs, lhs_params = qn.compile(self.lhs)
        # Biggest problem is the following line - implement date cast template
        # for different backends. MySQL seems to be date(%s), PostgreSQL %s::date.
        date_cast_sql = connection.ops.date_cast_template
        return date_cast_template % lhs, lhs_params
DateTimeField.register_lookup(DateTransform)

comment:25 by Aymeric Augustin, 10 years ago

Still, I would support adding proper support for this feature in Django, if only to make sure time zones are handled correctly when USE_TZ is True. It's very easy to mess them up!

comment:26 by Aymeric Augustin, 10 years ago

Owner: changed from nobody to Aymeric Augustin
Status: newassigned

comment:27 by Jon Dufresne, 9 years ago

Cc: jon.dufresne@… added

Now that #16187 is fixed this should be relatively easy to implement:
...
DateTimeField.register_lookup(DateTransform)

I've started down the path suggested above with commit: https://github.com/jdufresne/django/commit/5bb3f1881982bc1248d5891a887725eeff7841bd

However, I'm not sure where this code should live if added to Django generally. What is the correct area of the code to call register_lookup()?

From the docs:

We can now use foo__ne for any field foo. You will need to ensure that this registration happens before you try to create any querysets using it. You could place the implementation in a models.py file, or register the lookup in the ready() method of an AppConfig.

As far as I can tell, these options don't really apply to *built-in* transforms.

I notice there is a default_lookups in django/db/models/lookups.py, but I'm not sure how to make this work for field specific transforms, instead of baisc lookups.

I tried putting register_lookup() inside django/db/fields/__init__.py, however this lead to issues with settings.

django.core.exceptions.ImproperlyConfigured: Requested setting DEFAULT_INDEX_TABLESPACE, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

Any thoughts?

Last edited 9 years ago by Jon Dufresne (previous) (diff)

comment:28 by Aymeric Augustin, 9 years ago

Owner: Aymeric Augustin removed
Status: assignednew

comment:29 by Aymeric Augustin, 9 years ago

The code looks correct when USE_TZ = False. You also need to handle the more complicated case when USE_TZ = True.

See the current implementation datetime_trunc_sql for an idea of what's needed.

comment:30 by Jon Dufresne, 9 years ago

Patch needs improvement: unset

Created a PR that is ready for review.

https://github.com/django/django/pull/4281

comment:31 by Tim Graham, 9 years ago

Patch needs improvement: set

Some test failures on Oracle need to be fixed.

comment:32 by Jon Dufresne, 9 years ago

Patch needs improvement: unset

Test fixes and other feedback has been incorporated into the latest version of the PR. Additional feedback is welcome.

comment:33 by Tim Graham, 9 years ago

Patch needs improvement: set
Summary: Comparing a DateTimeField to a date is too hardAdd a __date lookup for DateTimeField

Left some mostly cosmetic comments, and tests are still failing on Oracle (but passing on my local version of Oracle, so might need some expertise about Oracle version differences to resolve that).

comment:34 by Jon Dufresne, 9 years ago

Patch needs improvement: unset

Updated patch for Oracle based on feedback. All tests are now passing.

comment:35 by Tim Graham <timograham@…>, 9 years ago

Owner: set to Tim Graham <timograham@…>
Resolution: fixed
Status: newclosed

In 44f3ee77:

Fixed #9596 -- Added date transform for DateTimeField.

comment:36 by Thomas Güttler, 9 years ago

Thank you very much :-)

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