Skip to main content

Fast PostgreSQL -> ClickHouse ETL with a Rust engine (parallel, bounded-memory, Arrow-based).

Project description

etlhouse

Fast PostgreSQL → ClickHouse ETL with a Rust engine, driven from Python.

The hot path is native Rust: PostgreSQL binary COPY → Apache Arrow → ClickHouse FORMAT ArrowStream, with parallel range-partitioned reads, bounded-memory streaming (backpressure), automatic DDL, and both full-refresh and incremental (watermark) sync modes. The Python layer is a thin, typed API.

import etlhouse

src = etlhouse.Postgres("postgresql://user:pw@localhost:5432/odoo")
dst = etlhouse.ClickHouse("http://localhost:8123", database="analytics")

result = etlhouse.sync(
    src, dst,
    dest_table="account_move_line",
    source_table="account_move_line",
    mode="incremental",           # or "full"
    watermark="write_date",       # required for incremental
    key=["id"],                   # ORDER BY / dedup key
    create_if_missing=True,       # auto-generate the ClickHouse table
    parallelism=8,
    batch_rows=100_000,
    exclude=["display_name"],
    rename={"amount": "amt"},
    type_overrides={"amt": "Decimal(18, 2)"},
    on_progress=lambda p: print(f"{p.rows_written:,} rows @ {p.rows_per_sec:,.0f}/s"),
)
print(result)   # rows_read, rows_written, bytes_written, duration_secs, new_watermark

Why it's fast

Concern Approach
Deserialization PostgreSQL binary COPY decoded straight into Arrow columns in Rust (no per-row Python objects)
Parallelism Table split into key ranges; one COPY stream + Tokio task per partition
Memory Batches flushed every batch_rows rows and streamed to ClickHouse — RSS stays flat regardless of table size
GIL Entire transfer runs inside Python::allow_threads; the GIL is only touched for on_progress
Insert Arrow IPC stream ingested by ClickHouse's native ArrowStream format, gzip on the wire

Sync modes

  • full — loads into a staging table, then EXCHANGE TABLES to swap it into place atomically. A crash mid-run never leaves the destination empty/partial.
  • incremental — reads the last watermark from an internal _etlhouse_state table in ClickHouse, copies only rows past it (snapshotting the current max up front for consistency), and dedupes via ReplacingMergeTree(<watermark>). Re-running with no new data is a no-op.

Prerequisites

  • Rust toolchain (1.75+): install from https://rustup.rs
  • Python 3.9+
  • maturin: pip install maturin

Build & install (local dev)

# From the repo root
pip install maturin
maturin develop --release        # compiles the Rust engine, installs into the active venv
python -c "import etlhouse; print(etlhouse.version())"

Build a wheel to distribute:

maturin build --release          # produces target/wheels/etlhouse-*.whl

Running the tests

Integration tests need live services (skipped automatically if unavailable):

docker compose up -d             # PostgreSQL + ClickHouse
pip install -e '.[test]'
maturin develop --release
pytest -v

# Rust unit tests (decoders, type map, DDL) need no services:
cargo test -p etlhouse-core

sync() parameters

Parameter Meaning
source_table / source_query Read a whole table, or a custom SELECT (one is required)
dest_table Destination table in the ClickHouse database
mode "full" or "incremental"
watermark Monotonic column (e.g. write_date, id) — required for incremental
key Business key → ClickHouse ORDER BY and dedup key
create_if_missing Auto-run generated CREATE TABLE when the destination is absent
engine, order_by, partition_by, primary_key DDL knobs (sensible defaults per mode)
parallelism Number of concurrent partition streams
batch_rows Rows per Arrow batch / insert flush (memory vs round-trips)
partition_column Integer column to range-split on (defaults to first key)
type_overrides Per-column ClickHouse type (e.g. {"qty": "Decimal(18, 3)"})
rename Source → destination column renames
include / exclude Column allow/deny lists
on_progress Callback receiving a Progress (rows_written, rows_per_sec, …)

Type mapping

PostgreSQL Arrow ClickHouse
int2/4/8 Int16/32/64 Int16/32/64
float4/8, numeric* Float32/64 Float32/64
bool Boolean Bool
text/varchar/json/jsonb Utf8 String
uuid Utf8 UUID
date Date32 Date32
timestamp[tz] Timestamp(µs) DateTime64(6[, tz])

Nullable PostgreSQL columns become Nullable(T). numeric maps to Float64 by default (arbitrary precision is unknown from the type OID); override to a Decimal(P, S) via type_overrides and ClickHouse will convert on insert.

Project layout

crates/etlhouse-core/   # pure-Rust engine (unit-testable, no Python)
crates/etlhouse-py/     # PyO3 bindings (cdylib -> etlhouse._etlhouse)
python/etlhouse/        # typed Python surface (__init__.py, .pyi stubs)
tests/                  # pytest integration tests
docker-compose.yml      # local PostgreSQL + ClickHouse

Limitations / roadmap (v1)

  • TLS to PostgreSQL is not yet wired (uses NoTls); add a connector in source/postgres.rs.
  • Array and time types have limited support; extend types.rs + decode.rs.
  • No CLI yet — a config-driven CLI over the same engine is planned.
  • Logical-replication CDC and arbitrary transform callbacks are future work.

Releasing

CI (.github/workflows/release.yml) builds wheels for Linux (manylinux x86_64), macOS (Intel + Apple Silicon), and Windows (x86_64) plus an sdist, then publishes via PyPI Trusted Publishing (OIDC — no API tokens stored anywhere).

One-time setup:

  1. In the GitHub repo settings, create two Environments: testpypi and pypi (Settings → Environments). On pypi, add yourself as a required reviewer — this gives you a manual approval gate before the irreversible real-PyPI publish.
  2. On test.pypi.org and pypi.org, add a Trusted Publisher for the etlhouse project (Account settings → Publishing), pointing at this repo, workflow file release.yml, and the matching environment name (testpypi / pypi). Since the project doesn't exist yet on either index, use each site's "publish a new project" / pending-publisher flow.

Cutting a release:

# bump the version in Cargo.toml and pyproject.toml, commit, then:
git tag v0.1.0
git push origin v0.1.0

Pushing the tag triggers the workflow: it builds all wheels, publishes to TestPyPI automatically, then waits for your approval on the pypi environment before publishing the real release. Verify the TestPyPI install first:

pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ etlhouse

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

etlhouse-0.1.0.tar.gz (47.8 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

etlhouse-0.1.0-cp39-abi3-win_amd64.whl (2.3 MB view details)

Uploaded CPython 3.9+Windows x86-64

etlhouse-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.5 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

etlhouse-0.1.0-cp39-abi3-macosx_11_0_arm64.whl (2.3 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

Details for the file etlhouse-0.1.0.tar.gz.

File metadata

  • Download URL: etlhouse-0.1.0.tar.gz
  • Upload date:
  • Size: 47.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for etlhouse-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7d5fd3f59d4059f506ca15c4a8b56d479818aa9427fde4329c0da661a1641d66
MD5 aeb223604b8851ef426c6d341367625b
BLAKE2b-256 493694284485c871fc32a401a76e869f239014320135e2959feaa292a725397e

See more details on using hashes here.

Provenance

The following attestation bundles were made for etlhouse-0.1.0.tar.gz:

Publisher: release.yml on mmirzafahmi/etl-house

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file etlhouse-0.1.0-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: etlhouse-0.1.0-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 2.3 MB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for etlhouse-0.1.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 f2c0143364168185f36af5c609c95dcae63353c3b70bf323d72c70cc1fa43ff2
MD5 f8746ee665522dc5d011288055085a47
BLAKE2b-256 4c79958f20e0154e9e4297fff99df893130138eee9b530ef082fbcbe78a66ca3

See more details on using hashes here.

Provenance

The following attestation bundles were made for etlhouse-0.1.0-cp39-abi3-win_amd64.whl:

Publisher: release.yml on mmirzafahmi/etl-house

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file etlhouse-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for etlhouse-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0643f8a1705e2a7788b4b060a1e9b2fb7bd8bce7c7eea7b43f14c1e956c206b2
MD5 48cf8e2651e4661c20d41dc26d11b2c0
BLAKE2b-256 c4e37f9523f7929795296dd70e7391a6b578c3eec741e61e651ca9a66e0a0932

See more details on using hashes here.

Provenance

The following attestation bundles were made for etlhouse-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on mmirzafahmi/etl-house

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file etlhouse-0.1.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for etlhouse-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 4b9629be28c6043210007db4a79b015d66f781115c707641b1d3e0b4fa258793
MD5 c0c197e88186805c49c8b463aac5920a
BLAKE2b-256 7acd0b3ef1b3c4ed664116133944f1aa1773b962ead935fda43489410201ca9a

See more details on using hashes here.

Provenance

The following attestation bundles were made for etlhouse-0.1.0-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on mmirzafahmi/etl-house

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page