django/django · error · ValueError

Composite exclusion constraints using Hash indexes are not s

Error message

Composite exclusion constraints using Hash indexes are not supported.

What it means

Raised when index_type='hash' and len(expressions) > 1. A PostgreSQL hash index can only index a single column with a single operator, so multi-column (composite) exclusion with hash is impossible. Django fails fast at model load rather than producing a confusing migration error.

Source

Thrown at django/contrib/postgres/constraints.py:70

            raise ValueError("The expressions must be a list of 2-tuples.")
        if not isinstance(condition, (NoneType, Q)):
            raise ValueError("ExclusionConstraint.condition must be a Q instance.")
        if not isinstance(deferrable, (NoneType, Deferrable)):
            raise ValueError(
                "ExclusionConstraint.deferrable must be a Deferrable instance."
            )
        if not isinstance(include, (NoneType, list, tuple)):
            raise ValueError("ExclusionConstraint.include must be a list or tuple.")
        if index_type and index_type.lower() == "hash":
            if include:
                raise ValueError(
                    "Covering exclusion constraints using Hash indexes are not "
                    "supported."
                )
            if not expressions:
                pass
            elif len(expressions) > 1:
                raise ValueError(
                    "Composite exclusion constraints using Hash indexes are not "
                    "supported."
                )
            elif expressions[0][1] != RangeOperators.EQUAL:
                raise ValueError(
                    "Exclusion constraints using Hash indexes only support the EQUAL "
                    "operator."
                )
        self.expressions = expressions
        self.index_type = index_type or "GIST"
        self.condition = condition
        self.deferrable = deferrable
        self.include = tuple(include) if include else ()
        super().__init__(
            name=name,
            violation_error_code=violation_error_code,
            violation_error_message=violation_error_message,
        )

View on GitHub (pinned to b5388a3a80)

Solutions

  1. Reduce expressions to a single 2-tuple, or switch index_type to 'gist' (the default, which supports composites).
  2. For multi-column overlap detection (e.g. booking conflicts), GiST with the && operator is the standard choice.

Example fix

// before
ExclusionConstraint(name='x', expressions=[('room_id', '='), ('period', '&&')], index_type='hash')
// after
ExclusionConstraint(name='x', expressions=[('room_id', '='), ('period', '&&')], index_type='gist')
Defensive patterns

Strategy: validation

Validate before calling

def validate_hash_composite(index_type, expressions):
    if index_type and index_type.lower() == 'hash' and len(expressions) > 1:
        raise ValueError('Hash exclusion constraints cannot be composite')
    return expressions

Type guard

def hash_index_allows_composite(index_type: str, expressions: list) -> bool:
    return not (index_type and index_type.lower() == 'hash' and len(expressions) > 1)

Try / catch

try:
    ExclusionConstraint(..., index_type='hash', expressions=exprs)
except ValueError as e:
    if 'Composite exclusion constraints using Hash' in str(e):
        index_type = 'gist'  # fall back to GIST for multi-column
    else:
        raise

Prevention

When it happens

Trigger: ExclusionConstraint(name='x', expressions=[('a', '='), ('b', '=')], index_type='hash'). Any two-or-more element expressions list with hash triggers it.

Common situations: Building a no-overlap constraint across two range columns and trying hash for speed. Assuming hash works like btree for composites.

Related errors


AI-assisted analysis of django/django@b5388a3a80 (2026-08-10). Data as JSON: /api/errors/1e0340e43a33975f. Report an issue: GitHub.