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.pyDabei 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: MergeTreeDas 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-runnerFlux 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.