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, thenEXCHANGE TABLESto swap it into place atomically. A crash mid-run never leaves the destination empty/partial.incremental— reads the last watermark from an internal_etlhouse_statetable in ClickHouse, copies only rows past it (snapshotting the current max up front for consistency), and dedupes viaReplacingMergeTree(<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 insource/postgres.rs. - Array and
timetypes have limited support; extendtypes.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:
- In the GitHub repo settings, create two Environments:
testpypiandpypi(Settings → Environments). Onpypi, add yourself as a required reviewer — this gives you a manual approval gate before the irreversible real-PyPI publish. - On test.pypi.org and pypi.org,
add a Trusted Publisher for the
etlhouseproject (Account settings → Publishing), pointing at this repo, workflow filerelease.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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d5fd3f59d4059f506ca15c4a8b56d479818aa9427fde4329c0da661a1641d66
|
|
| MD5 |
aeb223604b8851ef426c6d341367625b
|
|
| BLAKE2b-256 |
493694284485c871fc32a401a76e869f239014320135e2959feaa292a725397e
|
Provenance
The following attestation bundles were made for etlhouse-0.1.0.tar.gz:
Publisher:
release.yml on mmirzafahmi/etl-house
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etlhouse-0.1.0.tar.gz -
Subject digest:
7d5fd3f59d4059f506ca15c4a8b56d479818aa9427fde4329c0da661a1641d66 - Sigstore transparency entry: 2182423183
- Sigstore integration time:
-
Permalink:
mmirzafahmi/etl-house@14fbf7dcddd94adb2c1a65839798e5f9afd74036 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mmirzafahmi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@14fbf7dcddd94adb2c1a65839798e5f9afd74036 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f2c0143364168185f36af5c609c95dcae63353c3b70bf323d72c70cc1fa43ff2
|
|
| MD5 |
f8746ee665522dc5d011288055085a47
|
|
| BLAKE2b-256 |
4c79958f20e0154e9e4297fff99df893130138eee9b530ef082fbcbe78a66ca3
|
Provenance
The following attestation bundles were made for etlhouse-0.1.0-cp39-abi3-win_amd64.whl:
Publisher:
release.yml on mmirzafahmi/etl-house
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etlhouse-0.1.0-cp39-abi3-win_amd64.whl -
Subject digest:
f2c0143364168185f36af5c609c95dcae63353c3b70bf323d72c70cc1fa43ff2 - Sigstore transparency entry: 2182423671
- Sigstore integration time:
-
Permalink:
mmirzafahmi/etl-house@14fbf7dcddd94adb2c1a65839798e5f9afd74036 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mmirzafahmi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@14fbf7dcddd94adb2c1a65839798e5f9afd74036 -
Trigger Event:
push
-
Statement type:
File details
Details for the file etlhouse-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: etlhouse-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.5 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0643f8a1705e2a7788b4b060a1e9b2fb7bd8bce7c7eea7b43f14c1e956c206b2
|
|
| MD5 |
48cf8e2651e4661c20d41dc26d11b2c0
|
|
| BLAKE2b-256 |
c4e37f9523f7929795296dd70e7391a6b578c3eec741e61e651ca9a66e0a0932
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etlhouse-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
0643f8a1705e2a7788b4b060a1e9b2fb7bd8bce7c7eea7b43f14c1e956c206b2 - Sigstore transparency entry: 2182423998
- Sigstore integration time:
-
Permalink:
mmirzafahmi/etl-house@14fbf7dcddd94adb2c1a65839798e5f9afd74036 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mmirzafahmi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@14fbf7dcddd94adb2c1a65839798e5f9afd74036 -
Trigger Event:
push
-
Statement type:
File details
Details for the file etlhouse-0.1.0-cp39-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: etlhouse-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.3 MB
- Tags: CPython 3.9+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b9629be28c6043210007db4a79b015d66f781115c707641b1d3e0b4fa258793
|
|
| MD5 |
c0c197e88186805c49c8b463aac5920a
|
|
| BLAKE2b-256 |
7acd0b3ef1b3c4ed664116133944f1aa1773b962ead935fda43489410201ca9a
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etlhouse-0.1.0-cp39-abi3-macosx_11_0_arm64.whl -
Subject digest:
4b9629be28c6043210007db4a79b015d66f781115c707641b1d3e0b4fa258793 - Sigstore transparency entry: 2182424498
- Sigstore integration time:
-
Permalink:
mmirzafahmi/etl-house@14fbf7dcddd94adb2c1a65839798e5f9afd74036 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mmirzafahmi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@14fbf7dcddd94adb2c1a65839798e5f9afd74036 -
Trigger Event:
push
-
Statement type: