#9596 closed New feature (fixed)
Add a __date lookup for DateTimeField
Reported by: | Owned by: | ||
---|---|---|---|
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)
Change History (45)
by , 16 years ago
Attachment: | dj_date_lookup.patch added |
---|
comment:3 by , 16 years ago
milestone: | → post-1.0 |
---|---|
Needs tests: | set |
Patch needs improvement: | set |
Version: | 1.0 → 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.
comment:4 by , 16 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.
follow-up: 7 comment:5 by , 16 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 , 16 years ago
For tests: take a look at the structure of tests/modeltests/lookup/models.py, and add them to the tests there.
comment:7 by , 16 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.
comment:8 by , 16 years ago
Needs tests: | unset |
---|
comment:10 by , 16 years ago
Patch needs improvement: | unset |
---|
comment:11 by , 16 years ago
Cc: | added |
---|
by , 16 years ago
Attachment: | t9596-r10032-date_lookup.diff added |
---|
Silly me patch with corrected name.
follow-up: 17 comment:14 by , 16 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 , 16 years ago
Cc: | added |
---|
comment:16 by , 16 years ago
Cc: | added |
---|
comment:17 by , 16 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 , 15 years ago
Patch needs improvement: | set |
---|---|
Triage Stage: | Accepted → 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.
by , 15 years ago
new patch (with failing test, showing backwards incompatibility)
comment:19 by , 14 years ago
Severity: | → Normal |
---|---|
Type: | → New feature |
comment:20 by , 13 years ago
Cc: | added |
---|---|
Easy pickings: | unset |
UI/UX: | unset |
comment:22 by , 13 years ago
Cc: | added |
---|
comment:23 by , 13 years ago
Triage Stage: | Design decision needed → 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 DateField
s 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 , 11 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 , 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 , 10 years ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
comment:27 by , 10 years ago
Cc: | 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 amodels.py
file, or register the lookup in theready()
method of anAppConfig
.
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?
comment:28 by , 10 years ago
Owner: | removed |
---|---|
Status: | assigned → new |
comment:29 by , 10 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:31 by , 10 years ago
Patch needs improvement: | set |
---|
Some test failures on Oracle need to be fixed.
comment:32 by , 10 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 , 10 years ago
Patch needs improvement: | set |
---|---|
Summary: | Comparing a DateTimeField to a date is too hard → Add 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 , 9 years ago
Patch needs improvement: | unset |
---|
Updated patch for Oracle based on feedback. All tests are now passing.
Incomplete patch for date lookup type