Source code for alchemiq.clickhouse.types

"""ClickHouse-typed FieldType descriptors and the Python-to-CH type mapping table."""

from __future__ import annotations

import datetime as dt
import enum as _enum
from decimal import Decimal
from typing import Any
from uuid import UUID

from clickhouse_sqlalchemy import types as ch  # ty: ignore[unresolved-import]
from sqlalchemy.types import TypeEngine

from alchemiq.exceptions import ConfigError
from alchemiq.types.base import FieldType


class _CHField(FieldType):
    """Base for fixed-CH-type fields; subclasses set python_type + _ch."""

    _ch: type[TypeEngine[Any]]

    def column_type(self) -> TypeEngine[Any]:
        return self._ch()


[docs] class UInt8(_CHField): """ClickHouse UInt8 column field (unsigned 8-bit integer, range 0-255).""" python_type = int _ch = ch.UInt8
[docs] class UInt16(_CHField): """ClickHouse UInt16 column field (unsigned 16-bit integer).""" python_type = int _ch = ch.UInt16
[docs] class UInt32(_CHField): """ClickHouse UInt32 column field (unsigned 32-bit integer).""" python_type = int _ch = ch.UInt32
[docs] class UInt64(_CHField): """ClickHouse UInt64 column field (unsigned 64-bit integer).""" python_type = int _ch = ch.UInt64
[docs] class Int8(_CHField): """ClickHouse Int8 column field (signed 8-bit integer).""" python_type = int _ch = ch.Int8
[docs] class Int16(_CHField): """ClickHouse Int16 column field (signed 16-bit integer).""" python_type = int _ch = ch.Int16
[docs] class Int32(_CHField): """ClickHouse Int32 column field (signed 32-bit integer).""" python_type = int _ch = ch.Int32
[docs] class Int64(_CHField): """ClickHouse Int64 column field (signed 64-bit integer).""" python_type = int _ch = ch.Int64
[docs] class Float32(_CHField): """ClickHouse Float32 column field (single-precision float).""" python_type = float _ch = ch.Float32
[docs] class Float64(_CHField): """ClickHouse Float64 column field (double-precision float).""" python_type = float _ch = ch.Float64
[docs] class LowCardinality(FieldType): """ClickHouse LowCardinality wrapper - dictionary-encodes a column for low-cardinality data. :param inner: Python type whose default CH scalar is used as the inner type (default ``str``). """ def __init__(self, inner: type = str, **kw: Any) -> None: super().__init__(python_type=inner, **kw)
[docs] def column_type(self) -> TypeEngine[Any]: """Return ``LowCardinality(<inner_ch_type>)``.""" return ch.LowCardinality(_scalar_ch(self.python_type))
[docs] class DateTime64(FieldType): """ClickHouse DateTime64 column field with configurable sub-second precision. When ``nullable=True`` is passed, the column type is wrapped in ``Nullable(DateTime64)`` because ClickHouse requires the explicit ``Nullable`` wrapper in DDL. :param precision: Sub-second precision digits (0-9; default ``3`` = milliseconds). """ python_type = dt.datetime def __init__(self, precision: int = 3, **kw: Any) -> None: super().__init__(**kw) self.precision = precision
[docs] def column_type(self) -> TypeEngine[Any]: """Return ``DateTime64(precision)`` or ``Nullable(DateTime64(precision))``.""" inner = ch.DateTime64(self.precision) # ClickHouse uses Nullable(Type) syntax for nullable columns; # SA's nullable=True alone does not produce the Nullable wrapper in CH DDL. if self.config.nullable: return ch.Nullable(inner) return inner
[docs] class Enum8(FieldType): """ClickHouse Enum8 column field defined by an explicit name-to-integer mapping. :param members: Dict mapping member name to integer value, e.g. ``{"active": 1, "inactive": 2}``. """ python_type = str def __init__(self, members: dict[str, int], **kw: Any) -> None: super().__init__(**kw) self.members = members
[docs] def column_type(self) -> TypeEngine[Any]: """Return a ``ch.Enum8`` type built from the members mapping.""" ch_enum = _enum.IntEnum("ch_enum", self.members) return ch.Enum8(ch_enum)
_SCALAR: dict[type, type[TypeEngine[Any]]] = { int: ch.Int64, str: ch.String, float: ch.Float64, bool: ch.UInt8, dt.datetime: ch.DateTime64, dt.date: ch.Date, Decimal: ch.Decimal, UUID: ch.UUID, } def _scalar_ch(python_type: type) -> TypeEngine[Any]: try: factory = _SCALAR[python_type] except KeyError as e: raise ConfigError(f"No default ClickHouse type for {python_type!r}") from e if factory is ch.DateTime64: return factory(3) if factory is ch.Decimal: return factory(38, 9) return factory() def ch_column_type(python_type: type) -> TypeEngine[Any]: """Default Python->ClickHouse column type for a bare annotation.""" return _scalar_ch(python_type)