Alembic-based migrations for ClickHouse, optimized for ClickHouse Cloud.
ch-migrate is a CLI tool for managing ClickHouse schema migrations. It:
- Initializes project structure with config files
- Bootstraps databases with proper roles and users
- Runs Alembic migrations against ClickHouse
- Multi-environment support - Manage dev, staging, and production with separate configs
- ClickHouse Cloud compatible - Uses standard engines (auto-upgraded to Shared* on Cloud), handles non-transactional DDL
- Role-based access control - Creates roles for migration, read-only (MCP), and dictionary access
- Object-centric SQL history - Track all versions of each table/view/dictionary
- Zero-downtime migrations -
EXCHANGE TABLESpattern for safe schema changes - Bootstrap command - One-command database and user setup (idempotent)
- SSM support - Store credentials in AWS SSM Parameter Store
- YAML + env vars - Config in version control, secrets in environment
For CLI usage (recommended for most users):
# Install globally - ch-migrate available everywhere
uv tool install git+https://github.com/DRYCodeWorks/clickhouse-migrate.git
# Verify installation
ch-migrate --versionAs a project dependency (for importing in Python code):
# Add to your project
uv add git+https://github.com/DRYCodeWorks/clickhouse-migrate.git
# Run via uv
uv run ch-migrate --version
# Or with pip
pip install git+https://github.com/DRYCodeWorks/clickhouse-migrate.gitPin to a specific version:
uv tool install git+https://github.com/DRYCodeWorks/clickhouse-migrate.git@v0.1.0# Create a new project
mkdir my-clickhouse-project
cd my-clickhouse-project
# Initialize with ch-migrate
ch-migrate init
# Or initialize in current directory with custom name
ch-migrate init --name my_projectThis creates:
my-clickhouse-project/
├── config.yaml # ClickHouse hosts and settings
├── .env.local.example # Template for secrets
├── .gitignore
├── alembic.ini
└── migrations/
├── env.py
├── script.py.mako
├── versions/ # Migration files go here
└── sql/
├── bootstrap/
└── history/
├── tables/
├── views/
└── dictionaries/
- Edit
config.yamlwith your ClickHouse hosts:
project:
name: my_project
defaults:
port: 8443
secure: true
admin_user: default
# Optional users (uncomment to enable):
# dict_reader_name: dict_reader
# mcp_user_name: mcp_reader
environments:
dev:
host: my-dev-instance.clickhouse.cloud
database: my_project_dev
migration_user: migration_dev
production:
host: my-prod-instance.clickhouse.cloud
database: my_project
migration_user: migration_prod- Set up credentials (choose one):
Option A: Environment file
cp .env.local.example .env.local
# Edit with your passwords:
CH_DEV_MIGRATION_PASSWORD=your-dev-password
CH_DEV_ADMIN_PASSWORD=your-dev-admin-passwordOption B: AWS SSM (for production)
# In config.yaml, add ssm paths:
environments:
production:
host: my-prod-instance.clickhouse.cloud
database: my_project
migration_user: migration_prod
ssm:
admin_password: /my_project/prod/admin_password
migration_password: /my_project/prod/migration_password# Creates database, roles, and users
ch-migrate bootstrap dev
# Preview without executing
ch-migrate bootstrap dev --dry-run# Create a new migration
ch-migrate new dev create_users_table
# Run pending migrations
ch-migrate up dev
# Check status
ch-migrate status dev
# Rollback last migration
ch-migrate down dev
# Show history
ch-migrate history dev- Create SQL file in
migrations/sql/history/tables/users/001_<revision>.sql:
CREATE TABLE {db}.users (
id UInt64,
email String,
name String,
created_at DateTime DEFAULT now()
)
ENGINE = MergeTree
ORDER BY id- Reference in migration:
from alembic import op
from clickhouse_alembic import get_db, read_sql
def upgrade():
db = get_db()
op.execute(read_sql("history/tables/users/001_abc123.sql", db=db))
def downgrade():
db = get_db()
op.execute(f"DROP TABLE IF EXISTS {db}.users")Use EXCHANGE TABLES for data-preserving migrations:
def upgrade():
db = get_db()
# 1. Create new table with updated schema
op.execute(read_sql("history/tables/users/002_def456.sql", db=db)
.replace(f"{db}.users", f"{db}.users_new"))
# 2. Migrate data with transformations
op.execute(f"""
INSERT INTO {db}.users_new
SELECT id, email, name, created_at, '' as phone
FROM {db}.users
""")
# 3. Atomic swap
op.execute(f"EXCHANGE TABLES {db}.users AND {db}.users_new")
# 4. Drop old table
op.execute(f"DROP TABLE {db}.users_new")from clickhouse_alembic import create_dictionary
def upgrade():
# Automatically grants SELECT to dict_reader on source table
create_dictionary("history/dictionaries/dict_users/001_abc123.sql")# Initialize new project
ch-migrate init [PATH] [--name NAME]
# Bootstrap database (create DB, roles, users)
ch-migrate bootstrap <environment> [--dry-run]
# Apply pending migrations
ch-migrate up <environment>
# Rollback last migration
ch-migrate down <environment> [--revision REV]
# Show current status
ch-migrate status <environment>
# Show migration history
ch-migrate history <environment>
# Create new migration
ch-migrate new <environment> <name>project:
name: my_project # Project identifier
defaults: # Inherited by all environments
port: 8443
secure: true
admin_user: default
# dict_reader_name: dict_reader # Uncomment to enable
# mcp_user_name: mcp_reader # Uncomment to enable
environments:
dev:
host: dev.clickhouse.cloud
database: my_project_dev
migration_user: migration_dev
# SSM paths (optional, if set fetches from SSM directly):
# ssm:
# admin_password: /my_project/dev/admin_password
# migration_password: /my_project/dev/migration_password| Variable | Required | Description |
|---|---|---|
CH_<ENV>_MIGRATION_PASSWORD |
Yes | Migration user password |
CH_<ENV>_ADMIN_PASSWORD |
For bootstrap | Admin password for creating users |
CH_<ENV>_DICT_READER_PASSWORD |
If dict_reader enabled | dict_reader user password |
CH_<ENV>_MCP_PASSWORD |
If mcp_user enabled | MCP user password |
Legacy CH_<ENV>_PASSWORD is supported for backward compatibility.
Bootstrap creates the following roles:
| Role | Purpose |
|---|---|
{project}_migration_role |
Full migration access, including explicit system.grants introspection (required) |
{project}_readonly_role |
Read-only for MCP tools (optional) |
{project}_dict_role |
Dictionary source access (optional) |
- Uses standard engines (
MergeTree,ReplacingMergeTree) - auto-upgraded toShared*variants on Cloud - Native port 9440 maps to HTTP port 8443
- No transactional DDL - migrations can't be atomically rolled back
- Each
op.execute()runs one statement (no multi-statement batches)
MIT License - see LICENSE file for details.
Dan Young