diff options
| author | Mads Jensen <mje@inducks.org> | 2019-07-12 13:08:00 +0200 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2019-07-16 18:04:41 +0200 |
| commit | a3417282ac0464a9a2d1d7685bcfef10feed2597 (patch) | |
| tree | a041b9f3f50c7199c0574c66fb185f0337831cf9 /docs | |
| parent | 7174cf0b009711c41e5f76f6c0c68137898716a0 (diff) | |
Fixed #29824 -- Added support for database exclusion constraints on PostgreSQL.
Thanks to Nick Pope and Mariusz Felisiak for review.
Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/ref/contrib/postgres/constraints.txt | 151 | ||||
| -rw-r--r-- | docs/ref/contrib/postgres/fields.txt | 23 | ||||
| -rw-r--r-- | docs/ref/contrib/postgres/index.txt | 1 | ||||
| -rw-r--r-- | docs/releases/3.0.txt | 11 |
4 files changed, 186 insertions, 0 deletions
diff --git a/docs/ref/contrib/postgres/constraints.txt b/docs/ref/contrib/postgres/constraints.txt new file mode 100644 index 0000000000..fe9e72e605 --- /dev/null +++ b/docs/ref/contrib/postgres/constraints.txt @@ -0,0 +1,151 @@ +======================================== +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 <django.db.models.Options.constraints>` option. + +``ExclusionConstraint`` +======================= + +.. versionadded:: 3.0 + +.. class:: ExclusionConstraint(*, name, expressions, index_type=None, condition=None) + + Creates an exclusion constraint in the database. Internally, PostgreSQL + implements exclusion constraints using indexes. The default index type is + `GiST <https://www.postgresql.org/docs/current/gist.html>`_. To use them, + you need to activate the `btree_gist extension + <https://www.postgresql.org/docs/current/btree-gist.html>`_ 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. + +``name`` +-------- + +.. attribute:: ExclusionConstraint.name + +The name of the constraint. + +``expressions`` +--------------- + +.. attribute:: ExclusionConstraint.expressions + +An iterable of 2-tuples. The first element is an expression or string. The +second element is a 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. + +``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`. + +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 <https://www.postgresql.org/docs/ +current/rangetypes.html#RANGETYPES-INCLUSIVITY>`_. 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), + ), + ] diff --git a/docs/ref/contrib/postgres/fields.txt b/docs/ref/contrib/postgres/fields.txt index 387eb0f02e..14cb1d00cb 100644 --- a/docs/ref/contrib/postgres/fields.txt +++ b/docs/ref/contrib/postgres/fields.txt @@ -944,3 +944,26 @@ corresponding lookups. NOT_LT = '&>' NOT_GT = '&<' ADJACENT_TO = '-|-' + +RangeBoundary() expressions +--------------------------- + +.. versionadded:: 3.0 + +.. class:: RangeBoundary(inclusive_lower=True, inclusive_upper=False) + + .. attribute:: inclusive_lower + + If ``True`` (default), the lower bound is inclusive ``'['``, otherwise + it's exclusive ``'('``. + + .. attribute:: inclusive_upper + + If ``False`` (default), the upper bound is exclusive ``')'``, otherwise + it's inclusive ``']'``. + +A ``RangeBoundary()`` expression represents the range boundaries. It can be +used with a custom range functions that expected boundaries, for example to +define :class:`~django.contrib.postgres.constraints.ExclusionConstraint`. See +`the PostgreSQL documentation for the full details <https://www.postgresql.org/ +docs/current/rangetypes.html#RANGETYPES-INCLUSIVITY>`_. diff --git a/docs/ref/contrib/postgres/index.txt b/docs/ref/contrib/postgres/index.txt index 9485f56409..03ff6da1e0 100644 --- a/docs/ref/contrib/postgres/index.txt +++ b/docs/ref/contrib/postgres/index.txt @@ -29,6 +29,7 @@ a number of PostgreSQL specific data types. :maxdepth: 2 aggregates + constraints fields forms functions diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 6aa21975bf..34152573f0 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -66,6 +66,14 @@ async code before, this may trigger if you were doing it incorrectly. If you see a ``SynchronousOnlyOperation`` error, then closely examine your code and move any database operations to be in a synchronous child thread. +Exclusion constraints on PostgreSQL +----------------------------------- + +The new :class:`~django.contrib.postgres.constraints.ExclusionConstraint` class +enable adding exclusion constraints on PostgreSQL. Constraints are added to +models using the +:attr:`Meta.constraints <django.db.models.Options.constraints>` option. + Minor features -------------- @@ -137,6 +145,9 @@ Minor features avoid typos in SQL operators that can be used together with :class:`~django.contrib.postgres.fields.RangeField`. +* The new :class:`~django.contrib.postgres.fields.RangeBoundary` expression + represents the range boundaries. + :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
