From 49f39ca9115f9428a752cc70ec0bd4bb19903227 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sat, 24 Oct 2026 05:30:25 -0700 Subject: [PATCH] feat(maintenance): always back up before storing workflows once Final review fixes I1, I2 and I3 for the workflow storage dedupe: - I1: the migration always takes a verified backup, whatever maintenance.safety_backup says; the plan reports backup.policy "required" and the disk check counts it. - I2: that backup is prompts.db.pre-workflow-storage_, outside the pruned prompts.db.backup_* pool and kept until the user removes it; the job's last line and the preview name it, and the legacy restore accepts it. README's changelog warns that an older version opens and restores a rewritten database but shows no workflows, and says how to go back. - I3: tools/workflow_text_digests.py writes the sha256 of every record's stored texts (read-only; inline before the rewrite, through the accessor after), so a diff before and after proves byte identity. - A row the migration could not rewrite is listed in the job's errors and named in its last line, and Reclaim space still runs. --- README.md | 5 + TESTING.md | 28 +- py/api/maintenance.py | 41 ++- py/api/workflow_storage.py | 85 ++++-- tests/test_api_workflow_storage.py | 33 ++- tests/test_workflow_storage_review.py | 376 ++++++++++++++++++++++++++ tools/workflow_text_digests.py | 73 +++++ 7 files changed, 589 insertions(+), 52 deletions(-) create mode 100644 tests/test_workflow_storage_review.py create mode 100644 tools/workflow_text_digests.py diff --git a/README.md b/README.md index e259362d..a61043c5 100644 --- a/README.md +++ b/README.md @@ -970,6 +970,11 @@ MIT License - see LICENSE file for details. ## Changelog +### Unreleased (Workflows stored once) + +- **🗜️ Smaller database**: each distinct workflow and prompt graph is stored once, compressed, instead of again for every image. New images use it at once; the maintenance job "Store each workflow once" rewrites existing records and then reclaims the space. A 20,000-image test library went from 1.4 GB to 60 MB. The job always takes a verified backup first, `prompts.db.pre-workflow-storage_` beside the database, which is kept until you delete it. No image or video file is opened or changed. +- **⚠️ Downgrading after the rewrite**: an older Prompt Manager version still opens and even restores a rewritten database, but shows its images without workflows or prompt graphs. To go back to an older version, stop ComfyUI, delete `prompts.db-wal` and `prompts.db-shm`, and copy `prompts.db.pre-workflow-storage_` over `prompts.db` (records added since that backup are lost), or stay on this version. + ### v3.3.1 (Proxy Routing Fix) - **🌐 Proxy support**: the dashboard, gallery, tag page and metadata viewer now work when ComfyUI is reached through a proxy mount such as SwarmUI's `/ComfyBackendDirect/`. Scripts, images, API calls, progress streams and gallery folder filters stay under the mount, and the canvas node's buttons use ComfyUI's `fileURL()`. Direct ComfyUI URLs are unchanged. Thanks to @ghosttigerllc-bit ([#164](https://github.com/ComfyAssets/ComfyUI_PromptManager/pull/164)) diff --git a/TESTING.md b/TESTING.md index dbd959bb..7f8cf27a 100644 --- a/TESTING.md +++ b/TESTING.md @@ -2721,10 +2721,25 @@ table definition, the id columns, their foreign keys and indexes, the stored val dangling ids (`TestSchemaErrors`); a pre-dedupe file is accepted and migrated on open and on restore. The `workflow_storage` maintenance plan counts the old-form records (one pass over record headers) and its apply starts "Store each workflow once" -(`records`, cancellable): the verified safety backup, then batches of at most 200 rows or -16 MB of text, each one `BEGIN IMMEDIATE` transaction found by a rowid range search and -read one row at a time; each text is read back through the accessor before its row -changes, and a mismatch or any error rolls the batch back whole. A crash or Cancel leaves +(`records`, cancellable): first, always and whatever `maintenance.safety_backup` says, +a verified pre-migration backup named `.pre-workflow-storage_`, +outside the pruned `.backup_*` pool and kept until the user removes it (the preview +reports `backup.policy: "required"`; `test_the_backup_is_taken_even_with_the_policy_off`, +`test_the_backup_is_outside_the_pool_never_pruned_and_restores`); then batches of at most +200 rows, 16 MB of text or 0.5 s, each one `BEGIN IMMEDIATE` transaction found by a rowid +range search and read one row at a time, followed by 0.15 s with the write lock free so +a writer on another connection gets in (`test_a_writer_on_another_connection_gets_in_between_batches`). +Each text is read back through the accessor before its row changes. Each row has its own +savepoint: a row that cannot be rewritten (undecodable text, a text that would not come +back) stays inline, is listed in `errors` and named in the job's last line, and the rest +go on, Reclaim space included (`TestRowErrors`); any other failure rolls the batch back +whole. A reused document is decompressed and checked first and repaired from the text +when damaged, and `verify_database_file` (restore and backup verification) reads every +document and refuses a damaged one (`TestDamagedDocuments`). The legacy table rebuilds +refuse a file that already has the document id columns (`TestRebuildGuards`). +`tools/workflow_text_digests.py ` writes the sha256 of every record's stored +texts (read-only), so a `diff` of its output before and after the rewrite proves byte +identity. A crash or Cancel leaves committed batches new form and the rest old form, and the next apply continues (`test_a_crash_mid_migration_leaves_every_row_readable_and_resumes`, `test_cancel_stops_after_the_current_batch_and_a_rerun_continues`). Values that are not @@ -2735,8 +2750,9 @@ does not start and the finished migration names Optimize database. No image or v file is opened (`test_the_jobs_never_open_a_media_file` tracks every Python open under the output folder and compares bytes and mtimes). Memory stays bounded by one row plus the batch's distinct documents (`test_memory_stays_bounded_by_one_row`, tracemalloc). -Restoring a deduped backup on a version before this one is not supported (that version -reads rewritten images without their workflow). +Restoring a deduped backup on a version before this one is not supported: that +version's restore accepts the file and then reads rewritten images without their +workflow (README changelog). ## Workspace data fields for the concept pages (#249) diff --git a/py/api/maintenance.py b/py/api/maintenance.py index dad45dda..640a460d 100644 --- a/py/api/maintenance.py +++ b/py/api/maintenance.py @@ -69,6 +69,8 @@ PREVIEW_BODY_MAX = 4096 DUPLICATE_PREVIEW_BODY_MAX = 1024 * 1024 DUPLICATE_FILES = "duplicate_files" WORKFLOW_STORAGE = "workflow_storage" # applied by py/api/workflow_storage.py +# Its backup's name, outside the pruned ``.backup_*`` pool. +PRE_MIGRATION_LABEL = "pre-workflow-storage" PREVIEW_OPERATIONS = MAINTENANCE_OPERATIONS + (DUPLICATE_FILES,) # Room left over beyond what a step writes, so a disk is never filled to 0. DISK_MARGIN_BYTES = 16 * 2**20 @@ -245,9 +247,9 @@ def _remove_quietly(*paths): pass -def _reserve_backup_name(db_path): - """Create and return an unused ``.backup_[_n]`` (O_EXCL).""" - for candidate in _backup_names(db_path): +def _reserve_backup_name(db_path, label="backup"): + """Create and return an unused ``.