Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .evergreen/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
set -eux

# Install django-mongodb-backend
/opt/python/3.10/bin/python3 -m venv venv
/opt/python/3.12/bin/python3 -m venv venv
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Python 10 -> 12 for Django requirements (what's the lowest Python Django 6 works with) ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

. venv/bin/activate
python -m pip install -U pip
pip install -e .

# Install django and test dependencies
git clone --branch mongodb-5.2.x https://github.com/mongodb-forks/django django_repo
git clone --branch mongodb-6.0.x https://github.com/mongodb-forks/django django_repo
pushd django_repo/tests/
pip install -e ..
pip install -r requirements/py3.txt
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.10'
python-version: '3.12'
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
- name: Install Python dependencies
Expand All @@ -39,7 +39,7 @@ jobs:
with:
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
python-version: '3.10'
python-version: '3.12'
- name: Install dependencies
run: |
pip install -U pip
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-python-atlas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
uses: actions/checkout@v6
with:
repository: 'mongodb-forks/django'
ref: 'mongodb-5.2.x'
ref: 'mongodb-6.0.x'
path: 'django_repo'
persist-credentials: false
- name: Install system packages for Django's Python test dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-python-geo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
uses: actions/checkout@v6
with:
repository: 'mongodb-forks/django'
ref: 'mongodb-5.2.x'
ref: 'mongodb-6.0.x'
path: 'django_repo'
persist-credentials: false
- name: Install system packages for Django's Python test dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
uses: actions/checkout@v6
with:
repository: 'mongodb-forks/django'
ref: 'mongodb-5.2.x'
ref: 'mongodb-6.0.x'
path: 'django_repo'
persist-credentials: false
- name: Install system packages for Django's Python test dependencies
Expand Down
4 changes: 2 additions & 2 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ python:
- docs

build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
python: "3.11"
python: "3.12"
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ https://django-mongodb-backend.readthedocs.io/en/latest/.
### Install

Use the version of `django-mongodb-backend` that corresponds to your version of
Django. For example, to get the latest compatible release for Django 5.2.x:
Django. For example, to get the latest compatible release for Django 6.0.x:

```bash
pip install django-mongodb-backend==5.2.*
pip install django-mongodb-backend==6.0.*
```

### Create a project

From your shell, run the following command to create a new Django project
called `example` using our project template. Make sure the end of the template
URL corresponds to your version of Django (e.g. `5.2.x.zip` for any Django
5.2.x version).
URL corresponds to your version of Django (e.g. `6.0.x.zip` for any Django
6.0.x version).

```bash
django-admin startproject example --template https://github.com/mongodb-labs/django-mongodb-project/archive/refs/heads/5.2.x.zip
django-admin startproject example --template https://github.com/mongodb-labs/django-mongodb-project/archive/refs/heads/6.0.x.zip
```

You can check what version of Django you're using with:
Expand Down
6 changes: 2 additions & 4 deletions django_mongodb_backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
__version__ = "5.2.4.dev0"
__version__ = "6.0.0.dev0"

# Check Django compatibility before other imports which may fail if the
# wrong version of Django is installed.
from .utils import check_django_compatability, parse_uri
from .utils import check_django_compatability

check_django_compatability()

Expand All @@ -15,8 +15,6 @@
from .lookups import register_lookups # noqa: E402
from .query import register_nodes # noqa: E402

__all__ = ["parse_uri"]

register_aggregates()
register_checks()
register_expressions()
Expand Down
64 changes: 47 additions & 17 deletions django_mongodb_backend/aggregates.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from django.db.models.aggregates import Aggregate, Count, StdDev, Variance
from django.db import NotSupportedError
from django.db.models.aggregates import (
Aggregate,
Count,
StdDev,
StringAgg,
Variance,
)
from django.db.models.expressions import Case, Value, When
from django.db.models.lookups import IsNull
from django.db.models.sql.where import WhereNode
Expand All @@ -11,13 +18,19 @@

def aggregate(self, compiler, connection, operator=None, resolve_inner_expression=False):
agg_expression, *_ = self.get_source_expressions()
if self.filter:
agg_expression = Case(
When(self.filter, then=agg_expression),
# Skip rows that don't meet the criteria.
default=Remove(),
)
lhs_mql = agg_expression.as_mql(compiler, connection, as_expr=True)
lhs_mql = None
if self.filter is not None:
try:
lhs_mql = self.filter.as_mql(compiler, connection, as_expr=True)
except NotSupportedError:
# Generate a CASE statement for this AggregateFilter.
agg_expression = Case(
When(self.filter.condition, then=agg_expression),
# Skip rows that don't meet the criteria.
default=Remove(),
)
if lhs_mql is None:
lhs_mql = agg_expression.as_mql(compiler, connection, as_expr=True)
if resolve_inner_expression:
return lhs_mql
operator = operator or MONGO_AGGREGATIONS.get(self.__class__, self.function.lower())
Expand All @@ -32,18 +45,30 @@ def count(self, compiler, connection, resolve_inner_expression=False):
"""
agg_expression, *_ = self.get_source_expressions()
if not self.distinct or resolve_inner_expression:
lhs_mql = None
conditions = [IsNull(agg_expression, False)]
if self.filter:
conditions.append(self.filter)
inner_expression = Case(
When(WhereNode(conditions), then=agg_expression if self.distinct else Value(1)),
# Skip rows that don't meet the criteria.
default=Remove(),
)
inner_expression = inner_expression.as_mql(compiler, connection, as_expr=True)
try:
lhs_mql = self.filter.as_mql(compiler, connection, as_expr=True)
except NotSupportedError:
# Generate a CASE statement for this AggregateFilter.
conditions.append(self.filter.condition)
condition = When(
WhereNode(conditions),
then=agg_expression if self.distinct else Value(1),
)
inner_expression = Case(condition, default=Remove())
else:
inner_expression = Case(
When(WhereNode(conditions), then=agg_expression if self.distinct else Value(1)),
# Skip rows that don't meet the criteria.
default=Remove(),
)
if lhs_mql is None:
lhs_mql = inner_expression.as_mql(compiler, connection, as_expr=True)
if resolve_inner_expression:
return inner_expression
return {"$sum": inner_expression}
return lhs_mql
return {"$sum": lhs_mql}
# If distinct=True or resolve_inner_expression=False, sum the size of the
# set.
return {"$size": agg_expression.as_mql(compiler, connection, as_expr=True)}
Expand All @@ -57,8 +82,13 @@ def stddev_variance(self, compiler, connection):
return aggregate(self, compiler, connection, operator=operator)


def string_agg(self, compiler, connection): # noqa: ARG001
raise NotSupportedError("StringAgg is not supported.")


def register_aggregates():
Aggregate.as_mql_expr = aggregate
Count.as_mql_expr = count
StdDev.as_mql_expr = stddev_variance
StringAgg.as_mql_expr = string_agg
Variance.as_mql_expr = stddev_variance
20 changes: 20 additions & 0 deletions django_mongodb_backend/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ class DatabaseFeatures(GISFeatures, BaseDatabaseFeatures):
"model_fields.test_jsonfield.TestSaveLoad.test_bulk_update_custom_get_prep_value",
# To debug: https://github.com/mongodb/django-mongodb-backend/issues/362
"constraints.tests.UniqueConstraintTests.test_validate_case_when",
# bulk_create() population of _order doesn't work because of ObjectId
# type mismatch when querying object_id CharField.
# https://github.com/django/django/commit/953095d1e603fe0f8f01175b1409ca23818dcff9
"contenttypes_tests.test_order_with_respect_to.OrderWithRespectToGFKTests.test_bulk_create_allows_duplicate_order_values",
"contenttypes_tests.test_order_with_respect_to.OrderWithRespectToGFKTests.test_bulk_create_mixed_scenario",
"contenttypes_tests.test_order_with_respect_to.OrderWithRespectToGFKTests.test_bulk_create_respects_mixed_manual_order",
"contenttypes_tests.test_order_with_respect_to.OrderWithRespectToGFKTests.test_bulk_create_with_existing_children",
}
# $bitAnd, #bitOr, and $bitXor are new in MongoDB 6.3.
_django_test_expected_failures_bitwise = {
Expand Down Expand Up @@ -139,6 +146,7 @@ def django_test_expected_failures(self):
"validation.test_unique.PerformUniqueChecksTest.test_unique_db_default",
},
"Insert expressions aren't supported.": {
"basic.tests.ModelTest.test_save_expressions",
"bulk_create.tests.BulkCreateTests.test_bulk_insert_now",
"bulk_create.tests.BulkCreateTests.test_bulk_insert_expressions",
"expressions.tests.BasicExpressionsTests.test_new_object_create",
Expand Down Expand Up @@ -201,6 +209,7 @@ def django_test_expected_failures(self):
"prefetch_related.tests.LookupOrderingTest.test_order",
"prefetch_related.tests.MultiDbTests.test_using_is_honored_m2m",
"prefetch_related.tests.MultiTableInheritanceTest",
"prefetch_related.tests.PrefetchRelatedMTICacheTests",
"prefetch_related.tests.PrefetchRelatedTests",
"prefetch_related.tests.ReadPrefetchedObjectsCacheTests",
"prefetch_related.tests.Ticket21410Tests",
Expand Down Expand Up @@ -563,6 +572,7 @@ def django_test_expected_failures(self):
"Custom lookups are not supported.": {
"custom_lookups.tests.BilateralTransformTests",
"custom_lookups.tests.LookupTests.test_basic_lookup",
"custom_lookups.tests.LookupTests.test_custom_lookup_with_subquery",
"custom_lookups.tests.LookupTests.test_custom_name_lookup",
"custom_lookups.tests.LookupTests.test_div3_extract",
"custom_lookups.tests.SubqueryTransformTests.test_subquery_usage",
Expand All @@ -580,6 +590,16 @@ def django_test_expected_failures(self):
"test_utils.tests.DisallowedDatabaseQueriesTests.test_disallowed_database_queries",
"test_utils.tests.DisallowedDatabaseQueriesTests.test_disallowed_thread_database_connection",
},
"search lookup not supported on non-Atlas.": {
"expressions.tests.BasicExpressionsTests.test_lookups_subquery",
},
"StringAgg is not supported.": {
"aggregation.tests.AggregateTestCase.test_distinct_on_stringagg",
"aggregation.tests.AggregateTestCase.test_string_agg_escapes_delimiter",
"aggregation.tests.AggregateTestCase.test_string_agg_filter",
"aggregation.tests.AggregateTestCase.test_string_agg_filter_in_subquery",
"aggregation.tests.AggregateTestCase.test_stringagg_default_value",
},
}

@cached_property
Expand Down
9 changes: 7 additions & 2 deletions django_mongodb_backend/fields/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
def valid_path_key_name(key_name):
# A lookup can use path syntax (field.subfield) unless it contains a dollar
# sign or period.
return not any(char in key_name for char in ("$", "."))
return not key_name.startswith("-") and not any(char in key_name for char in ("$", "."))


def build_json_mql_path(lhs, key_transforms, as_expr=False):
Expand All @@ -36,7 +36,12 @@ def build_json_mql_path(lhs, key_transforms, as_expr=False):
get_field = {"$getField": {"input": result, "field": key}}
# Handle array indexing if the key is a digit. If key is something
# like '001', it's not an array index despite isdigit() returning True.
if key.isdigit() and str(int(key)) == key:
try:
int(key)
is_digit = str(int(key)) == key
except ValueError:
is_digit = False
if is_digit:
result = {
"$cond": {
"if": {"$isArray": result},
Expand Down
2 changes: 2 additions & 0 deletions django_mongodb_backend/gis/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def gis_operators(self):
"FromWKT",
"GeoHash",
"GeometryDistance",
"GeometryType",
"Intersection",
"IsEmpty",
"IsValid",
Expand All @@ -52,6 +53,7 @@ def gis_operators(self):
"Perimeter",
"PointOnSurface",
"Reverse",
"Rotate",
"Scale",
"SnapToGrid",
"SymDifference",
Expand Down
4 changes: 3 additions & 1 deletion django_mongodb_backend/query_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.core.exceptions import FullResultSet
from django.core.exceptions import EmptyResultSet, FullResultSet
from django.db.models import F
from django.db.models.expressions import CombinedExpression, Func, Value
from django.db.models.sql.query import Query
Expand All @@ -19,6 +19,8 @@ def process_lhs(node, compiler, connection, as_expr=False):
result.append(expr.as_mql(compiler, connection, as_expr=as_expr))
except FullResultSet:
result.append(Value(True).as_mql(compiler, connection, as_expr=as_expr))
except EmptyResultSet:
result.append(Value(None).as_mql(compiler, connection, as_expr=as_expr))
return result
# node is a Transform with just one source expression, aliased as "lhs".
if is_direct_value(node.lhs):
Expand Down
47 changes: 0 additions & 47 deletions django_mongodb_backend/utils.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import copy
import time
import warnings

import django
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db.backends.utils import logger
from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.functional import SimpleLazyObject
from django.utils.text import format_lazy
from django.utils.version import get_version_tuple
from pymongo.uri_parser import parse_uri as pymongo_parse_uri


def check_django_compatability():
Expand All @@ -30,50 +27,6 @@ def check_django_compatability():
)


def parse_uri(uri, *, db_name=None, options=None, test=None):
"""
Convert the given uri into a dictionary suitable for Django's DATABASES
setting.
"""
warnings.warn(
'parse_uri() is deprecated. Put the connection string in DATABASES["HOST"] instead.',
RemovedInDjango60Warning,
stacklevel=2,
)
uri = pymongo_parse_uri(uri)
host = None
port = None
if uri["fqdn"]:
# This is a SRV URI and the host is the fqdn.
host = f"mongodb+srv://{uri['fqdn']}"
else:
nodelist = uri.get("nodelist")
if len(nodelist) == 1:
host, port = nodelist[0]
elif len(nodelist) > 1:
host = ",".join([f"{host}:{port}" for host, port in nodelist])
db_name = db_name or uri["database"]
if not db_name:
raise ImproperlyConfigured("You must provide the db_name parameter.")
opts = uri.get("options")
if options:
opts.update(options)
settings_dict = {
"ENGINE": "django_mongodb_backend",
"NAME": db_name,
"HOST": host,
"PORT": port,
"USER": uri.get("username"),
"PASSWORD": uri.get("password"),
"OPTIONS": opts,
}
if "authSource" not in settings_dict["OPTIONS"] and uri["database"]:
settings_dict["OPTIONS"]["authSource"] = uri["database"]
if test:
settings_dict["TEST"] = test
return settings_dict


def prefix_validation_error(error, prefix, code, params):
"""
Prefix a validation error message while maintaining the existing
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@

intersphinx_mapping = {
"django": (
"https://docs.djangoproject.com/en/5.2/",
"https://docs.djangoproject.com/en/5.2/_objects/",
"https://docs.djangoproject.com/en/6.0/",
"https://docs.djangoproject.com/en/6.0/_objects/",
),
"mongodb": ("https://www.mongodb.com/docs/languages/python/django-mongodb/v5.2/", None),
"pymongo": ("https://www.mongodb.com/docs/languages/python/pymongo-driver/current/", None),
Expand Down
Loading