From 764af7a3d6c0b543dcf659a2c327f214da768fe4 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Thu, 3 Apr 2025 11:15:27 -0400 Subject: [PATCH] Fixed #36289 -- Fixed bulk_create() crash with nullable geometry fields on PostGIS. Swapped to an allow list instead of a deny list for field types to determine if the UNNEST optimization can be enabled to avoid further surprises with other types that would require further specialization to adapt. Regression in a16eedcf9c69d8a11d94cac1811018c5b996d491. Thanks Joshua Goodwin for the report and Sarah Boyce for the test. --- django/db/backends/postgresql/compiler.py | 15 +++++++++------ docs/releases/5.2.1.txt | 4 ++++ tests/gis_tests/geo3d/models.py | 2 +- tests/gis_tests/geo3d/tests.py | 4 ++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/django/db/backends/postgresql/compiler.py b/django/db/backends/postgresql/compiler.py index 3b972b5ba5..c4080ac037 100644 --- a/django/db/backends/postgresql/compiler.py +++ b/django/db/backends/postgresql/compiler.py @@ -27,8 +27,8 @@ class InsertUnnest(list): class SQLInsertCompiler(BaseSQLInsertCompiler): def assemble_as_sql(self, fields, value_rows): - # Specialize bulk-insertion of literal non-array values through - # UNNEST to reduce the time spent planning the query. + # Specialize bulk-insertion of literal values through UNNEST to + # reduce the time spent planning the query. if ( # The optimization is not worth doing if there is a single # row as it will result in the same number of placeholders. @@ -36,15 +36,18 @@ 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) + # Fields that don't use standard internal types might not be + # unnest'able (e.g. array and geometry types are known to be + # problematic). + or any( + field.get_internal_type() not in self.connection.data_types + for field in fields + ) # Compilable cannot be combined in an array of literal values. or any(any(hasattr(value, "as_sql") for value in row) for row in value_rows) ): return super().assemble_as_sql(fields, value_rows) db_types = [field.db_type(self.connection) for field in fields] - # Abort if any of the fields are arrays as UNNEST indiscriminately - # flatten them instead of reducing their nesting by one. - if any(db_type.endswith("]") for db_type in db_types): - return super().assemble_as_sql(fields, value_rows) return InsertUnnest(["(%%s)::%s[]" % db_type for db_type in db_types]), [ list(map(list, zip(*value_rows))) ] diff --git a/docs/releases/5.2.1.txt b/docs/releases/5.2.1.txt index b17e63ebd1..0f95eda848 100644 --- a/docs/releases/5.2.1.txt +++ b/docs/releases/5.2.1.txt @@ -15,3 +15,7 @@ Bugfixes * Fixed a regression in Django 5.2 that caused unnecessary queries when prefetching nullable foreign key relationships (:ticket:`36290`). + +* Fixed a regression in Django 5.2 that caused a crash of + ``QuerySet.bulk_create()`` with nullable geometry fields on PostGIS + (:ticket:`36289`). diff --git a/tests/gis_tests/geo3d/models.py b/tests/gis_tests/geo3d/models.py index 456be077f0..a09c599b02 100644 --- a/tests/gis_tests/geo3d/models.py +++ b/tests/gis_tests/geo3d/models.py @@ -58,7 +58,7 @@ class SimpleModel(models.Model): class Point2D(SimpleModel): - point = models.PointField() + point = models.PointField(null=True) class Point3D(SimpleModel): diff --git a/tests/gis_tests/geo3d/tests.py b/tests/gis_tests/geo3d/tests.py index b37deabb46..6a9376f2f8 100644 --- a/tests/gis_tests/geo3d/tests.py +++ b/tests/gis_tests/geo3d/tests.py @@ -206,6 +206,10 @@ class Geo3DTest(Geo3DLoadingHelper, TestCase): lm.save() self.assertEqual(3, MultiPoint3D.objects.count()) + def test_bulk_create_point_field(self): + objs = Point2D.objects.bulk_create([Point2D(), Point2D()]) + self.assertEqual(len(objs), 2) + @skipUnlessDBFeature("supports_3d_functions") def test_union(self): """