-
Notifications
You must be signed in to change notification settings - Fork 27
add support for queries with joins #63
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 |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
from django.db.models.constants import LOOKUP_SEP | ||
from django.db.models.sql import compiler | ||
from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, MULTI | ||
from django.utils.functional import cached_property | ||
|
||
from .base import Cursor | ||
from .query import MongoQuery, wrap_database_errors | ||
|
@@ -82,7 +83,15 @@ def _make_result(self, entity, columns, converters, tuple_expected=False): | |
result = [] | ||
for name, col in columns: | ||
field = col.field | ||
value = entity.get(name, NOT_PROVIDED) | ||
column_alias = getattr(col, "alias", None) | ||
obj = ( | ||
# Use the related object... | ||
entity.get(column_alias, {}) | ||
# ...if this column refers to an object for select_related(). | ||
if column_alias is not None and column_alias != self.collection_name | ||
else entity | ||
) | ||
value = obj.get(name, NOT_PROVIDED) | ||
if value is NOT_PROVIDED: | ||
value = field.get_default() | ||
elif converters: | ||
|
@@ -110,10 +119,6 @@ def check_query(self): | |
raise NotSupportedError("QuerySet.distinct() is not supported on MongoDB.") | ||
if self.query.extra: | ||
raise NotSupportedError("QuerySet.extra() is not supported on MongoDB.") | ||
if self.query.select_related: | ||
raise NotSupportedError("QuerySet.select_related() is not supported on MongoDB.") | ||
if len([a for a in self.query.alias_map if self.query.alias_refcount[a]]) > 1: | ||
raise NotSupportedError("Queries with multiple tables are not supported on MongoDB.") | ||
if any( | ||
isinstance(a, Aggregate) and not isinstance(a, Count) | ||
for a in self.query.annotations.values() | ||
|
@@ -147,6 +152,7 @@ def build_query(self, columns=None): | |
self.check_query() | ||
self.setup_query() | ||
query = self.query_class(self, columns) | ||
query.lookup_pipeline = self.get_lookup_pipeline() | ||
try: | ||
query.mongo_query = {"$expr": self.query.where.as_mql(self, self.connection)} | ||
except FullResultSet: | ||
|
@@ -163,9 +169,17 @@ def get_columns(self): | |
columns = ( | ||
self.get_default_columns(select_mask) if self.query.default_cols else self.query.select | ||
) | ||
# Populate QuerySet.select_related() data. | ||
related_columns = [] | ||
if self.query.select_related: | ||
self.get_related_selections(related_columns, select_mask) | ||
if related_columns: | ||
related_columns, _ = zip(*related_columns, strict=True) | ||
|
||
annotation_idx = 1 | ||
result = [] | ||
for column in columns: | ||
|
||
def project_field(column): | ||
nonlocal annotation_idx | ||
if hasattr(column, "target"): | ||
# column is a Col. | ||
target = column.target.column | ||
|
@@ -174,8 +188,13 @@ def get_columns(self): | |
# name for $proj. | ||
target = f"__annotation{annotation_idx}" | ||
annotation_idx += 1 | ||
result.append((target, column)) | ||
return tuple(result) + tuple(self.query.annotation_select.items()) | ||
return target, column | ||
|
||
return ( | ||
tuple(map(project_field, columns)) | ||
+ tuple(self.query.annotation_select.items()) | ||
+ tuple(map(project_field, related_columns)) | ||
) | ||
|
||
def _get_ordering(self): | ||
""" | ||
|
@@ -212,8 +231,20 @@ def _get_ordering(self): | |
field_ordering.append((opts.get_field(name), ascending)) | ||
return field_ordering | ||
|
||
@cached_property | ||
def collection_name(self): | ||
return self.query.get_meta().db_table | ||
|
||
def get_collection(self): | ||
return self.connection.get_collection(self.query.get_meta().db_table) | ||
return self.connection.get_collection(self.collection_name) | ||
|
||
def get_lookup_pipeline(self): | ||
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. Should this method be private? 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. I suppose it would be consistent with This class doesn't have any methods Django developers call themselves, so whether or not to underscore is mainly a decision about how we want to organize things. |
||
result = [] | ||
for alias in tuple(self.query.alias_map): | ||
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 do we need to cast this as a tuple before iterating on it? 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. It was copied from Django, but may be obsolete: django/django#18357 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. There is an answer there, saying that the query.alias_map is modified during the for loops. 🤔 I didn't noticed that, maybe I have to check it. |
||
if not self.query.alias_refcount[alias] or self.collection_name == alias: | ||
continue | ||
result += self.query.alias_map[alias].as_mql(self, self.connection) | ||
return result | ||
|
||
|
||
class SQLInsertCompiler(SQLCompiler): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,7 +43,9 @@ def case(self, compiler, connection): | |
|
||
|
||
def col(self, compiler, connection): # noqa: ARG001 | ||
return f"${self.target.column}" | ||
# Add the column's collection's alias for columns in joined collections. | ||
prefix = f"{self.alias}." if self.alias != compiler.collection_name else "" | ||
return f"${prefix}{self.target.column}" | ||
Comment on lines
+47
to
+48
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 do we prefix the target column with the alias instead of using only the alias? 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 you misread it as "column's alias" instead of "column's collection's alias"? 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. Will explain the idea of the prefix: |
||
|
||
|
||
def combined_expression(self, compiler, connection): | ||
|
Uh oh!
There was an error while loading. Please reload this page.