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)
Change History (12)
comment:1 by , 19 months ago
| Has patch: | set |
|---|---|
| Needs documentation: | set |
| Needs tests: | set |
| Patch needs improvement: | set |
comment:2 by , 19 months ago
| Cc: | added |
|---|
comment:3 by , 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 , 19 months ago
| Cc: | added |
|---|
comment:5 by , 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 , 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 , 19 months ago
| Attachment: | ticket_36350_test.patch added |
|---|
comment:7 by , 19 months ago
| Cc: | 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 , 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:10 by , 7 weeks ago
| Cc: | added |
|---|---|
| Owner: | set to |
comment:11 by , 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.
Draft PR (missing tests and release notes) is here: https://github.com/django/django/pull/19412