How to migrate with zodbconvert¶
This guide shows you how to migrate an existing ZODB database (FileStorage or RelStorage) to zodb-pgjsonb using zodb-convert or the Python API.
Write a zodbconvert configuration file¶
Create a file named migrate.cfg:
From FileStorage¶
<source>
<filestorage>
path /path/to/Data.fs
blob-dir /path/to/blobstorage
</filestorage>
</source>
<destination>
%import zodb_pgjsonb
<pgjsonb>
dsn dbname=zodb_new user=zodb host=localhost port=5432
</pgjsonb>
</destination>
From RelStorage¶
<source>
<relstorage>
<postgresql>
dsn dbname=zodb_old user=zodb host=localhost
</postgresql>
</relstorage>
</source>
<destination>
%import zodb_pgjsonb
<pgjsonb>
dsn dbname=zodb_new user=zodb host=localhost port=5432
</pgjsonb>
</destination>
Run the migration¶
zodb-convert migrate.cfg
This copies all transactions sequentially, including blobs. The destination storage creates its schema automatically.
zodb-convert is a standalone tool that works with any ZODB-compatible storage.
Install it with pip install zodb-convert (or uv pip install zodb-convert).
Run a parallel migration¶
For large databases, use multiple worker threads for faster migration:
zodb-convert -w 4 migrate.cfg
This delegates to the destination storage’s copyTransactionsFrom(source, workers=4).
The main thread iterates the source and decodes pickles; worker threads write to PostgreSQL concurrently.
Set pool-max-size in your destination config to at least the number of workers plus one.
Python API¶
If you need more control, use the Python API directly:
from zodb_pgjsonb.storage import PGJsonbStorage
from ZODB.FileStorage import FileStorage
from ZODB.blob import BlobStorage
source = BlobStorage("/path/to/blobstorage", FileStorage("/path/to/Data.fs"))
dest = PGJsonbStorage(
dsn="dbname=zodb_new user=zodb host=localhost",
pool_max_size=10,
)
dest.copyTransactionsFrom(source, workers=4)
dest.close()
source.close()
Resume an interrupted migration¶
If a migration is interrupted (Ctrl-C, crash, network failure), resume with --incremental:
zodb-convert --incremental -w 4 migrate.cfg
During parallel copy, the storage tracks a watermark — the highest TID where all
prior TIDs are also committed.
On resume, iteration starts from the watermark (not lastTransaction()) to fill any
gaps left by out-of-order worker commits.
Already-committed transactions are skipped automatically.
The watermark table is dropped on successful completion, adding zero overhead to
non-incremental imports.
Python API¶
from zodb_pgjsonb.storage import PGJsonbStorage
from ZODB.FileStorage import FileStorage
from ZODB.blob import BlobStorage
from ZODB.utils import p64, u64
source = BlobStorage("/path/to/blobstorage", FileStorage("/path/to/Data.fs"))
dest = PGJsonbStorage(
dsn="dbname=zodb_new user=zodb host=localhost",
pool_max_size=10,
)
# Determine where to resume from
start_tid = p64(u64(dest.lastTransaction()) + 1)
dest.copyTransactionsFrom(source, workers=4, start_tid=start_tid)
dest.close()
source.close()
Migrate blobs¶
Blobs are migrated automatically by both zodbconvert and copyTransactionsFrom.
The destination storage applies its blob tiering rules (PG bytea vs S3) based on the configured blob-threshold.
If you have S3 tiering configured on the destination, large blobs are uploaded to S3 during the migration.
Faster blob migration with S3¶
When migrating large databases with many blobs to S3-backed storage, S3 upload latency can be the bottleneck. Two modes decouple S3 uploads from PostgreSQL writes:
Background mode (recommended)¶
Uploads blobs to S3 in a background thread pool while PostgreSQL writes continue independently:
zodb-convert -w 8 --background-blobs migrate.cfg
PostgreSQL throughput is no longer gated by S3 latency. All uploads complete before the migration finishes. If any upload fails (after retries), the migration aborts.
Deferred mode¶
Skips S3 uploads entirely during migration, writing a manifest file instead. Upload blobs later in a separate step:
# Step 1: Fast PG-only migration
zodb-convert -w 8 --deferred-blobs /tmp/blob-manifest.tsv migrate.cfg
# Step 2: Upload blobs from manifest
zodb-convert --upload-blobs /tmp/blob-manifest.tsv --dest-zope-conf zope.conf -w 16
This is useful when S3 is temporarily unavailable or when you want to run the PG import at maximum speed and handle S3 as a separate operational step.
Note: Temp blob files are preserved during deferred mode (they are needed for the upload step). Ensure the staging directory has sufficient disk space.
Python API¶
# Background mode
dest.copyTransactionsFrom(source, workers=4, blob_mode="background")
# Deferred mode
dest.copyTransactionsFrom(source, workers=4, blob_mode="deferred:/tmp/manifest.tsv")
Verify the migration¶
Connect to the destination database and check the object count:
SELECT count(*) FROM object_state;
SELECT count(*) FROM transaction_log;
SELECT count(*) FROM blob_state;
Open the database with ZODB and verify application data:
from zodb_pgjsonb.storage import PGJsonbStorage
import ZODB
storage = PGJsonbStorage(dsn="dbname=zodb_new user=zodb host=localhost")
db = ZODB.DB(storage)
conn = db.open()
root = conn.root()
print(list(root.keys()))
conn.close()
db.close()