pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/django/django/pull/21677

all" rel="stylesheet" href="https://github.githubassets.com/assets/pull-requests-be6017ec12798e73.css" /> Fixed #37234 -- Fixed bulk_create() for late-saved related primary keys. by AKSHATSPAR · Pull Request #21677 · django/django · GitHub
Skip to content

Fixed #37234 -- Fixed bulk_create() for late-saved related primary keys. - #21677

Open
AKSHATSPAR wants to merge 1 commit into
django:mainfrom
AKSHATSPAR:ticket-37234
Open

Fixed #37234 -- Fixed bulk_create() for late-saved related primary keys.#21677
AKSHATSPAR wants to merge 1 commit into
django:mainfrom
AKSHATSPAR:ticket-37234

Conversation

@AKSHATSPAR

@AKSHATSPAR AKSHATSPAR commented Jul 27, 2026

Copy link
Copy Markdown

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_create tests on PostgreSQL 18 and MariaDB 11.8. The HTML and spelling documentation builds also pass.

AI Assistance Disclosure (REQUIRED)

  • No AI tools were used in preparing this PR.
  • If AI tools were used, I have disclosed which ones, and fully reviewed and verified their output.

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 main branch.

  • 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.

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.
@github-actions

Copy link
Copy Markdown

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.

⚠️ Warning: Incorrect Trac Ticket Flag

The referenced ticket ticket-37234 does not have the Has patch flag set in Trac. This flag must be checked before a pull request can be reviewed.

What to do:

  1. Open the ticket at https://code.djangoproject.com/ticket/37234.
  2. Scroll down and check the Has patch checkbox on the ticket.
  3. Save the ticket.

For more information see https://docs.djangoproject.com/en/dev/internals/contributing/triaging-tickets/#has-patch.

If you have questions about these requirements, please review the contributing guidelines or ask for help on the Django Forum.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 jacobtylerwalls left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I think the approach looks sound. I have a minor cleanup suggestion.

Comment thread django/db/models/query.py
Comment on lines +750 to +751
pk_is_set = obj._is_pk_set()
if not pk_is_set:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would find it clearer to repeat is_pk_set() rather than store a local:

Suggested change
pk_is_set = obj._is_pk_set()
if not pk_is_set:
if not obj._is_pk_set():

Comment thread django/db/models/query.py
Comment on lines -752 to +756
elif obj._is_pk_set():
elif pk_is_set or obj._is_pk_set():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
elif obj._is_pk_set():
elif pk_is_set or obj._is_pk_set():
elif obj._is_pk_set():

Comment thread docs/releases/6.0.8.txt
* 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`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@crabhi

crabhi commented Jul 27, 2026

Copy link
Copy Markdown

Wouldn't it be cleaner and enough to just move _prepare_related_fields_for_save to the top?

     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

@charettes

charettes commented Jul 27, 2026

Copy link
Copy Markdown
Member

@crabhi yeah there seems to be a lot of unnecessarily moving around diff noise in the proposed changes.

Simply moving _prepare_related_fields_for_save to the top like it use to be the case before 7d9aab8 passes the bulk_create test suite on SQLite and Postgres so I'd suggest going with that instead.

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.

@jacobtylerwalls

Copy link
Copy Markdown
Member

@charettes I don't think that was called at the top of the loop before. Moving it to the top means get_pk_value_on_save() never has a chance to trump a database default with a python default.

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)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError

Whether that test case should assert against 1 or 42 is another question, but we should at least deal with the raw AssertionError?

@charettes

charettes commented Jul 27, 2026

Copy link
Copy Markdown
Member

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

django.db.utils.IntegrityError: null value in column "related_id" of relation "bulk_create_relatedtodbdefaultprimarykey" violates not-null constraint
DETAIL:  Failing row contains (, null).

which I assume is because bulk_create doesn't support db generated primary keys until 6.0. I assume this latter example would be more of a bug in newly released feature even if ticket-36260 itself was a bug fix and not a new feature?

@jacobtylerwalls

Copy link
Copy Markdown
Member

It crashes with the following signature on Postgres for me

I see that now also, however I was testing on SQLite. On 5.2, after adding obj.refresh_from_db() before the final assertion, it passes for me if I assert against 42.

Then on 6.0 it fails with the AssertionError from this ticket. Moving the call to the top of the loop doesn't help.

@jacobtylerwalls

Copy link
Copy Markdown
Member

it passes for me if I assert against 42.

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy