Proposal: Check constraints at the model field level

95 views
Skip to first unread message

David Sanders

unread,
Apr 5, 2023, 2:18:51 PM4/5/23
to django-d...@googlegroups.com
Hi folks,

We've had check constraints for a while now and they're awesome.

I'd like to propose an alternative way to declare check constraints: at the field level. This sounds like it's duplicating the feature but there are some advantages that make this distinct from declaring at the model-level from the meta:
  • Colocality: the check rules are close to the field they're concerned with
  • Reusability: Allows for bundling with custom field types
  • Good for checks concerned only with the field its declared on, for multiple fields recommended to continue to use Meta. (Kind of analogous to unique=True vs UniqueConstraint)
For example:

class Product(Model):
    price = DecimalField(..., check=Q(price__gte=0))
    ...
    other fields
    ...

    class Meta:
        constraints = [
            ... declare constraints here that are concerned with multiple fields...
        ]

For more complex fields you can then bundle the check for reusability:

class PriceField(DecimalField):
    def contribute_to_class(self, ...):
        super().contribute_to_class(...)
        self.check = Q(**{f'{self.name}__gte': 0})

Some other points:
  • To be consistent with model-level check constraints they'd also need to participate in validate_constraints().
  • (Small)PositiveIntegerField already has its own implementation of a check constraint, enforcing values >- 0 via the private db_check() method. I think this is an example of how bundling checks can be useful.
  • I won't go into implementation alternatives but making use of this existing db_check() method is one possibility. How participation in validation would work would still need to be decided upon.
  • See this Stupid Django Trick for some experimentation with this idea.

Cheers,
David

Adam Johnson

unread,
Apr 5, 2023, 5:04:39 PM4/5/23
to django-d...@googlegroups.com
I agree this feature would be useful, at least to allow bundling check constraints with custom field classes. As you point out the PositiveIntegerField classes do this within Django, and doubtless many custom fields have used the db_check() method.

The only thing I'm not a fan of in your proposal is repeating the field name within the check expression, like "price" in:

price = DecimalField(..., check=Q(price__gte=0))

This seems repetitive at least, and would increase error refactoring. Also Django field classes typically don't “know” their name until the Model meta class logic runs, and contribute_to_class() is called. Requiring the name within check() would mean it would need to accept any name and validate it later.

Perhaps we could support only a special name instead, like “self” or the shorter “f”?

price = DecimalField(..., check=Q(f__gte=0))

--
You received this message because you are subscribed to the Google Groups "Django developers (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-develop...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-developers/CADyZw-6Odv0dpo-En3Jm2NqPhBtK7ebaGKBi4OROe1n%2BvHEE-A%40mail.gmail.com.

Mariusz Felisiak

unread,
Apr 6, 2023, 12:11:39 AM4/6/23
to Django developers (Contributions to Django itself)
Hi,

This proposal is not really nice from a maintenance point of view as we will end with the same complicated situation we currently have with uniqueness checks or indexes i.e. many ways to define the same:

- Field.unique/index
- Meta.unique_together/index_together
- Meta.constraints/indexes

It's especially error-prone in migrations and different database behavior on fields already covered by the same constraints/indexes. I'm pretty sure that we've introduced Meta.contraints/indexes to avoid this happening in the future, and we are rather leaning to leave only Meta.constraints/indexes and remove other options in the future. Not creating a new one.

Initial -1 from me.

Best,
Mariusz

Adam Johnson

unread,
Apr 6, 2023, 1:47:48 AM4/6/23
to django-d...@googlegroups.com
Mariusz, I agree with the burden, but it should be noted that SQL has both CHECK on the field and table level, and CheckConstraint only defines table-level constraints. This is not true for unique constraints or indices.

Also, what do you think of a way for custom field classes to add constraints, at least? db_check() is somewhat limiting given it must return raw SQL, plus it's undocumented.

--
You received this message because you are subscribed to the Google Groups "Django developers (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-develop...@googlegroups.com.

charettes

unread,
Apr 6, 2023, 2:55:24 PM4/6/23
to Django developers (Contributions to Django itself)
Small clarification here.

> it should be noted that SQL has both CHECK on the field and table level,

From my understanding CREATE TABLE / ADD COLUMN checks on the field level are really just syntactic sugar for checks at the table level like just like `REFERENCES` usage is syntactic sugar for foreign key constraints.

> This is not true for unique constraints or indices.

Unique constraints can be defined using the UNIQUE keyword just like CHECK is used to define a check per field, it's really just a different way of defining that a unique constraint on a field must be created.

The question of whether or not we want to provide some Django syntactic sugar in the form of `Field.check` and the benefits it might provide to third-parties (and even ourselves for dog fooding PositiveIntegerField) remains but I think that it's important to point out that there's no distinction between field and table level constraints at the database level AFAIK (for Postgres and SQLite at least).

Simon

David Sanders

unread,
Apr 16, 2023, 1:47:11 PM4/16/23
to django-d...@googlegroups.com
Hi Adam, Mariusz & Simon,


> The only thing I'm not a fan of in your proposal is repeating the field name within the check expression, like "price" in
> ... 8< ...
Perhaps we could support only a special name instead, like “self” or the shorter “f”?

I was thinking the same thing +1  I wanted to throw up the proposal to elicit ideas on this because I wasn't sure whether reserving a field name like this might've been the way to go 🤔


but it should be noted that SQL has both CHECK on the field and table level
and from Simon:

> CREATE TABLE / ADD COLUMN checks on the field level are really just syntactic sugar for checks at the table level

Simon is correct here in that checks at column & table level are both declarations for the same underlying mechanism (at least from what I've seen with Postgres & MySQL).


> This proposal is not really nice from a maintenance point of view

Felix if there was no maintenance headache would it sound like a good idea though? What if there was some way to make it work nicely? I was wondering what ways that could be achieved: 
 - if you use db_check() then there's no migration state conflict; only potential conflict in the database which would be resolved with unique constraint naming. 
 - as an alternative could it somehow then be syntactic sugar to add to meta.constraints? this would then have the benefit of automatically being added to validation. The catch is migrations would need to take the fact that fields could add to meta into consideration because from what I've seen it keeps track of the "original" user-defined meta options.


Reply all
Reply to author
Forward
0 new messages