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/django_mongodb_backend/__init__.py b/django_mongodb_backend/__init__.py index d8529e9d..9ecbaaec 100644 --- a/django_mongodb_backend/__init__.py +++ b/django_mongodb_backend/__init__.py @@ -1,4 +1,4 @@ -__version__ = "5.1.0b1.dev0" +__version__ = "5.2.0a0" # 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..2bf7e0ee 100644 --- a/django_mongodb_backend/expressions.py +++ b/django_mongodb_backend/expressions.py @@ -10,6 +10,7 @@ Col, CombinedExpression, Exists, + ExpressionList, ExpressionWrapper, F, NegatedExpression, @@ -24,6 +25,8 @@ ) from django.db.models.sql import Query +from .query_utils import process_lhs + def case(self, compiler, connection): case_parts = [] @@ -83,6 +86,10 @@ def expression_wrapper(self, compiler, connection): return self.expression.as_mql(compiler, connection) +def expression_list(self, compiler, connection): + return process_lhs(self, compiler, connection) + + def f(self, compiler, connection): # noqa: ARG001 return f"${self.name}" @@ -150,7 +157,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 @@ -175,6 +186,12 @@ def when(self, compiler, connection): def value(self, compiler, connection): # noqa: ARG001 value = self.value + output_field = self._output_field_or_none + if output_field is not None: + if self.for_save: + value = output_field.get_db_prep_save(value, connection=connection) + else: + value = output_field.get_db_prep_value(value, connection=connection) if isinstance(value, int): # Wrap numbers in $literal to prevent ambiguity when Value appears in # $project. @@ -202,6 +219,7 @@ def register_expressions(): Col.as_mql = col CombinedExpression.as_mql = combined_expression Exists.as_mql = exists + ExpressionList.as_mql = expression_list 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..bb709b1f 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,19 @@ 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", + # JSONArray not implemented. + "db_functions.json.test_json_array.JSONArrayTests", + # Some usage of prefetch_related() raises "ColPairs is not supported." + "known_related_objects.tests.ExistingRelatedInstancesTests.test_one_to_one_multi_prefetch_related", + "known_related_objects.tests.ExistingRelatedInstancesTests.test_one_to_one_prefetch_related", + "prefetch_related.tests.DeprecationTests.test_prefetch_one_level_fallback", + "prefetch_related.tests.MultiDbTests.test_using_is_honored_fkey", + "prefetch_related.tests.MultiDbTests.test_using_is_honored_inheritance", + "prefetch_related.tests.NestedPrefetchTests.test_nested_prefetch_is_not_overwritten_by_related_object", + "prefetch_related.tests.NullableTest.test_prefetch_nullable", + "prefetch_related.tests.Ticket19607Tests.test_bug", + # {'$project': {'name': Decimal128('1')} is broken? (gives None) + "expressions.tests.ValueTests.test_output_field_decimalfield", } # $bitAnd, #bitOr, and $bitXor are new in MongoDB 6.3. _django_test_expected_failures_bitwise = { @@ -112,6 +124,7 @@ def django_test_expected_failures(self): # bson.errors.InvalidDocument: cannot encode object: #
-
+
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).