Move whole tables between databases at wire speed, in bounded memory
Project description
apitap
Move whole tables between databases at wire speed, in bounded memory.
apitap is the open-source transfer engine behind apitap cloud — a Rust core with Python bindings, in the spirit of Polars. It moves data the way the databases themselves would: raw wire-format streams, parallel range pipes, atomic swaps, and memory that stays flat no matter how big the table is.
pip install apitap
import apitap
report = apitap.transfer(
"postgres://user:pass@src-host/db",
"postgres://user:pass@dst-host/db",
table="public.events",
)
print(f"{report.rows:,} rows in {report.elapsed_ms} ms over {report.parallel} pipes")
Why it's fast
- No per-row decode. Postgres→Postgres pipes raw
COPY (FORMAT binary)bytes straight through — byte-for-byte, likepsql | psqlwithout the shell. - Parallel range pipes. The table is split into N contiguous ranges of its integer primary key (auto-detected) and each range streams concurrently.
- Bounded memory. Bytes stream with TCP backpressure; memory is
parallel × chunk_bytes, not the table size. A 256 MB container moves 10M+ rows.
Measured against ingestr — running their own benchmark (their exact schema, value generators, and CLI invocation, at their latest release), on the same box, stock databases, checksum-validated. Reproduce it yourself:
./benchmarks/run-server.sh # or benchmarks/run.py on a laptop
10M rows, both tools at 16 vCPU / 4 GB (auto settings, no tuning for either tool):
| route | apitap 0.1.0 | ingestr 1.0.75 | speedup |
|---|---|---|---|
| Postgres → Postgres | 18.5 s | 545 s | 29× |
| Postgres → ClickHouse | 10.7 s | 119 s | 11× |
| MySQL → ClickHouse | 10.8 s | 100 s | 9× |
| MySQL → Postgres | 22.6 s | 520 s | 23× |
On a tiny 0.5 vCPU / 256 MB container — the box you'd actually pay for — apitap completes every route (memory-bounded by design; the pipe count auto-sizes to the cgroup's CPU and memory):
| route | apitap | ingestr 1.0.75 | speedup |
|---|---|---|---|
| Postgres → Postgres | 68 s | 868 s | 12.8× |
| Postgres → ClickHouse | 28 s | 428 s | 15.3× |
| MySQL → ClickHouse | 53 s | 399 s | 7.5× |
| MySQL → Postgres | 58 s | 849 s | 14.6× |
apitap scales with the cores you give it and with the databases'; ingestr barely moves between 0.5 and 16 vCPUs — a mostly serial pipeline. Full per-tier numbers and methodology live in benchmarks/README.md.
The ClickHouse route moves 10M rows in ~10s because the tool never touches text:
Postgres streams COPY (FORMAT binary), apitap transcodes it in-flight to ClickHouse
RowBinary (byte-swaps, epoch rebasing, exact NUMERIC→Decimal scaling), and at that
point the SOURCE database is the measured bottleneck — 16 parallel binary COPYs to
/dev/null with no apitap involved take ~11 s on the same box. Give the databases more
cores and the transfer keeps scaling; the engine no longer costs anything.
Part of the gap is structural: dlt's full refresh loads into a temp table and then
rewrites all rows into the final one (every row written twice), while apitap COPYs
once into staging and swaps it in with a metadata-only RENAME.
The same 15-column schema also backs the end-to-end test suite
(py-apitap/tests/test_ingestr_schema.py) — every distinct Postgres type is asserted
byte-faithful, values and column types both.
Postgres destinations also take durable=False: the load runs through an UNLOGGED
table (no WAL — the measured write wall), cutting pg→pg from ~24 s to ~15.5 s and
mysql→pg from ~27 s to ~19.5 s at 10M rows. The tradeoff is explicit: the
resulting table is truncated by crash recovery until you ALTER TABLE … SET LOGGED —
use it for rebuildable destinations.
Guarantees
- Atomic — rows land in a staging table, swapped in with
DROP+RENAMEin one transaction. Readers never see a partial load; a mid-run failure leaves the previous table untouched. - 0-row guard — an empty source never wipes an existing destination table.
Roadmap
- Postgres → Postgres (raw binary passthrough, parallel)
- Postgres → ClickHouse (binary→RowBinary transcode; TSV fallback for exotic types; parallelizes even without a primary key via TID range scans)
- MySQL → ClickHouse (wire decode → RowBinary; lossless unsigned ints, exact decimals, UTC-normalized timestamps)
- MySQL → Postgres (wire decode → binary COPY; exact NUMERIC encoding up to
DECIMAL(65),
BIGINT UNSIGNED→numeric(20,0), JSON→jsonb) - Postgres → Parquet / Arrow (
read_postgres()→ pyarrow / Polars, zero-copy FFI) - Postgres → Snowflake / BigQuery
- MySQL → MySQL
Development
cargo test -p apitap-core # engine tests
uv pip install -e py-apitap # build the Python package (needs Rust)
License
MIT. The managed cloud (scheduling, per-tenant workers, incremental sync, a UI) is apitap.dev.
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 Distributions
Built Distribution
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 apitap-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: apitap-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.8 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 |
e2277f34d138ca66effd306f83d1a09f653e7412e998c6fefa4894140bf0e9a0
|
|
| MD5 |
47ff3ae0dd174a8eeae0e363d1c793d0
|
|
| BLAKE2b-256 |
63d5f2d67cec0a0be9c3432286917949a1e0dccf8e7d8eba1389f9d4ea38679d
|
Provenance
The following attestation bundles were made for apitap-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish.yml on apitap/apitap-lib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
apitap-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
e2277f34d138ca66effd306f83d1a09f653e7412e998c6fefa4894140bf0e9a0 - Sigstore transparency entry: 2148049668
- Sigstore integration time:
-
Permalink:
apitap/apitap-lib@0419981038f5bb65c3ab7fd5da369d64a2f784c0 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/apitap
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0419981038f5bb65c3ab7fd5da369d64a2f784c0 -
Trigger Event:
push
-
Statement type: