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.

Looking forward to reading what you all think of this. Also, this test was failing before the code updates, and is now passing. I can prepare a PR for this, but wanted to wait for some expert input first.

Version 1, edited 4 weeks ago by Jason Hall (previous) (next) (diff)

comment:13 by Jason Hall, 4 weeks ago

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