From 73b8c2f1fd2c5334dcef38f418febfef946e06b2 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 5 Aug 2026 20:48:06 -0600 Subject: [PATCH 01/11] Document that copy-db copies file-backed blobs beside the target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit copy-db now writes the database's blob roots to `-blobs//` and refuses a target that already exists (HarperFast/harper#2048). Blob files live outside the database file and are addressed by database name, so a copy is not restorable without them — document the layout, the restore steps for restoring under a different name, and that copy compaction is LMDB-only. Co-Authored-By: Claude Opus 5 --- reference/cli/commands.md | 6 ++++-- reference/database/compaction.md | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/reference/cli/commands.md b/reference/cli/commands.md index 549ef2cb5..3f678545c 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -250,8 +250,8 @@ harper copy-db **Parameters**: -- `` - Name of the source database -- `` - Full path to the target database file +- `` - Name of the source database (a name, not a file path) +- `` - Full path to the target database file, which must not already exist **Example**: @@ -261,6 +261,8 @@ harper copy-db data /home/user/hdb/database/copy.mdb This copies the default `data` database to a new location with compaction applied. +The database's file-backed blobs are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name. The copy cannot be restored without that directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-travel-separately) for the restore steps. LMDB databases only; RocksDB databases compact themselves. + **Use Cases**: - Database optimization diff --git a/reference/database/compaction.md b/reference/database/compaction.md index 4b947a64b..31a717918 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -27,7 +27,7 @@ Run using the [CLI](../cli/commands.md): harper copy-db ``` -The `source-database` is the database name (not a file path). The target is the full file path where the compacted copy will be written. +The `source-database` is the database name (not a file path). The target is the full file path where the compacted copy will be written, and it must not already exist — `copy-db` refuses to write into an existing file rather than merging the copy into whatever it holds. To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. @@ -37,6 +37,29 @@ To replace the original database with the compacted copy, move or rename the out harper copy-db data /home/user/hdb/database/copy.mdb ``` +Copy compaction applies to LMDB databases. RocksDB databases compact themselves and are skipped. + +### File-backed blobs travel separately + +A database's file-backed blob values (`Blob` and large `Bytes` attributes) are not stored inside the database file. They live in the configured blob roots — `storage.blobPaths[n]`, or `/blobs/` when `blobPaths` is not configured — and are addressed by **database name**, not by the path of the database file. + +`copy-db` therefore writes them alongside the copy: + +``` +-blobs//… +``` + +`` is the position of the source root in the database's blob-root list, preserved so a multi-root database restores each root to its original slot. A `README.md` in that directory records the mapping. + +**The copy is not restorable without this directory.** To restore the copy under a database name, put each `` tree into that name's matching blob root — for example, restoring the copy above as a database named `archive` with no `storage.blobPaths` configured: + +```bash +cp -r /home/user/hdb/database/copy.mdb /home/user/hdb/database/archive.mdb +cp -r /home/user/hdb/database/copy.mdb-blobs/0/. /home/user/hdb/blobs/archive/ +``` + +Restoring the copy under its original database name in the same installation needs only the database file, since the blob roots it already references are untouched. + ## Compact on Start Automatically compacts all non-system databases when Harper starts. Harper will not start until compaction is complete. Under the hood, it loops through all user databases, creates a backup of each, compacts it, replaces the original with the compacted copy, and removes the backup. From 5d016086096419de410a3a9dd64eb77b91b51237 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 25 Aug 2026 23:49:28 -0600 Subject: [PATCH 02/11] Address review feedback on copy-db blob docs - Add for the new blob copying and target-existence behavior on both surfaces - Scope the "not restorable" warning to databases that hold file-backed values, and note that others restore from the database file alone - Rename the section to "File-backed blobs copied separately" - Use ASCII "..." in the blob path notation Co-Authored-By: Claude Opus --- reference/cli/commands.md | 2 +- reference/database/compaction.md | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/reference/cli/commands.md b/reference/cli/commands.md index 3f678545c..cb90e0f05 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -261,7 +261,7 @@ harper copy-db data /home/user/hdb/database/copy.mdb This copies the default `data` database to a new location with compaction applied. -The database's file-backed blobs are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name. The copy cannot be restored without that directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-travel-separately) for the restore steps. LMDB databases only; RocksDB databases compact themselves. + — the database's file-backed blobs are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name, and the target path must not already exist. If the database holds file-backed values, the copy is not restorable without that companion directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-copied-separately) for the restore steps. LMDB databases only; RocksDB databases compact themselves. **Use Cases**: diff --git a/reference/database/compaction.md b/reference/database/compaction.md index 31a717918..aa692a714 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -27,7 +27,9 @@ Run using the [CLI](../cli/commands.md): harper copy-db ``` -The `source-database` is the database name (not a file path). The target is the full file path where the compacted copy will be written, and it must not already exist — `copy-db` refuses to write into an existing file rather than merging the copy into whatever it holds. +The `source-database` is the database name (not a file path). The target is the full file path where the compacted copy will be written. + + — the target path must not already exist: `copy-db` refuses to write into an existing file rather than merging the copy into whatever it holds. Earlier v5 releases merged the copy into whatever an existing target held. To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. @@ -39,19 +41,23 @@ harper copy-db data /home/user/hdb/database/copy.mdb Copy compaction applies to LMDB databases. RocksDB databases compact themselves and are skipped. -### File-backed blobs travel separately +### File-backed blobs copied separately + + — `copy-db` copies the database's file-backed blobs alongside the copy. Earlier v5 releases copied only the database file, leaving the blobs behind. A database's file-backed blob values (`Blob` and large `Bytes` attributes) are not stored inside the database file. They live in the configured blob roots — `storage.blobPaths[n]`, or `/blobs/` when `blobPaths` is not configured — and are addressed by **database name**, not by the path of the database file. `copy-db` therefore writes them alongside the copy: ``` --blobs//… +-blobs//... ``` `` is the position of the source root in the database's blob-root list, preserved so a multi-root database restores each root to its original slot. A `README.md` in that directory records the mapping. -**The copy is not restorable without this directory.** To restore the copy under a database name, put each `` tree into that name's matching blob root — for example, restoring the copy above as a database named `archive` with no `storage.blobPaths` configured: +**If the database holds file-backed values, the copy is not restorable without this directory** — moving the database file on its own silently loses every blob it references. (A database with no `Blob` or large `Bytes` values has no companion directory, and restores from the database file alone.) + +To restore the copy under a database name, put each `` tree into that name's matching blob root — for example, restoring the copy above as a database named `archive` with no `storage.blobPaths` configured: ```bash cp -r /home/user/hdb/database/copy.mdb /home/user/hdb/database/archive.mdb From 8efb7941f46d28c1db8c337b109ba0807f5e5f89 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 26 Aug 2026 00:03:13 -0600 Subject: [PATCH 03/11] Correct blob copy claims from pre-push review - Bytes values live inside the record; only Blob values are file-backed - Document that the -blobs companion directory must also be absent - Limit the "database file alone is enough" case to an immediate in-place replacement; a retained backup needs the companion directory because the source can reclaim blob files afterwards - Do not assert that a blob-free database emits no companion directory - Place version badges on their own line under the nearest heading Co-Authored-By: Claude Opus --- reference/cli/commands.md | 6 ++++-- reference/database/compaction.md | 14 +++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/reference/cli/commands.md b/reference/cli/commands.md index cb90e0f05..e082a706f 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -242,6 +242,8 @@ This command regenerates the self-signed SSL/TLS certificates used by Harper. Available since: v4.1.0 + + Copy a Harper database with compaction to eliminate free-space and fragmentation. ```bash @@ -251,7 +253,7 @@ harper copy-db **Parameters**: - `` - Name of the source database (a name, not a file path) -- `` - Full path to the target database file, which must not already exist +- `` - Full path to the target database file; neither it nor its `-blobs` companion directory may already exist **Example**: @@ -261,7 +263,7 @@ harper copy-db data /home/user/hdb/database/copy.mdb This copies the default `data` database to a new location with compaction applied. - — the database's file-backed blobs are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name, and the target path must not already exist. If the database holds file-backed values, the copy is not restorable without that companion directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-copied-separately) for the restore steps. LMDB databases only; RocksDB databases compact themselves. +As of v5.3.0 the database's `Blob` files are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name. If the database holds `Blob` values, the copy is not restorable without that companion directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-copied-separately) for the restore steps. LMDB databases only; RocksDB databases compact themselves. **Use Cases**: diff --git a/reference/database/compaction.md b/reference/database/compaction.md index aa692a714..049a5882f 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -17,6 +17,8 @@ Compaction is also the mechanism to apply storage configuration changes (such as ## Copy Compaction + + Creates a compacted copy of a database file. The original database is left unchanged. > **Recommendation:** Stop Harper before performing copy compaction to prevent any record loss during the copy operation. @@ -29,7 +31,7 @@ harper copy-db The `source-database` is the database name (not a file path). The target is the full file path where the compacted copy will be written. - — the target path must not already exist: `copy-db` refuses to write into an existing file rather than merging the copy into whatever it holds. Earlier v5 releases merged the copy into whatever an existing target held. +As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. Retrying an interrupted copy means removing both. To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. @@ -43,9 +45,11 @@ Copy compaction applies to LMDB databases. RocksDB databases compact themselves ### File-backed blobs copied separately - — `copy-db` copies the database's file-backed blobs alongside the copy. Earlier v5 releases copied only the database file, leaving the blobs behind. + + +`copy-db` copies the database's blob files alongside the copy. Earlier v5 releases copied only the database file, leaving the blobs behind. -A database's file-backed blob values (`Blob` and large `Bytes` attributes) are not stored inside the database file. They live in the configured blob roots — `storage.blobPaths[n]`, or `/blobs/` when `blobPaths` is not configured — and are addressed by **database name**, not by the path of the database file. +`Blob` values are stored outside the database file (unlike `Bytes` values, which are stored inside the record). These blob files live in the configured blob roots — `storage.blobPaths[n]`, or `/blobs/` when `blobPaths` is not configured — and are addressed by **database name**, not by the path of the database file. `copy-db` therefore writes them alongside the copy: @@ -55,7 +59,7 @@ A database's file-backed blob values (`Blob` and large `Bytes` attributes) are n `` is the position of the source root in the database's blob-root list, preserved so a multi-root database restores each root to its original slot. A `README.md` in that directory records the mapping. -**If the database holds file-backed values, the copy is not restorable without this directory** — moving the database file on its own silently loses every blob it references. (A database with no `Blob` or large `Bytes` values has no companion directory, and restores from the database file alone.) +**If the database holds `Blob` values, the copy is not restorable without this directory** — moving the database file on its own silently loses every blob it references. A database with no live blob references does not need the companion directory; one may still be written (possibly empty) if a blob root directory exists for the database. To restore the copy under a database name, put each `` tree into that name's matching blob root — for example, restoring the copy above as a database named `archive` with no `storage.blobPaths` configured: @@ -64,7 +68,7 @@ cp -r /home/user/hdb/database/copy.mdb /home/user/hdb/database/archive.mdb cp -r /home/user/hdb/database/copy.mdb-blobs/0/. /home/user/hdb/blobs/archive/ ``` -Restoring the copy under its original database name in the same installation needs only the database file, since the blob roots it already references are untouched. +One narrow exception: if the copy immediately replaces its own source in place — same installation, same database name, before anything writes to the source — the database file alone is enough, since the blob roots it references are still exactly as the copy left them. A copy kept as a backup does not qualify: once the source is written to, Harper can reclaim the blob files an older copy still references, so restore the companion directory along with the database file. ## Compact on Start From d25adafa53230924c65999a8d660699dd9533d9a Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 26 Aug 2026 00:07:12 -0600 Subject: [PATCH 04/11] Say precisely what copy-db does with a RocksDB database copy-db throws on a RocksDB source; only compact-on-start skips them. Co-Authored-By: Claude Opus --- reference/cli/commands.md | 2 +- reference/database/compaction.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/cli/commands.md b/reference/cli/commands.md index e082a706f..c577b510e 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -263,7 +263,7 @@ harper copy-db data /home/user/hdb/database/copy.mdb This copies the default `data` database to a new location with compaction applied. -As of v5.3.0 the database's `Blob` files are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name. If the database holds `Blob` values, the copy is not restorable without that companion directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-copied-separately) for the restore steps. LMDB databases only; RocksDB databases compact themselves. +As of v5.3.0 the database's `Blob` files are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name. If the database holds `Blob` values, the copy is not restorable without that companion directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-copied-separately) for the restore steps. LMDB databases only — `copy-db` fails if the source database is stored in RocksDB, which compacts itself. **Use Cases**: diff --git a/reference/database/compaction.md b/reference/database/compaction.md index 049a5882f..543d2c1df 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -41,7 +41,7 @@ To replace the original database with the compacted copy, move or rename the out harper copy-db data /home/user/hdb/database/copy.mdb ``` -Copy compaction applies to LMDB databases. RocksDB databases compact themselves and are skipped. +Copy compaction applies to LMDB databases only. `copy-db` fails if the source database is stored in RocksDB, and [compact on start](#compact-on-start) skips RocksDB databases — RocksDB compacts itself. ### File-backed blobs copied separately From d4ba5571f1a85bd514bac1ccae0b12aefb4e59b7 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 26 Aug 2026 00:10:23 -0600 Subject: [PATCH 05/11] Sharpen blob-root paths and multi-root restore steps From the pre-push review: configured blob roots are /, not the bare configured path; copy-db writes no companion directory at all when every blob root is missing on disk; a multi-root database needs every tree restored. Co-Authored-By: Claude Opus --- reference/cli/commands.md | 2 +- reference/database/compaction.md | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/reference/cli/commands.md b/reference/cli/commands.md index c577b510e..9afb6aa4d 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -253,7 +253,7 @@ harper copy-db **Parameters**: - `` - Name of the source database (a name, not a file path) -- `` - Full path to the target database file; neither it nor its `-blobs` companion directory may already exist +- `` - Full path to the target database file; neither it nor its `-blobs` companion directory may already exist. Retrying an interrupted copy means removing both. **Example**: diff --git a/reference/database/compaction.md b/reference/database/compaction.md index 543d2c1df..c065dcbc1 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -33,7 +33,7 @@ The `source-database` is the database name (not a file path). The target is the As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. Retrying an interrupted copy means removing both. -To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. +To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. That is the one case where the database file travels alone — any other destination also needs the blob companion directory described in [File-backed blobs copied separately](#file-backed-blobs-copied-separately). **Example — compact the default `data` database:** @@ -49,7 +49,7 @@ Copy compaction applies to LMDB databases only. `copy-db` fails if the source da `copy-db` copies the database's blob files alongside the copy. Earlier v5 releases copied only the database file, leaving the blobs behind. -`Blob` values are stored outside the database file (unlike `Bytes` values, which are stored inside the record). These blob files live in the configured blob roots — `storage.blobPaths[n]`, or `/blobs/` when `blobPaths` is not configured — and are addressed by **database name**, not by the path of the database file. +`Blob` values are stored outside the database file (unlike `Bytes` values, which are stored inside the record). These blob files live in the configured blob roots — `/`, or `/blobs/` when `blobPaths` is not configured — and are addressed by **database name**, not by the path of the database file. `copy-db` therefore writes them alongside the copy: @@ -59,7 +59,9 @@ Copy compaction applies to LMDB databases only. `copy-db` fails if the source da `` is the position of the source root in the database's blob-root list, preserved so a multi-root database restores each root to its original slot. A `README.md` in that directory records the mapping. -**If the database holds `Blob` values, the copy is not restorable without this directory** — moving the database file on its own silently loses every blob it references. A database with no live blob references does not need the companion directory; one may still be written (possibly empty) if a blob root directory exists for the database. +**If the database holds `Blob` values, the copy is not restorable without this directory** — moving the database file on its own silently loses every blob it references. A database with no live blob references does not need the companion directory, though one may still be written (possibly empty) whenever a blob root directory exists. + +The directory is written only for blob roots that exist on disk, and is not written at all when every root is missing — an unmounted `blobPaths` volume, for instance, yields a database-file-only copy without failing the command. Confirm the roots are mounted before copying, and confirm `-blobs` is there afterwards, before treating the copy as a restorable backup. To restore the copy under a database name, put each `` tree into that name's matching blob root — for example, restoring the copy above as a database named `archive` with no `storage.blobPaths` configured: @@ -68,6 +70,8 @@ cp -r /home/user/hdb/database/copy.mdb /home/user/hdb/database/archive.mdb cp -r /home/user/hdb/database/copy.mdb-blobs/0/. /home/user/hdb/blobs/archive/ ``` +A database with several `storage.blobPaths` entries has one `` tree per root: restore every one of them into the slot of the same index. Leaving a tree behind loses exactly the blobs that lived on that root, and neither the copy nor Harper reports it. + One narrow exception: if the copy immediately replaces its own source in place — same installation, same database name, before anything writes to the source — the database file alone is enough, since the blob roots it references are still exactly as the copy left them. A copy kept as a backup does not qualify: once the source is written to, Harper can reclaim the blob files an older copy still references, so restore the companion directory along with the database file. ## Compact on Start From f69fe9112f6ceb20601a01d2be115aae4df18c44 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 26 Aug 2026 00:18:20 -0600 Subject: [PATCH 06/11] Scope the companion-directory rule and note the compact-on-start backup From the round-3 pre-push review: the companion directory only matters for databases with Blob values; the backup compactOnStartKeepBackup retains carries no blobs; copy.mdb is a file, so cp needs no -r. Co-Authored-By: Claude Opus --- reference/database/compaction.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/reference/database/compaction.md b/reference/database/compaction.md index c065dcbc1..5e99df563 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -33,7 +33,7 @@ The `source-database` is the database name (not a file path). The target is the As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. Retrying an interrupted copy means removing both. -To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. That is the one case where the database file travels alone — any other destination also needs the blob companion directory described in [File-backed blobs copied separately](#file-backed-blobs-copied-separately). +To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. That is the one case where the database file travels alone; if the database has `Blob` values, any other destination also needs the blob companion directory described in [File-backed blobs copied separately](#file-backed-blobs-copied-separately). **Example — compact the default `data` database:** @@ -66,7 +66,7 @@ The directory is written only for blob roots that exist on disk, and is not writ To restore the copy under a database name, put each `` tree into that name's matching blob root — for example, restoring the copy above as a database named `archive` with no `storage.blobPaths` configured: ```bash -cp -r /home/user/hdb/database/copy.mdb /home/user/hdb/database/archive.mdb +cp /home/user/hdb/database/copy.mdb /home/user/hdb/database/archive.mdb cp -r /home/user/hdb/database/copy.mdb-blobs/0/. /home/user/hdb/blobs/archive/ ``` @@ -78,6 +78,10 @@ One narrow exception: if the copy immediately replaces its own source in place Automatically compacts all non-system databases when Harper starts. Harper will not start until compaction is complete. Under the hood, it loops through all user databases, creates a backup of each, compacts it, replaces the original with the compacted copy, and removes the backup. +Compact on start replaces each database in place under its own name, so the blob roots keep resolving and no blob companion directory is involved. As of v5.3.0 it skips RocksDB databases, and skips a database whose tables span more than one storage environment (table-specific paths), which compaction cannot replace as a single file. + +> **Note:** the backup `compactOnStartKeepBackup` retains is the pre-compaction database file only. It carries no blobs, and blob files are shared by database name, so blobs deleted or superseded after the compaction are gone from that backup's point of view. Treat it as a rollback for the compaction itself, not as a point-in-time backup — use [`create_backup`](../backups/overview.md) or `copy-db` with its companion directory for that. + Configure in `harper-config.yaml`: ```yaml From 3a2ba6a4c83ff7338762bec853a6413ad9f328ce Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 26 Aug 2026 00:22:09 -0600 Subject: [PATCH 07/11] Point LMDB users at an LMDB backup path, flag the stricter target rule create_backup is RocksDB-only, so the compact-on-start note now names volume snapshots, get_backup, or a copy-db copy kept with its companion directory. Also warns that refusing an existing target breaks scripts that re-copy to a fixed path. Co-Authored-By: Claude Opus --- reference/database/compaction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/database/compaction.md b/reference/database/compaction.md index 5e99df563..a5372d0fb 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -31,7 +31,7 @@ harper copy-db The `source-database` is the database name (not a file path). The target is the full file path where the compacted copy will be written. -As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. Retrying an interrupted copy means removing both. +As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. Retrying an interrupted copy means removing both. This is stricter than earlier v5 releases, which wrote into an existing target: a script that re-copies to a fixed path on a schedule has to remove the previous copy and its companion directory first, or it now fails. To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. That is the one case where the database file travels alone; if the database has `Blob` values, any other destination also needs the blob companion directory described in [File-backed blobs copied separately](#file-backed-blobs-copied-separately). @@ -80,7 +80,7 @@ Automatically compacts all non-system databases when Harper starts. Harper will Compact on start replaces each database in place under its own name, so the blob roots keep resolving and no blob companion directory is involved. As of v5.3.0 it skips RocksDB databases, and skips a database whose tables span more than one storage environment (table-specific paths), which compaction cannot replace as a single file. -> **Note:** the backup `compactOnStartKeepBackup` retains is the pre-compaction database file only. It carries no blobs, and blob files are shared by database name, so blobs deleted or superseded after the compaction are gone from that backup's point of view. Treat it as a rollback for the compaction itself, not as a point-in-time backup — use [`create_backup`](../backups/overview.md) or `copy-db` with its companion directory for that. +> **Note:** the backup `compactOnStartKeepBackup` retains is the pre-compaction database file only. It carries no blobs, and blob files are shared by database name, so blobs deleted or superseded after the compaction are gone from that backup's point of view. Treat it as a rollback for the compaction itself, not as a point-in-time backup. For that, LMDB databases need a volume snapshot, [`get_backup`](../backups/operations.md#get_backup), or a `copy-db` copy kept with its blob companion directory. Configure in `harper-config.yaml`: From 5caf51522c0482d60a4841dc196f6966df9adb25 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 26 Aug 2026 00:24:04 -0600 Subject: [PATCH 08/11] Note that LMDB get_backup streams the database file only It carries no blobs, so it is not a complete point-in-time backup for a database with Blob values. Co-Authored-By: Claude Opus --- reference/database/compaction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/database/compaction.md b/reference/database/compaction.md index a5372d0fb..da7766c9e 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -80,7 +80,7 @@ Automatically compacts all non-system databases when Harper starts. Harper will Compact on start replaces each database in place under its own name, so the blob roots keep resolving and no blob companion directory is involved. As of v5.3.0 it skips RocksDB databases, and skips a database whose tables span more than one storage environment (table-specific paths), which compaction cannot replace as a single file. -> **Note:** the backup `compactOnStartKeepBackup` retains is the pre-compaction database file only. It carries no blobs, and blob files are shared by database name, so blobs deleted or superseded after the compaction are gone from that backup's point of view. Treat it as a rollback for the compaction itself, not as a point-in-time backup. For that, LMDB databases need a volume snapshot, [`get_backup`](../backups/operations.md#get_backup), or a `copy-db` copy kept with its blob companion directory. +> **Note:** the backup `compactOnStartKeepBackup` retains is the pre-compaction database file only. It carries no blobs, and blob files are shared by database name, so blobs deleted or superseded after the compaction are gone from that backup's point of view. Treat it as a rollback for the compaction itself, not as a point-in-time backup. For that, an LMDB database needs a volume snapshot covering the database file and its blob roots together, or a `copy-db` copy kept with its blob companion directory — [`get_backup`](../backups/operations.md#get_backup) on an LMDB database streams the `.mdb` file only. Configure in `harper-config.yaml`: From dcaa55b607e5fccf543ae157ad57f1382522bdf9 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Thu, 27 Aug 2026 09:21:10 -0600 Subject: [PATCH 09/11] Say what copy-db does with a multi-environment database A database whose tables use per-table `path` settings spans several storage environments. The page already said copy-db is LMDB-only and that compact on start skips such a database, but never said which way copy-db goes -- refuse, or copy one environment and exit 0. Say that it errors out. Also from review: - Use `` for engine scope instead of prose, and badge Compact on Start's v5.3.0 behaviour changes, matching the two other sections this branch touches. - Promote the unmounted-blob-root case to an `> **Important:**` callout and point at the warning copy-db emits at copy time (naming each missing root) rather than at post-hoc inspection of the companion directory. Co-Authored-By: Claude Opus --- reference/cli/commands.md | 4 ++-- reference/database/compaction.md | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/reference/cli/commands.md b/reference/cli/commands.md index 9afb6aa4d..87b2fcc5a 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -242,7 +242,7 @@ This command regenerates the self-signed SSL/TLS certificates used by Harper. Available since: v4.1.0 - + Copy a Harper database with compaction to eliminate free-space and fragmentation. @@ -263,7 +263,7 @@ harper copy-db data /home/user/hdb/database/copy.mdb This copies the default `data` database to a new location with compaction applied. -As of v5.3.0 the database's `Blob` files are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name. If the database holds `Blob` values, the copy is not restorable without that companion directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-copied-separately) for the restore steps. LMDB databases only — `copy-db` fails if the source database is stored in RocksDB, which compacts itself. +As of v5.3.0 the database's `Blob` files are copied to `-blobs//`, since blob files live outside the database file and are addressed by database name. If the database holds `Blob` values, the copy is not restorable without that companion directory — see [Database Compaction](../database/compaction.md#file-backed-blobs-copied-separately) for the restore steps. LMDB databases only — `copy-db` fails if the source database is stored in RocksDB, which compacts itself. It also fails, rather than copying part of the database, if the source database's tables span more than one storage environment (per-table `path` settings). **Use Cases**: diff --git a/reference/database/compaction.md b/reference/database/compaction.md index da7766c9e..e156c0ed2 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -17,7 +17,7 @@ Compaction is also the mechanism to apply storage configuration changes (such as ## Copy Compaction - + Creates a compacted copy of a database file. The original database is left unchanged. @@ -41,7 +41,9 @@ To replace the original database with the compacted copy, move or rename the out harper copy-db data /home/user/hdb/database/copy.mdb ``` -Copy compaction applies to LMDB databases only. `copy-db` fails if the source database is stored in RocksDB, and [compact on start](#compact-on-start) skips RocksDB databases — RocksDB compacts itself. +`copy-db` fails if the source database is stored in RocksDB, and [compact on start](#compact-on-start) skips RocksDB databases — RocksDB compacts itself. + +`copy-db` also fails if the source database's tables span more than one storage environment. Per-table `path` settings put a database's tables in separate root stores — the same configuration that puts a database out of scope for [managed backups](../backups/overview.md#limitations) — and the target is a single environment file, so `copy-db` errors out rather than copying one environment and exiting successfully. [Compact on start](#compact-on-start) skips such a database for the same reason. ### File-backed blobs copied separately @@ -61,7 +63,7 @@ Copy compaction applies to LMDB databases only. `copy-db` fails if the source da **If the database holds `Blob` values, the copy is not restorable without this directory** — moving the database file on its own silently loses every blob it references. A database with no live blob references does not need the companion directory, though one may still be written (possibly empty) whenever a blob root directory exists. -The directory is written only for blob roots that exist on disk, and is not written at all when every root is missing — an unmounted `blobPaths` volume, for instance, yields a database-file-only copy without failing the command. Confirm the roots are mounted before copying, and confirm `-blobs` is there afterwards, before treating the copy as a restorable backup. +> **Important:** the companion directory holds only the blob roots that existed on disk when the copy ran, and is not written at all when every root is missing — an unmounted `blobPaths` volume, for instance, yields a database-file-only copy and still exits successfully. `copy-db` says so as it runs: it warns naming each missing root and stating that the copy is missing those blobs if the database ever wrote to them, and on a successful blob copy logs how many roots it copied and where. Read that output before treating the copy as a restorable backup — a root that was unmounted at copy time leaves no `` tree behind, while the mapping in the companion directory's `README.md` still lists it. To restore the copy under a database name, put each `` tree into that name's matching blob root — for example, restoring the copy above as a database named `archive` with no `storage.blobPaths` configured: @@ -76,9 +78,11 @@ One narrow exception: if the copy immediately replaces its own source in place ## Compact on Start + + Automatically compacts all non-system databases when Harper starts. Harper will not start until compaction is complete. Under the hood, it loops through all user databases, creates a backup of each, compacts it, replaces the original with the compacted copy, and removes the backup. -Compact on start replaces each database in place under its own name, so the blob roots keep resolving and no blob companion directory is involved. As of v5.3.0 it skips RocksDB databases, and skips a database whose tables span more than one storage environment (table-specific paths), which compaction cannot replace as a single file. +Compact on start replaces each database in place under its own name, so the blob roots keep resolving and no blob companion directory is involved. It skips RocksDB databases, and skips a database whose tables span more than one storage environment (table-specific paths), which compaction cannot replace as a single file. Both skips are logged and the remaining databases still compact. > **Note:** the backup `compactOnStartKeepBackup` retains is the pre-compaction database file only. It carries no blobs, and blob files are shared by database name, so blobs deleted or superseded after the compaction are gone from that backup's point of view. Treat it as a rollback for the compaction itself, not as a point-in-time backup. For that, an LMDB database needs a volume snapshot covering the database file and its blob roots together, or a `copy-db` copy kept with its blob companion directory — [`get_backup`](../backups/operations.md#get_backup) on an LMDB database streams the `.mdb` file only. From 8623bf56a0cf04a2a6ab9c2ff842f0220f8571f6 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Thu, 27 Aug 2026 09:26:15 -0600 Subject: [PATCH 10/11] Correct the copy-db retry claim and harden the restore example From the pre-push review: - A failed `copy-db` removes the target, its lock file, and the companion directory it created before reporting the error (harper#2098 `copyDb`'s catch), so "retrying an interrupted copy means removing both" was wrong about the common case. Say what actually needs clearing by hand: a previous *successful* copy to the same path, or a run killed before cleanup could run. - Say that the exit status does not carry the missing-blob-root warning, so a scheduled copy checking only the exit code keeps rotating incomplete copies. - `cp -r src/. dest/` creates one missing directory level but not two, so the restore example fails for a database whose blob root does not exist yet. Add `mkdir -p`. Co-Authored-By: Claude Opus --- reference/cli/commands.md | 2 +- reference/database/compaction.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/reference/cli/commands.md b/reference/cli/commands.md index 87b2fcc5a..42e81997a 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -253,7 +253,7 @@ harper copy-db **Parameters**: - `` - Name of the source database (a name, not a file path) -- `` - Full path to the target database file; neither it nor its `-blobs` companion directory may already exist. Retrying an interrupted copy means removing both. +- `` - Full path to the target database file; neither it nor its `-blobs` companion directory may already exist. A failed copy removes what it created, so a retry is not blocked by it; a previous successful copy to the same path has to be removed first. **Example**: diff --git a/reference/database/compaction.md b/reference/database/compaction.md index e156c0ed2..0056acaf6 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -31,7 +31,7 @@ harper copy-db The `source-database` is the database name (not a file path). The target is the full file path where the compacted copy will be written. -As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. Retrying an interrupted copy means removing both. This is stricter than earlier v5 releases, which wrote into an existing target: a script that re-copies to a fixed path on a schedule has to remove the previous copy and its companion directory first, or it now fails. +As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. A copy that fails removes the target, its lock file, and the companion directory it created before reporting the error, so a retry is not blocked by the failed attempt; clearing them by hand is for a previous successful copy to the same path, or a run killed outright before it could clean up. This is stricter than earlier v5 releases, which wrote into an existing target: a script that re-copies to a fixed path on a schedule has to remove the previous copy and its companion directory first, or it now fails. To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. That is the one case where the database file travels alone; if the database has `Blob` values, any other destination also needs the blob companion directory described in [File-backed blobs copied separately](#file-backed-blobs-copied-separately). @@ -63,12 +63,13 @@ harper copy-db data /home/user/hdb/database/copy.mdb **If the database holds `Blob` values, the copy is not restorable without this directory** — moving the database file on its own silently loses every blob it references. A database with no live blob references does not need the companion directory, though one may still be written (possibly empty) whenever a blob root directory exists. -> **Important:** the companion directory holds only the blob roots that existed on disk when the copy ran, and is not written at all when every root is missing — an unmounted `blobPaths` volume, for instance, yields a database-file-only copy and still exits successfully. `copy-db` says so as it runs: it warns naming each missing root and stating that the copy is missing those blobs if the database ever wrote to them, and on a successful blob copy logs how many roots it copied and where. Read that output before treating the copy as a restorable backup — a root that was unmounted at copy time leaves no `` tree behind, while the mapping in the companion directory's `README.md` still lists it. +> **Important:** the companion directory holds only the blob roots that existed on disk when the copy ran, and is not written at all when every root is missing — an unmounted `blobPaths` volume, for instance, yields a database-file-only copy and still exits successfully. `copy-db` says so as it runs: it warns naming each missing root and stating that the copy is missing those blobs if the database ever wrote to them, and on a successful blob copy logs how many roots it copied and where. The exit status carries none of it, so a scheduled copy that checks only the exit code will keep rotating incomplete copies — check the warning, or that every expected `` tree is present, before treating a copy as a restorable backup. A root that was unmounted at copy time leaves no `` tree behind, while the mapping in the companion directory's `README.md` still lists it. To restore the copy under a database name, put each `` tree into that name's matching blob root — for example, restoring the copy above as a database named `archive` with no `storage.blobPaths` configured: ```bash cp /home/user/hdb/database/copy.mdb /home/user/hdb/database/archive.mdb +mkdir -p /home/user/hdb/blobs/archive cp -r /home/user/hdb/database/copy.mdb-blobs/0/. /home/user/hdb/blobs/archive/ ``` From 04536bd8499c9e00ed8b087fcb074e55270eb0a6 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Thu, 27 Aug 2026 09:28:48 -0600 Subject: [PATCH 11/11] Say the failed-copy cleanup is best effort `copyDb`'s catch removes each artifact with `.catch(() => {})`, so a removal that itself fails is swallowed and the next run is still refused for an existing target. The previous wording promised the cleanup unconditionally. Co-Authored-By: Claude Opus --- reference/cli/commands.md | 2 +- reference/database/compaction.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/cli/commands.md b/reference/cli/commands.md index 42e81997a..3151c448f 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -253,7 +253,7 @@ harper copy-db **Parameters**: - `` - Name of the source database (a name, not a file path) -- `` - Full path to the target database file; neither it nor its `-blobs` companion directory may already exist. A failed copy removes what it created, so a retry is not blocked by it; a previous successful copy to the same path has to be removed first. +- `` - Full path to the target database file; neither it nor its `-blobs` companion directory may already exist. A failed copy makes a best-effort attempt to remove what it created, so a retry is usually not blocked by it; remove both by hand if a retry is refused, as it is after a previous successful copy to the same path. **Example**: diff --git a/reference/database/compaction.md b/reference/database/compaction.md index 0056acaf6..21ee03430 100644 --- a/reference/database/compaction.md +++ b/reference/database/compaction.md @@ -31,7 +31,7 @@ harper copy-db The `source-database` is the database name (not a file path). The target is the full file path where the compacted copy will be written. -As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. A copy that fails removes the target, its lock file, and the companion directory it created before reporting the error, so a retry is not blocked by the failed attempt; clearing them by hand is for a previous successful copy to the same path, or a run killed outright before it could clean up. This is stricter than earlier v5 releases, which wrote into an existing target: a script that re-copies to a fixed path on a schedule has to remove the previous copy and its companion directory first, or it now fails. +As of v5.3.0 neither the target path nor its `-blobs` companion directory may already exist — `copy-db` refuses both rather than merging the copy into whatever they hold. A copy that fails tries to remove the target, its lock file, and the companion directory it created before reporting the error, so a retry is usually not blocked by the failed attempt. That cleanup is best effort and a removal that itself fails is not reported, so clear both by hand whenever a retry is refused for an existing target — as it also is after a previous successful copy to the same path, or a run killed outright before cleanup could run. This is stricter than earlier v5 releases, which wrote into an existing target: a script that re-copies to a fixed path on a schedule has to remove the previous copy and its companion directory first, or it now fails. To replace the original database with the compacted copy, move or rename the output file to the original database path after Harper is stopped. That is the one case where the database file travels alone; if the database has `Blob` values, any other destination also needs the blob companion directory described in [File-backed blobs copied separately](#file-backed-blobs-copied-separately).