refactor(tasks): drop in-memory task dedup, rely on unique index

The duplicate task rows getTasksForProjects deduplicated came from the
LEFT JOIN multiplying when duplicate task_positions rows existed. The new
unique index on (task_id, project_view_id) removes the root cause at the
SQL layer (the migration also runs before serving), so the join can no
longer multiply. Revert getTasksForProjects and getRawTasksForProjects to
their pre-dedup shape.
This commit is contained in:
kolaente 2026-06-17 22:55:37 +02:00
parent efb103ce0f
commit 822554cf30
5 changed files with 11 additions and 27 deletions

View File

@ -257,7 +257,7 @@ func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Pr
}
}
ts, total, err := getRawTasksForProjects(s, projects, auth, opts)
ts, _, total, err := getRawTasksForProjects(s, projects, auth, opts)
if err != nil {
return nil, err
}

View File

@ -1370,7 +1370,7 @@ func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
// Delete all tasks on that project
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
tasks, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskSearchOptions{})
tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskSearchOptions{})
if err != nil {
return
}

View File

@ -1898,7 +1898,7 @@ func TestTaskSearchWithExpandSubtasks(t *testing.T) {
expand: []TaskCollectionExpandable{TaskCollectionExpandSubtasks},
}
tasks, _, err := getRawTasksForProjects(s, []*Project{project}, &user.User{ID: 15}, opts)
tasks, _, _, err := getRawTasksForProjects(s, []*Project{project}, &user.User{ID: 15}, opts)
require.NoError(t, err)
require.NotEmpty(t, tasks)
}

View File

@ -139,7 +139,7 @@ func BenchmarkTaskSearch(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
s := db.NewSession()
resultSlice, _, err := getRawTasksForProjects(s, projects, auth, opts)
resultSlice, _, _, err := getRawTasksForProjects(s, projects, auth, opts)
if len(resultSlice) == 0 {
b.Fatalf("no results found for needle %q", needle)
}

View File

@ -288,11 +288,11 @@ func getTaskIndexFromSearchString(s string) (index int64) {
return
}
func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, totalItems int64, err error) {
func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
// If the user does not have any projects, don't try to get any tasks
if len(projects) == 0 {
return nil, 0, nil
return nil, 0, 0, nil
}
// Get all project IDs and get the tasks
@ -324,18 +324,17 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
}
tasks, totalItems, err = dbSearcher.Search(opts)
return tasks, totalItems, err
return tasks, len(tasks), totalItems, err
}
func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions, view *ProjectView) (tasks []*Task, resultCount int, totalItems int64, err error) {
tasks, totalItems, err = getRawTasksForProjects(s, projects, a, opts)
tasks, resultCount, totalItems, err = getRawTasksForProjects(s, projects, a, opts)
if err != nil {
return nil, 0, 0, err
}
rawTasks := tasks
taskMap := make(map[int64]*Task, len(rawTasks))
for _, t := range rawTasks {
taskMap := make(map[int64]*Task, len(tasks))
for _, t := range tasks {
taskMap[t.ID] = t
}
@ -344,22 +343,7 @@ func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts
return nil, 0, 0, err
}
// A task can appear more than once in the raw result when it has duplicate
// task_positions rows for the view (the LEFT JOIN multiplies it). Return one
// entry per task, in the original sort order, referencing the enriched map
// value so its identifier and other data are set. totalItems already counts
// distinct tasks, so this also aligns the page size with it.
tasks = make([]*Task, 0, len(taskMap))
seen := make(map[int64]bool, len(taskMap))
for _, t := range rawTasks {
if seen[t.ID] {
continue
}
seen[t.ID] = true
tasks = append(tasks, taskMap[t.ID])
}
return tasks, len(tasks), totalItems, err
return tasks, resultCount, totalItems, err
}
// GetTaskByIDSimple returns a raw task without extra data by the task ID