Skip to content

django-pgconstraints

Declarative PostgreSQL constraint triggers for Django, with foreign-key traversal that the built-in constraints can't express.

Django 6.0 ships UniqueConstraint, CheckConstraint, and GeneratedField, and they compile down to plain PostgreSQL constraints — so they can only reference columns on the same table. That rules out a handful of common patterns:

  • A CheckConstraint whose right-hand side lives on a related model
  • A unique constraint that has to follow a foreign-key chain
  • A generated column whose value depends on a foreign row

This package provides three trigger-based classes that accept F("related__field") expressions and compile to PL/pgSQL triggers via django-pgtrigger. Their APIs mirror the Django equivalents so the mental model carries over:

  • UniqueConstraintTrigger — a unique constraint with FK chains, Django expressions, partial conditions, and deferred timing.
  • CheckConstraintTrigger — a check whose Q can cross foreign keys.
  • GeneratedFieldTrigger — a computed column whose expression can reference related rows, with reverse triggers that keep the value in sync and RETURNING wiring so save() / bulk_create() populate the computed value onto the Python instance without a follow-up query.

A quick taste

from django.db import models
from django.db.models import F, Q
from django_pgconstraints import CheckConstraintTrigger

class OrderLine(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.IntegerField()

    class Meta:
        triggers = [
            CheckConstraintTrigger(
                condition=Q(quantity__lte=F("product__stock")),
                name="orderline_qty_lte_stock",
            ),
        ]

Nothing else is needed. On every INSERT or UPDATE the trigger runs, resolves F("product__stock") via a subquery, and raises IntegrityError (error code 23514) if the condition is violated. The same rule is also enforced at Python level through full_clean().

Next: installation or jump straight to the quickstart.