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) + ")", []