Repository navigation
Conversation
Mirrors each source table into a ReplacingMergeTree keyed by the source primary key and versioned by the event's LSN, created from the source catalog on first sight so there is no target DDL to write. The design follows from GTC's existing guarantees: - **Versioned by LSN** — replays are idempotent (a redelivered event has the same version) and a live change always beats a backfilled one for the same row, so backfill and streaming interleave safely. - **Deletes become tombstones** (_deleted), so a delete arriving out of order cannot resurrect a row. Query with FINAL WHERE _deleted = 0. - **Unchanged TOAST columns are carried forward.** ReplacingMergeTree replaces whole rows, so an update that left a large column untouched would blank it; the stored value is read back and rewritten. - **Batched inserts without weakening delivery.** A change stream is exactly the many-small-inserts pattern ClickHouse dislikes, so inserts go through server-side async batching *and wait for the flush*. CLICKHOUSE_WAIT_FOR_INSERT=false trades that for throughput and is warned about at startup and by doctor. - **DDL follows automatically** — the sink implements ports.SchemaObserver, so a column added upstream is added to the mirror. Dropped and retyped columns are logged, not applied: the mirror holds history the source no longer has, and discarding it silently would be worse than a warning. Column types come from the source catalog rather than from guessing at runtime, since CDC events carry values and not types; the sink keeps its own non-replication connection for that. Mapping is exact where ClickHouse has an equivalent and falls back to String (keeping PostgreSQL's exact text) for unconstrained numerics, arrays, JSON and user-defined types. Tables without a primary key cannot be deduplicated or addressed by a delete, so they are skipped with an error naming them while the rest keep mirroring. Verified end to end against a real ClickHouse 26.8 and PostgreSQL 16: auto-created schema, backfill, TOAST-preserving updates, delete tombstones, and ALTER TABLE ADD COLUMN reaching the mirror. Unit tests cover type mapping, DDL generation, value coercion and target naming; the integration test asserts the mirror's contents and picks up GTC_TEST_CLICKHOUSE_URL, which CI now provides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dEj24Ta2o9QxzW9ZrSPtb
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.
What does this PR do?
Mirrors each source table into a ClickHouse
ReplacingMergeTree, keyed by the source primary key and versioned by the event's LSN. Target tables are created from the source catalog on first sight, so there is no target DDL to write:Current state is
SELECT * FROM gtc.orders FINAL WHERE _deleted = 0.Each design decision follows from a guarantee GTC already makes:
ReplacingMergeTreereplaces whole rows, so an update that left a large column untouched would otherwise blank it; the stored value is read back and rewritten.ports.SchemaObserver, so a column added upstream is added to the mirror. Dropped and retyped columns are logged but not applied: the mirror holds history the source no longer has, and discarding it silently would be worse than a warning.Column types come from introspecting the source catalog, since CDC events carry values but not types (the sink keeps its own non-replication connection for this). Mapping is exact where ClickHouse has an equivalent —
int4→Int32,numeric(10,2)→Decimal(10, 2),timestamptz→DateTime64(6, 'UTC'),uuid→UUID— and falls back toString, preserving PostgreSQL's exact text, for unconstrainednumeric, arrays, JSON and user-defined types.Type of change
Checklist
go test ./...passes (including-race)gofmt/go vetare cleanGTC_TEST_CLICKHOUSE_URL, which CI now provides with a readiness waitREADME.md/CLAUDE.mdupdated (new "ClickHouse mirror" section, all env vars,config/sinks.example.yaml)CLICKHOUSE_WAIT_FOR_INSERT=falsedeliberately trades that for throughput and is warned about both at startup and bygtc doctor.Notes for the reviewer
Verified end to end against a real ClickHouse 26.8 and PostgreSQL 16, not a mock: auto-created schema, backfill, a TOAST-preserving update (a 5000-character column survived an update that touched only a sibling column), delete tombstones, and
ALTER TABLE ... ADD COLUMNreaching the mirror. The integration test ran green four times in a row.Tables without a primary key are skipped, with an error naming them, while the rest keep mirroring — without a key there is no
ORDER BY, so row versions cannot be collapsed and deletes cannot address a row. Failing the whole pipeline over one unmirrorable table would be worse.TRUNCATEis not mirrored. Emptying the target would race with rows still in flight, so it is logged as a warning and the target keeps the truncated rows. Worth a follow-up if you want it handled.🤖 Generated with Claude Code
https://claude.ai/code/session_019dEj24Ta2o9QxzW9ZrSPtb
Generated by Claude Code