1from __future__ import annotations
2
3from typing import TYPE_CHECKING, Any
4
5from plain.postgres.expressions import Func
6from plain.postgres.fields import TextField
7
8if TYPE_CHECKING:
9 from plain.postgres.connection import DatabaseConnection
10 from plain.postgres.sql.compiler import SQLCompiler
11
12
13_UUID_HEX_CHARS = 32
14
15
16class RandomString(Func):
17 """Parameter-free SQL expression that produces an N-char random hex string.
18
19 Slices ``gen_random_uuid()`` (OS CSPRNG-backed) directly: each call
20 contributes 32 hex characters; longer values concatenate additional UUID
21 slices. Suitable for tokens, slugs, and short identifiers.
22 """
23
24 output_field = TextField()
25
26 def __init__(self, length: int) -> None:
27 if length < 1:
28 raise ValueError("RandomString length must be >= 1")
29 self.length = length
30 super().__init__()
31
32 def as_sql(
33 self,
34 compiler: SQLCompiler,
35 connection: DatabaseConnection,
36 function: str | None = None,
37 template: str | None = None,
38 arg_joiner: str | None = None,
39 **extra_context: Any,
40 ) -> tuple[str, list[Any]]:
41 full, leftover = divmod(self.length, _UUID_HEX_CHARS)
42 parts = [
43 f"substr(replace(gen_random_uuid()::text, '-', ''), 1, {_UUID_HEX_CHARS})"
44 ] * full
45 if leftover:
46 parts.append(
47 f"substr(replace(gen_random_uuid()::text, '-', ''), 1, {leftover})"
48 )
49 if len(parts) == 1:
50 return parts[0], []
51 return "(" + " || ".join(parts) + ")", []