$ psql -q -c "CREATE TABLE notes_demo (id bigserial PRIMARY KEY, created_at timestamptz NOT NULL, touched int NOT NULL DEFAULT 0, body text NOT NULL); INSERT INTO notes_demo(created_at, body) SELECT now() - interval '2 days' - g * interval '1 minute', CASE WHEN g % 10 = 0 THEN (SELECT string_agg(md5(random()::text), '') FROM generate_series(1,128)) ELSE 'short' END FROM generate_series(1,20000) g; ANALYZE notes_demo" $ chlift migrate plan --tables public.notes_demo --min-rows 1 --mirror chlift_unsafe --database chlift_unsafe --plan unsafe.yaml migrate public.notes_demo 20000 rows ORDER BY (created_at, id) toYYYYMM(created_at) note: sort key and partitioning cannot change after the table is created (PeerDB #4604): review them now note: needs REPLICA IDENTITY FULL: its sort key starts with created_at, and a DELETE (retention cleanup included) would otherwise leave the row live in ClickHouse postgres: ALTER TABLE "public"."notes_demo" REPLICA IDENTITY FULL postgres: CREATE PUBLICATION chlift_unsafe_pub FOR TABLE "public"."notes_demo" wrote unsafe.yaml: review it, then run `chlift migrate start` $ sed -i "/REPLICA IDENTITY FULL/d" unsafe.yaml && grep -A3 postgres_fixes unsafe.yaml postgres_fixes: - CREATE PUBLICATION chlift_unsafe_pub FOR TABLE "public"."notes_demo" $ chlift migrate start --plan unsafe.yaml --apply-postgres postgres: CREATE PUBLICATION chlift_unsafe_pub FOR TABLE "public"."notes_demo" clickhouse: database chlift_unsafe (Replicated) on 2 replica(s) peerdb: mirror chlift_unsafe created ({"workflowId":"chlift_unsafe-peerflow"}); snapshot then CDC. Follow with `chlift migrate status` $ sleep 40 $ psql -c "UPDATE notes_demo SET touched = touched + 1 WHERE id % 5 = 0" -c "DELETE FROM notes_demo WHERE id <= 1000" UPDATE 4000 DELETE 1000 $ sleep 40 $ chlift migrate check --plan unsafe.yaml --checksums FAIL public.notes_demo: postgres 19000 rows; replicas map[node-a:20000 node-b:20000] day 2026-10-01 pg=2 node-a=1002 node-b=1002 checksum public.notes_demo: 64 buckets x 5 columns (count, id, created_at, touched, body), 64 differ bucket id 1001..1297: pg=[297|341253|531868795722|59|120124] node-a=[593|593593|1061952053258|119|2670] node-b=[593|593593|1061952053258|119|2670] bucket id 1298..1594: pg=[297|429462|531863503182|59|124215] node-a=[297|429462|531863503182|59|1335] node-b=[297|429462|531863503182|59|1335] bucket id 1595..1891: pg=[297|517671|531858210642|60|124215] node-a=[297|517671|531858210642|60|1335] node-b=[297|517671|531858210642|60|1335] bucket id 1892..2188: pg=[297|605880|531852918102|59|120124] node-a=[297|605880|531852918102|59|1340] node-b=[297|605880|531852918102|59|1340] bucket id 2189..2485: pg=[297|694089|531847625562|60|124215] node-a=[297|694089|531847625562|60|1335] node-b=[297|694089|531847625562|60|1335] ... 59 more buckets differ in public.notes_demo chlift: 6 difference(s) found $ chlift migrate abort --plan unsafe.yaml peerdb: mirror chlift_unsafe dropped clickhouse: database chlift_unsafe dropped on 2 replica(s) postgres: publication chlift_unsafe_pub dropped; mirror slots left: 0 $ chlift migrate plan --tables public.notes_demo --min-rows 1 --mirror chlift_safe --database chlift_safe --plan safe.yaml migrate public.notes_demo 19000 rows ORDER BY (id) note: updated or deleted rows: sorted by primary key so de-duplication stays correct; queries need FINAL note: sort key and partitioning cannot change after the table is created (PeerDB #4604): review them now note: needs REPLICA IDENTITY FULL: it is updated and has TOAST-able columns (else unchanged long values arrive empty) postgres: ALTER TABLE "public"."notes_demo" REPLICA IDENTITY FULL postgres: CREATE PUBLICATION chlift_safe_pub FOR TABLE "public"."notes_demo" wrote safe.yaml: review it, then run `chlift migrate start` $ chlift migrate start --plan safe.yaml --apply-postgres postgres: ALTER TABLE "public"."notes_demo" REPLICA IDENTITY FULL postgres: CREATE PUBLICATION chlift_safe_pub FOR TABLE "public"."notes_demo" clickhouse: database chlift_safe (Replicated) on 2 replica(s) peerdb: mirror chlift_safe created ({"workflowId":"chlift_safe-peerflow"}); snapshot then CDC. Follow with `chlift migrate status` $ sleep 40 $ psql -c "UPDATE notes_demo SET touched = touched + 1 WHERE id % 5 = 1" -c "DELETE FROM notes_demo WHERE id BETWEEN 1001 AND 2000" UPDATE 3800 DELETE 1000 $ sleep 40 $ chlift migrate check --plan safe.yaml --checksums ok public.notes_demo: postgres 18000 rows; replicas map[node-a:18000 node-b:18000] checksum public.notes_demo: 64 buckets x 5 columns (count, id, created_at, touched, body), 0 differ