-
Notifications
You must be signed in to change notification settings - Fork 22
add support for datetime database functions #37
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,7 +63,7 @@ jobs: | |
- name: Start MongoDB | ||
uses: supercharge/[email protected] | ||
with: | ||
mongodb-version: 4.4 | ||
mongodb-version: 5.0 | ||
- name: Run tests | ||
run: > | ||
python3 django_repo/tests/runtests.py --settings mongodb_settings -v 2 | ||
|
@@ -74,6 +74,7 @@ jobs: | |
bulk_create | ||
dates | ||
datetimes | ||
db_functions.datetime | ||
db_functions.math | ||
empty | ||
defer | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,19 @@ | ||
from django.db import NotSupportedError | ||
from django.db.models.expressions import Func | ||
from django.db.models.functions.datetime import Extract | ||
from django.db.models.functions.datetime import ( | ||
Extract, | ||
ExtractDay, | ||
ExtractHour, | ||
ExtractIsoWeekDay, | ||
ExtractIsoYear, | ||
ExtractMinute, | ||
ExtractMonth, | ||
ExtractSecond, | ||
ExtractWeek, | ||
ExtractWeekDay, | ||
ExtractYear, | ||
TruncBase, | ||
) | ||
from django.db.models.functions.math import Ceil, Cot, Degrees, Log, Power, Radians, Random, Round | ||
from django.db.models.functions.text import Upper | ||
|
||
|
@@ -14,6 +27,18 @@ | |
Random: "rand", | ||
Upper: "toUpper", | ||
} | ||
EXTRACT_OPERATORS = { | ||
ExtractDay.lookup_name: "dayOfMonth", | ||
ExtractHour.lookup_name: "hour", | ||
ExtractIsoWeekDay.lookup_name: "isoDayOfWeek", | ||
ExtractIsoYear.lookup_name: "isoWeekYear", | ||
ExtractMinute.lookup_name: "minute", | ||
ExtractMonth.lookup_name: "month", | ||
ExtractSecond.lookup_name: "second", | ||
ExtractWeek.lookup_name: "isoWeek", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the switch to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Django's |
||
ExtractWeekDay.lookup_name: "dayOfWeek", | ||
ExtractYear.lookup_name: "year", | ||
} | ||
|
||
|
||
def cot(self, compiler, connection): | ||
|
@@ -23,15 +48,12 @@ def cot(self, compiler, connection): | |
|
||
def extract(self, compiler, connection): | ||
lhs_mql = process_lhs(self, compiler, connection) | ||
if self.lookup_name == "week": | ||
operator = "$week" | ||
elif self.lookup_name == "month": | ||
operator = "$month" | ||
elif self.lookup_name == "year": | ||
operator = "$year" | ||
else: | ||
operator = EXTRACT_OPERATORS.get(self.lookup_name) | ||
if operator is None: | ||
raise NotSupportedError("%s is not supported." % self.__class__.__name__) | ||
return {operator: lhs_mql} | ||
if timezone := self.get_tzname(): | ||
lhs_mql = {"date": lhs_mql, "timezone": timezone} | ||
return {f"${operator}": lhs_mql} | ||
|
||
|
||
def func(self, compiler, connection): | ||
|
@@ -53,9 +75,18 @@ def round_(self, compiler, connection): | |
return {"$round": [expr.as_mql(compiler, connection) for expr in self.get_source_expressions()]} | ||
|
||
|
||
def trunc(self, compiler, connection): | ||
lhs_mql = process_lhs(self, compiler, connection) | ||
lhs_mql = {"date": lhs_mql, "unit": self.kind, "startOfWeek": "mon"} | ||
if timezone := self.get_tzname(): | ||
lhs_mql["timezone"] = timezone | ||
return {"$dateTrunc": lhs_mql} | ||
|
||
|
||
def register_functions(): | ||
Cot.as_mql_agg = cot | ||
Cot.as_mql = cot | ||
Extract.as_mql = extract | ||
Func.as_mql_agg = func | ||
Log.as_mql_agg = log | ||
Round.as_mql_agg = round_ | ||
Func.as_mql = func | ||
Log.as_mql = log | ||
Round.as_mql = round_ | ||
TruncBase.as_mql = trunc |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
from django.conf import settings | ||
from django.db.backends.base.operations import BaseDatabaseOperations | ||
from django.utils import timezone | ||
from django.utils.regex_helper import _lazy_re_compile | ||
|
||
|
||
class DatabaseOperations(BaseDatabaseOperations): | ||
|
@@ -133,3 +134,43 @@ def _prep_lookup_value(self, value, field, field_kind, lookup): | |
if field_kind == "DecimalField": | ||
value = self.adapt_decimalfield_value(value, field.max_digits, field.decimal_places) | ||
return value | ||
|
||
"""Django uses these methods to generate SQL queries before it generates MQL queries.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we make these SQL queries just for logging/debugging purposes? Could we get away with not rendering them at all? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, we cannot avoid it. It has to do with how this library hooks in to the query generation process. See also: |
||
|
||
# EXTRACT format cannot be passed in parameters. | ||
_extract_format_re = _lazy_re_compile(r"[A-Z_]+") | ||
|
||
def date_extract_sql(self, lookup_type, sql, params): | ||
if lookup_type == "week_day": | ||
# For consistency across backends, we return Sunday=1, Saturday=7. | ||
return f"EXTRACT(DOW FROM {sql}) + 1", params | ||
if lookup_type == "iso_week_day": | ||
return f"EXTRACT(ISODOW FROM {sql})", params | ||
if lookup_type == "iso_year": | ||
return f"EXTRACT(ISOYEAR FROM {sql})", params | ||
|
||
lookup_type = lookup_type.upper() | ||
if not self._extract_format_re.fullmatch(lookup_type): | ||
raise ValueError(f"Invalid lookup type: {lookup_type!r}") | ||
return f"EXTRACT({lookup_type} FROM {sql})", params | ||
|
||
def datetime_extract_sql(self, lookup_type, sql, params, tzname): | ||
if lookup_type == "second": | ||
# Truncate fractional seconds. | ||
return f"EXTRACT(SECOND FROM DATE_TRUNC(%s, {sql}))", ("second", *params) | ||
return self.date_extract_sql(lookup_type, sql, params) | ||
|
||
def datetime_trunc_sql(self, lookup_type, sql, params, tzname): | ||
return f"DATE_TRUNC(%s, {sql})", (lookup_type, *params) | ||
|
||
def date_trunc_sql(self, lookup_type, sql, params, tzname=None): | ||
return f"DATE_TRUNC(%s, {sql})", (lookup_type, *params) | ||
|
||
def datetime_cast_date_sql(self, sql, params, tzname): | ||
return f"({sql})::date", params | ||
|
||
def datetime_cast_time_sql(self, sql, params, tzname): | ||
return f"({sql})::time", params | ||
|
||
def time_trunc_sql(self, lookup_type, sql, params, tzname=None): | ||
return f"DATE_TRUNC(%s, {sql})::time", (lookup_type, *params) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reservation to running these tests using at least mongodb version 6.0? Or is the goal to test with our oldest supported version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4.4 was copied from the pymongo config. I bumped it here because
$dateTrunc
is new in 5.0. For this CI, I'd suggest using the oldest version we decide to support. That choice doesn't necessarily have to follow MongoDB's supported versions, but thus far, I haven't encountered anything that makes supporting 5.0 more difficult.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. My one hesitation is we may eventually run into a functionality only available in later versions. With that being said, we'll cross that bridge when we get there. I'll file an issue ticket to identify the min-supported version.