fix: propagate is_archived from parent to child projects in ReadAll CTE

Replace the Go-side propagateArchivedState function with in-CTE
propagation. The recursive SELECT uses (ap.is_archived OR p.is_archived)
to inherit archived state from parent projects. The outer query uses
GROUP BY with MAX(CAST(is_archived AS int)) to handle projects
accessible via both direct permissions and parent traversal. When
getArchived=false, a HAVING clause filters out archived projects.

The is_archived filter is removed from getUserProjectsStatement so
archived parents enter the CTE and propagation works correctly.
This commit is contained in:
kolaente 2026-03-25 09:57:30 +01:00 committed by kolaente
parent dca041459f
commit e3045dfd00
2 changed files with 31 additions and 18 deletions

View File

@ -70,7 +70,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxPerm
if isLinkShare {
where = builder.Eq{"project_id": linkShare.ProjectID}
} else {
where = builder.In("project_id", getUserProjectsStatement(a.GetID(), "", false).Select("l.id"))
where = builder.In("project_id", getUserProjectsStatement(a.GetID(), "").Select("l.id"))
createdByID = a.GetID()
}

View File

@ -454,7 +454,7 @@ type projectOptions struct {
getArchived bool
}
func getUserProjectsStatement(userID int64, search string, getArchived bool) *builder.Builder {
func getUserProjectsStatement(userID int64, search string) *builder.Builder {
dialect := db.GetDialect()
conds := []builder.Cond{
@ -507,16 +507,8 @@ func getUserProjectsStatement(userID int64, search string, getArchived bool) *bu
conds = append(conds, filterCond, parentCondition)
}
if !getArchived {
conds = append(conds,
builder.And(
builder.Eq{"l.is_archived": false},
),
)
}
return builder.Dialect(dialect).
Select("l.*").
Select("l.id, l.title, l.description, l.identifier, l.hex_color, l.owner_id, l.parent_project_id, l.is_archived, l.background_file_id, l.background_blur_hash, l.position, l.created, l.updated").
From("projects", "l").
Join("LEFT", "team_projects tl", "tl.project_id = l.id").
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
@ -541,14 +533,14 @@ func accessibleProjectIDsSubquery(a web.Auth, column string) builder.Cond {
}
return builder.In(column,
getUserProjectsStatement(u.ID, "", false).Select("l.id"),
getUserProjectsStatement(u.ID, "").Select("l.id"),
)
}
func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions) (projects []*Project, totalCount int64, err error) {
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
query := getUserProjectsStatement(userID, opts.search, opts.getArchived)
query := getUserProjectsStatement(userID, opts.search)
querySQLString, args, err := query.ToSQL()
if err != nil {
@ -562,7 +554,7 @@ func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions)
baseQuery := querySQLString + `
UNION ALL
SELECT p.* FROM projects p
SELECT p.id, p.title, p.description, p.identifier, p.hex_color, p.owner_id, p.parent_project_id, (ap.is_archived OR p.is_archived) AS is_archived, p.background_file_id, p.background_blur_hash, p.position, p.created, p.updated FROM projects p
INNER JOIN all_projects ap ON p.parent_project_id = ap.id`
columnStr := strings.Join([]string{
@ -573,17 +565,38 @@ INNER JOIN all_projects ap ON p.parent_project_id = ap.id`
"all_projects.hex_color",
"all_projects.owner_id",
"CASE WHEN all_projects.parent_project_id IS NULL THEN 0 ELSE all_projects.parent_project_id END AS parent_project_id",
"all_projects.is_archived",
"MAX(CAST(all_projects.is_archived AS int)) AS is_archived",
"all_projects.background_file_id",
"all_projects.background_blur_hash",
"all_projects.position",
"all_projects.created",
"all_projects.updated",
}, ", ")
groupByStr := strings.Join([]string{
"all_projects.id",
"all_projects.title",
"all_projects.description",
"all_projects.identifier",
"all_projects.hex_color",
"all_projects.owner_id",
"all_projects.parent_project_id",
"all_projects.background_file_id",
"all_projects.background_blur_hash",
"all_projects.position",
"all_projects.created",
"all_projects.updated",
}, ", ")
var archivedFilter string
if !opts.getArchived {
archivedFilter = "HAVING MAX(CAST(all_projects.is_archived AS int)) = 0 "
}
currentProjects := []*Project{}
err = s.SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`)
SELECT DISTINCT `+columnStr+` FROM all_projects
ORDER BY all_projects.position `+limitSQL, args...).Find(&currentProjects)
SELECT `+columnStr+` FROM all_projects
GROUP BY `+groupByStr+` `+archivedFilter+`ORDER BY all_projects.position `+limitSQL, args...).Find(&currentProjects)
if err != nil {
return
}
@ -594,7 +607,7 @@ ORDER BY all_projects.position `+limitSQL, args...).Find(&currentProjects)
totalCount, err = s.
SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`)
SELECT COUNT(DISTINCT all_projects.id) FROM all_projects`, args...).
SELECT COUNT(*) FROM (SELECT all_projects.id FROM all_projects GROUP BY all_projects.id `+archivedFilter+`) sub`, args...).
Count(&Project{})
if err != nil {
return nil, 0, err