Skip to content

Commit 4502a15

Browse files
committed
Fix contains, startwith and endwith
1 parent 397fc8d commit 4502a15

File tree

2 files changed

+7
-213
lines changed

2 files changed

+7
-213
lines changed

django_mongodb/_helpers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ def process_rhs(node, compiler, connection):
1515
_, value = node.process_rhs(compiler, connection)
1616
if node.lookup_name not in ("in", "range"):
1717
value = value[0]
18+
if node.lookup_name in ("startswith", "istartswith", "contains"):
19+
# remove the wildcard % for like expressions
20+
value = value[:-1]
21+
if node.lookup_name in ("contains", "endswith", "iendswith"):
22+
# remove the initial wildcard % for like expressions
23+
value = value[1:]
1824

1925
return compiler.connection.ops.prep_lookup_value(value, node.lhs.output_field, node.lookup_name)
2026

django_mongodb/query.py

Lines changed: 1 addition & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import re
22
from functools import wraps
33

4-
from django.core.exceptions import FullResultSet
5-
from django.db import DatabaseError, IntegrityError, NotSupportedError
6-
from django.db.models.lookups import UUIDTextMixin
7-
from django.db.models.query import QuerySet
8-
from django.db.models.sql.where import OR, SubqueryConstraint
9-
from django.utils.tree import Node
4+
from django.db import DatabaseError, IntegrityError
105
from pymongo import ASCENDING, DESCENDING
116
from pymongo.errors import DuplicateKeyError, PyMongoError
127

@@ -138,210 +133,3 @@ def get_cursor(self):
138133
if self.query.high_mark is not None:
139134
cursor.limit(int(self.query.high_mark - self.query.low_mark))
140135
return cursor
141-
142-
def add_filters(self, filters, query=None):
143-
children = self._get_children(filters.children)
144-
145-
if query is None:
146-
query = self.mongo_query
147-
148-
if filters.connector == OR:
149-
assert "$or" not in query, "Multiple ORs are not supported."
150-
or_conditions = query["$or"] = []
151-
152-
if filters.negated:
153-
self._negated = not self._negated
154-
155-
for child in children:
156-
subquery = {} if filters.connector == OR else query
157-
158-
if isinstance(child, Node):
159-
if filters.connector == OR and child.connector == OR and len(child.children) > 1:
160-
raise DatabaseError("Nested ORs are not supported.")
161-
162-
if filters.connector == OR and filters.negated:
163-
raise NotImplementedError("Negated ORs are not supported.")
164-
165-
self.add_filters(child, query=subquery)
166-
167-
if filters.connector == OR and subquery:
168-
or_conditions.extend(subquery.pop("$or", []))
169-
if subquery:
170-
or_conditions.append(subquery)
171-
172-
continue
173-
174-
try:
175-
field, lookup_type, value = self._decode_child(child)
176-
except FullResultSet:
177-
continue
178-
179-
column = field.column
180-
existing = subquery.get(column)
181-
182-
if self._negated and lookup_type in NEGATED_OPERATORS_MAP:
183-
op_func = NEGATED_OPERATORS_MAP[lookup_type]
184-
already_negated = True
185-
else:
186-
op_func = OPERATORS_MAP[lookup_type]
187-
if self._negated:
188-
already_negated = False
189-
190-
lookup = op_func(value)
191-
192-
if existing is None:
193-
if self._negated and not already_negated:
194-
lookup = {"$not": lookup}
195-
subquery[column] = lookup
196-
if filters.connector == OR and subquery:
197-
or_conditions.append(subquery)
198-
continue
199-
200-
if not isinstance(existing, dict):
201-
if not self._negated:
202-
# {'a': o1} + {'a': o2} --> {'a': {'$all': [o1, o2]}}
203-
assert not isinstance(lookup, dict)
204-
subquery[column] = {"$all": [existing, lookup]}
205-
else:
206-
# {'a': o1} + {'a': {'$not': o2}} --> {'a': {'$all': [o1], '$nin': [o2]}}
207-
if already_negated:
208-
assert list(lookup) == ["$ne"]
209-
lookup = lookup["$ne"]
210-
assert not isinstance(lookup, dict)
211-
subquery[column] = {"$all": [existing], "$nin": [lookup]}
212-
else:
213-
not_ = existing.pop("$not", None)
214-
if not_:
215-
assert not existing
216-
if isinstance(lookup, dict):
217-
assert list(lookup) == ["$ne"]
218-
lookup = next(iter(lookup.values()))
219-
assert not isinstance(lookup, dict), (not_, lookup)
220-
if self._negated:
221-
# {'not': {'a': o1}} + {'a': {'not': o2}} --> {'a': {'nin': [o1, o2]}}
222-
subquery[column] = {"$nin": [not_, lookup]}
223-
else:
224-
# {'not': {'a': o1}} + {'a': o2} -->
225-
# {'a': {'nin': [o1], 'all': [o2]}}
226-
subquery[column] = {"$nin": [not_], "$all": [lookup]}
227-
else:
228-
if isinstance(lookup, dict):
229-
if "$ne" in lookup:
230-
if "$nin" in existing:
231-
# {'$nin': [o1, o2]} + {'$ne': o3} --> {'$nin': [o1, o2, o3]}
232-
assert "$ne" not in existing
233-
existing["$nin"].append(lookup["$ne"])
234-
elif "$ne" in existing:
235-
# {'$ne': o1} + {'$ne': o2} --> {'$nin': [o1, o2]}
236-
existing["$nin"] = [existing.pop("$ne"), lookup["$ne"]]
237-
else:
238-
existing.update(lookup)
239-
else:
240-
if "$in" in lookup and "$in" in existing:
241-
# {'$in': o1} + {'$in': o2} --> {'$in': o1 union o2}
242-
existing["$in"] = list(set(lookup["$in"] + existing["$in"]))
243-
else:
244-
# {'$gt': o1} + {'$lt': o2} --> {'$gt': o1, '$lt': o2}
245-
assert all(key not in existing for key in lookup), [
246-
lookup,
247-
existing,
248-
]
249-
existing.update(lookup)
250-
else:
251-
key = "$nin" if self._negated else "$all"
252-
existing.setdefault(key, []).append(lookup)
253-
254-
if filters.connector == OR and subquery:
255-
or_conditions.append(subquery)
256-
257-
if filters.negated:
258-
self._negated = not self._negated
259-
260-
def _decode_child(self, child):
261-
"""
262-
Produce arguments suitable for add_filter from a WHERE tree leaf
263-
(a tuple).
264-
"""
265-
if isinstance(child, UUIDTextMixin):
266-
raise NotSupportedError("Pattern lookups on UUIDField are not supported.")
267-
268-
rhs, rhs_params = child.process_rhs(self.compiler, self.connection)
269-
lookup_type = child.lookup_name
270-
value = rhs_params
271-
packed = child.lhs.get_group_by_cols()[0]
272-
alias = packed.alias
273-
column = packed.target.column
274-
field = child.lhs.output_field
275-
opts = self.query.model._meta
276-
if alias and alias != opts.db_table:
277-
raise NotSupportedError("MongoDB doesn't support JOINs and multi-table inheritance.")
278-
279-
# For parent.child_set queries the field held by the constraint
280-
# is the parent's primary key, while the field the filter
281-
# should consider is the child's foreign key field.
282-
if column != field.column:
283-
if not field.primary_key:
284-
raise NotSupportedError(
285-
"MongoDB doesn't support filtering on non-primary key ForeignKey fields."
286-
)
287-
288-
field = next(f for f in opts.fields if f.column == column)
289-
290-
value = self._normalize_lookup_value(lookup_type, value, field)
291-
292-
return field, lookup_type, value
293-
294-
def _normalize_lookup_value(self, lookup_type, value, field):
295-
"""
296-
Undo preparations done by lookups not suitable for MongoDB, and pass
297-
the lookup argument through DatabaseOperations.prep_lookup_value().
298-
"""
299-
# Undo Lookup.get_db_prep_lookup() putting params in a list.
300-
if lookup_type not in ("in", "range"):
301-
if len(value) > 1:
302-
raise DatabaseError(
303-
"Filter lookup type was %s; expected the filter argument "
304-
"not to be a list. Only 'in'-filters can be used with "
305-
"lists." % lookup_type
306-
)
307-
value = value[0]
308-
309-
# Remove percent signs added by PatternLookup.process_rhs() for LIKE
310-
# queries.
311-
if lookup_type in ("startswith", "istartswith"):
312-
value = value[:-1]
313-
elif lookup_type in ("endswith", "iendswith"):
314-
value = value[1:]
315-
elif lookup_type in ("contains", "icontains"):
316-
value = value[1:-1]
317-
318-
return self.ops.prep_lookup_value(value, field, lookup_type)
319-
320-
def _get_children(self, children):
321-
"""
322-
Filter out nodes of the given constraint tree not needed for
323-
MongoDB queries. Check that the given constraints are supported.
324-
"""
325-
result = []
326-
for child in children:
327-
if isinstance(child, SubqueryConstraint):
328-
raise NotSupportedError("Subqueries are not supported.")
329-
330-
if isinstance(child, tuple):
331-
constraint, lookup_type, _, value = child
332-
333-
# When doing a lookup using a QuerySet Django would use
334-
# a subquery, but this won't work for MongoDB.
335-
# TODO: Add a supports_subqueries feature and let Django
336-
# evaluate subqueries instead of passing them as SQL
337-
# strings (QueryWrappers) to filtering.
338-
if isinstance(value, QuerySet):
339-
raise NotSupportedError("Subqueries are not supported.")
340-
341-
# Remove leafs that were automatically added by
342-
# sql.Query.add_filter() to handle negations of outer joins.
343-
if lookup_type == "isnull" and constraint.field is None:
344-
continue
345-
346-
result.append(child)
347-
return result

0 commit comments

Comments
 (0)