Opened 5 weeks ago

Closed 5 weeks ago

#36073 closed Bug (invalid)

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 (3)

comment:1 by Sarah Boyce, 5 weeks ago

Cc: Csirmaz Bendegúz added
Triage Stage: UnreviewedAccepted

comment:2 by Csirmaz Bendegúz, 5 weeks ago

I don't think this is related to composite primary keys, it errors if it's a regular primary key as well

comment:3 by Sarah Boyce, 5 weeks ago

Resolution: invalid
Status: newclosed

Yes, it's the same for regular primary keys which makes me think whether this is not valid
It's possible that the test code from serializers that inspired this is not fully defined.
Jacob if you can add more information we can look at this again

(for reference, same thing for regular primary keys)

  • tests/custom_pk/fields.py

    diff --git a/tests/custom_pk/fields.py b/tests/custom_pk/fields.py
    index 2d70c6b6dc..3908b426c9 100644
    a b class MyWrapper:  
    2020        return self.value == other
    2121
    2222
    23 class MyWrapperField(models.CharField):
     23class MyWrapperFieldWithoutGetDbPrepValue(models.CharField):
    2424    def __init__(self, *args, **kwargs):
    2525        kwargs["max_length"] = 10
    2626        super().__init__(*args, **kwargs)
    class MyWrapperField(models.CharField):  
    5151            return str(value)
    5252        return value
    5353
     54class MyWrapperField(models.CharField):
    5455    def get_db_prep_value(self, value, connection, prepared=False):
    5556        if not value:
    5657            return
  • tests/custom_pk/models.py

    diff --git a/tests/custom_pk/models.py b/tests/custom_pk/models.py
    index 000ea7a29d..75b1031e10 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 PKWithoutGetDbPrepValue(models.Model):
     46    id = MyWrapperFieldWithoutGetDbPrepValue(primary_key=True)
  • tests/custom_pk/tests.py

    diff --git a/tests/custom_pk/tests.py b/tests/custom_pk/tests.py
    index 5f865b680a..3bfd32c29c 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, CustomAutoFieldModel, Employee, Foo, PKWithoutGetDbPrepValue
    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        obj = PKWithoutGetDbPrepValue()
     169        obj.id = MyWrapper("1")
     170        obj.save()  # passes with force_insert=True
     171
    167172    def test_unicode_pk(self):
    168173        # Primary key may be Unicode string.
    169174        Business.objects.create(name="jaźń")
Note: See TracTickets for help on using tickets.
Back to Top