StatusTypeRelevance

Status


Dieses Dokument ergänzt die Implementierungsbeschreibung durch konkrete Beispielartefakte.

Die Beispiele zeigen, wie die im Architektur- und Implementierungsdokument beschriebenen Komponenten praktisch zusammenspielen:

  • Beispiel-Repository für Modelle, Templates und Migrationstools
  • Beispiel-Renderer (model2ddl)
  • Beispiel-Migration-Runner
  • Argo WorkflowTemplates für Schema-Apply und Ingestion

Die Artefakte sind bewusst minimal gehalten. Sie sollen das Zusammenspiel der Bausteine illustrieren, nicht eine vollständige Produktionsimplementierung vorwegnehmen.


1️⃣ Beispiel-Repo: Modelle, Template, Renderer, Runner

Ein mögliches zentrales Repository könnte z. B. sdk-clickhouse-models heißen:

sdk-clickhouse-models/
├─ data-models/
│  └─ tables/
│     └─ fact_reservationdata_device/
│        ├─ model.yaml
│        └─ template.sql.j2
├─ migrations/
│  ├─ V000__create_schema_migrations.sql
│  └─ V001__baseline_changes.sql
└─ tools/
   ├─ model2ddl/
   │  ├─ Dockerfile
   │  └─ render.py
   └─ migration-runner/
      ├─ Dockerfile
      └─ run_migrations.py

Dabei gilt:

  • data-models/ enthält die deklarativen Tabellenmodelle.
  • migrations/ enthält optionale handgeschriebene Migrationen.
  • tools/ enthält die ausführbaren Komponenten der Schema-Pipeline.

Das Repository ist die führende Quelle für modellierte Tabellenstrukturen. ClickHouse selbst wird nicht als Ort für manuelle Schema-Pflege verwendet.


data-models/tables/fact_reservationdata_device/model.yaml

name: fact_reservationdata_device__space_reservationdata
 
columns:
  - name: device_id
    type: String
    nullable: false
  - name: ts
    type: DateTime
  - name: status_code
    type: LowCardinality(String)
  - name: attrs_json
    type: String
 
partition_by: toYYYYMM(ts)
order_by: [device_id, ts]
ttl: ts + INTERVAL 365 DAY DELETE
engine: MergeTree

Das Modell beschreibt die Struktur der Tabelle. Welche Felder genau erlaubt sind und wie Defaults behandelt werden, wird in der Implementierung des Modellformats festgelegt.


data-models/tables/fact_reservationdata_device/template.sql.j2

CREATE TABLE IF NOT EXISTS {{ db }}.{{ model.name }}
(
{% for col in model.columns %}
  {{ col.name }} {% if col.nullable is defined and col.nullable == false %}{{ col.type }}{% else %}Nullable({{ col.type }}){% endif %}{% if not loop.last %},{% endif %}
{% endfor %}
)
ENGINE = {{ model.engine }}
PARTITION BY {{ model.partition_by }}
ORDER BY ({% for x in model.order_by %}{{ x }}{% if not loop.last %}, {% endif %}{% endfor %})
TTL {{ model.ttl }};

Das Template übersetzt das Modell in konkretes ClickHouse-DDL.

Der Renderer ersetzt dabei Variablen wie {{ db }} oder {{ model.* }} und erzeugt daraus ausführbare SQL-Statements.


Migrationen (optional)

Ein leichtgewichtiger Mechanismus für zusätzliche SQL-Änderungen kann über ein Migrationsverzeichnis erfolgen.

migrations/V000__create_schema_migrations.sql

CREATE TABLE IF NOT EXISTS system.schema_migrations
(
  org_db     String,
  version    String,
  applied_at DateTime DEFAULT now()
)
ENGINE = MergeTree
ORDER BY (org_db, version);

migrations/V001__baseline_changes.sql

SELECT 'baseline migration for org db' as info;

Die Migrationstabelle dient dazu, ausgeführte Änderungen pro Organisation nachvollziehbar zu machen.


Beispiel: model2ddl Renderer

Der Renderer liest alle model.yaml Dateien und erzeugt daraus SQL-Dateien.

import os
import glob
import yaml
from jinja2 import Template
 
MODELS_DIR = os.environ.get("MODELS_DIR", "/models")
OUT_DIR = os.environ.get("OUT_DIR", "/out")
ORG_DB = os.environ["ORG_DB"]
 
def render_model(model_path: str):
    with open(model_path) as f:
        model = yaml.safe_load(f)
 
    tmpl_path = model_path.replace("model.yaml", "template.sql.j2")
 
    with open(tmpl_path) as f:
        tmpl = Template(f.read())
 
    sql = tmpl.render(db=ORG_DB, model=model)
 
    name = model["name"]
    out_path = os.path.join(OUT_DIR, f"001_{name}.sql")
 
    os.makedirs(OUT_DIR, exist_ok=True)
 
    with open(out_path, "w") as f:
        f.write(sql)
 
    print(f"generated {out_path}")
 
 
def main():
    pattern = os.path.join(MODELS_DIR, "**", "model.yaml")
 
    for path in glob.glob(pattern, recursive=True):
        render_model(path)
 
 
if __name__ == "__main__":
    main()

Der Renderer erzeugt ausschließlich SQL-Artefakte. Das Modell selbst bleibt unverändert im Git-Repository.


Dockerfile

FROM python:3.12-alpine
 
RUN pip install pyyaml jinja2
 
WORKDIR /app
COPY render.py /app/
 
ENTRYPOINT ["python", "/app/render.py"]

Beispiel: Migration Runner

Der Migration-Runner führt generierte SQL-Dateien und optionale Migrationen gegen ClickHouse aus.

import os
import glob
import subprocess
 
CH_HOST = os.environ.get("CH_HOST", "clickhouse")
CH_PORT = os.environ.get("CH_PORT", "9000")
CH_USER = os.environ.get("CH_USER", "default")
CH_PASS = os.environ.get("CH_PASS", "")
ORG_DB = os.environ["ORG_DB"]
 
DDL_DIR = os.environ.get("DDL_DIR", "/ddl")
MIGRATIONS_DIR = os.environ.get("MIGRATIONS_DIR", "/migrations")
 
 
def run_sql_file(path: str):
 
    print(f">> applying {path}")
 
    cmd = [
        "clickhouse-client",
        "--host", CH_HOST,
        "--port", CH_PORT,
        "--user", CH_USER,
    ]
 
    if CH_PASS:
        cmd += ["--password", CH_PASS]
 
    cmd += [
        "--multiquery",
        "--query",
        open(path, "r", encoding="utf-8").read(),
    ]
 
    subprocess.run(cmd, check=True)
 
 
def main():
 
    for f in sorted(glob.glob(os.path.join(DDL_DIR, "*.sql"))):
        run_sql_file(f)
 
    for f in sorted(glob.glob(os.path.join(MIGRATIONS_DIR, "V*.sql"))):
        run_sql_file(f)
 
 
if __name__ == "__main__":
    main()

Der Runner führt SQL sequenziell aus. Erweiterungen wie Version-Tracking oder Rollbacks können bei Bedarf ergänzt werden.


2️⃣ Argo Workflows

Die Orchestrierung der Schema-Pipeline erfolgt über Argo Workflows.

WorkflowTemplate: Schema-Apply

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: ch-schema-apply
spec:
  entrypoint: main
  arguments:
    parameters:
      - name: orgDb
        value: "org_test"
 
  volumes:
    - name: models
      configMap:
        name: ch-data-models
 
    - name: ddl-out
      emptyDir: {}
 
    - name: migrations
      configMap:
        name: ch-migrations
 
  templates:
    - name: main
      steps:
        - - name: render-ddl
            template: model2ddl
          - name: apply-ddl
            template: migration-runner

Flux synchronisiert Repository-Artefakte als ConfigMaps in den Cluster. Der Workflow verwendet diese Artefakte, um Tabellen zu erzeugen oder Migrationen auszuführen.


Zweck dieses Dokuments

Die Beispiele zeigen lediglich das Zusammenspiel der Komponenten:

model.yaml
   ↓
model2ddl
   ↓
SQL
   ↓
migration-runner
   ↓
ClickHouse

Die konkrete Implementierung kann später erweitert oder vereinfacht werden, solange diese grundlegende Pipeline erhalten bleibt.