Skip to content

Commit b1efaab

Browse files
jensensclaude
andcommitted
Add generic zodb-convert support for all storage backends
Generate storage-agnostic convert-import.conf and convert-export.conf for all non-filestorage backends (RelStorage, PGJsonb, ZEO) using the generic zodb-convert tool. Filestorage is always the portable format on the other side of the conversion. - Add tagmarker parameter to pgjsonb() and zeo() macros in db.conf - Add db_convert_import/export_* settings to cookiecutter.json - Create convert-import.conf and convert-export.conf templates - Update post_gen_project.py for directory creation and file cleanup - Deprecate RelStorage-specific zodbconvert configs in favor of zodb-convert - Add migration how-to guide (docs/sources/how-to/migrate-storage.md) - Update storage-backends explanation and backend how-to guides - Keep existing relstorage-import/export.conf for backward compatibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b15dfe6 commit b1efaab

13 files changed

Lines changed: 557 additions & 9 deletions

File tree

CHANGES.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
## 2.4.1 (unreleased)
44

5-
- No changes yet.
5+
- Feature: Generate generic `convert-import.conf` and `convert-export.conf`
6+
for all non-filestorage backends (RelStorage, PGJsonb, ZEO) using the
7+
storage-agnostic [zodb-convert](https://pypi.org/project/zodb-convert/)
8+
tool. Deprecate RelStorage-specific `zodbconvert` configuration in favor
9+
of `zodb-convert`. Add migration how-to guide.
10+
[@jensens]
611

712
## 2.4.0 (2026-02-25)
813

cookiecutter.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@
154154
"db_pgjsonb_blob_cache_dir": "",
155155
"db_pgjsonb_blob_cache_size": "",
156156

157+
"db_convert_import_filestorage_location": "",
158+
"db_convert_import_blobs_location": "",
159+
"db_convert_export_filestorage_location": "",
160+
"db_convert_export_blobs_location": "",
161+
157162
"db_z3blobs_enabled": false,
158163
"db_z3blobs_bucket_name": "",
159164
"db_z3blobs_s3_prefix": "",

docs/sources/explanation/storage-backends.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,12 @@ For most teams, the decision comes down to:
161161
5. **Need S3 blob storage with direct, relstorage, or zeo?** Enable `db_z3blobs_enabled`.
162162

163163
All four backends support the same ZODB API, so switching between them is a
164-
configuration change (plus data migration). Your application code does not
165-
need to change.
164+
configuration change plus a one-time data migration. The
165+
[zodb-convert](https://pypi.org/project/zodb-convert/) tool handles this
166+
migration generically -- it can copy data between any two ZODB-compatible
167+
storages. Because `cookiecutter-zope-instance` generates a complete
168+
`zope.conf` for each backend, `zodb-convert` can read the storage
169+
configuration directly from these files. See {doc}`/how-to/migrate-storage`
170+
for a step-by-step guide.
171+
172+
Your application code does not need to change.

docs/sources/how-to/configure-pgjsonb.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ used** with PGJsonb. PGJsonb manages blob storage through its own settings
9898

9999
## Next steps
100100

101+
- {doc}`migrate-storage` -- Migrate data from an existing storage backend
102+
into PGJsonb using `zodb-convert`.
101103
- {doc}`/reference/database-pgjsonb` -- Full reference for all PGJsonb
102104
options including connection pool, cache, and S3 settings.
103105
- {doc}`/explanation/storage-backends` -- Comparison of all storage backends.

docs/sources/how-to/configure-relstorage.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,18 @@ for RelStorage command-line utilities:
104104
settings are provided. Used with `zodbconvert` to import data from a
105105
filestorage.
106106

107+
:::{tip}
108+
For migrating data between storage backends, the generic
109+
[zodb-convert](https://pypi.org/project/zodb-convert/) tool is now the
110+
recommended approach. It works with all storage backends and can read storage
111+
configuration directly from `zope.conf` files.
112+
See {doc}`migrate-storage` for a step-by-step guide.
113+
:::
114+
107115
## Next steps
108116

117+
- {doc}`migrate-storage` -- Migrate data between any storage backends using
118+
`zodb-convert`.
109119
- {doc}`/reference/database-relstorage` -- Full reference for all RelStorage
110120
options including caching, replication, MySQL, Oracle, and SQLite settings.
111121
- {doc}`/explanation/storage-backends` -- Comparison of all storage backends.

docs/sources/how-to/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Goal-oriented guides for common tasks.
1111
- {doc}`configure-zeo`
1212
- {doc}`configure-z3blobs`
1313

14+
## Storage Migration
15+
16+
- {doc}`migrate-storage`
17+
1418
## Web Layer
1519

1620
- {doc}`configure-cors`
@@ -31,6 +35,7 @@ configure-relstorage
3135
configure-pgjsonb
3236
configure-zeo
3337
configure-z3blobs
38+
migrate-storage
3439
configure-cors
3540
use-environment-variables
3641
upgrade-v1-to-v2
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Migrate Data Between Storage Backends
2+
3+
<!-- diataxis: how-to -->
4+
5+
This guide shows how to copy ZODB data from one storage backend to another
6+
using the generic [zodb-convert](https://pypi.org/project/zodb-convert/) tool.
7+
8+
## Prerequisites
9+
10+
- The `zodb-convert` package installed in your virtual environment
11+
- The storage-specific packages for both source and destination installed
12+
(e.g. `ZODB` for filestorage, `RelStorage` for relstorage, `zodb-pgjsonb`
13+
for pgjsonb)
14+
15+
## Approach 1: Generated configuration files
16+
17+
When your instance uses a non-filestorage backend (RelStorage, PGJsonb, or
18+
ZEO), the cookiecutter generates `convert-import.conf` and
19+
`convert-export.conf` files in the `etc/` directory. These are ready-made
20+
configuration files for `zodb-convert` that convert between your configured
21+
backend and a FileStorage.
22+
23+
### Step 1: Set the conversion paths
24+
25+
In your `instance.yaml`, provide the FileStorage paths for the "other side"
26+
of the conversion:
27+
28+
```yaml
29+
default_context:
30+
db_storage: pgjsonb
31+
db_pgjsonb_dsn: "dbname='zodb' user='zodb' host='localhost' port='5433'"
32+
33+
# Conversion settings (FileStorage side)
34+
db_convert_import_filestorage_location: var/import/Data.fs
35+
db_convert_import_blobs_location: var/import/blobs
36+
db_convert_export_filestorage_location: var/export/Data.fs
37+
db_convert_export_blobs_location: var/export/blobs
38+
```
39+
40+
These settings work with all non-filestorage backends.
41+
42+
### Step 2: Import data from FileStorage
43+
44+
Place your `Data.fs` and blobs at the import locations, then run:
45+
46+
```bash
47+
zodb-convert etc/convert-import.conf
48+
```
49+
50+
### Step 3: Export data to FileStorage
51+
52+
To create a portable FileStorage backup:
53+
54+
```bash
55+
zodb-convert etc/convert-export.conf
56+
```
57+
58+
## Approach 2: Using existing zope.conf files
59+
60+
If you have two Zope instances with different storage backends, `zodb-convert`
61+
can read storage configuration directly from their `zope.conf` files. No
62+
extra configuration files are needed.
63+
64+
### Step 1: Generate the destination instance
65+
66+
Create a second instance configured for the new storage backend:
67+
68+
```yaml
69+
# instance-new.yaml
70+
default_context:
71+
db_storage: pgjsonb
72+
db_pgjsonb_dsn: "dbname='zodb' user='zodb' host='localhost' port='5433'"
73+
```
74+
75+
Run cookiecutter to generate:
76+
77+
```bash
78+
cookiecutter -f --no-input --config-file instance-new.yaml \
79+
gh:plone/cookiecutter-zope-instance
80+
```
81+
82+
### Step 2: Run the conversion
83+
84+
```bash
85+
zodb-convert \
86+
--source-zope-conf /old-instance/etc/zope.conf \
87+
--dest-zope-conf /new-instance/etc/zope.conf
88+
```
89+
90+
### Specifying a named database
91+
92+
If your `zope.conf` defines multiple `<zodb_db>` sections:
93+
94+
```bash
95+
zodb-convert \
96+
--source-zope-conf old/etc/zope.conf --source-db main \
97+
--dest-zope-conf new/etc/zope.conf --dest-db main
98+
```
99+
100+
The default database name is `main`.
101+
102+
## Useful options
103+
104+
- `--dry-run` -- preview what would be copied without writing data
105+
- `--incremental` -- resume from the last transaction in the destination
106+
(useful for large databases or after interruptions)
107+
- `-v` -- show INFO-level progress; `-vv` for DEBUG-level detail
108+
109+
## Common migration scenarios
110+
111+
| From | To | Notes |
112+
|---|---|---|
113+
| direct (filestorage) | relstorage | Classic scale-out migration |
114+
| direct (filestorage) | pgjsonb | Move to SQL-queryable storage |
115+
| relstorage | pgjsonb | Switch to JSONB representation |
116+
| zeo | relstorage | Remove ZEO server dependency |
117+
| any | direct (filestorage) | Create a portable backup |
118+
119+
All combinations work -- `zodb-convert` is storage-agnostic.
120+
121+
## Configuration reference
122+
123+
| Setting | Default | Description |
124+
|---|---|---|
125+
| `db_convert_import_filestorage_location` | *(unset)* | Path to the source FileStorage for import |
126+
| `db_convert_import_blobs_location` | *(unset)* | Path to the source blob directory for import |
127+
| `db_convert_export_filestorage_location` | *(unset)* | Path for the destination FileStorage on export |
128+
| `db_convert_export_blobs_location` | *(unset)* | Path for the destination blob directory on export |
129+
130+
The `convert-import.conf` file is generated when both `db_convert_import_*`
131+
settings are provided. The `convert-export.conf` file is generated when both
132+
`db_convert_export_*` settings are provided. Neither file is generated for
133+
the `direct` (filestorage) backend since it *is* the portable format.
134+
135+
## Next steps
136+
137+
- [zodb-convert on PyPI](https://pypi.org/project/zodb-convert/) -- Full
138+
documentation and source code
139+
- {doc}`/explanation/storage-backends` -- Understanding the trade-offs
140+
between backends

docs/sources/reference/database-relstorage.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@ replications. For details read
8383
RelStorage provides helper scripts for packing (`zodbpack`) and
8484
import/export from filestorage (`zodbconvert`).
8585

86+
:::{deprecated} 2.5
87+
The RelStorage-specific `zodbconvert` import/export configuration files
88+
(`relstorage-import.conf`, `relstorage-export.conf`) and their corresponding
89+
`db_relstorage_import_*` / `db_relstorage_export_*` settings are deprecated
90+
in favor of the generic
91+
[zodb-convert](https://pypi.org/project/zodb-convert/) tool, which works
92+
with any ZODB storage backend. Use the new `db_convert_*` settings and the
93+
generated `convert-import.conf` / `convert-export.conf` files instead.
94+
See {doc}`/how-to/migrate-storage` for details.
95+
96+
The existing settings and generated files continue to work but may be
97+
removed in a future major version.
98+
:::
99+
86100
The file `relstorage-pack.conf` for the command line utility `zodbpack` is
87101
always generated for all RelStorage configurations. For usage information
88102
read [Packing Or Reference Checking A ZODB Storage: zodbpack](https://relstorage.readthedocs.io/en/latest/zodbpack.html).

hooks/post_gen_project.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@
7474
if db_relstorage_export_filestorage_location:
7575
filepath = Path(db_relstorage_export_filestorage_location)
7676
filepath.parent.mkdir(parents=True, exist_ok=True)
77+
if "{{ cookiecutter.db_storage }}" != "direct":
78+
# generic convert import directories
79+
db_convert_import_blobs = (
80+
"{{ cookiecutter.db_convert_import_blobs_location | abspath }}"
81+
)
82+
if db_convert_import_blobs:
83+
Path(db_convert_import_blobs).mkdir(parents=True, exist_ok=True)
84+
db_convert_import_fs = (
85+
"{{ cookiecutter.db_convert_import_filestorage_location | abspath }}"
86+
)
87+
if db_convert_import_fs:
88+
Path(db_convert_import_fs).parent.mkdir(parents=True, exist_ok=True)
89+
# generic convert export directories
90+
db_convert_export_blobs = (
91+
"{{ cookiecutter.db_convert_export_blobs_location | abspath }}"
92+
)
93+
if db_convert_export_blobs:
94+
Path(db_convert_export_blobs).mkdir(parents=True, exist_ok=True)
95+
db_convert_export_fs = (
96+
"{{ cookiecutter.db_convert_export_filestorage_location | abspath }}"
97+
)
98+
if db_convert_export_fs:
99+
Path(db_convert_export_fs).parent.mkdir(parents=True, exist_ok=True)
77100
if "{{ cookiecutter.db_storage }}" == "pgjsonb":
78101
blob_temp_dir = "{{ cookiecutter.db_pgjsonb_blob_temp_dir }}"
79102
if blob_temp_dir:
@@ -85,6 +108,10 @@
85108
z3blobs_cache_dir = "{{ cookiecutter.db_z3blobs_cache_dir }}"
86109
if z3blobs_cache_dir:
87110
Path(z3blobs_cache_dir).mkdir(parents=True, exist_ok=True)
111+
if "{{ cookiecutter.db_storage }}" == "direct":
112+
# cleanup convert files — filestorage doesn't need conversion configs
113+
(etc / "convert-import.conf").unlink()
114+
(etc / "convert-export.conf").unlink()
88115
if "{{ cookiecutter.db_storage }}" != "relstorage":
89116
# cleanup relstorage files if no relstorage is configured
90117
(etc / "relstorage-export.conf").unlink()

templates/db.conf

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,10 @@
192192
drop_cache_rather_verify,
193193
client_label,
194194
storage,
195-
wait
195+
wait,
196+
tagmarker=""
196197
)-%}
197-
<zeoclient>
198+
<zeoclient{{ tagmarker }}>
198199
{%- if not z3blobs_active %}
199200
# blobs
200201
shared-blob-dir {{ "true" if blob_mode == "shared" else "false" }}
@@ -248,7 +249,7 @@
248249
{%- if wait %}
249250
wait {{ wait }}
250251
{%- endif %}
251-
</zeoclient>
252+
</zeoclient{{ tagmarker }}>
252253
{%- endmacro %}
253254

254255
# =============================================================================
@@ -271,10 +272,11 @@
271272
s3_secret_key,
272273
s3_use_ssl,
273274
blob_cache_dir,
274-
blob_cache_size
275+
blob_cache_size,
276+
tagmarker=""
275277
)-%}
276278
%import zodb_pgjsonb
277-
<pgjsonb>
279+
<pgjsonb{{ tagmarker }}>
278280
dsn {{ dsn }}
279281
{%- if name and name != "pgjsonb" %}
280282
name {{ name }}
@@ -326,7 +328,7 @@
326328
blob-cache-size {{ blob_cache_size }}
327329
{%- endif %}
328330
{%- endif %}
329-
</pgjsonb>
331+
</pgjsonb{{ tagmarker }}>
330332
{%- endmacro %}
331333

332334
# =============================================================================

0 commit comments

Comments
 (0)