1from __future__ import annotations
 2
 3from collections.abc import Callable, Sequence
 4from functools import cached_property
 5from typing import TYPE_CHECKING, Any
 6from uuid import UUID
 7
 8from plain import exceptions
 9
10from .base import ColumnField
11
12if TYPE_CHECKING:
13    from plain.postgres.connection import DatabaseConnection
14    from plain.postgres.expressions import Func
15
16
17class UUIDField(ColumnField[UUID]):
18    db_type_sql = "uuid"
19    empty_strings_allowed = False
20
21    def __init__(
22        self,
23        *,
24        generate: bool = False,
25        required: bool = True,
26        allow_null: bool = False,
27        validators: Sequence[Callable[..., Any]] = (),
28    ):
29        self.generate = generate
30        super().__init__(
31            required=required,
32            allow_null=allow_null,
33            validators=validators,
34        )
35
36    @cached_property
37    def _db_default_expression(self) -> Func | None:
38        if self.generate:
39            from plain.postgres.functions.uuid import GenRandomUUID
40
41            return GenRandomUUID()
42        return None
43
44    def get_db_default_expression(self) -> Func | None:
45        return self._db_default_expression
46
47    def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
48        name, path, args, kwargs = super().deconstruct()
49        if self.generate:
50            kwargs["generate"] = True
51        return name, path, args, kwargs
52
53    def get_prep_value(self, value: Any) -> Any:
54        value = super().get_prep_value(value)
55        return self.to_python(value)
56
57    def get_db_prep_value(
58        self, value: Any, connection: DatabaseConnection, prepared: bool = False
59    ) -> UUID | None:
60        # PostgreSQL has native UUID type
61        if value is None:
62            return None
63        if not isinstance(value, UUID):
64            value = self.to_python(value)
65        return value
66
67    def to_python(self, value: Any) -> UUID | None:
68        if value is not None and not isinstance(value, UUID):
69            input_form = "int" if isinstance(value, int) else "hex"
70            try:
71                return UUID(**{input_form: value})
72            except (AttributeError, ValueError):
73                raise exceptions.ValidationError(
74                    '"%(value)s" is not a valid UUID.',
75                    code="invalid",
76                    params={"value": value},
77                )
78        return value