Skip to content

Add clickhouse-replicated dialect (#869) - #1107

Open
ringerc wants to merge 3 commits into
pressly:mainfrom
ringerc:869-clickhouse-replicated
Open

Add clickhouse-replicated dialect (#869)#1107
ringerc wants to merge 3 commits into
pressly:mainfrom
ringerc:869-clickhouse-replicated

Conversation

@ringerc

@ringerc ringerc commented Aug 12, 2026

Copy link
Copy Markdown

Adds a new dialect clickhouse-replicated alongside the existing clickhouse dialect. It targets multi-replica ClickHouse clusters and produces a goose_db_version that reliably replicates the schema version across the cluster.

Note

This PR was prepared with LLM assistance, but backed by close human design review within the limits of my knowledge of the codebase.

Design

The version table is created as ReplicatedReplacingMergeTree(tstamp) keyed on version_id, with ON CLUSTER DDL so it exists on every replica with consistent schema. State is derived from an append-only log of rows:

  • Up-migration: INSERT (version_id, is_applied=1) SETTINGS insert_quorum=<Q>.
  • Down-migration: INSERT (version_id, is_applied=0) SETTINGS insert_quorum=<Q> (a tombstone row). Same replicated durability guarantee as the up-insert. No ALTER … DELETE mutation is ever issued.
  • Reads: collapse per version_id using argMax(is_applied, tstamp), so the latest-tstamp row (whether apply or tombstone) wins.
  • All reads set SETTINGS select_sequential_consistency=1 so a query landing on a lagging replica waits until it has caught up to the last quorum-committed write.
  • Duplicate rows for the same version_id are collapsed automatically by background ReplacingMergeTree merges; No manual pruner, no lightweight-DELETE code path.

Why insert-mostly

ClickHouse mutations (ALTER … DELETE / ALTER … UPDATE) generate replication-log entries, contend with merges, and can stall on lagging replicas. For a schema-versioning tool, down-migrations are a routine, expected operation. Clickhouse docs advise against routinely using mutations, even lightweight delete/alter. So this dialect avoids them entirely.

Column layout

Deliberately diverges from stock clickhouse (no vestigial date Date; tstamp upgraded to DateTime64(6) so tombstones can be ordered at microsecond resolution) so an old CLI cannot silently mis-mutate a replicated table.

Configuration

Exposed as functional options on database.NewClickhouseReplicated(...), each defaulting to a GOOSE_CLICKHOUSE_* env var so the CLI path works with no code changes:

GOOSE_CLICKHOUSE_CLUSTER            (required)
GOOSE_CLICKHOUSE_ZK_PATH
GOOSE_CLICKHOUSE_REPLICA_NAME
GOOSE_CLICKHOUSE_INSERT_QUORUM      (default: auto — applied to both up and down writes)

Entry points

Wired into all three dialect switches for parity with clickhouse: database.NewStore, internal/legacystore.NewStore, and top-level goose.SetDialect. Hopefully I got this right.

Tests

Unit tests cover the SQL shape for every Querier method under both option-driven and env-var-driven configs, the missing-cluster error path, option-overrides-env, and quorum quoting on both up and down inserts. A two-node docker compose stack (ch1 + ch2 + embedded Keeper) is provided under internal/testing/integration/clickhouse-replicated/ for local end-to-end testing but is NOT wired into CI in this change.

Cluster-safety notes

  • This dialect does not make it safe to run goose migrators concurrently. Interlocking to prevent concurrent runs must still be done outside of Clickhouse itself to prevent this, as Clickhouse itself lacks a suitable feature like distributed advisory locks that would allow robust cross-cluster mutual exclusion.
  • Tombstone inserts reuse the same insert_quorum value as up-inserts → symmetric replicated durability for both directions.
  • select_sequential_consistency=1 is placed on the read queries (where it takes effect), not the INSERT (where it is a no-op).
  • tstamp is DateTime64(6); goose serializes writes to the version table under external locking, so tstamp collisions are effectively impossible. A formal tuple tiebreaker (e.g. argMax(is_applied, (tstamp, is_applied = 0))) is a documented future-proofing hook.

Refs: #869
Related: https://clickhouse.com/docs/concepts/best-practices/avoid-mutations

Adds a new dialect `clickhouse-replicated` alongside the existing
`clickhouse` dialect. It targets multi-replica ClickHouse clusters and
produces a `goose_db_version` that is safe under concurrent migrators.

- `ReplicatedMergeTree` table created with `ON CLUSTER`; relies on
  server macros for zk_path/replica_name by default, with explicit
  overrides available.
- `INSERT` uses `insert_quorum` and `select_sequential_consistency`.
- `ALTER … DELETE` uses `mutations_sync`; optional `ON CLUSTER`.
- Column layout deliberately diverges from stock `clickhouse` (no
  vestigial `date Date`, `tstamp` upgraded to `DateTime64(6)`) so an
  old CLI cannot silently mis-mutate a replicated table.

Configuration is exposed as functional options on
`database.NewClickhouseReplicated(...)`, each defaulting to a
`GOOSE_CLICKHOUSE_*` env var so the CLI path works with no code changes:

  GOOSE_CLICKHOUSE_CLUSTER            (required)
  GOOSE_CLICKHOUSE_ZK_PATH
  GOOSE_CLICKHOUSE_REPLICA_NAME
  GOOSE_CLICKHOUSE_INSERT_QUORUM      (default: auto)
  GOOSE_CLICKHOUSE_MUTATIONS_SYNC     (default: 2)
  GOOSE_CLICKHOUSE_DELETE_ON_CLUSTER  (default: false)

Wired into all three dialect switches for parity with `clickhouse`:
`database.NewStore`, `internal/legacystore.NewStore`, and top-level
`goose.SetDialect`. Missing cluster fails up front with
`ErrClickhouseReplicatedNoCluster`.

Unit tests cover the SQL shape for every Querier method under both
option-driven and env-var-driven configs, the missing-cluster error
path, option-overrides-env, and quorum quoting. A two-node docker
compose stack (ch1 + ch2 + embedded Keeper) is provided under
`internal/testing/integration/clickhouse-replicated/` for local
end-to-end testing but is NOT wired into CI in this change.

Refs: pressly#869

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ringerc
ringerc force-pushed the 869-clickhouse-replicated branch 2 times, most recently from c28fc72 to e163a3b Compare August 13, 2026 02:24
…ReplacingMergeTree

Down-migrations now insert a tombstone row (is_applied = 0) instead of issuing
an ALTER ... DELETE mutation, per ClickHouse's avoid-mutations guidance
(https://clickhouse.com/docs/concepts/best-practices/avoid-mutations). The
version table uses ReplicatedReplacingMergeTree(tstamp) so background merges
collapse duplicate rows per version_id automatically.

Read queries derive current state per version using argMax(is_applied, tstamp)
with select_sequential_consistency=1 so a query landing on a lagging replica
waits until it has caught up to the last quorum-committed write.

Removes the (unreleased) WithClickhouseMutationsSync and
WithClickhouseDeleteOnCluster options plus their env vars, which are no
longer applicable. GOOSE_CLICKHOUSE_INSERT_QUORUM /
WithClickhouseInsertQuorum now applies to both up and down writes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ringerc
ringerc force-pushed the 869-clickhouse-replicated branch from e163a3b to b5b288a Compare August 13, 2026 02:32
The prior wording implied that insert_quorum + select_sequential_consistency=1
provided cross-replica read-after-write correctness for the dialect as a
whole. In reality those settings only scope visibility of already-written
goose_db_version rows on the read side:

- They do not turn the bookkeeping insert into a compare-and-swap.
- They do not serialize concurrent readers against each other.
- They give no coordination for the migration DDL itself; two racing goose
  runs can each read the same 'latest applied' state, each decide the next
  migration is theirs, and each submit its ON CLUSTER DDL independently.

Reword the package doc, README section, and CHANGELOG entry accordingly,
and spell out that concurrent goose runs against the same cluster remain
unsafe and must be interlocked outside ClickHouse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ringerc

ringerc commented Sep 7, 2026

Copy link
Copy Markdown
Author

@mfridman Given recent changes you've made in goose I wonder if this PR would be of interest to you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant