django / django/new-features

Dynamically create Field constraints

Open
#152 8 comments 7 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
188
Forks
7
PR merge metrics
No merged PRs in 30d

Description

### Code of Conduct

- [x] I agree to follow Django's Code of Conduct

### Feature Description

This proposes a new API for defining field-specific constraints dynamically.

This builds on top of previous attempts and ideas on this front. There had been some confusion initially about whether there is a bug around migrations involving `contribute_to_class()`, which likely stalled progress on the topic.

My hope is that the suggestion below can spark a restart of the discussion around a new API.

References:

* The ticket [#36350](https://code.djangoproject.com/ticket/36350), which was previously accepted on a "New feature" basis. Since this proposal diverges from the discussion there, I would like to get a sense of the community sentiment before jumping on a new PR.
* The [forum discussion on the topic](https://forum.djangoproject.com/t/dynamically-populate-constraints-in-field-contribute-to-class/22934/10).

### Problem

Users often want to reuse similar fields across models that come with similar set of constraints. Achieving this today requires duplicating the constraints in each model's `Meta.constraints`.
An especially common case is a field with `choices` accompanied by a `CheckConstraint` that enforces the choice values at the database level.

### Request or proposal

proposal

### Additional Details

Previous discussions (see [ticket](https://code.djangoproject.com/ticket/36350) and [forum](https://forum.djangoproject.com/t/dynamically-populate-constraints-in-field-contribute-to-class/22934/10)) touched on potential bugs in `contribute_to_class()` and `db_check()` when generating migrations for dynamic constraints. Since those methods aren't documented at all, encouraging their use for this purpose doesn't seem ideal. A new API dedicated to field-level constraints would be a better fit I think.

### Implementation Suggestions

The proposal is a new `get_constraints()` method on `Field`, with a signature similar to [`contribute_to_class()`](https://github.com/django/django/blob/d24160ab4855f6971aa1f437720adca6330048a2/django/contrib/gis/db/models/fields.py#L292), that returns an iterable of `UniqueConstraint` or `CheckConstraint` instances.

```python
class PositiveIntegerField(models.IntegerField):
def get_constraints(self, cls, name):
return [
models.CheckConstraint(
condition=models.Q(**{f"{name}__gt": 0}),
name=f"{cls._meta.model_name}_positive_{name}",
)
]
```

On `makemigrations` Django adds the field constraints to the involved models.

Separately but in the same spirit: The common case *Field with a CheckConstraint based on the choices* could be made even easier, via a simple `check_constraint` boolean kwarg.

```diff
class Task(models.Model):
class Priority(models.IntegerChoices):
LOW = 1, "Low"
MEDIUM = 2, "Medium"
HIGH = 3, "High"

priority = models.IntegerField(
choices=Priority.choices,
+ check_constraint=True,
default=Priority.MEDIUM,
)
```

which would internally generate a CheckConstraint:

```python
models.CheckConstraint(
name="core_task_priority_choices",
condition=models.Q(priority__in=[1, 2, 3]),
)
```

### Considerations / Limitations

* The `get_constraints()` API would allow returning constraints that involve unrelated field names (i.e. other fields on the model). That probably wouldn't make sense and would be confusing, since such constraints logically belong in `Meta.constraints`. Adding some validation could help, but it's tricky to cover every case, for example `CheckConstraint` can accept [`RawSQL` conditions](https://github.com/django/django/blob/dc467fdc3b5744cec71fab876c23a14013e2510b/django/db/models/constraints.py#L179), which we can't meaningfully inspect. TBD what can be done here.

* It probably makes sense to raise an error when `check_constraint=True` is passed to a field without `choices` defined.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.