clicko

package module
v0.0.0-...-99db599 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2026 License: MIT Imports: 14 Imported by: 0

README

clicko no smoking

A ClickHouse migration tool friendly for self-hosted sharded clusters, inspired by pressly/goose. Works with ClickHouse Cloud too. Supports engine selection, insert quorum, ON CLUSTER DDL, and both SQL file and Go function migrations.

Test Go Report Card

Features

  • Engine selection — Choose any ClickHouse table engine for the migration tracking table. Standalone mode defaults to MergeTree(); cluster mode defaults to ReplicatedMergeTree(...). Override with --engine to control the ZooKeeper path and replication topology.
  • Insert quorum — Set --insert-quorum (a number or "auto") to ensure migration records are replicated to N nodes before being considered applied. Prevents inconsistent migration state across replicas.
  • Cluster and sharding support — First-class ON CLUSTER support via --cluster. DDL propagates across all nodes; DELETE mutations use mutations_sync=2 for consistency.
  • SQL and Go migrations — Write migrations as plain .sql files or as Go functions.

Installation

go install github.com/arsura/clicko/cmd/clicko@latest

Migration files

SQL migration files follow the naming convention:

{version}_{description}.{up|down}.sql

Example directory:

migrations/
├── 00001_create_users.up.sql
├── 00001_create_users.down.sql
├── 00002_create_orders.up.sql
├── 00002_create_orders.down.sql
└── 00003_add_users_age_column.up.sql

Recommendations

Forward-only migrations

clicko supports down migrations, but for ClickHouse in production you may want to avoid writing them altogether.

ClickHouse tables tend to be large, and many DDL operations (like DROP COLUMN or ALTER TABLE ... DELETE) are executed as background mutations that can take a long time to complete and cannot be easily interrupted. Rolling back a migration by running a down file does not instantly undo the change — it queues another mutation on top, which can leave the cluster in an inconsistent state during the window between the two operations.

A forward-only approach means every change is expressed as a new up migration. Instead of rolling back, you write a follow-up migration that corrects or reverts the intent. This keeps the migration history append-only, predictable, and safe to apply in automated pipelines.

This is a recommendation, not a requirement. If your use case is well-suited to rollbacks (e.g. a small local cluster or a development environment), the down commands work fine.

Idempotent migrations

ClickHouse has no transactional DDL. If a migration with multiple statements fails halfway, the already-applied statements won't roll back. Re-running the migration will hit errors like "table already exists."

Write every statement in an idempotent form so re-runs are safe:

  • CREATE TABLE IF NOT EXISTS instead of CREATE TABLE
  • ALTER TABLE ... ADD COLUMN IF NOT EXISTS instead of ALTER TABLE ... ADD COLUMN
  • DROP TABLE IF EXISTS instead of DROP TABLE

One statement per migration

When possible, put a single DDL statement in each migration file. Since ClickHouse has no transactional DDL, a file with multiple statements can fail halfway — making it harder to tell what succeeded and what didn't. One statement per file keeps failures obvious and pairs well with idempotent writes.

Instead of one file with two statements:

-- 00002_create_orders.up.sql
CREATE TABLE IF NOT EXISTS orders (...) ENGINE = MergeTree() ORDER BY id;
CREATE TABLE IF NOT EXISTS order_items (...) ENGINE = MergeTree() ORDER BY id;

Split into two migration files:

-- 00002_create_orders.up.sql
CREATE TABLE IF NOT EXISTS orders (...) ENGINE = MergeTree() ORDER BY id;
-- 00003_create_order_items.up.sql
CREATE TABLE IF NOT EXISTS order_items (...) ENGINE = MergeTree() ORDER BY id;

Run migrations from CI/CD, not on application boot

ClickHouse has no advisory locks or distributed mutual exclusion. If you run migrations at application startup and deploy multiple instances at once, they will race — causing duplicate tracking rows, conflicting DDL, or partial failures.

Instead, run migrations as a dedicated, single-process step in your CI/CD pipeline (e.g. a Kubernetes Job or a CI stage) before deploying the new application version. This guarantees only one process touches the migration state at a time, sidestepping the locking problem entirely.

If you use the Go library, run it from a CI job — not as part of your application code.

ClickHouse Cloud

When using ClickHouse Cloud, you do not need to configure --cluster, --engine, or --insert-quorum — replication and clustering are managed automatically. Just connect with your URI and run migrations:

clicko --uri "clickhouse://default:YOUR_PASSWORD@YOUR_SERVICE.clickhouse.cloud:9440/default?secure=true" --dir migrations up

CLI usage

clicko --uri <uri> [flags] <command>

Flags

Flag Default Description
--uri (required) ClickHouse connection URI (e.g. clickhouse://user:pass@host:9000/db)
--dir migrations Directory containing migration files
--table migration_versions Migration tracking table name
--cluster ClickHouse cluster name (enables ON CLUSTER)
--engine Custom table engine for the tracking table
--insert-quorum Write quorum for cluster inserts (number or "auto")
--dry-run Print the SQL each command would execute without applying
--help Show help

Commands

Command Description
up Apply all pending migrations
up-to <version> Apply migrations up to a specific version
down Rollback the last applied migration
down-to <version> Rollback migrations down to a specific version
reset Rollback all applied migrations
status Show migration status

Example

clicko --uri "clickhouse://default:@localhost:9000/default" --dir migrations up

Dry-run mode

Use --dry-run to preview the SQL that would be executed without actually applying or reverting any migrations. This works with all commands (up, up-to, down, down-to, reset).

clicko --uri "clickhouse://default:@localhost:9000/default" --dir migrations up --dry-run

Output:

=== Version 1: create users (sql) ===
CREATE TABLE IF NOT EXISTS users (...) ENGINE = MergeTree() ORDER BY id;

=== Version 2: create orders (sql) ===
CREATE TABLE IF NOT EXISTS orders (...) ENGINE = MergeTree() ORDER BY id;

For Go function migrations, --dry-run captures every Exec and Query call the function makes against a no-op connection, so dynamically-built SQL is shown in its final form.

Cluster mode

clicko \
  --uri "clickhouse://default:@localhost:9000/default" \
  --dir migrations \
  --cluster migration \
  --engine "ReplicatedMergeTree('/clickhouse/migration/table/{database}/{table}', '{replica}')" \
  --insert-quorum 4 \
  up

Go library

Besides the CLI, clicko can be embedded as a Go library. This lets you run migrations as part of your CI pipeline, write integration tests against a local cluster, and programmatically target different environments — no risky manual access to the cluster required.

package main

import (
    "context"
    "log"

    "github.com/ClickHouse/clickhouse-go/v2"
    "github.com/arsura/clicko"

    _ "your/app/migrations" // blank import to register Go migrations via init()
)

func main() {
    ctx := context.Background()

    opts, err := clickhouse.ParseDSN("clickhouse://default:@localhost:9000/default")
    if err != nil {
        log.Fatal(err)
    }
    conn, err := clickhouse.Open(opts)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    migrator, err := clicko.New(conn, clicko.StoreConfig{
        TableName:    "migration_versions",
        Cluster:      "migration",
        CustomEngine: "ReplicatedMergeTree('/clickhouse/migration/table/{database}/{table}', '{replica}')",
        InsertQuorum: "4",
    })
    if err != nil {
        log.Fatal(err)
    }

    // Optional: preview SQL without applying.
    // migrator.SetDryRun(true)

    if err := migrator.Up(ctx); err != nil {
        log.Fatal(err)
    }
}

See Go integration example for the full walkthrough including Go function migrations with clicko.RegisterMigration.

Migrations on a sharded cluster

When your ClickHouse cluster has multiple shards, the data cluster (e.g. dev) splits nodes into separate shards — each shard only replicates within its own group. This is great for data but problematic for migration tracking: if you run ON CLUSTER dev, the migration table gets sharded too, and each shard may end up with an independent copy of migration state.

The solution is to define a logical cluster dedicated to migrations. This cluster puts all replicas from every shard into a single shard, so the migration tracking table replicates uniformly across the entire cluster.

For example, the dev/cluster setup defines two clusters:

  • dev — the data cluster with 2 shards x 2 replicas:
dev
├── shard 1: ch-1-1, ch-1-2
└── shard 2: ch-2-1, ch-2-2
  • migration — a logical cluster with a single shard containing all 4 nodes:
migration
└── shard 1: ch-1-1, ch-1-2, ch-2-1, ch-2-2

When you run clicko with --cluster migration, the migration tracking table is created ON CLUSTER migration and every node sees the same replicated migration state. Pair this with a custom engine whose ZooKeeper path does not include {shard}, and --insert-quorum to guarantee writes reach all replicas before returning:

clicko \
  --cluster migration \
  --engine "ReplicatedMergeTree('/clickhouse/migration/table/{database}/{table}', '{replica}')" \
  --insert-quorum 4 \
  ...

Your actual data migrations can still use ON CLUSTER dev inside the SQL files themselves.

Examples

  • CLI example — SQL file migrations via the CLI
  • Go example — Go function migrations embedded in an application

Development

The dev/cluster directory contains a Docker Compose setup for a local ClickHouse cluster (2 shards x 2 replicas + 1 ClickHouse Keeper node).

Start the cluster:

make cluster-up

Run tests:

make test

Other commands:

make cluster-down       # stop and remove volumes
make cluster-restart    # restart the cluster
make build              # build the CLI binary to bin/clicko

Documentation

Index

Constants

View Source
const (
	MigrationDirectionUp   string = "up"
	MigrationDirectionDown string = "down"
)
View Source
const (
	DefaultTableName = "migration_versions"
)

Variables

This section is empty.

Functions

func RegisterMigration

func RegisterMigration(up, down GoMigrationFunc)

RegisterMigration registers a Go migration using the caller's filename to derive the version number. The filename must start with a numeric prefix (e.g. 20250317141923_create_users.go).

Panics if the version conflicts with an already-registered migration.

func RegisterNamedMigration

func RegisterNamedMigration(filename string, up, down GoMigrationFunc)

RegisterNamedMigration registers a Go migration with an explicit filename. The version is parsed from the leading numeric component of the base filename (e.g. "20250317141923_create_users.go" → version 20250317141923).

Panics if the version conflicts with an already-registered migration.

func ResetGlobalMigrations

func ResetGlobalMigrations()

ResetGlobalMigrations clears all registered Go migrations. Intended for use in tests.

Types

type GoMigrationFunc

type GoMigrationFunc func(ctx context.Context, conn clickhouse.Conn) error

GoMigrationFunc is a Go migration function that receives a ClickHouse connection. ClickHouse has no transaction support, so the function operates directly on the connection.

type Loader

type Loader interface {
	Load() ([]*Migration, error)
}

Loader loads migration definitions from a source.

func NewGoLoader

func NewGoLoader() Loader

NewGoLoader returns a Loader that reads migrations from the global Go migration registry populated by RegisterMigration / RegisterNamedMigration.

func NewSQLLoader

func NewSQLLoader(dir string) Loader

NewSQLLoader returns a clicko.Loader that reads SQL migration files from dir.

type Migration

type Migration struct {
	Version     uint64
	Description string
	AppliedAt   time.Time
	Source      MigrationSource
}

Migration represents a single migration.

type MigrationSource

type MigrationSource struct {
	Type     MigrationSourceType
	UpSQL    string
	DownSQL  string
	UpFunc   GoMigrationFunc
	DownFunc GoMigrationFunc
}

MigrationSource holds the migration's executable content — either SQL strings loaded from files, Go functions registered programmatically, or both. When both are set, Go functions take precedence.

func (MigrationSource) HasDown

func (s MigrationSource) HasDown() bool

HasDown reports whether this migration source has a down (rollback) definition. Migrations without a down definition are treated as forward-only and skipped during rollback operations without error.

type MigrationSourceType

type MigrationSourceType string
const (
	MigrationSourceTypeSQL MigrationSourceType = "sql"
	MigrationSourceTypeGo  MigrationSourceType = "go"
)

type Migrator

type Migrator struct {
	// contains filtered or unexported fields
}

Migrator orchestrates loading migrations (SQL files or Go functions), comparing them against the applied state in ClickHouse, and executing them.

func New

func New(conn clickhouse.Conn, cfg StoreConfig) (*Migrator, error)

New creates a Migrator for Go-based migrations. It sets up the version-tracking store and Go migration loader internally.

func NewMigrator

func NewMigrator(conn clickhouse.Conn, loader Loader, store Store) *Migrator

NewMigrator creates a Migrator with the given connection, loader, and store. For most use cases, prefer New, which wires up the store and Go loader automatically.

func (*Migrator) Down

func (m *Migrator) Down(ctx context.Context) error

Down reverts the last applied migration.

func (*Migrator) DownTo

func (m *Migrator) DownTo(ctx context.Context, target uint64) error

DownTo reverts all applied migrations down to (but not including) the target version.

func (*Migrator) Reset

func (m *Migrator) Reset(ctx context.Context) error

Reset reverts all applied migrations.

func (*Migrator) SetDryRun

func (m *Migrator) SetDryRun(enabled bool)

SetDryRun enables or disables dry-run mode. When enabled, commands print the SQL each migration would execute instead of running it.

func (*Migrator) Status

func (m *Migrator) Status(ctx context.Context) error

Status prints a table showing each migration's version, description, status, and when it was applied.

func (*Migrator) Up

func (m *Migrator) Up(ctx context.Context) error

Up applies all pending migrations.

func (*Migrator) UpTo

func (m *Migrator) UpTo(ctx context.Context, target uint64) error

UpTo applies pending migrations up to and including the target version.

type Store

type Store interface {
	EnsureTable(ctx context.Context) error
	GetAppliedVersions(ctx context.Context) (map[uint64]*Migration, error)
	Add(ctx context.Context, version uint64, description string) error
	Remove(ctx context.Context, version uint64) error
}

Store provides read/write access to the migration state stored in ClickHouse.

func NewStore

func NewStore(conn clickhouse.Conn, config StoreConfig) (Store, error)

NewStore creates a Store backed by the given ClickHouse connection. Returns an error if any config value fails validation.

type StoreConfig

type StoreConfig struct {
	TableName    string
	Cluster      string
	CustomEngine string
	// InsertQuorum controls the insert_quorum setting for migration writes in cluster mode.
	// Set this to the total number of nodes in the cluster (shards × replicas per shard)
	// so every node must acknowledge the write before it is considered successful.
	// This is necessary because the migration table is replicated across all nodes via a single
	// ZooKeeper path — a node that missed the write would report the migration as not applied.
	// Accepts a positive integer (e.g. "6" for 3 shards × 2 replicas) or "auto".
	// Has no effect when Cluster is not set.
	// https://clickhouse.com/docs/operations/settings/settings#insert_quorum
	InsertQuorum string
}

StoreConfig holds configuration for the migration state store. All string values that are interpolated into SQL (TableName, Cluster, InsertQuorum) are validated in NewStore to prevent injection.

func (StoreConfig) IsCluster

func (c StoreConfig) IsCluster() bool

func (StoreConfig) ResolveEngine

func (c StoreConfig) ResolveEngine() string

ResolveEngine returns the engine clause to use when creating the migration table. Priority: CustomEngine > ReplicatedMergeTree (cluster, with warning) > MergeTree.

Directories

Path Synopsis
cmd
clicko command
example
go command
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL