From 4ce4ed72a4ddc7d101df0fd31f1d0e449d8af501 Mon Sep 17 00:00:00 2001 From: JaeHyuck Sa Date: Thu, 15 Jan 2026 00:09:14 +0900 Subject: [PATCH 1/3] Fixed #36821 -- Treated empty strings as NULL for iexact lookups on Oracle. Signed-off-by: JaeHyuck Sa --- django/db/models/sql/query.py | 2 +- tests/null_queries/models.py | 2 +- tests/null_queries/tests.py | 14 +++++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 8fffce5dec91..e3e6100f24f0 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1444,7 +1444,7 @@ def build_lookup(self, lookups, lhs, rhs): # DEFAULT_DB_ALIAS isn't nice but it's the best that can be done here. # A similar thing is done in is_nullable(), too. if ( - lookup_name == "exact" + lookup_name in ("exact", "iexact") and lookup.rhs == "" and connections[DEFAULT_DB_ALIAS].features.interprets_empty_strings_as_nulls ): diff --git a/tests/null_queries/models.py b/tests/null_queries/models.py index 2ed7fef2cce2..6259ab55c694 100644 --- a/tests/null_queries/models.py +++ b/tests/null_queries/models.py @@ -7,7 +7,7 @@ class Poll(models.Model): class Choice(models.Model): poll = models.ForeignKey(Poll, models.CASCADE) - choice = models.CharField(max_length=200) + choice = models.CharField(max_length=200, null=True) # A set of models with an inner one pointing to two outer ones. diff --git a/tests/null_queries/tests.py b/tests/null_queries/tests.py index 828c68d9214d..9c061ae3235b 100644 --- a/tests/null_queries/tests.py +++ b/tests/null_queries/tests.py @@ -1,5 +1,5 @@ from django.core.exceptions import FieldError -from django.test import TestCase +from django.test import TestCase, skipUnlessDBFeature from .models import Choice, Inner, OuterA, OuterB, Poll @@ -44,6 +44,18 @@ def test_none_as_null(self): with self.assertRaisesMessage(ValueError, "Cannot use None as a query value"): Choice.objects.filter(id__gt=None) + @skipUnlessDBFeature("interprets_empty_strings_as_nulls") + def test_empty_string_is_null(self): + p = Poll.objects.create(question="?") + c1 = Choice.objects.create(poll=p, choice=None) + c2 = Choice.objects.create(poll=p, choice="") + cases = [{"choice__exact": ""}, {"choice__iexact": ""}] + for lookup in cases: + with self.subTest(lookup): + self.assertSequenceEqual( + Choice.objects.filter(**lookup).order_by("id"), [c1, c2] + ) + def test_unsaved(self): poll = Poll(question="How?") msg = ( From 924156072ecf61ef9cf50fa0d4d553a4c0d416c2 Mon Sep 17 00:00:00 2001 From: kundan223 Date: Tue, 13 Jan 2026 23:19:20 +0530 Subject: [PATCH 2/3] Fixed #36855, Refs #27222 -- Mentioned multiple invocations of Field.pre_save() in 6.0 release notes. Co-authored-by: Jacob Walls --- docs/releases/6.0.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index de874ad85586..ff810b03542d 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -481,6 +481,12 @@ Miscellaneous * The minimum supported version of ``asgiref`` is increased from 3.8.1 to 3.9.1. +.. currentmodule:: django.db.models + +* :meth:`Field.pre_save` may now be called more than once when saving model + instances, so custom implementations should be idempotent and free of side + effects. + .. _deprecated-features-6.0: Features deprecated in 6.0 From a876ada18b622926db5c7a9f3ec6ee20df223818 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 14 Jan 2026 14:40:53 -0500 Subject: [PATCH 3/3] Bumped linter versions in requirements files and tox.ini. Follow-up to 64ac4385c75b12a4065055b3d8d60bd5e0e61f04. --- docs/internals/contributing/writing-code/unit-tests.txt | 2 +- package.json | 2 +- tests/requirements/py3.txt | 2 +- tox.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index f304970a290a..657027a0334b 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -85,7 +85,7 @@ default environments can be seen as follows: docs isort>=7.0.0 lint-docs - zizmor>=1.16.3 + zizmor>=1.19.0 Testing other Python versions and database backends ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/package.json b/package.json index 86be20ce3a2f..a0ae4ad873f7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "npm": ">=1.3.0" }, "devDependencies": { - "eslint": "^9.36.0", + "eslint": "^9.39.1", "puppeteer": "^24.22.0", "grunt": "^1.6.1", "grunt-cli": "^1.5.0", diff --git a/tests/requirements/py3.txt b/tests/requirements/py3.txt index e07f97ef6923..5efe491430d7 100644 --- a/tests/requirements/py3.txt +++ b/tests/requirements/py3.txt @@ -2,7 +2,7 @@ aiosmtpd >= 1.4.5 asgiref >= 3.9.1 argon2-cffi >= 23.1.0 bcrypt >= 4.1.1 -black >= 25.9.0 +black >= 25.12.0 docutils >= 0.22 geoip2 >= 4.8.0 jinja2 >= 2.11.0 diff --git a/tox.ini b/tox.ini index 9888bff0b805..31f374abe352 100644 --- a/tox.ini +++ b/tox.ini @@ -103,7 +103,7 @@ commands = [testenv:zizmor] basepython = python3 usedevelop = false -deps = zizmor >= 1.16.3 +deps = zizmor >= 1.19.0 changedir = {toxinidir} commands = zizmor .