From 376909dc434395ae0156ac74e998d828e9551989 Mon Sep 17 00:00:00 2001 From: SnippyCodes Date: Wed, 17 Jun 2026 13:49:08 +0530 Subject: [PATCH 1/4] Fixed #37140 -- Documented NULL handling of __in lookups. Thanks Lilian Tran and Raffaella Suardini for reviews. --- docs/ref/models/querysets.txt | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 44cb9ff497e5..fe97a2ea79f5 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -265,6 +265,11 @@ Note the second example is more restrictive. If you need to execute more complex queries (for example, queries with ``OR`` statements), you can use :class:`Q objects ` (``*args``). +.. admonition:: ``NULL`` values and three-valued logic + + You should avoid passing iterables with ``NULL`` values to the :lookup:`in` + lookup. See :ref:`this note ` for details. + ``annotate()`` ~~~~~~~~~~~~~~ @@ -749,6 +754,11 @@ You can also refer to fields on related models with reverse relations through pronounced if you include multiple such fields in your ``values()`` query, in which case all possible combinations will be returned. +.. admonition:: ``NULL`` values and three-valued logic + + You should avoid passing iterables with ``NULL`` values to the :lookup:`in` + lookup. See :ref:`this note ` for details. + .. admonition:: Special values for ``JSONField`` on SQLite Due to the way the ``JSON_EXTRACT`` and ``JSON_TYPE`` SQL functions are @@ -3362,6 +3372,32 @@ extract two field values, where only one is expected:: inner_qs = Blog.objects.filter(name__contains="Ch").values("name", "id") entries = Entry.objects.filter(blog__name__in=inner_qs) +.. _null-and-three-valued-logic: + +.. admonition:: ``NULL`` values and three-valued logic + + If the right-hand side of an ``__in`` lookup contains ``NULL`` (or + ``None``), it can produce unexpected or empty query results due to SQL's + three-valued logic. + + For example, when using ``exclude(field__in=...)``, if the right-hand side + contains even a single ``NULL`` value, the SQL ``NOT IN`` comparison + evaluates to ``UNKNOWN`` for all rows, resulting in an empty queryset. + + To avoid this, filter out ``NULL`` values from the right-hand side:: + + # For a subquery + inner_qs = Blog.objects.filter(name__isnull=False).values("pk") + entries = Entry.objects.exclude(blog__in=inner_qs) + + # For a list of values + values = [1, 2, None] + non_null_values = [v for v in values if v is not None] + entries = Entry.objects.exclude(blog__in=non_null_values) + + Alternatively, rewrite the query to use a NULL-safe + :class:`~django.db.models.Exists` subquery. + .. _nested-queries-performance: .. admonition:: Performance considerations From 6ade6258480fba84a7e895b4b1e1716dfc954778 Mon Sep 17 00:00:00 2001 From: Mariano Baragiola Date: Tue, 15 Sep 2026 19:05:22 -0300 Subject: [PATCH 2/4] Fixed #37344 -- Enabled FETCH_PEERS batching for instances loaded by select_related. Thank you to Mykhailo Havelia for the report. --- django/db/models/query.py | 4 ++++ docs/releases/6.1.2.txt | 3 +++ tests/select_related/tests.py | 13 +++++++++++++ 3 files changed, 20 insertions(+) diff --git a/django/db/models/query.py b/django/db/models/query.py index bc881fced9e2..9891001caea6 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -3101,6 +3101,7 @@ def __init__(self, klass_info, select, db, fetch_mode): self.related_populators = get_related_populators( klass_info, select, self.db, fetch_mode ) + self.peers = [] self.local_setter = klass_info["local_setter"] self.remote_setter = klass_info["remote_setter"] @@ -3117,6 +3118,9 @@ def populate(self, row, from_obj): self.init_list, obj_data, ) + if self.fetch_mode.track_peers: + self.peers.append(weak_ref(obj)) + obj._state.peers = self.peers for rel_iter in self.related_populators: rel_iter.populate(row, obj) self.local_setter(from_obj, obj) diff --git a/docs/releases/6.1.2.txt b/docs/releases/6.1.2.txt index bc5f485c51b9..c79d4efcf47d 100644 --- a/docs/releases/6.1.2.txt +++ b/docs/releases/6.1.2.txt @@ -14,3 +14,6 @@ Bugfixes * Fixed a bug in Django 6.1 where the ``fields.W225`` system check incorrectly warned that ``null`` has no effect on ``GeneratedField`` (:ticket:`37348`). + +* Fixed a bug in Django 6.1 where ``FETCH_PEERS`` did not batch instances + loaded by ``select_related()``, causing unnecessary queries (#37344). diff --git a/tests/select_related/tests.py b/tests/select_related/tests.py index 47baaaa6642e..ec4ff5cf1934 100644 --- a/tests/select_related/tests.py +++ b/tests/select_related/tests.py @@ -281,6 +281,19 @@ def test_fetch_mode_copied_fetching_many(self): FETCH_PEERS, ) + def test_fetch_peers_for_select_related_objects(self): + species = ( + Species.objects.select_related("genus__family") + .fetch_mode(FETCH_PEERS) + .order_by("pk") + ) + with self.assertNumQueries(2): + orders = [obj.genus.family.order.name for obj in species] + self.assertEqual( + orders, + ["Diptera", "Primates", "Fabales", "Agaricales"], + ) + class SelectRelatedValidationTests(SimpleTestCase): """ From 581deb9402edc70f85b63c170c199a393f0f2183 Mon Sep 17 00:00:00 2001 From: Thibaut Decombe Date: Fri, 11 Sep 2026 17:16:57 +0200 Subject: [PATCH 3/4] Fixed #37339 -- Allowed ipaddress objects in GenericIPAddressField. get_prep_value() tested for ":" before coercing the value to a string, which crashed with a TypeError for non-string values such as ipaddress.IPv4Address and ipaddress.IPv6Address instances, even though to_python() accepts them. --- AUTHORS | 1 + django/db/models/fields/__init__.py | 9 +--- docs/releases/6.2.txt | 5 ++ .../test_genericipaddressfield.py | 50 +++++++++++++++++++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index 438e77092894..d07da65a41cb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1045,6 +1045,7 @@ answer newbie questions, and generally made Django that much better: Terry Huang thebjorn Thejaswi Puthraya + Thibaut Decombe Thijs van Dien Thom Wiggers Thomas Chaumeny diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 0ad614a70036..b9cb0466cde9 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -2366,14 +2366,7 @@ def get_db_prep_value(self, value, connection, prepared=False): def get_prep_value(self, value): value = super().get_prep_value(value) - if value is None: - return None - if value and ":" in value: - try: - return clean_ipv6_address(value, self.unpack_ipv4) - except exceptions.ValidationError: - pass - return str(value) + return self.to_python(value) def formfield(self, **kwargs): return super().formfield( diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt index 4690c110b71d..5b9e9c70ac79 100644 --- a/docs/releases/6.2.txt +++ b/docs/releases/6.2.txt @@ -312,6 +312,11 @@ Models * Unsaved instances with a composite primary key or a ``db_default`` primary key no longer compare equal to other instances. +* :class:`django.db.models.GenericIPAddressField` now validates IPv6 input + strictly when saving and querying. Invalid IPv6 addresses now raise + ``ValidationError`` instead of being silently accepted, and surrounding + whitespace is stripped. + Tests ----- diff --git a/tests/model_fields/test_genericipaddressfield.py b/tests/model_fields/test_genericipaddressfield.py index 76845238e54f..a2b7cc6610d8 100644 --- a/tests/model_fields/test_genericipaddressfield.py +++ b/tests/model_fields/test_genericipaddressfield.py @@ -1,3 +1,5 @@ +from ipaddress import IPv4Address, IPv6Address + from django.core.exceptions import ValidationError from django.db import models from django.test import TestCase @@ -40,3 +42,51 @@ def test_save_load(self): instance = GenericIPAddress.objects.create(ip="::1") loaded = GenericIPAddress.objects.get() self.assertEqual(loaded.ip, instance.ip) + + def test_save_load_ipaddress(self): + """ + Inserts, updates, and lookups accept ipaddress objects. + """ + tests = [ + (IPv4Address("192.0.2.1"), IPv4Address("192.0.2.2")), + (IPv6Address("2001:db8::1"), IPv6Address("2001:db8::2")), + ] + for value, updated in tests: + with self.subTest(value=value): + instance = GenericIPAddress.objects.create(ip=value) + instance.refresh_from_db() + self.assertEqual(instance.ip, str(value)) + + instance.ip = updated + instance.save() + instance.refresh_from_db() + self.assertEqual(instance.ip, str(updated)) + + GenericIPAddress.objects.filter(pk=instance.pk).update(ip=value) + self.assertSequenceEqual( + GenericIPAddress.objects.filter(ip=value), [instance] + ) + + def test_save_load_strips_whitespace(self): + instance = GenericIPAddress.objects.create(ip=" 192.0.2.1 ") + instance.refresh_from_db() + self.assertEqual(instance.ip, "192.0.2.1") + self.assertSequenceEqual( + GenericIPAddress.objects.filter(ip=" 192.0.2.1 "), [instance] + ) + + def test_save_invalid_ipv6(self): + with self.assertRaisesMessage(ValidationError, "Enter a valid IPv6 address."): + GenericIPAddress.objects.create(ip="not:valid") + + def test_get_prep_value(self): + field = models.GenericIPAddressField() + tests = [ + (IPv4Address("192.0.2.1"), "192.0.2.1"), + (IPv6Address("::ffff:192.0.2.1"), "::ffff:192.0.2.1"), + (" 192.0.2.1 ", "192.0.2.1"), + ("2001:0db8:0000::0001", "2001:db8::1"), + ] + for value, expected in tests: + with self.subTest(value=value): + self.assertEqual(field.get_prep_value(value), expected) From a3f0642f69277f01611d0d3e829fc3b85aded2ec Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 18 Sep 2026 11:33:55 -0400 Subject: [PATCH 4/4] Refs #36947 -- Added missing max_length in GeneratedField test for Oracle. This was missed because the model was defined inline without running system checks. Follow-up to 64edef37e7b419dd584307d84650a192fb47dc4c. --- tests/schema/tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index c5b94ccc281a..16fbe11078dc 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -1120,7 +1120,9 @@ def test_alter_generated_field_base_field_comment(self): class GenFieldModelComment(Model): name = CharField(max_length=100) name_lower = GeneratedField( - expression=Lower("name"), db_persist=True, output_field=CharField() + expression=Lower("name"), + db_persist=True, + output_field=CharField(max_length=100), ) class Meta: