Skip to content

Commit

Permalink
feat(models): add database constraints to Value model
Browse files Browse the repository at this point in the history
  • Loading branch information
Dresdn committed Feb 25, 2025
1 parent 4deda2a commit fafe528
Show file tree
Hide file tree
Showing 3 changed files with 392 additions and 1 deletion.
54 changes: 54 additions & 0 deletions eav/migrations/0012_add_value_uniqueness_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from django.db import migrations, models


class Migration(migrations.Migration):
"""
Add uniqueness and integrity constraints to the Value model.
This migration adds database-level constraints to ensure:
1. Each entity (identified by UUID) can have only one value per attribute
2. Each entity (identified by integer ID) can have only one value per attribute
3. Each value must use either entity_id OR entity_uuid, never both or neither
These constraints ensure data integrity by preventing duplicate attribute values
for the same entity and enforcing the XOR relationship between the two types of
entity identification (integer ID vs UUID).
"""

dependencies = [
("eav", "0011_update_defaults_and_meta"),
]

operations = [
migrations.AddConstraint(
model_name="value",
constraint=models.UniqueConstraint(
fields=("entity_ct", "attribute", "entity_uuid"),
name="unique_entity_uuid_per_attribute",
),
),
migrations.AddConstraint(
model_name="value",
constraint=models.UniqueConstraint(
fields=("entity_ct", "attribute", "entity_id"),
name="unique_entity_id_per_attribute",
),
),
migrations.AddConstraint(
model_name="value",
constraint=models.CheckConstraint(
check=models.Q(
models.Q(
("entity_id__isnull", False),
("entity_uuid__isnull", True),
),
models.Q(
("entity_id__isnull", True),
("entity_uuid__isnull", False),
),
_connector="OR",
),
name="ensure_entity_id_xor_entity_uuid",
),
),
]
20 changes: 19 additions & 1 deletion eav/models/value.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ruff: noqa: UP007
from __future__ import annotations

from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, ClassVar, Optional

from django.contrib.contenttypes import fields as generic
from django.contrib.contenttypes.models import ContentType
Expand Down Expand Up @@ -173,6 +173,24 @@ class Meta:
verbose_name = _("Value")
verbose_name_plural = _("Values")

constraints: ClassVar[list[models.Constraint]] = [
models.UniqueConstraint(
fields=["entity_ct", "attribute", "entity_uuid"],
name="unique_entity_uuid_per_attribute",
),
models.UniqueConstraint(
fields=["entity_ct", "attribute", "entity_id"],
name="unique_entity_id_per_attribute",
),
models.CheckConstraint(
check=(
models.Q(entity_id__isnull=False, entity_uuid__isnull=True)
| models.Q(entity_id__isnull=True, entity_uuid__isnull=False)
),
name="ensure_entity_id_xor_entity_uuid",
),
]

def __str__(self) -> str:
"""String representation of a Value."""
entity = self.entity_pk_uuid if self.entity_uuid else self.entity_pk_int
Expand Down
Loading

0 comments on commit fafe528

Please sign in to comment.