fix: delete folders using postorder (#113493)

* fix: delete folders using postorder

* chore: use helper function and do not add method to Folder store

- addresses other review comments fixing log messages and cleans up the unit tests

* chore: run library element tests on modes 2,3,5 only

* chore: adjust to folder.SortByPostorder(folders []*Folder)

* chore: run library panels tests in mode 2,3,5 only

* chore: run tests in all modes and increase timeout

- adjusting the modes and tweaking configs will be done separately
This commit is contained in:
Rafael Bortolon Paulovic
2025-11-06 15:04:34 +01:00
committed by GitHub
parent fbf1cdd0ce
commit e69f3c55f7
8 changed files with 314 additions and 8 deletions
+3 -2
View File
@@ -1241,15 +1241,16 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold
s.log.ErrorContext(ctx, "failed to get descendant folders", "error", err)
return descendantUIDs, err
}
descendants = folder.SortByPostorder(descendants)
for _, f := range descendants {
descendantUIDs = append(descendantUIDs, f.UID)
}
s.log.InfoContext(ctx, "deleting folder descendants", "org_id", cmd.OrgID, "uid", cmd.UID)
s.log.InfoContext(ctx, "deleting legacy folder descendants", "org_id", cmd.OrgID, "uid", cmd.UID, "descendantsUIDs", strings.Join(descendantUIDs, ","))
err = s.store.Delete(ctx, descendantUIDs, cmd.OrgID)
if err != nil {
s.log.InfoContext(ctx, "failed deleting descendants", "org_id", cmd.OrgID, "parent_uid", cmd.UID, "err", err)
s.log.ErrorContext(ctx, "failed to delete legacy folder descendants", "org_id", cmd.OrgID, "parent_uid", cmd.UID, "descendantsUIDs", strings.Join(descendantUIDs, ","), "err", err)
return descendantUIDs, err
}
return descendantUIDs, nil
@@ -618,12 +618,14 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
if err != nil {
return err
}
descFolders = folder.SortByPostorder(descFolders)
folders := []string{}
for _, f := range descFolders {
folders = append(folders, f.UID)
}
// must delete children first, then the parent folder
s.log.InfoContext(ctx, "deleting folder with descendants", "org_id", cmd.OrgID, "uid", cmd.UID, "folderUIDs", strings.Join(folders, ","))
folders = append(folders, cmd.UID)
if cmd.ForceDeleteRules {
@@ -880,6 +880,7 @@ func TestIntegrationDeleteFoldersFromApiServer(t *testing.T) {
registry: make(map[string]folder.RegistryService),
features: featuremgmt.WithFeatures(),
tracer: tracer,
log: slog.New(logtest.NewNopHandler(t)),
}
user := &user.SignedInUser{OrgID: 1}
ctx := identity.WithRequester(context.Background(), user)
+47
View File
@@ -278,3 +278,50 @@ type GetDescendantCountsQuery struct {
}
type DescendantCounts map[string]int64
// SortByPostorder returns the folders in postorder traversal order.
// That is, children folders appear before their parents in the returned slice.
func SortByPostorder(folders []*Folder) []*Folder {
if len(folders) == 0 {
return folders
}
// Build parent-to-children map
tree := make(map[string][]*Folder)
folderMap := make(map[string]*Folder)
for _, f := range folders {
folderMap[f.UID] = f
tree[f.ParentUID] = append(tree[f.ParentUID], f)
}
// Find all roots (folders whose parents are not in the result set)
var roots []*Folder
for _, f := range folders {
if folderMap[f.ParentUID] == nil {
roots = append(roots, f)
}
}
// Traverse in postorder
result := make([]*Folder, 0, len(folders))
visited := make(map[string]bool)
var traverse func(f *Folder)
traverse = func(f *Folder) {
if visited[f.UID] {
return
}
visited[f.UID] = true
// First visit all children
for _, child := range tree[f.UID] {
traverse(child)
}
// Then add current folder
result = append(result, f)
}
for _, root := range roots {
traverse(root)
}
return result
}
+153
View File
@@ -0,0 +1,153 @@
package folder
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestFoldersSortByPostorder(t *testing.T) {
t.Run("empty list returns empty list", func(t *testing.T) {
var folders []*Folder
result := SortByPostorder(folders)
require.Empty(t, result)
})
t.Run("single folder returns single folder", func(t *testing.T) {
folders := []*Folder{
{UID: "a", ParentUID: "root"},
}
result := SortByPostorder(folders)
require.Len(t, result, 1)
require.Equal(t, "a", result[0].UID)
})
t.Run("linear hierarchy orders children before parents", func(t *testing.T) {
// Structure: root -> a -> b -> c
folders := []*Folder{
{UID: "a", ParentUID: "root"},
{UID: "c", ParentUID: "b"},
{UID: "b", ParentUID: "a"},
}
result := SortByPostorder(folders)
require.Len(t, result, 3)
// Postorder: c, b, a (children before parents)
require.Equal(t, "c", result[0].UID)
require.Equal(t, "b", result[1].UID)
require.Equal(t, "a", result[2].UID)
})
t.Run("branching hierarchy orders all children before their parent", func(t *testing.T) {
// Structure:
// root
// |
// a
// / | \
// b c d
folders := []*Folder{
{UID: "a", ParentUID: "root"},
{UID: "b", ParentUID: "a"},
{UID: "c", ParentUID: "a"},
{UID: "d", ParentUID: "a"},
}
result := SortByPostorder(folders)
require.Len(t, result, 4)
// 'a' must come after all its children (b, c, d)
aIndex := -1
for i, f := range result {
if f.UID == "a" {
aIndex = i
break
}
}
require.Equal(t, 3, aIndex, "parent 'a' should be last")
// All children should come before parent
for i := 0; i < 3; i++ {
require.Contains(t, []string{"b", "c", "d"}, result[i].UID)
}
})
t.Run("deep hierarchy orders by depth", func(t *testing.T) {
// Structure:
// root
// |
// a
// |
// b
// / \
// c d
// |
// e
folders := []*Folder{
{UID: "a", ParentUID: "root"},
{UID: "b", ParentUID: "a"},
{UID: "c", ParentUID: "b"},
{UID: "d", ParentUID: "b"},
{UID: "e", ParentUID: "c"},
}
result := SortByPostorder(folders)
require.Len(t, result, 5)
// e should come before c
eIndex := -1
cIndex := -1
for i, f := range result {
if f.UID == "e" {
eIndex = i
}
if f.UID == "c" {
cIndex = i
}
}
require.Less(t, eIndex, cIndex, "e should come before c")
// c and d should come before b
bIndex := -1
dIndex := -1
for i, f := range result {
if f.UID == "b" {
bIndex = i
}
if f.UID == "d" {
dIndex = i
}
}
require.Less(t, cIndex, bIndex, "c should come before b")
require.Less(t, dIndex, bIndex, "d should come before b")
// b should come before a
aIndex := -1
for i, f := range result {
if f.UID == "a" {
aIndex = i
}
}
require.Less(t, bIndex, aIndex, "b should come before a")
})
t.Run("multiple subtrees maintains postorder per subtree", func(t *testing.T) {
// Structure:
// root1 root2
// / \ |
// a b c
// | |
// d e
folders := []*Folder{
{UID: "a", ParentUID: "root1"},
{UID: "b", ParentUID: "root1"},
{UID: "d", ParentUID: "b"},
{UID: "c", ParentUID: "root2"},
{UID: "e", ParentUID: "c"},
}
result := SortByPostorder(folders)
require.Len(t, result, 5)
// Find indices
indices := make(map[string]int)
for i, f := range result {
indices[f.UID] = i
}
// Check postorder for first subtree: d before b, both before root1
require.Less(t, indices["d"], indices["b"], "d should come before b")
// Check postorder for second subtree: e before c, both before root2
require.Less(t, indices["e"], indices["c"], "e should come before c")
})
}
+1 -1
View File
@@ -46,7 +46,7 @@ type Store interface {
// GetFolders returns folders with given uids
GetFolders(ctx context.Context, q GetFoldersFromStoreQuery) ([]*Folder, error)
// GetDescendants returns all descendants of a folder
// GetDescendants returns all descendants of a folder (with no guaranteed order)
GetDescendants(ctx context.Context, orgID int64, anchestor_uid string) ([]*Folder, error)
// CountInOrg returns the number of folders in the given org