Source code django/contrib/postgres/constraints.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from django.db import NotSupportedError
from django.db.backends.ddl_references import Statement, Table
from django.db.models import Deferrable, F, Q
from django.db.models.constraints import BaseConstraint
from django.db.models.sql import Query

__all__ = ['ExclusionConstraint']


class ExclusionConstraint(BaseConstraint):
    template = 'CONSTRAINT %(name)s EXCLUDE USING %(index_type)s (%(expressions)s)%(include)s%(where)s%(deferrable)s'

    def __init__(
        self, *, name, expressions, index_type=None, condition=None,
        deferrable=None, include=None, opclasses=(),
    ):
        if index_type and index_type.lower() not in {'gist', 'spgist'}:
            raise ValueError(
                'Exclusion constraints only support GiST or SP-GiST indexes.'
            )
        if not expressions:
            raise ValueError(
                'At least one expression is required to define an exclusion '
                'constraint.'
            )
        if not all(
            isinstance(expr, (list, tuple)) and len(expr) == 2
            for expr in expressions
        ):
            raise ValueError('The expressions must be a list of 2-tuples.')
        if not isinstance(condition, (type(None), Q)):
            raise ValueError(
                'ExclusionConstraint.condition must be a Q instance.'
            )
        if condition and deferrable:
            raise ValueError(
                'ExclusionConstraint with conditions cannot be deferred.'
            )
        if not isinstance(deferrable, (type(None), Deferrable)):
            raise ValueError(
                'ExclusionConstraint.deferrable must be a Deferrable instance.'
            )
        if not isinstance(include, (type(None), list, tuple)):
            raise ValueError(
                'ExclusionConstraint.include must be a list or tuple.'
            )
        if include and index_type and index_type.lower() != 'gist':
            raise ValueError(
                'Covering exclusion constraints only support GiST indexes.'
            )
        if not isinstance(opclasses, (list, tuple)):
            raise ValueError(
                'ExclusionConstraint.opclasses must be a list or tuple.'
            )
        if opclasses and len(expressions) != len(opclasses):
            raise ValueError(
                'ExclusionConstraint.expressions and '
                'ExclusionConstraint.opclasses must have the same number of '
                'elements.'
            )
        self.expressions = expressions
        self.index_type = index_type or 'GIST'
        self.condition = condition
        self.deferrable = deferrable
        self.include = tuple(include) if include else ()
        self.opclasses = opclasses
        super().__init__(name=name)

    def _get_expression_sql(self, compiler, schema_editor, query):
        expressions = []
        for idx, (expression, operator) in enumerate(self.expressions):
            if isinstance(expression, str):
                expression = F(expression)
            expression = expression.resolve_expression(query=query)
            sql, params = compiler.compile(expression)
            try:
                opclass = self.opclasses[idx]
                if opclass:
                    sql = '%s %s' % (sql, opclass)
            except IndexError:
                pass
            sql = sql % tuple(schema_editor.quote_value(p) for p in params)
            expressions.append('%s WITH %s' % (sql, operator))
        return expressions

    def _get_condition_sql(self, compiler, schema_editor, query):
        if self.condition is None:
            return None
        where = query.build_where(self.condition)
        sql, params = where.as_sql(compiler, schema_editor.connection)
        return sql % tuple(schema_editor.quote_value(p) for p in params)

    def constraint_sql(self, model, schema_editor):
        query = Query(model, alias_cols=False)
        compiler = query.get_compiler(connection=schema_editor.connection)
        expressions = self._get_expression_sql(compiler, schema_editor, query)
        condition = self._get_condition_sql(compiler, schema_editor, query)
        include = [model._meta.get_field(field_name).column for field_name in self.include]
        return self.template % {
            'name': schema_editor.quote_name(self.name),
            'index_type': self.index_type,
            'expressions': ', '.join(expressions),
            'include': schema_editor._index_include_sql(model, include),
            'where': ' WHERE (%s)' % condition if condition else '',
            'deferrable': schema_editor._deferrable_constraint_sql(self.deferrable),
        }

    def create_sql(self, model, schema_editor):
        self.check_supported(schema_editor)
        return Statement(
            'ALTER TABLE %(table)s ADD %(constraint)s',
            table=Table(model._meta.db_table, schema_editor.quote_name),
            constraint=self.constraint_sql(model, schema_editor),
        )

    def remove_sql(self, model, schema_editor):
        return schema_editor._delete_constraint_sql(
            schema_editor.sql_delete_check,
            model,
            schema_editor.quote_name(self.name),
        )

    def check_supported(self, schema_editor):
        if self.include and not schema_editor.connection.features.supports_covering_gist_indexes:
            raise NotSupportedError(
                'Covering exclusion constraints requires PostgreSQL 12+.'
            )

    def deconstruct(self):
        path, args, kwargs = super().deconstruct()
        kwargs['expressions'] = self.expressions
        if self.condition is not None:
            kwargs['condition'] = self.condition
        if self.index_type.lower() != 'gist':
            kwargs['index_type'] = self.index_type
        if self.deferrable:
            kwargs['deferrable'] = self.deferrable
        if self.include:
            kwargs['include'] = self.include
        if self.opclasses:
            kwargs['opclasses'] = self.opclasses
        return path, args, kwargs

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return (
                self.name == other.name and
                self.index_type == other.index_type and
                self.expressions == other.expressions and
                self.condition == other.condition and
                self.deferrable == other.deferrable and
                self.include == other.include and
                self.opclasses == other.opclasses
            )
        return super().__eq__(other)

    def __repr__(self):
        return '<%s: index_type=%s, expressions=%s%s%s%s%s>' % (
            self.__class__.__qualname__,
            self.index_type,
            self.expressions,
            '' if self.condition is None else ', condition=%s' % self.condition,
            '' if self.deferrable is None else ', deferrable=%s' % self.deferrable,
            '' if not self.include else ', include=%s' % repr(self.include),
            '' if not self.opclasses else ', opclasses=%s' % repr(self.opclasses),
        )