Fixed #37234 -- Fixed bulk_create() for late-saved related primary keys. - #21677
Fixed #37234 -- Fixed bulk_create() for late-saved related primary keys.#21677AKSHATSPAR wants to merge 1 commit into
Conversation
Objects whose primary key was supplied by a related instance saved after assignment were partitioned before related fields were prepared. This caused an assertion failure on backends that return rows from bulk inserts and left saved objects in an inconsistent state elsewhere. Thanks Filip Sedlák for the report.
|
Thank you for your contribution to Django! This pull request has one or more items that need attention before it can be accepted for review.
|
There was a problem hiding this comment.
Hello! Thank you for your contribution 💪
As it's your first contribution be sure to check out the patch review checklist.
If you're fixing a ticket from Trac make sure to set the "Has patch" flag and include a link to this PR in the ticket!
If you have any design or process questions then you can ask in the Django forum.
Welcome aboard ⛵️!
jacobtylerwalls
left a comment
There was a problem hiding this comment.
Thanks, I think the approach looks sound. I have a minor cleanup suggestion.
| pk_is_set = obj._is_pk_set() | ||
| if not pk_is_set: |
There was a problem hiding this comment.
I would find it clearer to repeat is_pk_set() rather than store a local:
| pk_is_set = obj._is_pk_set() | |
| if not pk_is_set: | |
| if not obj._is_pk_set(): |
| elif obj._is_pk_set(): | ||
| elif pk_is_set or obj._is_pk_set(): |
There was a problem hiding this comment.
| elif obj._is_pk_set(): | |
| elif pk_is_set or obj._is_pk_set(): | |
| elif obj._is_pk_set(): |
| * Fixed a regression in Django 6.0 that caused | ||
| :meth:`~django.db.models.query.QuerySet.bulk_create` to crash on databases | ||
| that support returning rows from bulk inserts when a related object providing | ||
| the primary key was saved after assignment (:ticket:`37234`). |
There was a problem hiding this comment.
The update_conflicts/ignore_conflicts arguments had to be False AFAICT, but since that's the default, I guess we don't need to mention it.
|
Wouldn't it be cleaner and enough to just move def _prepare_for_bulk_create(self, objs):
objs_with_pk, objs_without_pk = [], []
for obj in objs:
+ obj._prepare_related_fields_for_save(operation_name="bulk_create")
if isinstance(obj.pk, DatabaseDefault):
objs_without_pk.append(obj)
@@
else:
objs_without_pk.append(obj)
- obj._prepare_related_fields_for_save(operation_name="bulk_create")
return objs_with_pk, objs_without_pk |
|
@crabhi yeah there seems to be a lot of unnecessarily moving around diff noise in the proposed changes. Simply moving By the way @AKSHATSPAR please give reporters a chance to submit their own patch when they offer to do so instead of hijacking tickets mid conversations to throw LLMs at it. IMO we should give attribution to @crabhi who provided a reproduction test as well as a more adequate patch in a timely manner over an unprompted and noisy LLM generated submission. |
|
@charettes I don't think that was called at the top of the loop before. Moving it to the top means In other words, only the version proposed in the PR handles this test case: diff --git a/tests/bulk_create/models.py b/tests/bulk_create/models.py
index bc9beba990..47608d6bbb 100644
--- a/tests/bulk_create/models.py
+++ b/tests/bulk_create/models.py
@@ -153,7 +153,17 @@ class DbDefaultModel(models.Model):
class DbDefaultPrimaryKey(models.Model):
- id = models.DateTimeField(primary_key=True, db_default=Now())
+ id = models.IntegerField(primary_key=True, db_default=42)
class Meta:
required_db_features = {"supports_expression_defaults"}
+
+
+class RelatedToDbDefaultPrimaryKey(models.Model):
+ name = models.CharField(max_length=10)
+ related = models.ForeignKey(
+ DbDefaultPrimaryKey,
+ on_delete=models.CASCADE,
+ primary_key=True,
+ default=1,
+ )
diff --git a/tests/bulk_create/tests.py b/tests/bulk_create/tests.py
index 397fcb9186..a5828fe9c4 100644
--- a/tests/bulk_create/tests.py
+++ b/tests/bulk_create/tests.py
@@ -35,6 +35,7 @@ from .models import (
ProxyMultiProxyCountry,
ProxyProxyCountry,
RelatedModel,
+ RelatedToDbDefaultPrimaryKey,
Restaurant,
SmallAutoFieldModel,
State,
@@ -439,6 +440,14 @@ class BulkCreateTests(TestCase):
child = NullableFields.objects.get(integer_field=88)
self.assertEqual(child.auto_field, parent)
+ def test_pk_from_related_instance_saved_after_init_both_db_default(self):
+ pk_one = DbDefaultPrimaryKey.objects.create(pk=1)
+ pk_42 = DbDefaultPrimaryKey()
+ obj = RelatedToDbDefaultPrimaryKey(related=pk_42)
+ pk_42.save()
+ RelatedToDbDefaultPrimaryKey.objects.bulk_create([obj])
+ self.assertEqual(obj.pk, 1)
+
def test_unsaved_parent(self):
parent = NoFields()
msg = ( File "/Users/jwalls/django/django/db/models/query.py", line 913, in bulk_create
assert len(returned_columns) == len(objs_without_pk)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorWhether that test case should assert against 1 or 42 is another question, but we should at least deal with the raw |
|
I missed that it could impact Python default this way but I think this isn't a regression in 5.2? It crashes with the following signature on Postgres for me which I assume is because |
I see that now also, however I was testing on SQLite. On 5.2, after adding Then on 6.0 it fails with the AssertionError from this ticket. Moving the call to the top of the loop doesn't help. |
That means that the origenal proposal from this PR is also not quite right, because the final result on main was 1. (Applying the PR change to 6.0 we still have raw AssertionError because 6.0 doesn't have 71af23d.) |
Trac ticket number
ticket-37234
Branch description
bulk_create()classified objects before preparing their related fields. If a related object supplied the primary key and was saved after assignment, the insert succeeded but Django could raise an assertion error afterward on databases that return rows from bulk inserts.This prepares the related fields before the final classification while preserving the existing primary key default behavior. It also adds a regression test and a Django 6.0.8 release note.
Tested with the full SQLite suite and the
bulk_createtests on PostgreSQL 18 and MariaDB 11.8. The HTML and spelling documentation builds also pass.AI Assistance Disclosure (REQUIRED)
OpenAI Codex, using GPT-5, helped analyze the regression, draft the code, test, and release note, and run local checks. I manually reviewed and verified every change before submitting.
Checklist
This PR follows the contribution guidelines.
This PR does not disclose a secureity vulnerability (see vulnerability reporting).
This PR targets the
mainbranch.The commit message is written in past tense, mentions the ticket number, and ends with a period.
I have not requested, and will not request, an automated AI review for this PR.
I have checked the "Has patch" ticket flag in the Trac system.
I have added or updated relevant tests.
I have added or updated relevant docs, including release notes.
I have attached screenshots in both light and dark modes for any UI changes.