From a7544eb3e0a6b411a5c540847a46ff0a0b715b97 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 28 Jun 2024 10:27:51 -0400 Subject: [PATCH 1/5] Added extra pattern lookup escaping cases in tests/expressions/tests.py. These new cases have regex characters that must be escaped by backends like MongoDB. --- tests/expressions/tests.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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") From 0dd9d5ee5c30e51754d93e926e24ff92654ecf5d Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 11 Feb 2026 00:26:15 +0000 Subject: [PATCH 2/5] Optimized SQLite `DatabaseOperations.check_expression_support()`. Avoided reconstructing the same tuples on every call by defining them as module-level constants. --- django/db/backends/sqlite3/operations.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py index 18ff204ae37b..33a39239ca3f 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,7 +47,7 @@ def check_expression_support(self, expression): # to ignore. pass else: - if isinstance(output_field, bad_fields): + if isinstance(output_field, DATETIME_FIELDS): raise NotSupportedError( "You cannot use Sum, Avg, StdDev, and Variance " "aggregations on date/time fields in sqlite3 " From 055d7a682f4923fc5a50d7c1a12fd7f52675f7e8 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 11 Feb 2026 23:25:47 +0000 Subject: [PATCH 3/5] Improved error message in SQLite `DatabaseOperations.check_expression_support()`. --- django/db/backends/sqlite3/operations.py | 6 +++--- tests/backends/sqlite/tests.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py index 33a39239ca3f..949cbd4ff9f5 100644 --- a/django/db/backends/sqlite3/operations.py +++ b/django/db/backends/sqlite3/operations.py @@ -48,10 +48,10 @@ def check_expression_support(self, expression): pass else: 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") From 8d2a925a0e0bc6d1161ab41ad35f8422c549cada Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Sat, 7 Sep 2024 12:57:59 -0300 Subject: [PATCH 4/5] Added tests for QuerySet.union() across different models and value aliases. These tests were developed during work on MongoDB and capture edge cases discovered there. --- tests/queries/test_qs_combinators.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) 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) From fee2cb2d6dfefad614b27e659f5648c793006ef8 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 12 Feb 2026 15:31:15 -0500 Subject: [PATCH 5/5] Refs #36620 -- Removed stray + from coverage comment workflow step. --- .github/workflows/coverage_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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);