Opened 12 hours ago

Last modified 5 hours ago

#36073 new Bug

Custom field without get_db_prep_value() errors on updates if part of composite primary key

Reported by: Jacob Walls Owned by:
Component: Database layer (models, ORM) Version: dev
Severity: Release blocker Keywords:
Cc: Csirmaz Bendegúz Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

A model with a custom field only implementing get_db_prep_save() but not get_db_prep_value() fails on save() when used in a composite primary key, but only for updates. Inserts work fine.

This fell out of #36062 where the test model I adjusted happened to have a custom field with these characteristics.

I didn't get as far as verifying this to be relevant, but I noticed while stepping through the code that SQLInsertCompiler has logic to add `pk` to the list of fields, so I wonder if it's not being prepared the same way in SQLUpdateCompiler?

  • tests/custom_pk/fields.py

    diff --git a/tests/custom_pk/fields.py b/tests/custom_pk/fields.py
    index 2d70c6b6dc..135b315ffe 100644
    a b class MyWrapperField(models.CharField):  
    5959        return value
    6060
    6161
     62class MyWrapperFieldWithoutGetDbPrepValue(models.CharField):
     63    def to_python(self, value):
     64        if not value:
     65            return
     66        if not isinstance(value, MyWrapper):
     67            value = MyWrapper(value)
     68        return value
     69
     70    def from_db_value(self, value, expression, connection):
     71        if not value:
     72            return
     73        return MyWrapper(value)
     74
     75    def get_db_prep_save(self, value, connection):
     76        if not value:
     77            return
     78        if isinstance(value, MyWrapper):
     79            return str(value)
     80        return value
     81
     82
    6283class MyAutoField(models.BigAutoField):
    6384    def from_db_value(self, value, expression, connection):
    6485        if value is None:
  • tests/custom_pk/models.py

    diff --git a/tests/custom_pk/models.py b/tests/custom_pk/models.py
    index 000ea7a29d..4ea5190d5f 100644
    a b this behavior by explicitly adding ``primary_key=True`` to a field.  
    77
    88from django.db import models
    99
    10 from .fields import MyAutoField, MyWrapperField
     10from .fields import MyAutoField, MyWrapperField, MyWrapperFieldWithoutGetDbPrepValue
    1111
    1212
    1313class Employee(models.Model):
    class Foo(models.Model):  
    4040
    4141class CustomAutoFieldModel(models.Model):
    4242    id = MyAutoField(primary_key=True)
     43
     44
     45class CompositePKWithoutGetDbPrepValue(models.Model):
     46    pk = models.CompositePrimaryKey("id", "tenant")
     47    id = MyWrapperFieldWithoutGetDbPrepValue()
     48    tenant = models.SmallIntegerField()
  • tests/custom_pk/tests.py

    diff --git a/tests/custom_pk/tests.py b/tests/custom_pk/tests.py
    index 5f865b680a..fa371942e6 100644
    a b from django.db import IntegrityError, transaction  
    22from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
    33
    44from .fields import MyWrapper
    5 from .models import Bar, Business, CustomAutoFieldModel, Employee, Foo
     5from .models import Bar, Business, CompositePKWithoutGetDbPrepValue, CustomAutoFieldModel, Employee, Foo
    66
    77
    88class BasicCustomPKTests(TestCase):
    class CustomPKTests(TestCase):  
    164164        Business.objects.create(name="Bears")
    165165        Business.objects.create(pk="Tears")
    166166
     167    def test_custom_composite_pk_save_force_update(self):
     168        """A custom primary key field that implements get_db_prep_save()
     169        rather than get_db_prep_value() still saves."""
     170        obj = CompositePKWithoutGetDbPrepValue()
     171        obj.id = MyWrapper("1")
     172        obj.tenant = 1
     173        obj.save()  # passes with force_insert=True
     174
    167175    def test_unicode_pk(self):
    168176        # Primary key may be Unicode string.
    169177        Business.objects.create(name="jaźń")

ERROR: test_custom_composite_pk_save_force_update (custom_pk.tests.CustomPKTests.test_custom_composite_pk_save_force_update)
A custom primary key field that implements get_db_prep_save()
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/.../django/django/db/backends/utils.py", line 105, in _execute
    return self.cursor.execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/.../django/django/db/backends/sqlite3/base.py", line 360, in execute
    return super().execute(query, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
sqlite3.ProgrammingError: Error binding parameter 3: type 'MyWrapper' is not supported

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/.../django/tests/custom_pk/tests.py", line 173, in test_custom_composite_pk_save_force_update
    bar.save()  # passes with force_insert=True
    ^^^^^^^^^^
...

  File "/Users/.../django/django/db/backends/sqlite3/base.py", line 360, in execute
    return super().execute(query, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
django.db.utils.ProgrammingError: Error binding parameter 3: type 'MyWrapper' is not supported

----------------------------------------------------------------------
Ran 16 tests in 0.011s

FAILED (errors=1, skipped=1)

Change History (1)

comment:1 by Sarah Boyce, 5 hours ago

Cc: Csirmaz Bendegúz added
Triage Stage: UnreviewedAccepted
Note: See TracTickets for help on using tickets.
Back to Top