Skip to content

Commit e509877

Browse files
committed
comment edits
1 parent 63fc99b commit e509877

File tree

3 files changed

+54
-84
lines changed

3 files changed

+54
-84
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,16 @@ Migrations for 'admin':
138138
- The `tzinfo` parameter of the `Trunc` database functions doesn't work
139139
properly because MongoDB converts the result back to UTC.
140140

141-
- In `jsonfield`, when a binary operator (like `exact`) is used, the behavior may differ from other backends if either operand (left-hand or right-hand side) is `null`.
141+
- When querying `JSONField`:
142+
- There is no way to distinguish between a JSON "null" (represented by
143+
`Value(None, JSONField())`) and a SQL null (queried using the `isnull`
144+
lookup). Both of these queries return both of these nulls.
145+
- Some queries with `Q` objects, e.g. `Q(value__foo="bar")`, don't work
146+
properly, particularly with `QuerySet.exclude()`.
147+
- Filtering for a `None` key, e.g. `QuerySet.filter(value__j=None)`
148+
incorrectly returns objects where the key doesn't exist.
149+
- You can study the skipped tests in `DatabaseFeatures.django_test_skips` for
150+
more details on known issues.
142151

143152
## Troubleshooting
144153

django_mongodb/features.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,11 +417,21 @@ def django_test_expected_failures(self):
417417
"db_functions.comparison.test_cast.CastTests.test_cast_from_python_to_datetime",
418418
"db_functions.comparison.test_cast.CastTests.test_cast_to_duration",
419419
},
420-
"MongoDB's null behavior is different from SQL's.": {
420+
"Known issue querying JSONField.": {
421+
# An ExpressionWrapper annotation with KeyTransform followed by
422+
# .filter(expr__isnull=False) doesn't use KeyTransformIsNull as it
423+
# needs to.
421424
"model_fields.test_jsonfield.TestQuerying.test_expression_wrapper_key_transform",
425+
# There is no way to distinguish between a JSON "null" (represented
426+
# by Value(None, JSONField())) and a SQL null (queried using the
427+
# isnull lookup). Both of these queries return both nulls.
422428
"model_fields.test_jsonfield.TestSaveLoad.test_json_null_different_from_sql_null",
429+
# Some queries with Q objects, e.g. Q(value__foo="bar"), don't work
430+
# properly, particularly with QuerySet.exclude().
423431
"model_fields.test_jsonfield.TestQuerying.test_lookup_exclude",
424432
"model_fields.test_jsonfield.TestQuerying.test_lookup_exclude_nonexistent_key",
433+
# Queries like like QuerySet.filter(value__j=None) incorrectly
434+
# returns objects where the key doesn't exist.
425435
"model_fields.test_jsonfield.TestQuerying.test_none_key",
426436
"model_fields.test_jsonfield.TestQuerying.test_none_key_exclude",
427437
},

django_mongodb/fields/json.py

Lines changed: 33 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,30 @@ def data_contains(self, compiler, connection): # noqa: ARG001
2626

2727

2828
def _has_key_predicate(path, root_column, negated=False):
29-
result = {"$and": [{"$ne": [{"$type": path}, "missing"]}, {"$ne": [root_column, None]}]}
29+
"""Return MQL to check for the existence of `path`."""
30+
result = {
31+
"$and": [
32+
# The path must exist (i.e. not be "missing").
33+
{"$ne": [{"$type": path}, "missing"]},
34+
# If the JSONField value is None, an additional check for not null
35+
# is needed since $type returns null instead of "missing".
36+
{"$ne": [root_column, None]},
37+
]
38+
}
3039
if negated:
3140
result = {"$not": result}
3241
return result
3342

3443

3544
def has_key_lookup(self, compiler, connection):
36-
"""
37-
Performs a key lookup in a JSONField to check for the existence of a key.
38-
39-
The key lookup in a JSONField is defined by the following conditions:
40-
1. The path type must be different from "missing".
41-
2. In cases where the JSONField is None (as the $type returns Null instead of "missing"),
42-
an additional check for not null is added.
43-
44-
Returns:
45-
A dictionary representing the MongoDB query for key lookup.
46-
47-
Raises:
48-
AssertionError: If `self.mongo_operator` is None and there are multiple keys.
49-
50-
Note:
51-
The function handles key lookup in two ways:
52-
- Via `KeyTransform`
53-
- Directly via key
54-
55-
Both are transformed into a `KeyTransform` for consistent handling.
56-
"""
45+
"""Return MQL to check for the existence of a key."""
5746
rhs = self.rhs
5847
lhs = process_lhs(self, compiler, connection)
5948
if not isinstance(rhs, list | tuple):
6049
rhs = [rhs]
6150
paths = []
62-
# Transform all keys into KeyTransform instances for consistent handling
51+
# Transform any "raw" keys into KeyTransforms to allow consistent handling
52+
# in the code that follows.
6353
for key in rhs:
6454
rhs_json_path = key if isinstance(key, KeyTransform) else KeyTransform(key, self.lhs)
6555
paths.append(rhs_json_path.as_mql(compiler, connection))
@@ -85,19 +75,12 @@ def json_exact_process_rhs(self, compiler, connection):
8575

8676
def key_transform(self, compiler, connection):
8777
"""
88-
Transforms a Django KeyTransform (JSON path) into a MongoDB query path.
89-
90-
In MongoDB, JSON paths cannot always be represented simply as $var.key1.key2.key3
91-
due to potential array types. Therefore, indexing arrays requires the use of
92-
`arrayElemAt`. Additionally, a conditional check (if statement) is necessary
93-
to verify the type before performing the operation.
94-
95-
Returns:
96-
A dictionary representing the MongoDB query for the JSON path.
78+
Return MQL for this KeyTransform (JSON path).
9779
98-
Note:
99-
The function constructs the MongoDB path by iterating through the key transforms
100-
and handling both field access and array indexing appropriately.
80+
JSON paths cannot always be represented simply as $var.key1.key2.key3 due
81+
to possible array types. Therefore, indexing arrays requires the use of
82+
`arrayElemAt`. Additionally, $cond is necessary to verify the type before
83+
performing the operation.
10184
"""
10285
key_transforms = [self.key_name]
10386
previous = self.lhs
@@ -107,11 +90,11 @@ def key_transform(self, compiler, connection):
10790
previous = previous.lhs
10891
lhs_mql = previous.as_mql(compiler, connection)
10992
result = lhs_mql
110-
# Build the MongoDB path using the collected key transforms
93+
# Build the MQL path using the collected key transforms.
11194
for key in key_transforms:
11295
get_field = {"$getField": {"input": result, "field": key}}
113-
# Handle array indexing if the key is a digit
114-
# if we have an index like '001' is not an array index.
96+
# Handle array indexing if the key is a digit. If key is something
97+
# like '001', it's not an array index despite isdigit() returning True.
11598
if key.isdigit() and str(int(key)) == key:
11699
result = {
117100
"$cond": {
@@ -127,54 +110,33 @@ def key_transform(self, compiler, connection):
127110

128111
def key_transform_in(self, compiler, connection):
129112
"""
130-
Checks if a JSON path's values are within a set of specified values and ensures the key exists.
131-
132-
This function performs two main checks:
133-
1. Verifies that the resulting path values are within the right-hand side (rhs) values.
134-
2. Ensures that the key exists.
135-
136-
Returns:
137-
A dictionary representing the MongoDB query that checks both conditions.
138-
139-
Note:
140-
The function processes the left-hand side (lhs) and right-hand side (rhs)
141-
of the query, constructs the MongoDB expression, and checks both the value
142-
existence in rhs and the key existence in the JSON document.
113+
Return MQL to check if a JSON path exists and that its values are in the
114+
set of specified values (rhs).
143115
"""
144116
lhs_mql = process_lhs(self, compiler, connection)
145-
# Traverse to the root column
117+
# Traverse to the root column.
146118
previous = self.lhs
147119
while isinstance(previous, KeyTransform):
148120
previous = previous.lhs
149121
root_column = previous.as_mql(compiler, connection)
150122
value = process_rhs(self, compiler, connection)
151-
# Construct the expression to check if lhs_mql values are in rhs values
123+
# Construct the expression to check if lhs_mql values are in rhs values.
152124
expr = connection.mongo_operators[self.lookup_name](lhs_mql, value)
153125
return {"$and": [_has_key_predicate(lhs_mql, root_column), expr]}
154126

155127

156128
def key_transform_is_null(self, compiler, connection):
157129
"""
158-
Handles the KeyTransformIsNull lookup.
159-
160-
This function borrows logic from HasKey for `isnull=False`. If `isnull=True`,
161-
the query should match only objects that do not have the key.
162-
163-
Returns:
164-
A dictionary representing the MongoDB query for checking the nullability of the key.
130+
Return MQL to check the nullability of a key.
165131
166-
Note:
167-
If `isnull=True`, the query matches objects where the key is missing
168-
or the root column is null.
169-
If `isnull=False`, the query negates the result to match objects where the key exists
170-
and the root column isn't null.
132+
If `isnull=True`, the query matches objects where the key is missing or the
133+
root column is null. If `isnull=False`, the query negates the result to
134+
match objects where the key exists.
171135
172-
Reference:
173-
https://code.djangoproject.com/ticket/32252
136+
Reference: https://code.djangoproject.com/ticket/32252
174137
"""
175138
lhs_mql = process_lhs(self, compiler, connection)
176139
rhs_mql = process_rhs(self, compiler, connection)
177-
178140
# Get the root column.
179141
previous = self.lhs
180142
while isinstance(previous, KeyTransform):
@@ -185,23 +147,12 @@ def key_transform_is_null(self, compiler, connection):
185147

186148
def key_transform_numeric_lookup_mixin(self, compiler, connection):
187149
"""
188-
Checks if the field exists and fulfills the given numeric lookup expression.
189-
190-
This function ensures that the field in the JSON document:
191-
1. Exists (i.e., is not "missing" or "null").
192-
2. Satisfies the specified numeric lookup expression.
193-
194-
Returns:
195-
A dictionary representing the MongoDB query that checks both conditions.
196-
197-
Note:
198-
The function constructs the MongoDB expression to check if the field exists
199-
and fulfills the numeric lookup expression, and combines these conditions
200-
using $and.
150+
Return MQL to check if the field exists (i.e., is not "missing" or "null")
151+
and that the field matches the given numeric lookup expression.
201152
"""
202153
expr = builtin_lookup(self, compiler, connection)
203154
lhs = process_lhs(self, compiler, connection)
204-
# Check if the type of lhs is not "missing" or "null"
155+
# Check if the type of lhs is not "missing" or "null".
205156
not_missing_or_null = {"$not": {"$in": [{"$type": lhs}, ["missing", "null"]]}}
206157
return {"$and": [expr, not_missing_or_null]}
207158

0 commit comments

Comments
 (0)