Source code for alchemiq.clickhouse.engines

"""ClickHouse MergeTree-family engine descriptors used in ClickHouseModel.Meta.engine."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


def _cols(value: str | tuple[str, ...] | None) -> str | None:
    if value is None:
        return None
    if isinstance(value, str):
        return value
    return "(" + ", ".join(value) + ")"


@dataclass(frozen=True)
class _Engine:
    order_by: str | tuple[str, ...]
    partition_by: str | tuple[str, ...] | None = None
    primary_key: str | tuple[str, ...] | None = None
    ttl: str | None = None
    sample_by: str | None = None
    settings: dict[str, Any] | None = None

    _name: str = field(default="MergeTree", init=False)

    def _engine_head(self) -> str:
        return self._name

    def engine_clause(self) -> str:
        parts = [f"ENGINE = {self._engine_head()}", f"ORDER BY {_cols(self.order_by)}"]
        if self.partition_by is not None:
            parts.append(f"PARTITION BY {_cols(self.partition_by)}")
        if self.primary_key is not None:
            parts.append(f"PRIMARY KEY {_cols(self.primary_key)}")
        if self.sample_by is not None:
            parts.append(f"SAMPLE BY {self.sample_by}")
        if self.ttl is not None:
            parts.append(f"TTL {self.ttl}")
        if self.settings:
            kv = ", ".join(f"{k} = {v}" for k, v in self.settings.items())
            parts.append(f"SETTINGS {kv}")
        return " ".join(parts)


[docs] @dataclass(frozen=True) class MergeTree(_Engine): """Standard MergeTree engine - general-purpose append-only analytics table. E.g.:: from alchemiq.clickhouse.engines import MergeTree engine = MergeTree( order_by=("event_time", "user_id"), partition_by="toYYYYMM(event_time)", ttl="event_time + INTERVAL 90 DAY", ) .. seealso:: :class:`.ReplacingMergeTree` - for deduplication or soft-delete. """ _name: str = field(default="MergeTree", init=False)
[docs] @dataclass(frozen=True) class ReplacingMergeTree(_Engine): """ReplacingMergeTree engine - deduplicates rows by ORDER BY key on merge. Required for soft-delete models (``Meta.soft_delete=True``). When ``version`` and ``is_deleted`` are set (as :class:`.ClickHouseModel` does automatically for soft-delete models), ``SELECT ... FINAL`` retains the row with the highest ``version`` value and filters out rows where ``is_deleted=1``. E.g.:: from alchemiq.clickhouse.engines import ReplacingMergeTree # basic dedup by version column: engine = ReplacingMergeTree(order_by="key", version="ver") # with soft-delete support (set automatically by Meta.soft_delete=True): engine = ReplacingMergeTree( order_by=("key",), version="_version", is_deleted="is_deleted", ) :param version: Column name used as the deduplication version (latest value wins). :param is_deleted: Column name that marks tombstone rows (``1`` = deleted). When set, ``SELECT ... FINAL`` filters out rows where ``is_deleted=1``. .. seealso:: :class:`.MergeTree` - for non-deduplicated append-only tables. """ version: str | None = None is_deleted: str | None = None _name: str = field(default="ReplacingMergeTree", init=False) def _engine_head(self) -> str: args = [a for a in (self.version, self.is_deleted) if a is not None] return "ReplacingMergeTree" + (f"({', '.join(args)})" if args else "")
[docs] @dataclass(frozen=True) class AggregatingMergeTree(_Engine): """AggregatingMergeTree engine - merges rows by aggregating AggregateFunction columns.""" _name: str = field(default="AggregatingMergeTree", init=False)