======================================== PostgreSQL specific database constraints ======================================== .. module:: django.contrib.postgres.constraints :synopsis: PostgreSQL specific database constraint PostgreSQL supports additional data integrity constraints available from the ``django.contrib.postgres.constraints`` module. They are added in the model :attr:`Meta.constraints ` option. ``ExclusionConstraint`` ======================= .. class:: ExclusionConstraint(*, name, expressions, index_type=None, condition=None, deferrable=None, include=None, violation_error_code=None, violation_error_message=None) Creates an exclusion constraint in the database. Internally, PostgreSQL implements exclusion constraints using indexes. The default index type is `GiST `_. To use them, you need to activate the `btree_gist extension `_ on PostgreSQL. You can install it using the :class:`~django.contrib.postgres.operations.BtreeGistExtension` migration operation. If you attempt to insert a new row that conflicts with an existing row, an :exc:`~django.db.IntegrityError` is raised. Similarly, when update conflicts with an existing row. Exclusion constraints are checked during the :ref:`model validation `. ``name`` -------- .. attribute:: ExclusionConstraint.name See :attr:`.BaseConstraint.name`. ``expressions`` --------------- .. attribute:: ExclusionConstraint.expressions An iterable of 2-tuples. The first element is an expression or string. The second element is an SQL operator represented as a string. To avoid typos, you may use :class:`~django.contrib.postgres.fields.RangeOperators` which maps the operators with strings. For example:: expressions = [ ("timespan", RangeOperators.ADJACENT_TO), (F("room"), RangeOperators.EQUAL), ] .. admonition:: Restrictions on operators. Only commutative operators can be used in exclusion constraints. The :class:`OpClass() ` expression can be used to specify a custom `operator class`_ for the constraint expressions. For example:: expressions = [ (OpClass("circle", name="circle_ops"), RangeOperators.OVERLAPS), ] creates an exclusion constraint on ``circle`` using ``circle_ops``. .. _operator class: https://www.postgresql.org/docs/current/indexes-opclass.html ``index_type`` -------------- .. attribute:: ExclusionConstraint.index_type The index type of the constraint. Accepted values are ``GIST`` or ``SPGIST``. Matching is case insensitive. If not provided, the default index type is ``GIST``. ``condition`` ------------- .. attribute:: ExclusionConstraint.condition A :class:`~django.db.models.Q` object that specifies the condition to restrict a constraint to a subset of rows. For example, ``condition=Q(cancelled=False)``. These conditions have the same database restrictions as :attr:`django.db.models.Index.condition`. ``deferrable`` -------------- .. attribute:: ExclusionConstraint.deferrable Set this parameter to create a deferrable exclusion constraint. Accepted values are ``Deferrable.DEFERRED`` or ``Deferrable.IMMEDIATE``. For example:: from django.contrib.postgres.constraints import ExclusionConstraint from django.contrib.postgres.fields import RangeOperators from django.db.models import Deferrable ExclusionConstraint( name="exclude_overlapping_deferred", expressions=[ ("timespan", RangeOperators.OVERLAPS), ], deferrable=Deferrable.DEFERRED, ) By default constraints are not deferred. A deferred constraint will not be enforced until the end of the transaction. An immediate constraint will be enforced immediately after every command. .. warning:: Deferred exclusion constraints may lead to a `performance penalty `_. ``include`` ----------- .. attribute:: ExclusionConstraint.include A list or tuple of the names of the fields to be included in the covering exclusion constraint as non-key columns. This allows index-only scans to be used for queries that select only included fields (:attr:`~ExclusionConstraint.include`) and filter only by indexed fields (:attr:`~ExclusionConstraint.expressions`). ``include`` is supported for GiST indexes. PostgreSQL 14+ also supports ``include`` for SP-GiST indexes. ``violation_error_code`` ------------------------ .. attribute:: ExclusionConstraint.violation_error_code The error code used when ``ValidationError`` is raised during :ref:`model validation `. Defaults to ``None``. ``violation_error_message`` --------------------------- The error message used when ``ValidationError`` is raised during :ref:`model validation `. Defaults to :attr:`.BaseConstraint.violation_error_message`. Examples -------- The following example restricts overlapping reservations in the same room, not taking canceled reservations into account:: from django.contrib.postgres.constraints import ExclusionConstraint from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators from django.db import models from django.db.models import Q class Room(models.Model): number = models.IntegerField() class Reservation(models.Model): room = models.ForeignKey("Room", on_delete=models.CASCADE) timespan = DateTimeRangeField() cancelled = models.BooleanField(default=False) class Meta: constraints = [ ExclusionConstraint( name="exclude_overlapping_reservations", expressions=[ ("timespan", RangeOperators.OVERLAPS), ("room", RangeOperators.EQUAL), ], condition=Q(cancelled=False), ), ] In case your model defines a range using two fields, instead of the native PostgreSQL range types, you should write an expression that uses the equivalent function (e.g. ``TsTzRange()``), and use the delimiters for the field. Most often, the delimiters will be ``'[)'``, meaning that the lower bound is inclusive and the upper bound is exclusive. You may use the :class:`~django.contrib.postgres.fields.RangeBoundary` that provides an expression mapping for the `range boundaries `_. For example:: from django.contrib.postgres.constraints import ExclusionConstraint from django.contrib.postgres.fields import ( DateTimeRangeField, RangeBoundary, RangeOperators, ) from django.db import models from django.db.models import Func, Q class TsTzRange(Func): function = "TSTZRANGE" output_field = DateTimeRangeField() class Reservation(models.Model): room = models.ForeignKey("Room", on_delete=models.CASCADE) start = models.DateTimeField() end = models.DateTimeField() cancelled = models.BooleanField(default=False) class Meta: constraints = [ ExclusionConstraint( name="exclude_overlapping_reservations", expressions=[ ( TsTzRange("start", "end", RangeBoundary()), RangeOperators.OVERLAPS, ), ("room", RangeOperators.EQUAL), ], condition=Q(cancelled=False), ), ]