1from typing import TYPE_CHECKING, Any, cast
2
3from plain.preflight import PreflightResult
4
5from plain import exceptions
6
7from .base import ColumnField
8
9if TYPE_CHECKING:
10 from plain.postgres.base import Model
11 from plain.postgres.connection import DatabaseConnection
12
13
14class PrimaryKeyField(ColumnField[int]):
15 """Identity primary key — Postgres generates the value on INSERT.
16
17 Accepts no constructor arguments: the column is always ``bigint GENERATED
18 BY DEFAULT AS IDENTITY``, NOT NULL, with RETURNING populating the value
19 back onto the instance after save.
20 """
21
22 db_type_sql = "bigint"
23 cast_db_type_sql = "bigint"
24 db_type_suffix_sql = "GENERATED BY DEFAULT AS IDENTITY"
25 empty_strings_allowed = False
26
27 def __init__(self) -> None:
28 super().__init__(required=False)
29 self.primary_key = True
30 self.auto_created = True
31
32 @property
33 def db_returning(self) -> bool:
34 return True
35
36 def preflight(self, **kwargs: Any) -> list[PreflightResult]:
37 errors = super().preflight(**kwargs)
38 # `id` is the reserved PK name — Field._check_field_name rejects it
39 # for every other field type.
40 return [e for e in errors if e.id != "fields.reserved_field_name_id"]
41
42 def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
43 return (
44 self.name,
45 "plain.postgres.PrimaryKeyField",
46 cast(list[Any], []),
47 cast(dict[str, Any], {}),
48 )
49
50 def validate(self, value: Any, model_instance: Model) -> None:
51 # Identity columns aren't user-validated — Postgres owns the value.
52 pass
53
54 def to_python(self, value: Any) -> int | None:
55 if value is None:
56 return value
57 try:
58 return int(value)
59 except TypeError, ValueError:
60 raise exceptions.ValidationError(
61 '"%(value)s" value must be an integer.',
62 code="invalid",
63 params={"value": value},
64 )
65
66 def get_prep_value(self, value: Any) -> Any:
67 value = super().get_prep_value(value)
68 if value is None:
69 return None
70 try:
71 return int(value)
72 except (TypeError, ValueError) as e:
73 raise e.__class__(
74 f"Field '{self.name}' expected a number but got {value!r}.",
75 ) from e
76
77 def get_db_prep_value(
78 self, value: Any, connection: DatabaseConnection, prepared: bool = False
79 ) -> Any:
80 if not prepared:
81 value = self.get_prep_value(value)
82 return value
83
84 def rel_db_type(self) -> str:
85 return "bigint"