diff --git a/.github/workflows/coverage_comment.yml b/.github/workflows/coverage_comment.yml index 287cc558b50b..02b16efbb1bb 100644 --- a/.github/workflows/coverage_comment.yml +++ b/.github/workflows/coverage_comment.yml @@ -52,7 +52,7 @@ jobs: } const commentBody = ( '#### Uncovered lines in changed files\n\n```\n' + body + '```\n' + - + '**Note:** Missing lines are warnings only. Some database-specific lines may not be measured. [More information](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/#code-coverage-on-pull-requests)' + '**Note:** Missing lines are warnings only. Some database-specific lines may not be measured. [More information](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/#code-coverage-on-pull-requests)' ); const prNumber = parseInt(process.env.PR_NUMBER); diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py index 18ff204ae37b..949cbd4ff9f5 100644 --- a/django/db/backends/sqlite3/operations.py +++ b/django/db/backends/sqlite3/operations.py @@ -17,6 +17,14 @@ from .base import Database +UNSUPPORTED_DATETIME_AGGREGATES = ( + models.Sum, + models.Avg, + models.Variance, + models.StdDev, +) +DATETIME_FIELDS = (models.DateField, models.DateTimeField, models.TimeField) + class DatabaseOperations(BaseDatabaseOperations): cast_char_field_without_max_length = "text" @@ -30,9 +38,7 @@ class DatabaseOperations(BaseDatabaseOperations): jsonfield_datatype_values = frozenset(["null", "false", "true"]) def check_expression_support(self, expression): - bad_fields = (models.DateField, models.DateTimeField, models.TimeField) - bad_aggregates = (models.Sum, models.Avg, models.Variance, models.StdDev) - if isinstance(expression, bad_aggregates): + if isinstance(expression, UNSUPPORTED_DATETIME_AGGREGATES): for expr in expression.get_source_expressions(): try: output_field = expr.output_field @@ -41,11 +47,11 @@ def check_expression_support(self, expression): # to ignore. pass else: - if isinstance(output_field, bad_fields): + if isinstance(output_field, DATETIME_FIELDS): + klass = expression.__class__.__name__ raise NotSupportedError( - "You cannot use Sum, Avg, StdDev, and Variance " - "aggregations on date/time fields in sqlite3 " - "since date/time is saved as text." + f"SQLite does not support {klass} on date or time " + "fields, because they are stored as text." ) if ( isinstance(expression, models.Aggregate) diff --git a/tests/backends/sqlite/tests.py b/tests/backends/sqlite/tests.py index 34c0eca0ff41..f47e96be4e34 100644 --- a/tests/backends/sqlite/tests.py +++ b/tests/backends/sqlite/tests.py @@ -31,13 +31,17 @@ class Tests(TestCase): def test_aggregation(self): """Raise NotSupportedError when aggregating on date/time fields.""" for aggregate in (Sum, Avg, Variance, StdDev): - with self.assertRaises(NotSupportedError): + msg = ( + f"SQLite does not support {aggregate.__name__} on date or " + "time fields, because they are stored as text." + ) + with self.assertRaisesMessage(NotSupportedError, msg): Item.objects.aggregate(aggregate("time")) - with self.assertRaises(NotSupportedError): + with self.assertRaisesMessage(NotSupportedError, msg): Item.objects.aggregate(aggregate("date")) - with self.assertRaises(NotSupportedError): + with self.assertRaisesMessage(NotSupportedError, msg): Item.objects.aggregate(aggregate("last_modified")) - with self.assertRaises(NotSupportedError): + with self.assertRaisesMessage(NotSupportedError, msg): Item.objects.aggregate( **{ "complex": aggregate("last_modified") diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index e495012e079f..cb62d0fbd73f 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -1462,6 +1462,20 @@ def test_patterns_escape(self): Employee(firstname="Jean-Claude", lastname="Claude%"), Employee(firstname="Johnny", lastname="Joh\\n"), Employee(firstname="Johnny", lastname="_ohn"), + # These names have regex characters that must be escaped by + # backends (like MongoDB) that use regex matching rather than + # LIKE. + Employee(firstname="Johnny", lastname="^Joh"), + Employee(firstname="Johnny", lastname="Johnny$"), + Employee(firstname="Johnny", lastname="Joh."), + Employee(firstname="Johnny", lastname="[J]ohnny"), + Employee(firstname="Johnny", lastname="(J)ohnny"), + Employee(firstname="Johnny", lastname="J*ohnny"), + Employee(firstname="Johnny", lastname="J+ohnny"), + Employee(firstname="Johnny", lastname="J?ohnny"), + Employee(firstname="Johnny", lastname="J{1}ohnny"), + Employee(firstname="Johnny", lastname="J|ohnny"), + Employee(firstname="Johnny", lastname="J-ohnny"), ] ) claude = Employee.objects.create(firstname="Jean-Claude", lastname="Claude") diff --git a/tests/queries/test_qs_combinators.py b/tests/queries/test_qs_combinators.py index 7e1e01bd4be4..d1d6bfcbe3c5 100644 --- a/tests/queries/test_qs_combinators.py +++ b/tests/queries/test_qs_combinators.py @@ -24,6 +24,7 @@ ExtraInfo, Note, Number, + Report, ReservedName, Tag, ) @@ -157,6 +158,24 @@ def test_union_nested(self): ordered=False, ) + def test_union_with_different_models(self): + Celebrity.objects.create(name="Angel") + Celebrity.objects.create(name="Lionel") + Celebrity.objects.create(name="Emiliano") + Celebrity.objects.create(name="Demetrio") + Report.objects.create(name="Demetrio") + Report.objects.create(name="Daniel") + Report.objects.create(name="Javier") + expected = {"Angel", "Lionel", "Emiliano", "Demetrio", "Daniel", "Javier"} + qs1 = Celebrity.objects.values(alias=F("name")) + qs2 = Report.objects.values(alias_author=F("name")) + qs3 = qs1.union(qs2).values("name") + self.assertCountEqual((e["name"] for e in qs3), expected) + qs4 = qs1.union(qs2) + self.assertCountEqual((e["alias"] for e in qs4), expected) + qs5 = qs2.union(qs1) + self.assertCountEqual((e["alias_author"] for e in qs5), expected) + @skipUnlessDBFeature("supports_select_intersection") def test_intersection_with_empty_qs(self): qs1 = Number.objects.all() @@ -588,6 +607,15 @@ def test_count_union_with_select_related(self): qs = Author.objects.select_related("extra").order_by() self.assertEqual(qs.union(qs).count(), 1) + @skipUnlessDBFeature("supports_slicing_ordering_in_compound") + def test_count_union_with_select_related_in_values(self): + e1 = ExtraInfo.objects.create(value=1, info="e1") + a1 = Author.objects.create(name="a1", num=1, extra=e1) + qs = Author.objects.select_related("extra").values("pk", "name", "extra__value") + self.assertCountEqual( + qs.union(qs), [{"pk": a1.id, "name": "a1", "extra__value": 1}] + ) + @skipUnlessDBFeature("supports_select_difference") def test_count_difference(self): qs1 = Number.objects.filter(num__lt=10)