From ac9bdcabe10fb7ac0c7e9ebcd879f5e34bee776f Mon Sep 17 00:00:00 2001 From: Chris Wesseling Date: Thu, 20 Nov 2025 15:01:14 +0100 Subject: [5.2.x] Fixed #36748 -- Filtered non-standard placeholders from UNNEST queries. Backport of 5834643f43a767fe19f2c6d10217b204e7584ec8 from main. --- django/db/backends/postgresql/compiler.py | 5 +++++ docs/releases/5.2.9.txt | 3 +++ tests/postgres_tests/fields.py | 5 +++++ .../migrations/0002_create_test_models.py | 23 ++++++++++++++++++++++ tests/postgres_tests/models.py | 5 +++++ tests/postgres_tests/test_bulk_update.py | 8 ++++++++ 6 files changed, 49 insertions(+) diff --git a/django/db/backends/postgresql/compiler.py b/django/db/backends/postgresql/compiler.py index 344773fb7a..dc2db148ae 100644 --- a/django/db/backends/postgresql/compiler.py +++ b/django/db/backends/postgresql/compiler.py @@ -36,6 +36,11 @@ class SQLInsertCompiler(BaseSQLInsertCompiler): # Lack of fields denote the usage of the DEFAULT keyword # for the insertion of empty rows. or any(field is None for field in fields) + # Field.get_placeholder takes value as an argument, so the + # resulting placeholder might be dependent on the value. + # in UNNEST requires a single placeholder to "fit all values" in + # the array. + or any(hasattr(field, "get_placeholder") for field in fields) # Fields that don't use standard internal types might not be # unnest'able (e.g. array and geometry types are known to be # problematic). diff --git a/docs/releases/5.2.9.txt b/docs/releases/5.2.9.txt index 0d726de640..588c278be5 100644 --- a/docs/releases/5.2.9.txt +++ b/docs/releases/5.2.9.txt @@ -13,3 +13,6 @@ Bugfixes ``django.utils.feedgenerator.Stylesheet.__str__()`` did not escape the ``url``, ``mimetype``, and ``media`` attributes, potentially leading to invalid XML markup (:ticket:`36733`). + +* Fixed a bug in Django 5.2 on PostgreSQL where ``bulk_create()`` did not apply + a field's custom query placeholders (:ticket:`36748`). diff --git a/tests/postgres_tests/fields.py b/tests/postgres_tests/fields.py index c5dddf197f..d099effdd5 100644 --- a/tests/postgres_tests/fields.py +++ b/tests/postgres_tests/fields.py @@ -57,3 +57,8 @@ except ImportError: class EnumField(models.CharField): def get_prep_value(self, value): return value.value if isinstance(value, enum.Enum) else value + + +class OffByOneField(models.IntegerField): + def get_placeholder(self, value, compiler, connection): + return "(%s + 1)" diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py index 31705ae21a..1aa900f07a 100644 --- a/tests/postgres_tests/migrations/0002_create_test_models.py +++ b/tests/postgres_tests/migrations/0002_create_test_models.py @@ -9,6 +9,7 @@ from ..fields import ( EnumField, HStoreField, IntegerRangeField, + OffByOneField, SearchVectorField, ) from ..models import TagField @@ -570,4 +571,26 @@ class Migration(migrations.Migration): "required_db_vendor": "postgresql", }, ), + migrations.CreateModel( + name="OffByOneModel", + fields=[ + ( + "id", + models.BigAutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "one_off", + OffByOneField(), + ), + ], + options={ + "required_db_vendor": "postgresql", + }, + bases=(models.Model,), + ), ] diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py index 1563f6a35d..7c606a83a9 100644 --- a/tests/postgres_tests/models.py +++ b/tests/postgres_tests/models.py @@ -9,6 +9,7 @@ from .fields import ( EnumField, HStoreField, IntegerRangeField, + OffByOneField, SearchVectorField, ) @@ -207,3 +208,7 @@ class HotelReservation(PostgreSQLModel): end = models.DateTimeField() cancelled = models.BooleanField(default=False) requirements = models.JSONField(blank=True, null=True) + + +class OffByOneModel(PostgreSQLModel): + one_off = OffByOneField() diff --git a/tests/postgres_tests/test_bulk_update.py b/tests/postgres_tests/test_bulk_update.py index 85dfcedd09..f3e8ac8a40 100644 --- a/tests/postgres_tests/test_bulk_update.py +++ b/tests/postgres_tests/test_bulk_update.py @@ -6,6 +6,7 @@ from .models import ( IntegerArrayModel, NestedIntegerArrayModel, NullableIntegerArrayModel, + OffByOneModel, OtherTypesArrayModel, RangesModel, ) @@ -44,3 +45,10 @@ class BulkSaveTests(PostgreSQLTestCase): self.assertSequenceEqual( Model.objects.filter(**{field: new}), instances ) + + def test_bulk_create(self): + OffByOneModel.objects.bulk_create(OffByOneModel(one_off=0) for _ in range(20)) + + self.assertSequenceEqual( + [m.one_off for m in OffByOneModel.objects.all()], 20 * [1] + ) -- cgit v1.3