-
Notifications
You must be signed in to change notification settings - Fork 22
add support for QuerySet.annotate() #39
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
fa7fadc
fb71484
145844d
9112d93
9fb4ca5
b83c33c
5a810bd
db3b2d0
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 |
---|---|---|
@@ -1,6 +1,6 @@ | ||
from django.core.exceptions import EmptyResultSet, FullResultSet | ||
from django.db import DatabaseError, IntegrityError, NotSupportedError | ||
from django.db.models import NOT_PROVIDED, Count, Expression, Value | ||
from django.db.models import NOT_PROVIDED, Count, Expression | ||
from django.db.models.aggregates import Aggregate | ||
from django.db.models.constants import LOOKUP_SEP | ||
from django.db.models.sql import compiler | ||
|
@@ -22,8 +22,11 @@ def execute_sql( | |
# QuerySet.count() | ||
if self.query.annotations == {"__count": Count("*")}: | ||
return [self.get_count()] | ||
# Specify columns if there are any annotations so that annotations are | ||
# computed via $project. | ||
columns = self.get_columns() if self.query.annotations else None | ||
try: | ||
query = self.build_query() | ||
query = self.build_query(columns) | ||
except EmptyResultSet: | ||
return None | ||
return query.fetch() | ||
|
@@ -55,13 +58,18 @@ def results_iter( | |
def has_results(self): | ||
return bool(self.get_count(check_exists=True)) | ||
|
||
def get_converters(self, columns): | ||
def get_converters(self, expressions): | ||
converters = {} | ||
for column in columns: | ||
backend_converters = self.connection.ops.get_db_converters(column) | ||
field_converters = column.field.get_db_converters(self.connection) | ||
for name_expr in expressions: | ||
try: | ||
name, expr = name_expr | ||
except TypeError: | ||
# e.g., Count("*") | ||
continue | ||
backend_converters = self.connection.ops.get_db_converters(expr) | ||
field_converters = expr.get_db_converters(self.connection) | ||
if backend_converters or field_converters: | ||
converters[column.target.column] = backend_converters + field_converters | ||
converters[name] = backend_converters + field_converters | ||
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 sql compiler uses an index. Like an integer and maintain the order. 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. Right, however, MongoDB returns a document of (unordered?) fields, hence we may need to use the key lookup approach (or at least this was the reason for the original implementation in Django Mongo Engine + Python 2). Since Python 3.7 dictionaries are guaranteed to be insertion ordered. Not sure how the MongoDB server handles field ordering though (does it match the order of |
||
return converters | ||
|
||
def _make_result(self, entity, columns, converters, tuple_expected=False): | ||
|
@@ -72,15 +80,14 @@ def _make_result(self, entity, columns, converters, tuple_expected=False): | |
names as keys. | ||
""" | ||
result = [] | ||
for col in columns: | ||
for name, col in columns: | ||
field = col.field | ||
column = col.target.column | ||
value = entity.get(column, NOT_PROVIDED) | ||
value = entity.get(name, NOT_PROVIDED) | ||
if value is NOT_PROVIDED: | ||
value = field.get_default() | ||
elif converters: | ||
# Decode values using Django's database converters API. | ||
for converter in converters.get(column, ()): | ||
for converter in converters.get(name, ()): | ||
value = converter(value, col, self.connection) | ||
result.append(value) | ||
if tuple_expected: | ||
|
@@ -91,12 +98,6 @@ def check_query(self): | |
"""Check if the current query is supported by the database.""" | ||
if self.query.is_empty(): | ||
raise EmptyResultSet() | ||
# Supported annotations are Exists() and Count(). | ||
if self.query.annotations and self.query.annotations not in ( | ||
{"a": Value(1)}, | ||
{"__count": Count("*")}, | ||
): | ||
raise NotSupportedError("QuerySet.annotate() is not supported on MongoDB.") | ||
if self.query.distinct: | ||
# This is a heuristic to detect QuerySet.datetimes() and dates(). | ||
# "datetimefield" and "datefield" are the names of the annotations | ||
|
@@ -144,11 +145,17 @@ def build_query(self, columns=None): | |
return query | ||
|
||
def get_columns(self): | ||
"""Return columns which should be loaded by the query.""" | ||
""" | ||
Return a tuple of (name, expression) with the columns and annotations | ||
which should be loaded by the query. | ||
""" | ||
select_mask = self.query.get_select_mask() | ||
return ( | ||
columns = ( | ||
self.get_default_columns(select_mask) if self.query.default_cols else self.query.select | ||
) | ||
return tuple((column.target.column, column) for column in columns) + tuple( | ||
self.query.annotations.items() | ||
) | ||
|
||
def _get_ordering(self): | ||
""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,19 @@ | ||
from django.db.models.expressions import Col | ||
from django.db.models.expressions import Col, Value | ||
|
||
|
||
def col(self, compiler, connection): # noqa: ARG001 | ||
return self.target.column | ||
return f"${self.target.column}" | ||
|
||
|
||
def value(self, compiler, connection): # noqa: ARG001 | ||
return self.value | ||
|
||
|
||
def value_agg(self, compiler, connection): # noqa: ARG001 | ||
return {"$literal": self.value} | ||
|
||
|
||
def register_expressions(): | ||
Col.as_mql = col | ||
Value.as_mql = value | ||
Value.as_mql_agg = value_agg |
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.
Can you elaborate on this? Why does SQL get generated if we're running a mongodb query?
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.
It has to do with how this library hooks in to the query generation process. See also:
https://github.com/mongodb-labs/django-mongodb/blob/3c3ad0eb45a2572453aa228ddafac96391dc1ab8/django_mongodb/query.py#L23-L32