Code

Opened 5 years ago

Last modified 3 months ago

#9596 new New feature

Comparing a DateTimeField to a date is too hard

Reported by: django@… Owned by: nobody
Component: Database layer (models, ORM) Version: master
Severity: Normal Keywords: lookup_type date datetimefield compare comparison query_term field lookup
Cc: jarek.zgoda@…, flosch@…, hv@…, JMagnusson, curtis@… Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: yes
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@…> 5 years ago.
Incomplete patch for date lookup type
dj_date_lookup_b.patch (6.6 KB) - added by Scott <django@…> 5 years ago.
patch b
dj_date_lookup_c.patch (2.6 KB) - added by Scott <django@…> 5 years ago.
patch c
dj_date_lookup_c.2.patch (2.6 KB) - added by Scott <django@…> 5 years ago.
patch c_2
dj_date_lookup_c_3.patch (3.7 KB) - added by Scott <django@…> 5 years ago.
patch c_3 with tests
t9596-r9441-date_lookup.diff (3.8 KB) - added by Scott <django@…> 5 years ago.
patch with test fixed
t9596-r9441-date_lookup.2.diff (3.9 KB) - added by ssadler 5 years ago.
Updated patch
t9596-r10032-date_lookup.diff (3.9 KB) - added by ssadler 5 years ago.
Silly me patch with corrected name.
9596.diff (5.7 KB) - added by SmileyChris 5 years ago.
new patch (with failing test, showing backwards incompatibility)

Download all attachments as: .zip

Change History (33)

Changed 5 years ago by Scott <django@…>

Incomplete patch for date lookup type

comment:1 Changed 5 years ago by cgrady

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

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

comment:2 Changed 5 years ago by cgrady

woops, i mean __date :)

comment:3 Changed 5 years ago by Scott <django@…>

  • milestone set to post-1.0
  • Needs tests set
  • Patch needs improvement set
  • Version changed from 1.0 to SVN

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.

Changed 5 years ago by Scott <django@…>

patch b

comment:4 Changed 5 years ago by Scott <django@…>

  • 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.

Changed 5 years ago by Scott <django@…>

patch c

Changed 5 years ago by Scott <django@…>

patch c_2

comment:5 follow-up: Changed 5 years ago by 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.

comment:6 Changed 5 years ago by ikelly

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

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

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.

Changed 5 years ago by Scott <django@…>

patch c_3 with tests

comment:8 Changed 5 years ago by Scott <django@…>

  • Needs tests unset

comment:9 Changed 5 years ago by Scott <django@…>

  • Patch needs improvement set

Test is broken right now

Changed 5 years ago by Scott <django@…>

patch with test fixed

comment:10 Changed 5 years ago by Scott <django@…>

  • Patch needs improvement unset

comment:11 Changed 5 years ago by zgoda

  • Cc jarek.zgoda@… added

comment:12 Changed 5 years ago by anonymous

  • milestone post-1.0 deleted

Milestone post-1.0 deleted

comment:13 Changed 5 years ago by jacob

  • Triage Stage changed from Unreviewed to Accepted

Good idea.

Changed 5 years ago by ssadler

Updated patch

Changed 5 years ago by ssadler

Silly me patch with corrected name.

comment:14 follow-up: Changed 5 years ago by Alex

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

comment:15 Changed 5 years ago by anonymous

  • Cc flosch@… added

comment:16 Changed 5 years ago by guettli

  • Cc hv@… added

comment:17 in reply to: ↑ 14 Changed 5 years ago by ssadler

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 Changed 5 years ago by SmileyChris

  • Patch needs improvement set
  • Triage Stage changed from Accepted to Design 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.

Changed 5 years ago by SmileyChris

new patch (with failing test, showing backwards incompatibility)

comment:19 Changed 3 years ago by lukeplant

  • Severity set to Normal
  • Type set to New feature

comment:20 Changed 3 years ago by JMagnusson

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

comment:21 Changed 3 years ago by UloPe

Related: #16187 Refactor of the lookup system

comment:22 Changed 3 years ago by curtis@…

  • Cc curtis@… added

comment:23 Changed 2 years ago by aaugustin

  • Triage Stage changed from Design decision needed to Accepted

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 Changed 3 months ago by akaariai

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)

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as new
The owner will be changed from nobody to anonymous. Next status will be 'assigned'
as The resolution will be set. Next status will be 'closed'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.