diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index f383a71a..f49a0e9a 100644 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -9,7 +9,7 @@ python -m pip install -U pip pip install -e . # Install django and test dependencies -git clone --branch mongodb-5.1.x https://github.com/mongodb-forks/django django_repo +git clone --branch mongodb-5.2.x https://github.com/mongodb-forks/django django_repo pushd django_repo/tests/ pip install -e .. pip install -r requirements/py3.txt diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index cc791d55..d471f3f2 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'mongodb-forks/django' - ref: 'mongodb-5.1.x' + ref: 'mongodb-5.2.x' path: 'django_repo' persist-credentials: false - name: Install system packages for Django's Python test dependencies diff --git a/README.md b/README.md index 15902c86..fc97565f 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ 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.1.x: +Django. For example, to get the latest compatible release for Django 5.2.x: ```bash -pip install --pre django-mongodb-backend==5.1.* +pip install --pre django-mongodb-backend==5.2.* ``` (Until the package is out of beta, you must use pip's `--pre` option.) @@ -36,11 +36,11 @@ You can check what version of Django you're using with: django-admin --version ``` -The snippet below specifies `5.1.x.zip` at the end of -the template url to get the template for any Django version matching 5.1: +The snippet below specifies `5.2.x.zip` at the end of +the template url to get the template for any Django version matching 5.2: ```bash -django-admin startproject example --template https://github.com/mongodb-labs/django-mongodb-project/archive/refs/heads/5.1.x.zip +django-admin startproject example --template https://github.com/mongodb-labs/django-mongodb-project/archive/refs/heads/5.2.x.zip ``` diff --git a/django_mongodb_backend/__init__.py b/django_mongodb_backend/__init__.py index bc2eb46f..65b819b2 100644 --- a/django_mongodb_backend/__init__.py +++ b/django_mongodb_backend/__init__.py @@ -1,4 +1,4 @@ -__version__ = "5.1.0b3.dev0" +__version__ = "5.2.0b0.dev0" # Check Django compatibility before other imports which may fail if the # wrong version of Django is installed. diff --git a/django_mongodb_backend/compiler.py b/django_mongodb_backend/compiler.py index cf666619..66154902 100644 --- a/django_mongodb_backend/compiler.py +++ b/django_mongodb_backend/compiler.py @@ -17,7 +17,6 @@ from django.utils.functional import cached_property from pymongo import ASCENDING, DESCENDING -from .base import Cursor from .query import MongoQuery, wrap_database_errors @@ -403,12 +402,6 @@ def columns(self): columns = ( self.get_default_columns(select_mask) if self.query.default_cols else self.query.select ) - # Populate QuerySet.select_related() data. - related_columns = [] - if self.query.select_related: - self.get_related_selections(related_columns, select_mask) - if related_columns: - related_columns, _ = zip(*related_columns, strict=True) annotation_idx = 1 @@ -427,11 +420,28 @@ def project_field(column): annotation_idx += 1 return target, column - return ( - tuple(map(project_field, columns)) - + tuple(self.annotations.items()) - + tuple(map(project_field, related_columns)) - ) + selected = [] + if self.query.selected is None: + selected = [ + *(project_field(col) for col in columns), + *self.annotations.items(), + ] + else: + for expression in self.query.selected.values(): + # Reference to an annotation. + if isinstance(expression, str): + alias, expression = expression, self.annotations[expression] + # Reference to a column. + elif isinstance(expression, int): + alias, expression = project_field(columns[expression]) + selected.append((alias, expression)) + # Populate QuerySet.select_related() data. + related_columns = [] + if self.query.select_related: + self.get_related_selections(related_columns, select_mask) + if related_columns: + related_columns, _ = zip(*related_columns, strict=True) + return tuple(selected) + tuple(map(project_field, related_columns)) @cached_property def base_table(self): @@ -478,7 +488,11 @@ def get_combinator_queries(self): # If the columns list is limited, then all combined queries # must have the same columns list. Set the selects defined on # the query on all combined queries, if not already set. - if not compiler_.query.values_select and self.query.values_select: + selected = self.query.selected + if selected is not None and compiler_.query.selected is None: + compiler_.query = compiler_.query.clone() + compiler_.query.set_values(selected) + elif not compiler_.query.values_select and self.query.values_select: compiler_.query = compiler_.query.clone() compiler_.query.set_values( ( @@ -690,15 +704,12 @@ def collection_name(self): class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): def execute_sql(self, result_type=MULTI): - cursor = Cursor() try: query = self.build_query() except EmptyResultSet: - rowcount = 0 + return 0 else: - rowcount = query.delete() - cursor.rowcount = rowcount - return cursor + return query.delete() def check_query(self): super().check_query() diff --git a/django_mongodb_backend/expressions.py b/django_mongodb_backend/expressions.py index 8e8c1815..b8fbebf5 100644 --- a/django_mongodb_backend/expressions.py +++ b/django_mongodb_backend/expressions.py @@ -8,8 +8,10 @@ from django.db.models.expressions import ( Case, Col, + ColPairs, CombinedExpression, Exists, + ExpressionList, ExpressionWrapper, F, NegatedExpression, @@ -24,6 +26,8 @@ ) from django.db.models.sql import Query +from .query_utils import process_lhs + def case(self, compiler, connection): case_parts = [] @@ -71,6 +75,13 @@ def col(self, compiler, connection): # noqa: ARG001 return f"${prefix}{self.target.column}" +def col_pairs(self, compiler, connection): + cols = self.get_cols() + if len(cols) > 1: + raise NotSupportedError("ColPairs is not supported.") + return cols[0].as_mql(compiler, connection) + + def combined_expression(self, compiler, connection): expressions = [ self.lhs.as_mql(compiler, connection), @@ -150,7 +161,11 @@ def ref(self, compiler, connection): # noqa: ARG001 if isinstance(self.source, Col) and self.source.alias != compiler.collection_name else "" ) - return f"${prefix}{self.refs}" + if hasattr(self, "ordinal"): + refs, _ = compiler.columns[self.ordinal - 1] + else: + refs = self.refs + return f"${prefix}{refs}" def star(self, compiler, connection): # noqa: ARG001 @@ -200,8 +215,10 @@ def value(self, compiler, connection): # noqa: ARG001 def register_expressions(): Case.as_mql = case Col.as_mql = col + ColPairs.as_mql = col_pairs CombinedExpression.as_mql = combined_expression Exists.as_mql = exists + ExpressionList.as_mql = process_lhs ExpressionWrapper.as_mql = expression_wrapper F.as_mql = f NegatedExpression.as_mql = negated_expression diff --git a/django_mongodb_backend/features.py b/django_mongodb_backend/features.py index a286a2cb..0df1615b 100644 --- a/django_mongodb_backend/features.py +++ b/django_mongodb_backend/features.py @@ -12,6 +12,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): greatest_least_ignores_nulls = True has_json_object_function = False has_native_json_field = True + rounds_to_even = True supports_boolean_expr_in_select_clause = True supports_collation_on_charfield = False supports_column_check_constraints = False @@ -56,8 +57,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): # Pattern lookups that use regexMatch don't work on JSONField: # Unsupported conversion from array to string in $convert "model_fields.test_jsonfield.TestQuerying.test_icontains", - # MongoDB gives ROUND(365, -1)=360 instead of 370 like other databases. - "db_functions.math.test_round.RoundTests.test_integer_with_negative_precision", # Truncating in another timezone doesn't work becauase MongoDB converts # the result back to UTC. "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_func_with_timezone", @@ -88,6 +87,9 @@ class DatabaseFeatures(BaseDatabaseFeatures): # of $setIsSubset must be arrays. Second argument is of type: null" # https://jira.mongodb.org/browse/SERVER-99186 "model_fields_.test_arrayfield.QueryingTests.test_contained_by_subquery", + # Value.as_mql() doesn't call output_field.get_db_prep_save(): + # https://github.com/mongodb/django-mongodb-backend/issues/282 + "model_fields.test_jsonfield.TestSaveLoad.test_bulk_update_custom_get_prep_value", } # $bitAnd, #bitOr, and $bitXor are new in MongoDB 6.3. _django_test_expected_failures_bitwise = { @@ -112,6 +114,7 @@ def django_test_expected_failures(self): # bson.errors.InvalidDocument: cannot encode object: # `. + +Regarding new features in Django 5.2, +:class:`~django.db.models.CompositePrimaryKey` isn't supported. diff --git a/docs/source/releases/index.rst b/docs/source/releases/index.rst index b2649eed..bc14e221 100644 --- a/docs/source/releases/index.rst +++ b/docs/source/releases/index.rst @@ -5,11 +5,12 @@ Release notes The release notes will tell you what's new in each version and will also describe any backwards-incompatible changes. -Below are release notes through Django MongoDB backend 5.1.x. Newer versions of +Below are release notes through Django MongoDB backend 5.2.x. Newer versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 5.2.x 5.1.x 5.0.x diff --git a/docs/source/topics/known-issues.rst b/docs/source/topics/known-issues.rst index 2bf66807..4b34fecf 100644 --- a/docs/source/topics/known-issues.rst +++ b/docs/source/topics/known-issues.rst @@ -23,6 +23,7 @@ Model fields - :class:`~django.db.models.AutoField` (including :class:`~django.db.models.BigAutoField` and :class:`~django.db.models.SmallAutoField`) + - :class:`~django.db.models.CompositePrimaryKey` - :class:`~django.db.models.GeneratedField` Querying diff --git a/pyproject.toml b/pyproject.toml index 241874fd..7b412e1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ keywords = [ classifiers = [ "Development Status :: 4 - Beta", "Framework :: Django", - "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", diff --git a/requirements.txt b/requirements.txt index 04dc753d..5f081ffd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -django>=5.1,<5.2 +django>=5.2,<6.0 pymongo>=4.6,<5.0 diff --git a/tests/indexes_/test_condition.py b/tests/indexes_/test_condition.py index 5077dd0b..3c49f1ec 100644 --- a/tests/indexes_/test_condition.py +++ b/tests/indexes_/test_condition.py @@ -78,7 +78,7 @@ def test_composite_index(self): { "$and": [ {"number": {"$gte": 3}}, - {"$or": [{"body": {"$gt": "test1"}}, {"body": {"$in": ["A", "B"]}}]}, + {"$or": [{"body": {"$gt": "test1"}}, {"body": {"$in": ("A", "B")}}]}, ] }, ) diff --git a/tests/model_forms_/test_embedded_model.py b/tests/model_forms_/test_embedded_model.py index 4447b59f..d5eb71ea 100644 --- a/tests/model_forms_/test_embedded_model.py +++ b/tests/model_forms_/test_embedded_model.py @@ -182,9 +182,9 @@ def test_some_missing_data(self): required id="id_title">
-
+
Publisher: -
    +
    • Enter all required values.
    @@ -252,9 +252,9 @@ def test_invalid_field_data(self): maxlength="50" required id="id_title">
    -
    +
    Publisher: -
      +
      • Ensure this value has at most 2 characters (it has 8).