feat: fetch all projects with a recursive cte instead of recursive query

This change modifies the fetching of all projects to use a recursive common table expression instead of recursively calling the method.
This commit is contained in:
kolaente 2024-02-28 12:02:12 +01:00
parent 5d127c2897
commit 6b1e67485b
Signed by untrusted user: konrad
GPG Key ID: F40E70337AB24C9B
6 changed files with 57 additions and 55 deletions

View File

@ -76,7 +76,7 @@ sentry:
frontenddsn: "https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480"
database:
# Database type to use. Supported types are mysql, postgres and sqlite.
# Database type to use. Supported values are mysql, postgres and sqlite. Vikunja is able to run with MySQL 8.0+, Mariadb 10.2+, PostgreSQL 12+, and sqlite.
type: "sqlite"
# Database user which is used to connect to the database.
user: "vikunja"

View File

@ -234,6 +234,7 @@
title: Test25
owner_id: 6
parent_project_id: 12
position: 25
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -241,6 +242,7 @@
title: Test26
owner_id: 6
parent_project_id: 25
position: 26
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-

View File

@ -368,7 +368,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
@ -378,7 +378,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
// should equal duedate asc
t.Run("by due_date", func(t *testing.T) {

View File

@ -17,6 +17,7 @@
package models
import (
"fmt"
"math"
"strconv"
"strings"
@ -418,67 +419,53 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
parentCondition,
builder.NotIn("l.id", parentProjectIDs),
)).
OrderBy("position").
GroupBy("l.id")
}
func getAllProjectsForUser(s *xorm.Session, userID int64, parentProjectIDs []int64, opts *projectOptions, projects *[]*Project, oldTotalCount int64, archivedProjects map[int64]bool) (resultCount int, totalCount int64, err error) {
func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions) (projects []*Project, totalCount int64, err error) {
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
query := getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
if limit > 0 {
query = query.Limit(limit, start)
query := getUserProjectsStatement(nil, userID, opts.search, opts.getArchived)
querySQLString, args, err := query.ToSQL()
if err != nil {
return nil, 0, err
}
var limitSQL string
if limit > 0 {
limitSQL = fmt.Sprintf("LIMIT %d OFFSET %d", limit, start)
}
baseQuery := querySQLString + `
UNION ALL
SELECT p.* FROM projects p
INNER JOIN all_projects ap ON p.parent_project_id = ap.id`
currentProjects := []*Project{}
err = s.SQL(query).Find(&currentProjects)
err = s.SQL(`WITH RECURSIVE all_projects as (
`+baseQuery+`
ORDER BY position
`+limitSQL+`
)
SELECT * FROM all_projects GROUP BY all_projects.id ORDER BY position`, args...).Find(&currentProjects)
if err != nil {
return 0, 0, err
return
}
if len(currentProjects) == 0 {
return 0, oldTotalCount, err
return nil, 0, err
}
query = getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
totalCount, err = s.
SQL(query.Select("count(*)")).
SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`)
SELECT count(*) FROM all_projects`, args...).
Count(&Project{})
if err != nil {
return 0, 0, err
return nil, 0, err
}
parentIDsMap := make(map[int64]bool, len(parentProjectIDs))
for _, id := range parentProjectIDs {
parentIDsMap[id] = true
}
for _, project := range currentProjects {
parentIDsMap[project.ID] = true
}
newParentIDs := []int64{}
for _, project := range currentProjects {
if project.IsArchived {
archivedProjects[project.ID] = true
}
if archivedProjects[project.ParentProjectID] {
project.IsArchived = true
}
// Filter out parent project ids which we're not looking for to avoid leaking
// information about parent projects
if !parentIDsMap[project.ParentProjectID] {
project.ParentProjectID = 0
}
newParentIDs = append(newParentIDs, project.ID)
}
*projects = append(*projects, currentProjects...)
// If we don't reset the limit for subprojects, it will be impossible to fetch all subprojects.
opts.page = -1
return getAllProjectsForUser(s, userID, newParentIDs, opts, projects, oldTotalCount+totalCount, archivedProjects)
return currentProjects, totalCount, err
}
// Gets the projects with their children without any tasks
@ -488,9 +475,7 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P
return nil, 0, 0, err
}
allProjects := []*Project{}
archivedProjects := make(map[int64]bool)
resultCount, totalItems, err = getAllProjectsForUser(s, fullUser.ID, nil, opts, &allProjects, 0, archivedProjects)
allProjects, totalItems, err := getAllProjectsForUser(s, fullUser.ID, opts)
if err != nil {
return
}

View File

@ -349,11 +349,9 @@ func TestProject_ReadAll(t *testing.T) {
t.Run("all", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
projects := []*Project{}
archivedProjects := make(map[int64]bool)
_, _, err := getAllProjectsForUser(s, 1, nil, &projectOptions{}, &projects, 0, archivedProjects)
projects, _, err := getAllProjectsForUser(s, 6, &projectOptions{})
require.NoError(t, err)
assert.Len(t, projects, 24)
assert.Len(t, projects, 25)
_ = s.Close()
})
t.Run("only child projects for one project", func(t *testing.T) {
@ -369,12 +367,12 @@ func TestProject_ReadAll(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, reflect.Slice, reflect.TypeOf(projects3).Kind())
ls := projects3.([]*Project)
assert.Len(t, ls, 26)
assert.Len(t, ls, 28)
assert.Equal(t, int64(3), ls[0].ID) // Project 3 has a position of 1 and should be sorted first
assert.Equal(t, int64(1), ls[1].ID)
assert.Equal(t, int64(6), ls[2].ID)
assert.Equal(t, int64(-1), ls[24].ID)
assert.Equal(t, int64(-2), ls[25].ID)
assert.Equal(t, int64(-1), ls[26].ID)
assert.Equal(t, int64(-2), ls[27].ID)
_ = s.Close()
})
t.Run("projects for nonexistant user", func(t *testing.T) {

View File

@ -656,6 +656,18 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
}
task39 := &Task{
ID: 39,
Title: "task #39",
Identifier: "#0",
CreatedByID: 1,
CreatedBy: user1,
ProjectID: 25,
RelatedTasks: map[RelationKind][]*Task{},
BucketID: 0,
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
}
type fields struct {
ProjectID int64
@ -728,6 +740,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task32,
task33,
task35,
task39,
},
wantErr: false,
},
@ -772,6 +785,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task3,
task1,
task2,
task39,
},
wantErr: false,
},
@ -943,6 +957,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task32, // has nil dates
task33, // has nil dates
task35, // has nil dates
task39, // has nil dates
},
wantErr: false,
},
@ -1202,6 +1217,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task32,
task33,
task35,
task39,
},
},
{
@ -1218,6 +1234,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task6,
task5,
// The other ones don't have a due date
task39,
task35,
task33,
task32,