diff --git a/django_mongodb_backend/features.py b/django_mongodb_backend/features.py index 7a27e1681..7e29c5003 100644 --- a/django_mongodb_backend/features.py +++ b/django_mongodb_backend/features.py @@ -68,6 +68,12 @@ class DatabaseFeatures(GISFeatures, BaseDatabaseFeatures): "model_fields.test_jsonfield.TestQuerying.test_icontains", # Unexpected alias_refcount in alias_map. "queries.tests.Queries1Tests.test_order_by_tables", + # Pattern lookups (startswith, regex, etc.) don't work on non-string + # fields: https://jira.mongodb.org/browse/INTPYTHON-734 + "admin_changelist.tests.ChangeListTests.test_pk_in_search_fields", + "admin_changelist.tests.ChangeListTests.test_related_field_multiple_search_terms", + "lookup.tests.LookupTests.test_lookup_int_as_str", + "lookup.tests.LookupTests.test_regex_non_string", # The $sum aggregation returns 0 instead of None for null. "aggregation.test_filter_argument.FilteredAggregateTests.test_plain_annotate", "aggregation.tests.AggregateTestCase.test_aggregation_default_passed_another_aggregate", diff --git a/django_mongodb_backend/query_utils.py b/django_mongodb_backend/query_utils.py index 0bb292995..4b744241e 100644 --- a/django_mongodb_backend/query_utils.py +++ b/django_mongodb_backend/query_utils.py @@ -50,4 +50,4 @@ def process_rhs(node, compiler, connection): def regex_match(field, regex_vals, insensitive=False): regex = {"$concat": regex_vals} if isinstance(regex_vals, tuple) else regex_vals options = "i" if insensitive else "" - return {"$regexMatch": {"input": {"$toString": field}, "regex": regex, "options": options}} + return {"$regexMatch": {"input": field, "regex": regex, "options": options}} diff --git a/docs/source/releases/5.2.x.rst b/docs/source/releases/5.2.x.rst index ec7d65984..5f5971be9 100644 --- a/docs/source/releases/5.2.x.rst +++ b/docs/source/releases/5.2.x.rst @@ -2,6 +2,20 @@ Django MongoDB Backend 5.2.x ============================ +5.2.0 beta 3 +============ + +*Unreleased* + +Backwards incompatible changes +------------------------------ + +- Pattern matching lookups (``iexact``, ``startswith``, ``istartswith``, + ``endswith``, ``iendswith``, ``contains``, ``icontains``, ``regex``, + and ``iregex``) no longer support non-string fields. These lookups previously + cast their input using ``$toString`` but this caused some queries to perform + poorly because MongoDB couldn't use indexes when running the query. + 5.2.0 beta 2 ============ diff --git a/docs/source/topics/known-issues.rst b/docs/source/topics/known-issues.rst index df71a946d..96cd6dec1 100644 --- a/docs/source/topics/known-issues.rst +++ b/docs/source/topics/known-issues.rst @@ -50,6 +50,11 @@ Querying - You can study the skipped tests in ``DatabaseFeatures.django_test_skips`` for more details on known issues. +- Pattern matching lookups (:lookup:`iexact`, :lookup:`startswith`, + :lookup:`istartswith`, :lookup:`endswith`, :lookup:`iendswith`, + :lookup:`contains`, :lookup:`icontains`, :lookup:`regex`, + and :lookup:`iregex`) don't support non-string fields. + Database functions ================== diff --git a/tests/lookup_/models.py b/tests/lookup_/models.py index dd0f15475..61a99d8ab 100644 --- a/tests/lookup_/models.py +++ b/tests/lookup_/models.py @@ -1,6 +1,13 @@ from django.db import models +class Book(models.Model): + title = models.CharField(max_length=10) + + def __str__(self): + return self.title + + class Number(models.Model): num = models.IntegerField(blank=True, null=True) diff --git a/tests/lookup_/tests.py b/tests/lookup_/tests.py index 520976d13..448da6571 100644 --- a/tests/lookup_/tests.py +++ b/tests/lookup_/tests.py @@ -1,6 +1,6 @@ from django.test import TestCase -from .models import Number +from .models import Book, Number class NumericLookupTests(TestCase): @@ -15,3 +15,18 @@ def test_lt(self): def test_lte(self): self.assertQuerySetEqual(Number.objects.filter(num__lte=3), self.objs[:4]) + + +class RegexTests(TestCase): + def test_mql(self): + # $regexMatch must not cast the input to string, otherwise MongoDB + # can't use the field's indexes. + with self.assertNumQueries(1) as ctx: + list(Book.objects.filter(title__regex="Moby Dick")) + query = ctx.captured_queries[0]["sql"] + self.assertEqual( + query, + "db.lookup__book.aggregate([" + "{'$match': {'$expr': {'$regexMatch': {'input': '$title', " + "'regex': 'Moby Dick', 'options': ''}}}}])", + )