PostgreSQL to ClickHouse CDC library and replicator with durable checkpoints
///|
import {
"pangbit/mooncdc",
}///|
test "parse a PostgreSQL WAL position" {
let position = @mooncdc.Lsn::parse("16/B374D848")
assert_eq(position.to_string(), "16/B374D848")
assert_true(position > @mooncdc.Lsn::parse("0/FFFFFFFF"))
}///|
test "pipeline health before startup and during shutdown" {
let control = @mooncdc.ApplyControl()
assert_true(control.health().live)
assert_false(control.health().ready)
assert_eq(control.activities().length(), 0)
control.stop()
assert_eq(control.health().status, "stopping")
}let config = @mooncdc.ConnectionConfig::new(
host="127.0.0.1", port=55418, user="cdc",
password=password_from_environment, database="postgres",
security=@mooncdc.LocalPlaintext,
)
@mooncdc.subscribe(
config, slot="my_slot", publication="my_publication",
start=@mooncdc.Resume, checkpoint_path="./state/checkpoint",
sub => {
for ;; {
let tx = sub.next()
persist_transaction_idempotently(tx) // 业务内容与 tx.id 一起持久化
sub.ack(tx) // 之后才允许推进源端反馈
}
},
)moon update
docker compose -p mooncdc-test -f integration/compose.yaml up -d --wait
moon run tools/setup-db.mbtx pg18
moon run tools/setup-db.mbtx pg17
moon check --target native --deny-warn
moon test --target native
moon run tools/setup-tls.mbtx pg18
moon run tools/setup-tls.mbtx pg17
MOONCDC_TEST_PORT=55418 MOONCDC_TEST_CA=.test-artifacts/tls-pg18/server.crt moon run tools/live-test.mbtx
MOONCDC_TEST_PORT=55417 MOONCDC_TEST_CA=.test-artifacts/tls-pg17/server.crt moon run tools/live-test.mbtx
moon run examples/index
moon run examples/cache
moon run tools/recovery-test.mbtx
moon run tools/checkpoint-crash-test.mbtxpub(open) trait Destination {
fn supports_wal_batches(Self) -> Bool = _
async fn write_wal_batch(Self, WalBatch) -> WriteStatus = _
async fn bind_state(Self, StateSession) -> Unit = _
async fn release_state(Self) -> Unit = _
async fn bind_snapshot_schema(Self, Relation, SchemaSnapshot) -> Unit = _
async fn startup(Self) -> Unit = _
async fn shutdown(Self) -> Unit = _
async fn schema_retention(Self, UInt) -> SchemaId? = _
async fn reset_table(Self, Relation, attempt~ : String) -> Unit
async fn write_rows(Self, Relation, ReadOnlyArray[ReadOnlyArray[Value]], TableCopyBatchId?) -> WriteStatus
async fn finish_table_copy(Self, Relation) -> Unit = _
async fn write_transaction(Self, Transaction) -> WriteStatus
async fn flush(Self) -> Unit
}pub(open) trait SchemaStore {
async fn get(Self, UInt, SchemaId) -> SchemaSnapshot?
async fn all(Self) -> ReadOnlyArray[SchemaSnapshot]
async fn put(Self, SchemaSnapshot) -> Unit
async fn prune(Self, ReadOnlyArray[(UInt, SchemaId)]) -> Int
}pub(open) trait StateStore {
async fn acquire(Self, owner : String) -> Unit
async fn release(Self, owner : String) -> Unit
async fn load(Self, StateRecord) -> Bytes?
async fn save(Self, owner : String, StateRecord, Bytes) -> Unit
}pub(all) suberror CdcError {
Protocol(String)
Conversion(String)
Authentication(String)
Server(String, String)
InvalidConfig(String)
LimitExceeded(String)
UnsafeResume(String)
CheckpointError(String)
Closed
} derive(Debug)pub(all) suberror TableReplicationError {
Retryable(String)
Manual(String)
Permanent(String)
} derive(Debug)pub struct ApplyControl {
// private fields
}fn ApplyControl::prometheus(self : ApplyControl, pipeline_id? : String, destination? : String, project? : String, stall_timeout_ms? : Int) -> String raisepub struct BatchConfig {
memory_budget_ratio : Float
max_bytes : UInt64
max_fill_ms : Int
}fn BatchConfig::new(memory_budget_ratio? : Float, max_bytes? : UInt64, max_fill_ms? : Int) -> BatchConfigpub(all) enum Cell {
SqlNull
ToastUnchanged
Bool(Bool)
String(String)
I16(Int16)
I32(Int)
I64(Int64)
U32(UInt)
F32(Float)
F64(Double)
Numeric(String)
Date(PgDate)
Time(PgTime)
TimeTz(PgTime, offset_seconds~ : Int)
Timestamp(PgTimestamp)
TimestampTz(PgTimestamp)
Uuid(Bytes)
Json(Json)
Bytes(Bytes)
Array(ReadOnlyArray[Cell])
} derive(Debug)pub(all) enum Change {
RelationChanged(Relation)
TypeChanged(TypeInfo)
Insert(Relation, ReadOnlyArray[Value])
Update(Relation, OldTuple?, ReadOnlyArray[Value])
Delete(Relation, OldTuple)
Truncate(ReadOnlyArray[Relation], cascade~ : Bool, restart_identity~ : Bool)
Origin(Lsn, String)
Message(LogicalMessage)
} derive(Eq, ToJson, Debug)pub struct ClickHouseDestination {
// private fields
}impl Destination for ClickHouseDestinationasync fn bind_snapshot_schema(self : ClickHouseDestination, relation : Relation, schema : SchemaSnapshot) -> Unitasync fn write_rows(self : ClickHouseDestination, relation : Relation, rows : ReadOnlyArray[ReadOnlyArray[Value]], id : TableCopyBatchId?) -> WriteStatusasync fn ClickHouseDestination::bind_snapshot_schema(self : ClickHouseDestination, relation : Relation, schema : SchemaSnapshot) -> Unitasync fn ClickHouseDestination::bind_state(self : ClickHouseDestination, session : StateSession) -> Unitasync fn ClickHouseDestination::finish_table_copy(_self : ClickHouseDestination, _relation : Relation) -> Unitfn ClickHouseDestination::new(endpoint~ : String, user~ : String, password~ : String, database~ : String, publication~ : String, schema_store~ : &SchemaStore, engine? : ClickHouseEngine, max_insert_bytes? : Int, timeout_ms? : Int, connectivity_timeout_ms? : Int, schema_timeout_ms? : Int, ddl_timeout_ms? : Int, insert_timeout_ms? : Int, client_timeout_epsilon_ms? : Int) -> ClickHouseDestination raiseasync fn ClickHouseDestination::reset_table(self : ClickHouseDestination, relation : Relation, attempt~ : String) -> Unitasync fn ClickHouseDestination::schema_retention(self : ClickHouseDestination, oid : UInt) -> SchemaId?async fn ClickHouseDestination::write_rows(self : ClickHouseDestination, relation : Relation, rows : ReadOnlyArray[ReadOnlyArray[Value]], id : TableCopyBatchId?) -> WriteStatusasync fn ClickHouseDestination::write_transaction(self : ClickHouseDestination, tx : Transaction) -> WriteStatusasync fn ClickHouseDestination::write_wal_batch(self : ClickHouseDestination, batch : WalBatch) -> WriteStatuspub(all) struct ConnectionConfig {
host : String
hostaddr : String?
port : Int
user : String
password : String
database : String
security : Security
timeout_ms : Int
keepalive : TcpKeepaliveConfig
}fn ConnectionConfig::new(host~ : String, user~ : String, password? : String, database? : String, port? : Int, security? : Security, timeout_ms? : Int, hostaddr? : String, keepalive? : TcpKeepaliveConfig) -> ConnectionConfig raisefn DestinationEvents::replicated_schemas(self : DestinationEvents, index : Int) -> ReadOnlyArray[ReplicatedSchema]pub struct FileStateStore {
// private fields
}impl StateStore for FileStateStoreasync fn FileStateStore::save(self : FileStateStore, owner : String, key : StateRecord, bytes : Bytes) -> Unitfn InvalidatedSlotBehavior::not_equal(x : InvalidatedSlotBehavior, y : InvalidatedSlotBehavior) -> Boolfn LogicalMessage::schema_snapshot(self : LogicalMessage, commit_lsn~ : Lsn, publication~ : String) -> SchemaSnapshot? raisepub struct MemoryBackpressureConfig {
activate_threshold : Float
resume_threshold : Float
refresh_interval_ms : Int
}fn MemoryBackpressureConfig::new(activate_threshold? : Float, resume_threshold? : Float, refresh_interval_ms? : Int) -> MemoryBackpressureConfigpub struct PipelineHealth {
lifecycle : PipelineLifecycle
live : Bool
ready : Bool
status : String
} derive(Eq, Debug)pub struct PipelineStatus {
slot : String
destination_id : String
tables : ReadOnlyArray[TableSyncStatus]
} derive(Debug)pub struct PostgresStateStore {
// private fields
}impl StateStore for PostgresStateStorefn PostgresStateStore::new(config : ConnectionConfig, store_id~ : String, schema? : String, table? : String, control? : ApplyControl) -> PostgresStateStore raiseasync fn PostgresStateStore::save(self : PostgresStateStore, owner : String, key : StateRecord, bytes : Bytes) -> Unitpub struct ReplicatedSchema {
schema : SchemaSnapshot
replicated : ReadOnlyArray[Bool]
identity : ReadOnlyArray[Bool]
} derive(Eq, Debug)fn ReplicatedSchema::decode_row(self : ReplicatedSchema, values : ReadOnlyArray[Value]) -> ReadOnlyArray[Cell] raisefn ReplicatedSchema::new(schema : SchemaSnapshot, replicated~ : ReadOnlyArray[Bool], identity~ : ReadOnlyArray[Bool]) -> ReplicatedSchema raisefn ReplicatedSchema::plan_change(self : ReplicatedSchema, after : ReplicatedSchema, map_name? : (String) -> String) -> SchemaPlan raisefn ReplicatedSchema::with_schema(self : ReplicatedSchema, schema : SchemaSnapshot) -> ReplicatedSchema raisepub(all) enum SchemaOperation {
DropColumn(SchemaColumn, reason~ : ColumnPresenceReason)
AddColumn(SchemaColumn, reason~ : ColumnPresenceReason)
AlterColumn(before~ : SchemaColumn, after~ : SchemaColumn, kind~ : ColumnAlteration)
} derive(Eq, Debug)pub struct SchemaPlan {
operations : ReadOnlyArray[SchemaOperation]
primary_key_changed : Bool
has_rename_cycles : Bool
} derive(Eq, Debug)pub struct SchemaSnapshot {
id : SchemaId
oid : UInt
namespace_ : String
name : String
replica_identity : Byte
columns : ReadOnlyArray[SchemaColumn]
// private fields
} derive(Debug)impl Eq for SchemaSnapshotimpl ToJson for SchemaSnapshotfn SchemaSnapshot::relation(self : SchemaSnapshot, published : ReadOnlyArray[String]) -> Relation raisepub struct StateSession {
// private fields
}pub struct Subscription {
// private fields
}async fn Subscription::apply_to(self : Subscription, destination : &Destination, control~ : ApplyControl, flush_every? : Int, flush_interval_ms? : Int) -> Unitpub(all) enum TableSyncPhase {
Pending
Copying
Catchup(Lsn)
Ready(Lsn)
Errored(reason~ : String, retry~ : TableRetryPolicy, attempts~ : Int)
} derive(Eq, Debug)fn TcpKeepaliveConfig::new(idle_secs? : Int, interval_secs? : Int, retries? : Int) -> TcpKeepaliveConfigimpl ToJson for Transactionfn Transaction::replicated_schemas(self : Transaction, index : Int) -> ReadOnlyArray[ReplicatedSchema]async fn copy_snapshot(connection : ConnectionConfig, slot~ : String, publication~ : String, consume : async (SnapshotEvent) -> Unit, batch_rows? : Int, copy_connections? : Int, max_frame_bytes? : Int, max_batch_bytes? : Int, slot_failover? : Bool) -> SnapshotPositionasync fn copy_snapshot_to(connection : ConnectionConfig, destination : &Destination, slot~ : String, publication~ : String, batch_rows? : Int, copy_connections? : Int, max_frame_bytes? : Int, max_batch_bytes? : Int, schema_store? : &SchemaStore, slot_failover? : Bool) -> SnapshotPositionasync fn run_pipeline(connection : ConnectionConfig, destination : &Destination, publication~ : String, destination_id~ : String, control~ : ApplyControl, state_path? : String, state_store? : &StateStore, slot_prefix? : String, copy_concurrency? : Int, copy_connections_per_table? : Int, table_sync_monitor_refresh_interval_ms? : Int, batch_rows? : Int, schema_store? : &SchemaStore, table_retry_delay_ms? : Int, max_table_retries? : Int, table_sync_copy? : TableSyncCopy, source_migrations? : Bool, slot_failover? : Bool, invalidated_slot_behavior? : InvalidatedSlotBehavior, memory_backpressure? : MemoryBackpressureConfig, batch? : BatchConfig, memory_refresh_interval_ms? : Int) -> Unitasync fn[T] subscribe(connection : ConnectionConfig, slot~ : String, publication~ : String, start~ : Start, consume : async (Subscription) -> T, checkpoint_path? : String, checkpoint_store? : &StateStore, max_frame_bytes? : Int, max_transaction_bytes? : Int, max_transaction_events? : Int, max_pending_transactions? : Int, max_metadata_entries? : Int, heartbeat_ms? : Int, max_reconnects? : Int, reconnect_delay_ms? : Int, binary? : Bool, messages? : Bool, protocol? : PgOutputProtocol, nontransactional_message? : async (LogicalMessage) -> Unit, backpressure? : Bool) -> Tfn validate_metrics_listen_address(address : String) -> Unit raiseasync fn[T] with_file_schema_store(path : String, identity~ : String, run : async (&SchemaStore) -> T) -> Tasync fn[T] with_health_server(control : ApplyControl, run : async () -> T, port? : Int, stall_timeout_ms? : Int) -> Tasync fn[T] with_metrics_server(control : ApplyControl, run : async () -> T, port? : Int, listen_address? : String, pipeline_id? : String, destination? : String, project? : String, stall_timeout_ms? : Int) -> Tasync fn[T] with_state_schema_store(state : &StateStore, identity~ : String, run : async (&SchemaStore) -> T) -> TInstall
Download zipPostgreSQL to ClickHouse CDC library and replicator with durable checkpoints