Skip to content

Commit 461ddd4

Browse files
committed
fix(db): re-root a rescued folder whose parent is soft-deleted
Step 1's parent resolver checked existence and workspace but not reachability, so an ACTIVE stranded folder whose parent is soft-deleted kept that parent and landed in Recently Deleted — the same invisibility the re-root fallback exists to prevent, and the opposite of resolveRestoredFolderId, which re-roots a restored folder whose original parent is archived. A soft-deleted row is exempt: it may keep a soft-deleted parent, since that is the normal shape of an archived subtree and flattening it would destroy the hierarchy a later restore rebuilds. Verified differentially against Postgres 17.9: pre-fix both active rows keep the deleted parent, post-fix both re-root while the archived child keeps its parent.
1 parent 20c0105 commit 461ddd4

1 file changed

Lines changed: 21 additions & 5 deletions

File tree

packages/db/migrations/0275_drop_legacy_folder_tables.sql

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,18 @@ BEGIN
5353
FROM "workflow_folder" l
5454
WHERE NOT EXISTS (SELECT 1 FROM "folder" f WHERE f.id = l.id)
5555
),
56-
-- A parent is only usable if it will exist AND shares this row's workspace — the
57-
-- `folder_parent_resource_type_match` trigger enforces the workspace match, and the legacy
58-
-- self-FK never did, so a cross-workspace parent is representable in the source data.
56+
-- A parent is only usable if it will exist, shares this row's workspace, and leaves the row
57+
-- REACHABLE. The workspace match is enforced by the `folder_parent_resource_type_match`
58+
-- trigger and was never enforced by the legacy self-FK, so a cross-workspace parent is
59+
-- representable in the source data. Reachability is the subtler half: filing an ACTIVE
60+
-- folder under a soft-deleted parent hides it in Recently Deleted just as thoroughly as a
61+
-- dangling parent would, so it re-roots too — matching `resolveRestoredFolderId`, which
62+
-- re-roots a restored folder whose original parent is archived.
63+
--
64+
-- A soft-deleted row is exempt: it MAY keep a soft-deleted parent, because that is the
65+
-- normal shape of an archived subtree and flattening it would destroy the hierarchy a
66+
-- later restore rebuilds.
67+
--
5968
-- Anything else re-roots to the workspace root: losing one level of nesting beats losing the
6069
-- folder and stranding every workflow inside it.
6170
resolved AS (
@@ -65,9 +74,12 @@ BEGIN
6574
WHEN EXISTS (
6675
SELECT 1 FROM "folder" f
6776
WHERE f.id = s.parent_id AND f.resource_type = 'workflow' AND f.workspace_id = s.workspace_id
77+
AND (s.deleted_at IS NOT NULL OR f.deleted_at IS NULL)
6878
) THEN s.parent_id
6979
WHEN EXISTS (
70-
SELECT 1 FROM stranded s2 WHERE s2.id = s.parent_id AND s2.workspace_id = s.workspace_id
80+
SELECT 1 FROM stranded s2
81+
WHERE s2.id = s.parent_id AND s2.workspace_id = s.workspace_id
82+
AND (s.deleted_at IS NOT NULL OR s2.deleted_at IS NULL)
7183
) THEN s.parent_id
7284
ELSE NULL
7385
END AS resolved_parent
@@ -152,16 +164,20 @@ BEGIN
152164
FROM "workspace_file_folders" l
153165
WHERE NOT EXISTS (SELECT 1 FROM "folder" f WHERE f.id = l.id)
154166
),
167+
-- Same reachability rule as the workflow tree above.
155168
resolved AS (
156169
SELECT s.*,
157170
CASE
158171
WHEN s.parent_id IS NULL THEN NULL
159172
WHEN EXISTS (
160173
SELECT 1 FROM "folder" f
161174
WHERE f.id = s.parent_id AND f.resource_type = 'file' AND f.workspace_id = s.workspace_id
175+
AND (s.deleted_at IS NOT NULL OR f.deleted_at IS NULL)
162176
) THEN s.parent_id
163177
WHEN EXISTS (
164-
SELECT 1 FROM stranded s2 WHERE s2.id = s.parent_id AND s2.workspace_id = s.workspace_id
178+
SELECT 1 FROM stranded s2
179+
WHERE s2.id = s.parent_id AND s2.workspace_id = s.workspace_id
180+
AND (s.deleted_at IS NOT NULL OR s2.deleted_at IS NULL)
165181
) THEN s.parent_id
166182
ELSE NULL
167183
END AS resolved_parent

0 commit comments

Comments
 (0)