fix: cascade folder renames to the subtree and prune ghost folders - #303
fix: cascade folder renames to the subtree and prune ghost folders#303Monkey7539 wants to merge 1 commit into
Conversation
Renaming a folder with subfolders updated only the exact-path DB rows, while IMAP RENAME moves the whole subtree server-side. Every child folder (and its messages) stayed under the old path: the next folder sync upserted the renamed tree from LIST — a visible duplicate — while the per-folder message sync kept trying, and failing, to open the stale old child paths forever. Two-part fix: - /folders/rename rewrites the path prefix for the folder AND all descendants, in both folders and messages (substr-prefix matching, no LIKE escaping traps). If a sync raced the rename and already inserted rows at the new paths, the stale old rows are dropped first instead of colliding with the unique constraints. - syncFolders prunes DB folder rows absent from LIST, so ghosts from renames/deletes made by other clients (or pre-fix renames) heal on the next folder sync instead of erroring every tick. Guarded on a non-empty LIST; INBOX is exempt (many servers omit it from LIST); message rows are left to the existing deletion paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks, this is a real pair of bugs: the rename genuinely was not cascading to the subtree, and ghost folders did accumulate with no prune. The prune half is nicely done and well-tested. One framing note up front: since IMAP is the source of truth and High: the collision-DELETE can delete the real source rows. The Medium: no guard against a same-name (no-op) rename. Medium: the cascade is not transactional. It is now five sequential statements (two DELETEs, three UPDATEs) with no BEGIN/COMMIT. A crash or dropped connection mid-sequence leaves the subtree half-rewritten. Sync eventually reconciles, but wrapping the five in a transaction makes the rename atomic and is worth it given the added deletes. Medium: the prune trusts any non-empty LIST. The only guard is Tests. The prune has good coverage; the rename cascade, the half with all the destructive SQL, has none. Please add rename tests: a nested subtree rewrite, a child with its own children, the ghost-at-target case, and the same-name case. Happy to re-review once the collision-DELETE and the same-name guard are handled. Those two are the ones I would want fixed for sure; the transaction, the prune hardening, and the tests are strongly preferred. |
Problem
Renaming a folder that has subfolders (tested via the sidebar rename,
POST /folders/rename):RENAMEsucceeds and moves the whole subtree server-side (per RFC 3501 §6.3.5).LIST→ the sidebar shows a duplicate tree (real renamed folders + stale ghosts with all the same subfolders).syncFoldersnever removes rows for folders that no longer exist on the server, so the ghosts are permanent.The missing prune also means folders renamed/deleted by other IMAP clients leave the same permanent ghosts (I have one of those in my instance too — a folder that errors
Reconcile: could not open …on every cycle).Fix
/folders/renamecascades to the subtree. The path prefix is rewritten for the folder and all descendants in bothfoldersandmessages(substr-prefix matching onoldPath + delimiter, so no LIKE-escaping traps with%/_in folder names). If a sync raced the rename and already inserted rows at the new paths, the stale old rows are deleted first instead of colliding with the unique(account_id, path)/(account_id, uid, folder)constraints.syncFoldersprunes ghosts. After upserting theLISTresults, DB folder rows absent from the response are deleted, so ghosts (from external renames/deletes, or from pre-fix renames) heal on the next folder sync instead of erroring every tick. Safeguards: skipped entirely on an emptyLIST(a pathological response must not wipe the account's tree), andINBOXis exempt (many servers omit it fromLIST; the implicit-INBOX insert above depends on that). Message rows are deliberately left alone — in-app folder deletion already removes them explicitly, and orphans stop syncing once their folder row is gone.Testing
syncFolders pruningtests: ghosts deleted with the exact live-path list, emptyLISTnever prunes, upserts still precede the prune.could not opensync errors) and verified the failure mode disappears with the cascade.Written with AI assistance (Claude); I have reviewed and tested the changes and am authorized to submit them under the project's CLA.
🤖 Generated with Claude Code