Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@ answer newbie questions, and generally made Django that much better:
Terry Huang <terryh.tp@gmail.com>
thebjorn <bp@datakortet.no>
Thejaswi Puthraya <thejaswi.puthraya@gmail.com>
Thibaut Decombe <thibaut.decombe@gmail.com>
Thijs van Dien <thijs@vandien.net>
Thom Wiggers
Thomas Chaumeny <t.chaumeny@gmail.com>
Expand Down
9 changes: 1 addition & 8 deletions django/db/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions django/db/models/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <django.db.models.Q>` (``*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 <null-and-three-valued-logic>` for details.

``annotate()``
~~~~~~~~~~~~~~

Expand Down Expand Up @@ -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 <null-and-three-valued-logic>` for details.

.. admonition:: Special values for ``JSONField`` on SQLite

Due to the way the ``JSON_EXTRACT`` and ``JSON_TYPE`` SQL functions are
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/6.1.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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).
5 changes: 5 additions & 0 deletions docs/releases/6.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----

Expand Down
50 changes: 50 additions & 0 deletions tests/model_fields/test_genericipaddressfield.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
4 changes: 3 additions & 1 deletion tests/schema/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions tests/select_related/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
Loading