Opened 5 weeks ago

Last modified 4 weeks ago

#36550 assigned Bug

AssertionError raised when bulk creation model with primary key set via pre_save (e.g. auto_now_add) on databases with can_return_columns_from_insert

Reported by: Will-Ruddick Owned by: Jason Hall
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords:
Cc: Simon Charette Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Will-Ruddick)

I have a model that looks something like this

class CompositeModel(models.Model):
    pk = models.CompositePrimaryKey("other_model", "the_time", "created_at")

    other_model = models.ForeignKey("test.OtherModel", on_delete=models.CASCADE)
    the_time = models.DateTimeField()
    created_at = models.DateTimeField(auto_now_add=True)

And when I try and bulk create it in the code I get the following exception.

    CompositeModel.objects.bulk_create(new_items)
  File "django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "django/db/models/query.py", line 836, in bulk_create
    assert len(returned_columns) == len(objs_without_pk)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError

Where returned_columns is an empty list and objs_without_pk is a list of the objects I'm trying to create.

If I use ignore_conflicts=True then everything works fine and the objects are created properly in the DB. I think this is a bug, but it could be user error.

This is happening on version 5.2.5 with a postgres DB

Attachments (1)

ticket_36550_bulk_create_composite_auto_now_add.patch (5.9 KB ) - added by Jason Hall 4 weeks ago.

Download all attachments as: .zip

Change History (14)

comment:1 by Will-Ruddick, 5 weeks ago

Description: modified (diff)

comment:2 by Will-Ruddick, 5 weeks ago

Type: UncategorizedBug

comment:3 by Sarah Boyce, 5 weeks ago

Severity: NormalRelease blocker
Summary: AssertionError raised when bulk creation model with composite primary keyAssertionError raised when bulk creation model with composite primary key with auto_now_add field
Triage Stage: UnreviewedAccepted
  • tests/composite_pk/test_create.py

    diff --git a/tests/composite_pk/test_create.py b/tests/composite_pk/test_create.py
    index 38ad9690fb..60ad1cc671 100644
    a b  
    11from django.db import IntegrityError
    22from django.test import TestCase, skipUnlessDBFeature
    33
    4 from .models import Post, Tenant, User
     4from .models import Post, Tenant, User, TimeStamped
    55
    66
    77class CompositePKCreateTests(TestCase):
    class CompositePKCreateTests(TestCase):  
    7878        self.assertEqual(obj_3.pk, (obj_3.tenant_id, obj_3.id))
    7979        self.assertEqual(obj_3.email, "user8214@example.com")
    8080
     81    def test_bulk_create_auto_now_add_primary_key(self):
     82        objs = [TimeStamped(id=1), TimeStamped(id=2), TimeStamped(id=3)]
     83        self.assertEqual(len(TimeStamped.objects.bulk_create(objs)), 3)
     84
    8185    @skipUnlessDBFeature(
    8286        "supports_update_conflicts",
    8387        "supports_update_conflicts_with_target",

Thank you, replicated the error with the above test

comment:4 by Jason Hall, 5 weeks ago

Attached 36550.patch. The existing regression test for bulk_create() with a composite PK containing an auto_now_add field now passes with this change. This seems to fix the issue, but I’m not sure if this is the preferred approach.

comment:5 by Sarah Boyce, 5 weeks ago

Resolution: wontfix
Severity: Release blockerNormal
Status: newclosed
Summary: AssertionError raised when bulk creation model with composite primary key with auto_now_add fieldAssertionError raised when bulk creation model with primary key set via pre_save (e.g. auto_now_add)

Been looking into this more today

This actually doesn't have anything to do with CompositePrimaryKey and we can have the same error doing TimeStamped.objects.bulk_create([TimeStamped()]) when TimeStamped is:

class TimeStamped(models.Model):
    created = models.DateTimeField(auto_now_add=True, primary_key=True)

I believe the route of this is documented (https://docs.djangoproject.com/en/5.2/ref/models/querysets/#bulk-create):

The model’s save() method will not be called, and the pre_save and post_save signals will not be sent.

And the value for created is defined on the field's pre_save method

To support bulk_create to support primary_keys that are set of pre_save would be a new feature. We _might_ be able to detect this better and have a nicer error message

But in short, I do think this is roughly expected behavior with some documentation and so will close

comment:6 by Jacob Walls, 5 weeks ago

Wanted to mention that there's a difference between the model pre_save signal and the Field.pre_save() method. Looks like bulk_create() has always used the pre_save() value, so that part is not a new feature. It might be worth evaluating this a little further, as from the docs I don't see an admonition about this not working (happy to be corrected on this).

comment:7 by Sarah Boyce, 5 weeks ago

Resolution: wontfix
Status: closednew
Summary: AssertionError raised when bulk creation model with primary key set via pre_save (e.g. auto_now_add)AssertionError raised when bulk creation model with primary key set via pre_save (e.g. auto_now_add) on databases with can_return_columns_from_insert
Version: 5.2dev

Looked into it more and found this is only an issue for databases with can_return_columns_from_insert set to True (so is allowed on MySQL for example)

comment:8 by Jacob Walls, 5 weeks ago

Thanks. Jason, given Sarah's investigation, it looks like inserting logic to set the value is not necessary, since this already works on some databases.

comment:9 by Jason Hall, 5 weeks ago

Then would we want to modify the test logic and add @skipUnlessDBFeature("can_return_columns_from_insert")?

As far as a fix I was thinking we could ensure Field.pre_save() is applied before partitioning objects into with_pk and without_pk, adjust the classification to check completeness of all PK fields (including composite keys), and only request returning_fields when at least one object is missing a pk (skip requesting PKs when they're all known locally). Would that be acceptable?

comment:10 by Sarah Boyce, 4 weeks ago

Cc: Simon Charette added

Then would we want to modify the test logic and add @skipUnlessDBFeature("can_return_columns_from_insert")?

Adding the skipUnlessDBFeature shouldn't be necessary as we expect it to work for all backends, regardless of whether can_return_columns_from_insert is True or False
I don't know what the fix should be but I recently reviewed #36490 and Simon had a few ideas around improving bulk_create, so I would be interested to hear his thoughts here

comment:11 by Jason Hall, 4 weeks ago

Owner: set to Jason Hall
Status: newassigned

comment:12 by Jason Hall, 4 weeks ago

Here is what I came up with this morning...

This patch updates QuerySet._prepare_for_bulk_create() to correctly classify objects when using composite primary keys and auto_now_add fields. Instead of relying only on _is_pk_set(), it checks that all PK fields are populated (non-None and not DatabaseDefault) before treating an object as "with PK".

Also, I modifed the original regression test from previous comments. This is now backend agnostic and it ensures that objects bulk-created with a composite PK and auto_now_add values still have their primary key set correctly both in memory and in the database.

Last edited 4 weeks ago by Jason Hall (previous) (diff)

comment:13 by Jason Hall, 4 weeks ago

Has patch: set
Note: See TracTickets for help on using tickets.
Back to Top