mavlitov98/clickhouse-migrations
A Laravel package for managing ClickHouse database migrations. Create, run, and roll back schema changes for ClickHouse tables using familiar migration workflows, helping you version your analytics database alongside your application.
This is a Symfony bundle, not a Laravel package — there is no service provider, no config/clickhouse.php, and no Artisan commands. Treat it as a component you either (a) run inside a Symfony service that owns the ClickHouse schema, or (b) read/fork if you're a Laravel shop that wants the migration-runner logic without Symfony.
Minimal steps in a Symfony app:
composer require mavlitov98/clickhouse-migrationsconfig/bundles.php:
Mavlitov98\ClickHouseMigrations\ClickHouseMigrationsBundle::class => ['all' => true],
config/packages/clickhouse_migrations.yaml) and env vars. Check the bundle's DependencyInjection/Configuration.php for exact keys — the README is thin.bin/console list clickhouse (or list | grep -i click) to discover the actual command names before assuming migrations:migrate exists.Where to look first: the bundle's Command/ directory and Configuration.php. Those two files tell you more than the README about command names, config keys, and migration file format.
First use case: create a single migration that builds your first MergeTree table (e.g. events) with PARTITION BY toYYYYMM(created_at), ORDER BY (event_name, created_at), and a TTL. Run it against a scratch ClickHouse database, verify with clickhouse-client --query "SHOW CREATE TABLE events", then wire it into your deploy pipeline. Don't start with ALTERs or materialized views — prove the runner works end-to-end on a CREATE first.
Migration file convention. Expect timestamped/versioned PHP (or SQL) files in a configured directory. Each migration has an up/down (or forward-only) pair. Because ClickHouse DDL is largely non-transactional, write every migration to be idempotent: CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, DROP ... IF EXISTS. This makes re-runs after a partial failure safe.
Schema migrations vs data migrations — keep them separate. Use this package for DDL only: table creation, ALTER TABLE ... ADD COLUMN, index/skipping-index changes, TTL changes, materialized view definitions. ClickHouse UPDATE/DELETE are heavyweight mutations that run asynchronously; never hide them inside a schema migration. Push backfills through your normal ingestion/job layer.
Deploy integration. Run migrations as an explicit release step (bin/console clickhouse:migrate or whatever the command is named), not on app boot. In CI:
status command first → assert the expected pending set.SHOW CREATE TABLE on the critical tables.Environment separation. Point dev at a local ClickHouse (docker run clickhouse/clickhouse-server), staging at a clustered/replicated instance, prod at the real cluster. If you use ReplicatedMergeTree or ON CLUSTER, the migration files must be cluster-aware — the bundle will not infer that for you.
If you're actually on Laravel. Don't fight the bundle. Options in order of preference: (1) use a Laravel-native ClickHouse package plus a raw-SQL migration runner; (2) shell out to bin/console clickhouse:migrate in a sidecar Symfony service that owns ONLY the ClickHouse schema; (3) fork this bundle's migration-runner class and call it from a custom Artisan command — it's small enough that copying the core loop is often cleaner than bridging the DI container.
Versioned table + review. Whatever the bundle uses for bookkeeping (likely a migrations table inside ClickHouse itself), treat those rows as part of your schema contract. Review DDL in PRs the same way you'd review a Postgres migration.
Framework mismatch is the biggest gotcha. A Symfony bundle in a Laravel codebase gives you zero autoloaded services, zero config publishing, zero commands. Budget for a small bridge/fork, and verify the package doesn't pull in Symfony Console/DI constraints that conflict with your Laravel version.
Maintenance risk is real. 9 stars, 0 dependents, last release Feb 2023. That's a "read the source before you ship it" package. Pin the exact version in composer.json, vendor-lock it, and assume you may own the code within a year. The upside: it's a focused bundle, likely a few hundred lines, so forking is tractable.
ClickHouse DDL is not transactional. A failed migration can leave a half-applied table. Design migrations so each step is independently safe (IF NOT EXISTS everywhere) and so down-migrations never assume the up fully succeeded. Test the failure path, not just the happy path.
ALTER is asynchronous. ALTER TABLE ... ADD COLUMN on large tables, and especially mutations, return before completion. If your migration runner waits for success, it may report "done" while the cluster is still catching up. For replicated tables, check system.mutations and system.replication_queue after a deploy.
Distributed/clustered setups. ON CLUSTER syntax, ReplicatedMergeTree paths, and ZooKeeper/Keeper coordination are not abstracted by the bundle. You'll hand-write these and you'll need a real staging cluster to test them — a single-node local ClickHouse will hide every distributed bug.
Debugging. When a migration "doesn't run," check in this order: (1) the migrations directory path in config actually resolves; (2) the connection points at the database you think it does (SELECT currentDatabase()); (3) the bookkeeping table's contents — clear a stuck row manually in dev if a migration half-applied; (4) run the underlying command with verbose output and, if available, a dry-run flag.
Extension points. The useful ones are: swapping the connection service (to use a cluster-aware client), overriding the migration command to add logging/alerting, and changing the migrations directory so you can ship migrations inside your app repo rather than the bundle. If the bundle doesn't expose config for something you need, a compiler pass overriding the service definition is usually less painful than editing vendor code.
Back up before ALTER. Especially on prod: snapshot the table (ALTER TABLE ... FREEZE or a partitioned copy) before structural changes. Restoring a ClickHouse table is far cheaper than reconstructing it from Kafka.
How can I help you explore Laravel packages today?