Opened 19 months ago

Last modified 37 hours ago

#36350 assigned New feature

Custom field's db_check() not taken into account when deciding whether to recreate constraint.

Reported by: Baptiste Mispelon Owned by: burhanuddin6
Component: Database layer (models, ORM) Version: 5.2
Severity: Normal Keywords:
Cc: Balazs Endresz, Mariusz Felisiak, Simon Charette, Lily Foote, Carlton Gibson, burhanuddin6 Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

This is a regression introduced in 9953c804a9375956a542da94665662d306dff48d.

I have a custom field that implements db_check() in order to add a custom db constraint:

class CharChoiceField(models.CharField):
    """
    A custom CharField that automatically creates a db constraint to guarante
    that the stored value respects the field's `choices`.
    """
    @property
    def non_db_attrs(self):
        # Remove `choices` from non_db_attrs so that migrations that only change
        # choices still trigger a db operation and drop/create the constraint.
        attrs = super().non_db_attrs
        return tuple({*attrs} - {"choices"})

    def db_check(self, connection):
        constraint = CheckConstraint(
            condition=Q(**{f"{self.name}__in": dict(self.choices)}),
            name="",  # doesn't matter, Django will reassign one anyway
        )
        with connection.schema_editor() as schema_editor:
            return constraint.get_check_sql(self.model, schema_editor)

This used to work well (in Django < 5.0): the constraint was added alongside the field when the initial migration was run, and any changes to the field's choices would trigger a new migration (that's standard Django behavior) which when executed would drop/create the constraint with the new value of choices.

However the commit I linked to at the beginning broke this: now any changes to choices still creates a migration, but executing the migration is now a no-op (which can be confirmed by checking with sqlmigrate).

I'm not sure if Field.db_check() can be considered public API (it's not documented), but I have a fix which is fairly simple (PR incoming) so I hope you'll consider it.

Attachments (1)

ticket_36350_test.patch​ (2.6 KB ) - added by Baptiste Mispelon 19 months ago.

Download all attachments as: .zip

Change History (12)

comment:1 by Baptiste Mispelon, 19 months ago

Has patch: set
Needs documentation: set
Needs tests: set
Patch needs improvement: set

Draft PR (missing tests and release notes) is here: ​https://github.com/django/django/pull/19412

comment:2 by Balazs Endresz, 19 months ago

Cc: Balazs Endresz added

comment:3 by Natalia Bidart, 19 months ago

Thanks Baptiste for the details and PR! Could you please provide a failing test on main that demonstrates the regression? While this won’t be a release blocker since 5.0 is EOL, the test would still be valuable.

comment:4 by Natalia Bidart, 19 months ago

Cc: Mariusz Felisiak Simon Charette added

in reply to:  description comment:5 by Mariusz Felisiak, 19 months ago

Replying to Baptiste Mispelon:

I'm not sure if Field.db_check() can be considered public API (it's not documented), but I have a fix which is fairly simple (PR incoming) so I hope you'll consider it.

Unfortunately, the default implementation of db_check() already incorporates the column name in generated SQL, so using it will reintroduced issue fixed by 9953c804a9375956a542da94665662d306dff48d.

comment:6 by Baptiste Mispelon, 19 months ago

Has patch: unset
Needs documentation: unset
Needs tests: unset
Patch needs improvement: unset

Ah right, I didn't realize that my fix was just doing the %-formatting twice and therefore just turning off the optimization that commit 9953c804a9375956a542da94665662d306dff48d was adding (which explains why CI was failing on MariaDB). Maybe the fix is not as simple as I first thought (it was too good to be True!).

I'm attaching a patch that adds a failing test (targeting main). I tried to keep it simple but unfortunately it's still fairly verbose (it's quite hard to test this type of code).

by Baptiste Mispelon, 19 months ago

Attachment: ticket_36350_test.patch​ added

comment:7 by Sarah Boyce, 19 months ago

Cc: Lily Foote Carlton Gibson added

It looks like there isn't a simple fix here and this is not officially supported

I have found a forum discussion around wanting to be able to define a constraint for a custom field: ​https://forum.djangoproject.com/t/dynamically-populate-constraints-in-field-contribute-to-class/22934/5
A number of folks seem to be in favor of the change (will add them in cc), so I think we could have a "New Feature" ticket to add support for this

comment:8 by Carlton Gibson, 19 months ago

Triage Stage: Unreviewed → Accepted
Type: Bug → New feature

I agree this looks like a new feature. I'll accept on that basis. (Happy if someone wants to reclassify.)

comment:9 by Pravin, 12 months ago

HI Can I work on this ?

comment:10 by burhanuddin6, 7 weeks ago

Cc: burhanuddin6 added
Owner: set to burhanuddin6

comment:11 by Tomasz Wójcik, 37 hours ago

I'd like to ask for this to be reconsidered as a bug (a regression in 5.0), not a "new feature". Like Baptiste Mispelon pointed out in the first sentence, it's a regression and to me the current behavior is unexpected. Before 9953 it did emit DROP CONSTRAINT and ADD CONSTRAINT.

It's also backend dependent. On MySQL and on SQLite the new check is applied (the migration). Only PostgreSQL, Oracle and MariaDB skip it, because the base _field_db_check() ignores the field's own check.

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