ClickHouse Migrations#
Langfuse manages ClickHouse schema changes with golang-migrate-based SQL migration files under packages/shared/clickhouse/migrations/. Every schema change is expressed as a pair of numbered .up.sql / .down.sql files (e.g., 0001_traces.up.sql, 0001_traces.down.sql) .
Directory Layout: Clustered vs. Unclustered#
The migrations directory contains two parallel subdirectories :
| Path | Used when |
|---|---|
migrations/clustered/ | Multi-node ClickHouse (replicated) |
migrations/unclustered/ | Single-node (standalone) ClickHouse |
The migration runner up.sh reads the CLICKHOUSE_CLUSTER_ENABLED environment variable to select the correct path:
CLICKHOUSE_CLUSTER_ENABLED=false→ runsmigrations/unclustered/withx-migrations-table-engine=MergeTree- Any other value → runs
migrations/clustered/withx-migrations-table-engine=ReplicatedMergeTreeandx-cluster-name=${CLICKHOUSE_CLUSTER_NAME}
The only material difference between paired files (e.g., clustered/0020_analytics_observations.up.sql vs unclustered/0020_analytics_observations.up.sql) is the presence of ON CLUSTER default in the clustered DDL and the use of ReplicatedReplacingMergeTree vs ReplacingMergeTree table engines .
Companion scripts down.sh and drop.sh follow the same topology-selection pattern for rollbacks and full teardown.
View Migration Patterns#
Two distinct patterns are used for migrating ClickHouse views, depending on whether the view is being created from scratch or updated in-place.
Pattern 1: CREATE VIEW + DROP VIEW (initial creation)#
When a view is first introduced, the up migration uses a plain CREATE VIEW and the down migration uses DROP VIEW IF EXISTS. Example: the analytics_observations view (migration 0020):
- Up —
0020_analytics_observations.up.sql:CREATE VIEW analytics_observations AS SELECT ... - Down —
0020_analytics_observations.down.sql:DROP VIEW IF EXISTS analytics_observations;
Clustered variants add ON CLUSTER default .
Pattern 2: CREATE OR REPLACE VIEW (in-place column additions)#
When an existing view is updated (e.g., to add a column), both the up and down migrations use CREATE OR REPLACE VIEW. The down migration simply re-declares the prior column list — no DROP is involved. This avoids downtime from a destructive drop/recreate cycle.
Example: migration 0036 adds ingested_sdks Map(String, UInt64) to analytics_scores :
- Up —
CREATE OR REPLACE VIEW analytics_scores [ON CLUSTER default] AS SELECT ..., sumMap(...) AS ingested_sdks ... - Down —
CREATE OR REPLACE VIEW analytics_scores [ON CLUSTER default] AS SELECT ...(omitsingested_sdks, restoring the previous definition)
The CREATE OR REPLACE pattern is used consistently for both clustered and unclustered variants; the only difference is the ON CLUSTER default clause .
Key Reference Files#
| File | Purpose |
|---|---|
scripts/up.sh | Migration runner; selects clustered vs. unclustered path |
scripts/down.sh | Rollback runner |
scripts/dev-tables.sh | Dev-only view bootstrap (not migration-managed) |
migrations/clustered/ | All clustered SQL files (0001–0036+) |
migrations/unclustered/ | All unclustered SQL files (0001–0036+) |
Note: Some analytics views (e.g.,
analytics_events_core) are defined indev-tables.shrather than numbered migration files, and require manualCREATE OR REPLACE VIEWper region for production deployments .
Adding a New Migration#
- Create
XXXX_<description>.up.sqlandXXXX_<description>.down.sqlin bothclustered/andunclustered/with the appropriate engine/cluster clauses. - For view additions: use
CREATE VIEWin.up.sqlandDROP VIEW IF EXISTSin.down.sql. - For view modifications: use
CREATE OR REPLACE VIEWin both.up.sqland.down.sql; the down file should restore the previous column set. - Run
up.sh(ordown.shto roll back) with the required environment variables (CLICKHOUSE_URL,CLICKHOUSE_MIGRATION_URL,CLICKHOUSE_USER,CLICKHOUSE_PASSWORD,CLICKHOUSE_CLUSTER_ENABLED) .