Validation¶
UniqueConstraintTrigger and CheckConstraintTrigger implement the
validate() method that Django's built-in constraints use, so they
participate in Model.full_clean() and ModelForm validation the same
way.
Calling full_clean()¶
Django picks up Meta.triggers entries with a validate() method and
runs them during full_clean():
line = OrderLine(product=product, quantity=9999)
line.full_clean() # ValidationError — CheckConstraintTrigger caught it
No extra wiring is required — the triggers are discovered through the normal constraint-validation path.
UniqueConstraintTrigger¶
- Plain fields and
__-separated FK chains are validated by an ORM query built from the values on the instance. nulls_distinct=None/Trueshort-circuits when any field value is NULL (the default PostgreSQL semantics).- Expression-based triggers (
UniqueConstraintTrigger(Lower("email"), ...)) are not validated in Python. The database trigger is the sole enforcement path, matching the behaviour of Django'sUniqueConstraintwith expressions.
CheckConstraintTrigger¶
- Same-table conditions are validated by
Q.check()against the instance's field values, the same way Django'sCheckConstraintdoes it. - Conditions that reference related columns via
F("rel__field")skip the Python path — the ORM would have to issue a query for every hop, and the trigger will catch the violation atsave()time anyway.
Custom error codes and messages¶
Both triggers accept violation_error_code and violation_error_message
so the ValidationError raised by full_clean() carries the same code
and message that the rest of the application expects:
CheckConstraintTrigger(
condition=Q(quantity__gt=0),
violation_error_code="invalid_quantity",
violation_error_message="Quantity must be positive.",
name="orderline_qty_positive",
)
The database-level IntegrityError is unaffected — it still comes back
with the PostgreSQL SQLSTATE (23505 for unique, 23514 for check).
GeneratedFieldTrigger¶
GeneratedFieldTrigger does not implement validate(). It computes a
value rather than asserting one, so there is nothing for full_clean()
to raise. Whatever the trigger sets is what ends up in the column.