Skip to content

Commit 5e77b83

Browse files
WaVEVtimgraham
authored andcommitted
add support for queries with joins
Also fixes #35: add support for QuerySet.select_related()
1 parent 72a20d4 commit 5e77b83

File tree

6 files changed

+200
-105
lines changed

6 files changed

+200
-105
lines changed

.github/workflows/test-python.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,19 @@ jobs:
8989
defer_regress
9090
from_db_value
9191
lookup
92+
m2m_and_m2o
93+
m2m_intermediary
94+
m2m_multiple
95+
m2m_recursive
96+
m2m_regress
97+
m2m_signals
98+
m2m_through
99+
m2o_recursive
92100
model_fields
93101
ordering
94102
or_lookups
95103
queries.tests.Ticket12807Tests.test_ticket_12807
104+
select_related
96105
sessions_tests
97106
timezones
98107
update

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,10 @@ Migrations for 'admin':
114114
- `datetimes()`
115115
- `distinct()`
116116
- `extra()`
117-
- `select_related()`
118117

119118
- `Subquery`, `Exists`, and using a `QuerySet` in `QuerySet.annotate()` aren't
120119
supported.
121120

122-
- Queries with joins aren't supported.
123-
124121
- `DateTimeField` doesn't support microsecond precision, and correspondingly,
125122
`DurationField` stores milliseconds rather than microseconds.
126123

django_mongodb/compiler.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.db.models.constants import LOOKUP_SEP
66
from django.db.models.sql import compiler
77
from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, MULTI
8+
from django.utils.functional import cached_property
89

910
from .base import Cursor
1011
from .query import MongoQuery, wrap_database_errors
@@ -82,7 +83,15 @@ def _make_result(self, entity, columns, converters, tuple_expected=False):
8283
result = []
8384
for name, col in columns:
8485
field = col.field
85-
value = entity.get(name, NOT_PROVIDED)
86+
column_alias = getattr(col, "alias", None)
87+
obj = (
88+
# Use the related object...
89+
entity.get(column_alias, {})
90+
# ...if this column refers to an object for select_related().
91+
if column_alias is not None and column_alias != self.collection_name
92+
else entity
93+
)
94+
value = obj.get(name, NOT_PROVIDED)
8695
if value is NOT_PROVIDED:
8796
value = field.get_default()
8897
elif converters:
@@ -110,10 +119,6 @@ def check_query(self):
110119
raise NotSupportedError("QuerySet.distinct() is not supported on MongoDB.")
111120
if self.query.extra:
112121
raise NotSupportedError("QuerySet.extra() is not supported on MongoDB.")
113-
if self.query.select_related:
114-
raise NotSupportedError("QuerySet.select_related() is not supported on MongoDB.")
115-
if len([a for a in self.query.alias_map if self.query.alias_refcount[a]]) > 1:
116-
raise NotSupportedError("Queries with multiple tables are not supported on MongoDB.")
117122
if any(
118123
isinstance(a, Aggregate) and not isinstance(a, Count)
119124
for a in self.query.annotations.values()
@@ -147,6 +152,7 @@ def build_query(self, columns=None):
147152
self.check_query()
148153
self.setup_query()
149154
query = self.query_class(self, columns)
155+
query.lookup_pipeline = self.get_lookup_pipeline()
150156
try:
151157
query.mongo_query = {"$expr": self.query.where.as_mql(self, self.connection)}
152158
except FullResultSet:
@@ -163,9 +169,17 @@ def get_columns(self):
163169
columns = (
164170
self.get_default_columns(select_mask) if self.query.default_cols else self.query.select
165171
)
172+
# Populate QuerySet.select_related() data.
173+
related_columns = []
174+
if self.query.select_related:
175+
self.get_related_selections(related_columns, select_mask)
176+
if related_columns:
177+
related_columns, _ = zip(*related_columns, strict=True)
178+
166179
annotation_idx = 1
167-
result = []
168-
for column in columns:
180+
181+
def project_field(column):
182+
nonlocal annotation_idx
169183
if hasattr(column, "target"):
170184
# column is a Col.
171185
target = column.target.column
@@ -174,8 +188,13 @@ def get_columns(self):
174188
# name for $proj.
175189
target = f"__annotation{annotation_idx}"
176190
annotation_idx += 1
177-
result.append((target, column))
178-
return tuple(result) + tuple(self.query.annotation_select.items())
191+
return target, column
192+
193+
return (
194+
tuple(map(project_field, columns))
195+
+ tuple(self.query.annotation_select.items())
196+
+ tuple(map(project_field, related_columns))
197+
)
179198

180199
def _get_ordering(self):
181200
"""
@@ -212,8 +231,20 @@ def _get_ordering(self):
212231
field_ordering.append((opts.get_field(name), ascending))
213232
return field_ordering
214233

234+
@cached_property
235+
def collection_name(self):
236+
return self.query.get_meta().db_table
237+
215238
def get_collection(self):
216-
return self.connection.get_collection(self.query.get_meta().db_table)
239+
return self.connection.get_collection(self.collection_name)
240+
241+
def get_lookup_pipeline(self):
242+
result = []
243+
for alias in tuple(self.query.alias_map):
244+
if not self.query.alias_refcount[alias] or self.collection_name == alias:
245+
continue
246+
result += self.query.alias_map[alias].as_mql(self, self.connection)
247+
return result
217248

218249

219250
class SQLInsertCompiler(SQLCompiler):

django_mongodb/expressions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ def case(self, compiler, connection):
4343

4444

4545
def col(self, compiler, connection): # noqa: ARG001
46-
return f"${self.target.column}"
46+
# Add the column's collection's alias for columns in joined collections.
47+
prefix = f"{self.alias}." if self.alias != compiler.collection_name else ""
48+
return f"${prefix}{self.target.column}"
4749

4850

4951
def combined_expression(self, compiler, connection):

0 commit comments

Comments
 (0)