Skip to content
Draft
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
. 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.1.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.1.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.1.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.1.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.1.0b0.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
57 changes: 40 additions & 17 deletions django_mongodb_backend/aggregates.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
from django.db.models.aggregates import Aggregate, Count, StdDev, Variance
from django.core.exceptions import EmptyResultSet, FullResultSet
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

Expand All @@ -9,15 +17,20 @@


def aggregate(self, compiler, connection, operator=None, resolve_inner_expression=False):
if self.filter:
node = self.copy()
node.filter = None
source_expressions = node.get_source_expressions()
condition = When(self.filter, then=source_expressions[0])
node.set_source_expressions([Case(condition), *source_expressions[1:]])
if self.filter is not None:
# Generate a CASE statement for this aggregate.
try:
lhs_mql = self.filter.as_mql(compiler, connection, as_expr=True)
except NotSupportedError:
source_expressions = self.get_source_expressions()
condition = Case(When(self.filter.condition, then=source_expressions[0]))
lhs_mql = condition.as_mql(compiler, connection, as_expr=True)
except FullResultSet:
lhs_mql = source_expressions[0].as_mql(compiler, connection, as_expr=True)
except EmptyResultSet:
lhs_mql = Value(None).as_mql(compiler, connection, as_expr=True)
else:
node = self
lhs_mql = process_lhs(node, compiler, connection, as_expr=True)
lhs_mql = process_lhs(self, 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,14 +45,19 @@ def count(self, compiler, connection, resolve_inner_expression=False):
"""
if not self.distinct or resolve_inner_expression:
if self.filter:
node = self.copy()
node.filter = None
source_expressions = node.get_source_expressions()
condition = When(
self.filter, then=Case(When(IsNull(source_expressions[0], False), then=Value(1)))
)
node.set_source_expressions([Case(condition), *source_expressions[1:]])
inner_expression = process_lhs(node, compiler, connection, as_expr=True)
try:
inner_expression = self.filter.as_mql(compiler, connection, as_expr=True)
except NotSupportedError:
source_expressions = self.get_source_expressions()
condition = When(
self.filter.condition,
then=Case(When(IsNull(source_expressions[0], False), then=Value(1))),
)
inner_expression = Case(condition).as_mql(compiler, connection, as_expr=True)
except FullResultSet:
inner_expression = {"$sum": 1}
except EmptyResultSet:
inner_expression = {"$sum": 0}
else:
lhs_mql = process_lhs(self, compiler, connection, as_expr=True)
null_cond = {"$in": [{"$type": lhs_mql}, ["missing", "null"]]}
Expand All @@ -65,8 +83,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
98 changes: 49 additions & 49 deletions django_mongodb_backend/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,61 +661,61 @@ def get_combinator_queries(self):
combinator_pipeline.append({"$unset": "_id"})
return combinator_pipeline

def _get_pushable_conditions(self):
"""
Return a dict mapping each alias to a WhereNode holding its pushable
condition.
"""

def collect_pushable(expr, negated=False):
if expr is None or isinstance(expr, NothingNode):
@classmethod
def _collect_pushable(cls, expr, negated=False):
if expr is None or isinstance(expr, NothingNode):
return {}
if isinstance(expr, WhereNode):
# Apply De Morgan: track negation so connectors are flipped
# when needed.
negated ^= expr.negated
pushable_expressions = [
cls._collect_pushable(sub_expr, negated=negated)
for sub_expr in expr.children
if sub_expr is not None
]
operator = expr.connector
if operator == XOR:
return {}
if isinstance(expr, WhereNode):
# Apply De Morgan: track negation so connectors are flipped
# when needed.
negated ^= expr.negated
pushable_expressions = [
collect_pushable(sub_expr, negated=negated)
for sub_expr in expr.children
if sub_expr is not None
]
operator = expr.connector
if operator == XOR:
return {}
if negated:
operator = OR if operator == AND else AND
alias_children = defaultdict(list)
for pe in pushable_expressions:
for alias, expressions in pe.items():
alias_children[alias].append(expressions)
# Build per-alias pushable condition nodes.
if operator == AND:
return {
alias: WhereNode(children=children, negated=False, connector=operator)
for alias, children in alias_children.items()
}
# Only aliases shared across all branches are pushable for OR.
shared_alias = (
set.intersection(*(set(pe) for pe in pushable_expressions))
if pushable_expressions
else set()
)
if negated:
operator = OR if operator == AND else AND
alias_children = defaultdict(list)
for pe in pushable_expressions:
for alias, expressions in pe.items():
alias_children[alias].append(expressions)
# Build per-alias pushable condition nodes.
if operator == AND:
return {
alias: WhereNode(children=children, negated=False, connector=operator)
for alias, children in alias_children.items()
if alias in shared_alias
}
# A leaf is pushable only when comparing a field to a constant or
# simple value.
if isinstance(expr.lhs, Col) and (
is_constant_value(expr.rhs) or getattr(expr.rhs, "is_simple_column", False)
):
alias = expr.lhs.alias
expr = WhereNode(children=[expr], negated=negated)
return {alias: expr}
return {}
# Only aliases shared across all branches are pushable for OR.
shared_alias = (
set.intersection(*(set(pe) for pe in pushable_expressions))
if pushable_expressions
else set()
)
return {
alias: WhereNode(children=children, negated=False, connector=operator)
for alias, children in alias_children.items()
if alias in shared_alias
}
# A leaf is pushable only when comparing a field to a constant or
# simple value.
if isinstance(expr.lhs, Col) and (
is_constant_value(expr.rhs) or getattr(expr.rhs, "is_simple_column", False)
):
alias = expr.lhs.alias
expr = WhereNode(children=[expr], negated=negated)
return {alias: expr}
return {}

return collect_pushable(self.get_where())
def _get_pushable_conditions(self):
"""
Return a dict mapping each alias to a WhereNode holding its pushable
condition.
"""
return self._collect_pushable(self.get_where())

def get_lookup_pipeline(self):
result = []
Expand Down
23 changes: 23 additions & 0 deletions django_mongodb_backend/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class DatabaseFeatures(GISFeatures, BaseDatabaseFeatures):
supports_json_field_contains = False
# BSON Date type doesn't support microsecond precision.
supports_microsecond_precision = False
supports_on_delete_db_cascade = False
supports_on_delete_db_default = False
supports_on_delete_db_null = False
supports_paramstyle_pyformat = False
supports_select_difference = False
supports_select_intersection = False
Expand Down Expand Up @@ -99,6 +102,18 @@ 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",
# 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",
# bulk_create() population of _order not implemented.
# 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 +154,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 +217,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 @@ -467,6 +484,8 @@ def django_test_expected_failures(self):
# There is no way to distinguish between a JSON "null" (represented
# by Value(None, JSONField())) and a SQL null (queried using the
# isnull lookup). Both of these queries return both nulls.
"model_fields.test_jsonfield.JSONExactNoneDeprecationTests",
"model_fields.test_jsonfield.JSONNullTests",
"model_fields.test_jsonfield.TestSaveLoad.test_json_null_different_from_sql_null",
# Some queries with Q objects, e.g. Q(value__foo="bar"), don't work
# properly, particularly with QuerySet.exclude().
Expand Down Expand Up @@ -563,6 +582,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 +600,9 @@ 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",
},
}

@cached_property
Expand Down
2 changes: 0 additions & 2 deletions django_mongodb_backend/fields/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from .array import ArrayField
from .auto import ObjectIdAutoField
from .duration import register_duration_field
from .embedded_model import EmbeddedModelField
from .embedded_model_array import EmbeddedModelArrayField
from .json import register_json_field
Expand All @@ -21,5 +20,4 @@


def register_fields():
register_duration_field()
register_json_field()
Loading