Skip to content

Commit 28877d2

Browse files
committed
implement date lookups
1 parent 1f8ea14 commit 28877d2

File tree

3 files changed

+75
-23
lines changed

3 files changed

+75
-23
lines changed

django_mongodb/features.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
77
supports_ignore_conflicts = False
88
# Not implemented: https://github.com/mongodb-labs/django-mongodb/issues/8
99
supports_json_field = False
10+
supports_microsecond_precision = False
1011
# MongoDB stores datetimes in UTC.
1112
supports_timezones = False
1213
# Not implemented: https://github.com/mongodb-labs/django-mongodb/issues/7
@@ -17,20 +18,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1718
# Database defaults not supported: bson.errors.InvalidDocument:
1819
# cannot encode object: <django.db.models.expressions.DatabaseDefault
1920
"basic.tests.ModelInstanceCreationTests.test_save_primary_with_db_default",
20-
# Date lookups aren't implemented: https://github.com/mongodb-labs/django-mongodb/issues/9
21-
# (e.g. ExtractWeekDay is not supported.)
22-
"basic.tests.ModelLookupTest.test_does_not_exist",
23-
"basic.tests.ModelLookupTest.test_equal_lookup",
24-
"basic.tests.ModelLookupTest.test_rich_lookup",
25-
"basic.tests.ModelTest.test_year_lookup_edge_case",
26-
"lookup.tests.LookupTests.test_chain_date_time_lookups",
27-
"lookup.test_timefield.TimeFieldLookupTests.test_hour_lookups",
28-
"lookup.test_timefield.TimeFieldLookupTests.test_minute_lookups",
29-
"lookup.test_timefield.TimeFieldLookupTests.test_second_lookups",
30-
"timezones.tests.LegacyDatabaseTests.test_query_datetime_lookups",
21+
# TruncDate isn't implemented.
3122
"timezones.tests.NewDatabaseTests.test_query_convert_timezones",
32-
"timezones.tests.NewDatabaseTests.test_query_datetime_lookups",
33-
"timezones.tests.NewDatabaseTests.test_query_datetime_lookups_in_other_timezone",
23+
# Query for chained looksup not generated correctly.
24+
"lookup.tests.LookupTests.test_chain_date_time_lookups",
3425
# 'NulledTransform' object has no attribute 'as_mql'.
3526
"lookup.tests.LookupTests.test_exact_none_transform",
3627
# "Save with update_fields did not affect any rows."

django_mongodb/functions.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,52 @@
11
from django.db import NotSupportedError
2-
from django.db.models.functions.datetime import Extract
2+
from django.db.models.functions.datetime import (
3+
Extract,
4+
ExtractDay,
5+
ExtractHour,
6+
ExtractIsoWeekDay,
7+
ExtractIsoYear,
8+
ExtractMinute,
9+
ExtractMonth,
10+
ExtractSecond,
11+
ExtractWeek,
12+
ExtractWeekDay,
13+
ExtractYear,
14+
TruncBase,
15+
)
316

417
from .query_utils import process_lhs
518

19+
ExtractDay.mongo_operator = "$dayOfMonth"
20+
ExtractHour.mongo_operator = "$hour"
21+
ExtractIsoWeekDay.mongo_operator = "$isoDayOfWeek"
22+
ExtractIsoYear.mongo_operator = "$isoWeekYear"
23+
ExtractMinute.mongo_operator = "$minute"
24+
ExtractMonth.mongo_operator = "$month"
25+
ExtractSecond.mongo_operator = "$second"
26+
ExtractWeek.mongo_operator = "$week"
27+
ExtractWeekDay.mongo_operator = "$dayOfWeek"
28+
ExtractYear.mongo_operator = "$year"
29+
630

731
def extract(self, compiler, connection):
832
lhs_mql = process_lhs(self, compiler, connection)
9-
if self.lookup_name == "week":
10-
operator = "$week"
11-
elif self.lookup_name == "month":
12-
operator = "$month"
13-
elif self.lookup_name == "year":
14-
operator = "$year"
15-
else:
16-
raise NotSupportedError("%s is not supported." % self.__class__.__name__)
33+
try:
34+
operator = self.mongo_operator
35+
except AttributeError:
36+
raise NotSupportedError("%s is not supported." % self.__class__.__name__) from None
37+
if timezone := self.get_tzname():
38+
lhs_mql = {"date": lhs_mql, "timezone": timezone}
1739
return {operator: lhs_mql}
1840

1941

42+
def trunc(self, compiler, connection):
43+
lhs_mql = process_lhs(self, compiler, connection)
44+
lhs_mql = {"date": lhs_mql, "unit": self.kind, "startOfWeek": "mon"}
45+
if timezone := self.get_tzname():
46+
lhs_mql["timezone"] = timezone
47+
return {"$dateTrunc": lhs_mql}
48+
49+
2050
def register_functions():
21-
Extract.as_mql = extract
51+
Extract.as_mql = Extract.as_mql_agg = extract
52+
TruncBase.as_mql_agg = trunc

django_mongodb/operations.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.conf import settings
66
from django.db.backends.base.operations import BaseDatabaseOperations
77
from django.utils import timezone
8+
from django.utils.regex_helper import _lazy_re_compile
89

910

1011
class DatabaseOperations(BaseDatabaseOperations):
@@ -37,6 +38,35 @@ def adapt_timefield_value(self, value):
3738
raise ValueError("MongoDB backend does not support timezone-aware times.")
3839
return datetime.datetime.combine(datetime.datetime.min.date(), value)
3940

41+
# EXTRACT format cannot be passed in parameters.
42+
_extract_format_re = _lazy_re_compile(r"[A-Z_]+")
43+
44+
def date_extract_sql(self, lookup_type, sql, params):
45+
if lookup_type == "week_day":
46+
# For consistency across backends, we return Sunday=1, Saturday=7.
47+
return f"EXTRACT(DOW FROM {sql}) + 1", params
48+
if lookup_type == "iso_week_day":
49+
return f"EXTRACT(ISODOW FROM {sql})", params
50+
if lookup_type == "iso_year":
51+
return f"EXTRACT(ISOYEAR FROM {sql})", params
52+
53+
lookup_type = lookup_type.upper()
54+
if not self._extract_format_re.fullmatch(lookup_type):
55+
raise ValueError(f"Invalid lookup type: {lookup_type!r}")
56+
return f"EXTRACT({lookup_type} FROM {sql})", params
57+
58+
def datetime_extract_sql(self, lookup_type, sql, params, tzname):
59+
if lookup_type == "second":
60+
# Truncate fractional seconds.
61+
return f"EXTRACT(SECOND FROM DATE_TRUNC(%s, {sql}))", ("second", *params)
62+
return self.date_extract_sql(lookup_type, sql, params)
63+
64+
def datetime_trunc_sql(self, lookup_type, sql, params, tzname):
65+
return f"DATE_TRUNC(%s, {sql})", (lookup_type, *params)
66+
67+
def date_trunc_sql(self, lookup_type, sql, params, tzname=None):
68+
return f"DATE_TRUNC(%s, {sql})", (lookup_type, *params)
69+
4070
def get_db_converters(self, expression):
4171
converters = super().get_db_converters(expression)
4272
internal_type = expression.output_field.get_internal_type()

0 commit comments

Comments
 (0)