Add clickhouse-replicated dialect (#869) - #1107
Open
ringerc wants to merge 3 commits into
Open
Conversation
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
force-pushed
the
869-clickhouse-replicated
branch
2 times, most recently
from
August 13, 2026 02:24
c28fc72 to
e163a3b
Compare
…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
force-pushed
the
869-clickhouse-replicated
branch
from
August 13, 2026 02:32
e163a3b to
b5b288a
Compare
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>
Author
|
@mfridman Given recent changes you've made in goose I wonder if this PR would be of interest to you? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a new dialect
clickhouse-replicatedalongside the existingclickhousedialect. It targets multi-replica ClickHouse clusters and produces agoose_db_versionthat 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 onversion_id, withON CLUSTERDDL so it exists on every replica with consistent schema. State is derived from an append-only log of rows:INSERT (version_id, is_applied=1) SETTINGS insert_quorum=<Q>.INSERT (version_id, is_applied=0) SETTINGS insert_quorum=<Q>(a tombstone row). Same replicated durability guarantee as the up-insert. NoALTER … DELETEmutation is ever issued.version_idusingargMax(is_applied, tstamp), so the latest-tstamprow (whether apply or tombstone) wins.SETTINGS select_sequential_consistency=1so a query landing on a lagging replica waits until it has caught up to the last quorum-committed write.version_idare collapsed automatically by backgroundReplacingMergeTreemerges; 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 vestigialdate Date;tstampupgraded toDateTime64(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 aGOOSE_CLICKHOUSE_*env var so the CLI path works with no code changes:Entry points
Wired into all three dialect switches for parity with
clickhouse:database.NewStore,internal/legacystore.NewStore, and top-levelgoose.SetDialect. Hopefully I got this right.Tests
Unit tests cover the SQL shape for every
Queriermethod 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 underinternal/testing/integration/clickhouse-replicated/for local end-to-end testing but is NOT wired into CI in this change.Cluster-safety notes
goosemigrators 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.insert_quorumvalue as up-inserts → symmetric replicated durability for both directions.select_sequential_consistency=1is placed on the read queries (where it takes effect), not the INSERT (where it is a no-op).tstampisDateTime64(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