split off sync merge func
This commit is contained in:
parent
41a27a4799
commit
a2a0e9bb7b
|
@ -56,20 +56,21 @@ func (l *List) Process() (*ListResult, error) {
|
||||||
return &ListResult{}, ErrInvalidReqs
|
return &ListResult{}, ErrInvalidReqs
|
||||||
}
|
}
|
||||||
|
|
||||||
folders := []string{task.FOLDER_NEW, task.FOLDER_PLANNED, task.FOLDER_UNPLANNED}
|
potentialTasks, err := l.local.FindAll()
|
||||||
if l.reqs.Folder != "" {
|
if err != nil {
|
||||||
folders = []string{l.reqs.Folder}
|
return &ListResult{}, fmt.Errorf("%w: %v", ErrListProcess, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var potentialTasks []*task.LocalTask
|
// folder
|
||||||
for _, folder := range folders {
|
if l.reqs.Folder != "" {
|
||||||
folderTasks, err := l.local.FindAllInFolder(folder)
|
var folderTasks []*task.LocalTask
|
||||||
if err != nil {
|
for _, pt := range potentialTasks {
|
||||||
return &ListResult{}, fmt.Errorf("%w: %v", ErrListProcess, err)
|
if pt.Folder == l.reqs.Folder {
|
||||||
}
|
folderTasks = append(folderTasks, pt)
|
||||||
for _, ft := range folderTasks {
|
}
|
||||||
potentialTasks = append(potentialTasks, ft)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
potentialTasks = folderTasks
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.reqs.Due.IsZero() && l.reqs.Project == "" {
|
if l.reqs.Due.IsZero() && l.reqs.Project == "" {
|
||||||
|
@ -78,6 +79,7 @@ func (l *List) Process() (*ListResult, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// project
|
||||||
if l.reqs.Project != "" {
|
if l.reqs.Project != "" {
|
||||||
var projectTasks []*task.LocalTask
|
var projectTasks []*task.LocalTask
|
||||||
for _, pt := range potentialTasks {
|
for _, pt := range potentialTasks {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package process_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.ewintr.nl/go-kit/test"
|
"git.ewintr.nl/go-kit/test"
|
||||||
|
@ -47,9 +48,9 @@ func TestListProcess(t *testing.T) {
|
||||||
Project: "project2",
|
Project: "project2",
|
||||||
}
|
}
|
||||||
allTasks := []*task.Task{task1, task2, task3, task4}
|
allTasks := []*task.Task{task1, task2, task3, task4}
|
||||||
localTask2 := &task.LocalTask{Task: *task2, LocalId: 2}
|
localTask2 := &task.LocalTask{Task: *task2, LocalUpdate: &task.LocalUpdate{}}
|
||||||
localTask3 := &task.LocalTask{Task: *task3, LocalId: 3}
|
localTask3 := &task.LocalTask{Task: *task3, LocalUpdate: &task.LocalUpdate{}}
|
||||||
localTask4 := &task.LocalTask{Task: *task4, LocalId: 4}
|
localTask4 := &task.LocalTask{Task: *task4, LocalUpdate: &task.LocalUpdate{}}
|
||||||
local := storage.NewMemory()
|
local := storage.NewMemory()
|
||||||
test.OK(t, local.SetTasks(allTasks))
|
test.OK(t, local.SetTasks(allTasks))
|
||||||
|
|
||||||
|
@ -96,10 +97,18 @@ func TestListProcess(t *testing.T) {
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
list := process.NewList(local, tc.reqs)
|
list := process.NewList(local, tc.reqs)
|
||||||
|
actRes, err := list.Process()
|
||||||
act, err := list.Process()
|
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
test.Equals(t, tc.exp, act.Tasks)
|
act := actRes.Tasks
|
||||||
|
for _, a := range act {
|
||||||
|
a.LocalId = 0
|
||||||
|
}
|
||||||
|
sAct := task.ById(act)
|
||||||
|
sExp := task.ById(tc.exp)
|
||||||
|
sort.Sort(sAct)
|
||||||
|
sort.Sort(sExp)
|
||||||
|
|
||||||
|
test.Equals(t, sExp, sAct)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"git.ewintr.nl/gte/internal/storage"
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -24,13 +23,9 @@ func NewProjects(local storage.LocalRepository) *Projects {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Projects) Process() ([]string, error) {
|
func (p *Projects) Process() ([]string, error) {
|
||||||
allTasks := []*task.LocalTask{}
|
allTasks, err := p.local.FindAll()
|
||||||
for _, folder := range []string{task.FOLDER_NEW, task.FOLDER_PLANNED, task.FOLDER_UNPLANNED} {
|
if err != nil {
|
||||||
folderTasks, err := p.local.FindAllInFolder(folder)
|
return []string{}, fmt.Errorf("%w: %v", ErrCouldNotFetchProjects, err)
|
||||||
if err != nil {
|
|
||||||
return []string{}, fmt.Errorf("%w: %v", ErrCouldNotFetchProjects, err)
|
|
||||||
}
|
|
||||||
allTasks = append(allTasks, folderTasks...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
knownMap := map[string]bool{}
|
knownMap := map[string]bool{}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package process_test
|
package process_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.ewintr.nl/go-kit/test"
|
"git.ewintr.nl/go-kit/test"
|
||||||
|
@ -24,8 +25,8 @@ func TestSyncProcess(t *testing.T) {
|
||||||
Folder: task.FOLDER_UNPLANNED,
|
Folder: task.FOLDER_UNPLANNED,
|
||||||
}
|
}
|
||||||
|
|
||||||
localTask1 := &task.LocalTask{Task: *task1, LocalId: 1}
|
localTask1 := &task.LocalTask{Task: *task1, LocalUpdate: &task.LocalUpdate{}}
|
||||||
localTask2 := &task.LocalTask{Task: *task2, LocalId: 2}
|
localTask2 := &task.LocalTask{Task: *task2, LocalUpdate: &task.LocalUpdate{}}
|
||||||
|
|
||||||
mstorer, err := mstore.NewMemory(task.KnownFolders)
|
mstorer, err := mstore.NewMemory(task.KnownFolders)
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
|
@ -38,10 +39,16 @@ func TestSyncProcess(t *testing.T) {
|
||||||
actResult, err := syncer.Process()
|
actResult, err := syncer.Process()
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
test.Equals(t, 2, actResult.Count)
|
test.Equals(t, 2, actResult.Count)
|
||||||
actTasks1, err := local.FindAllInFolder(task.FOLDER_NEW)
|
actTasks, err := local.FindAll()
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
test.Equals(t, []*task.LocalTask{localTask1}, actTasks1)
|
for _, a := range actTasks {
|
||||||
actTasks2, err := local.FindAllInFolder(task.FOLDER_UNPLANNED)
|
a.LocalId = 0
|
||||||
test.OK(t, err)
|
a.Message = nil
|
||||||
test.Equals(t, []*task.LocalTask{localTask2}, actTasks2)
|
}
|
||||||
|
exp := task.ById([]*task.LocalTask{localTask1, localTask2})
|
||||||
|
sExp := task.ById(exp)
|
||||||
|
sAct := task.ById(actTasks)
|
||||||
|
sort.Sort(sAct)
|
||||||
|
sort.Sort(sExp)
|
||||||
|
test.Equals(t, sExp, sAct)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,25 @@ var (
|
||||||
type LocalRepository interface {
|
type LocalRepository interface {
|
||||||
LatestSync() (time.Time, error)
|
LatestSync() (time.Time, error)
|
||||||
SetTasks(tasks []*task.Task) error
|
SetTasks(tasks []*task.Task) error
|
||||||
FindAllInFolder(folder string) ([]*task.LocalTask, error)
|
FindAll() ([]*task.LocalTask, error)
|
||||||
FindAllInProject(project string) ([]*task.LocalTask, error)
|
|
||||||
FindById(id string) (*task.LocalTask, error)
|
FindById(id string) (*task.LocalTask, error)
|
||||||
FindByLocalId(id int) (*task.LocalTask, error)
|
FindByLocalId(id int) (*task.LocalTask, error)
|
||||||
SetLocalUpdate(tsk *task.LocalTask) error
|
SetLocalUpdate(tsk *task.LocalTask) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NextLocalId finds a new local id by incrememting to a variable limit.
|
||||||
|
//
|
||||||
|
// When tasks are edited, some get removed because they are done or deleted.
|
||||||
|
// It is very confusing if existing tasks get renumbered, or if a new one
|
||||||
|
// immediatly gets the id of an removed one. So it is better to just
|
||||||
|
// increment. However, local id's also benefit from being short, so we
|
||||||
|
// don't want to keep incrementing forever.
|
||||||
|
//
|
||||||
|
// This function takes a list if id's that are in use and sets the limit
|
||||||
|
// to the nearest power of ten depening on the current highest id used.
|
||||||
|
// The new id is an incremented one from that max. However, if the limit
|
||||||
|
// is reached, it first tries to find "holes" in the current sequence,
|
||||||
|
// starting from the bottom. If there are no holes, the limit is increased.
|
||||||
func NextLocalId(used []int) int {
|
func NextLocalId(used []int) int {
|
||||||
if len(used) == 0 {
|
if len(used) == 0 {
|
||||||
return 1
|
return 1
|
||||||
|
@ -57,3 +69,60 @@ func NextLocalId(used []int) int {
|
||||||
|
|
||||||
return limit
|
return limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MergeNewTaskSet updates a local set of tasks with a remote one
|
||||||
|
//
|
||||||
|
// New set is leading and tasks that are not in there get dismissed. Tasks that
|
||||||
|
// were created locally and got dispatched might temporarily dissappear if the
|
||||||
|
// remote inbox has a delay in processing.
|
||||||
|
func MergeNewTaskSet(oldTasks []*task.LocalTask, newTasks []*task.Task) []*task.LocalTask {
|
||||||
|
|
||||||
|
// create lookups
|
||||||
|
resultMap := map[string]*task.LocalTask{}
|
||||||
|
for _, nt := range newTasks {
|
||||||
|
resultMap[nt.Id] = &task.LocalTask{
|
||||||
|
Task: *nt,
|
||||||
|
LocalId: 0,
|
||||||
|
LocalUpdate: &task.LocalUpdate{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oldMap := map[string]*task.LocalTask{}
|
||||||
|
for _, ot := range oldTasks {
|
||||||
|
oldMap[ot.Id] = ot
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply local id rules:
|
||||||
|
// - keep id's that were present in the old set
|
||||||
|
// - find new id's for new tasks
|
||||||
|
// - assignment of local id's is non deterministic
|
||||||
|
var used []int
|
||||||
|
for _, ot := range oldTasks {
|
||||||
|
if _, ok := resultMap[ot.Id]; ok {
|
||||||
|
resultMap[ot.Id].LocalId = ot.LocalId
|
||||||
|
used = append(used, ot.LocalId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id, nt := range resultMap {
|
||||||
|
if nt.LocalId == 0 {
|
||||||
|
newLocalId := NextLocalId(used)
|
||||||
|
resultMap[id].LocalId = newLocalId
|
||||||
|
used = append(used, newLocalId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply local update rules:
|
||||||
|
// - only keep local updates if the new task hasn't moved to a new version yet
|
||||||
|
for _, ot := range oldTasks {
|
||||||
|
if nt, ok := resultMap[ot.Id]; ok {
|
||||||
|
if ot.LocalUpdate.ForVersion >= nt.Version {
|
||||||
|
resultMap[ot.Id].LocalUpdate = ot.LocalUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*task.LocalTask
|
||||||
|
for _, nt := range resultMap {
|
||||||
|
result = append(result, nt)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package storage_test
|
package storage_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.ewintr.nl/go-kit/test"
|
"git.ewintr.nl/go-kit/test"
|
||||||
"git.ewintr.nl/gte/internal/storage"
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
|
"git.ewintr.nl/gte/internal/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNextLocalId(t *testing.T) {
|
func TestNextLocalId(t *testing.T) {
|
||||||
|
@ -72,3 +74,105 @@ func TestNextLocalId(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeNewTaskSet(t *testing.T) {
|
||||||
|
task1 := &task.Task{Id: "id-1", Version: 1, Action: "action-1"}
|
||||||
|
task1v2 := &task.Task{Id: "id-1", Version: 2, Action: "action-1v2"}
|
||||||
|
task2 := &task.Task{Id: "id-2", Version: 2, Action: "action-2"}
|
||||||
|
emptyUpdate := &task.LocalUpdate{}
|
||||||
|
|
||||||
|
t.Run("local ids are added", func(t *testing.T) {
|
||||||
|
act1 := storage.MergeNewTaskSet([]*task.LocalTask{}, []*task.Task{task1})
|
||||||
|
test.Assert(t, len(act1) == 1, "length was not 1")
|
||||||
|
test.Equals(t, 1, act1[0].LocalId)
|
||||||
|
|
||||||
|
act2 := storage.MergeNewTaskSet(act1, []*task.Task{task1, task2})
|
||||||
|
var actIds []int
|
||||||
|
for _, t := range act2 {
|
||||||
|
actIds = append(actIds, t.LocalId)
|
||||||
|
}
|
||||||
|
sort.Ints(actIds)
|
||||||
|
test.Equals(t, []int{1, 2}, actIds)
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
oldTasks []*task.LocalTask
|
||||||
|
newTasks []*task.Task
|
||||||
|
exp []*task.LocalTask
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "add tasks and find local ids",
|
||||||
|
oldTasks: []*task.LocalTask{},
|
||||||
|
newTasks: []*task.Task{task1, task2},
|
||||||
|
exp: []*task.LocalTask{
|
||||||
|
{Task: *task1, LocalUpdate: emptyUpdate},
|
||||||
|
{Task: *task2, LocalUpdate: emptyUpdate},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update existing task",
|
||||||
|
oldTasks: []*task.LocalTask{
|
||||||
|
{Task: *task1, LocalUpdate: emptyUpdate},
|
||||||
|
{Task: *task2, LocalId: 2, LocalUpdate: emptyUpdate},
|
||||||
|
},
|
||||||
|
newTasks: []*task.Task{task1v2, task2},
|
||||||
|
exp: []*task.LocalTask{
|
||||||
|
{Task: *task1v2, LocalUpdate: emptyUpdate},
|
||||||
|
{Task: *task2, LocalUpdate: emptyUpdate},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove deleted task",
|
||||||
|
oldTasks: []*task.LocalTask{
|
||||||
|
{Task: *task1, LocalUpdate: emptyUpdate},
|
||||||
|
{Task: *task2, LocalUpdate: emptyUpdate},
|
||||||
|
},
|
||||||
|
newTasks: []*task.Task{task2},
|
||||||
|
exp: []*task.LocalTask{
|
||||||
|
{Task: *task2, LocalUpdate: emptyUpdate},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove only outdated updates",
|
||||||
|
oldTasks: []*task.LocalTask{
|
||||||
|
{
|
||||||
|
Task: *task1,
|
||||||
|
LocalUpdate: &task.LocalUpdate{
|
||||||
|
ForVersion: 1,
|
||||||
|
Project: "project-v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Task: *task2,
|
||||||
|
LocalUpdate: &task.LocalUpdate{
|
||||||
|
ForVersion: 2,
|
||||||
|
Project: "project-v3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newTasks: []*task.Task{task1v2, task2},
|
||||||
|
exp: []*task.LocalTask{
|
||||||
|
{Task: *task1v2, LocalUpdate: emptyUpdate},
|
||||||
|
{
|
||||||
|
Task: *task2,
|
||||||
|
LocalUpdate: &task.LocalUpdate{
|
||||||
|
ForVersion: 2,
|
||||||
|
Project: "project-v3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
sExp := task.ById(tc.exp)
|
||||||
|
sAct := task.ById(storage.MergeNewTaskSet(tc.oldTasks, tc.newTasks))
|
||||||
|
for i := range sAct {
|
||||||
|
sAct[i].LocalId = 0
|
||||||
|
}
|
||||||
|
sort.Sort(sExp)
|
||||||
|
sort.Sort(sAct)
|
||||||
|
test.Equals(t, sExp, sAct)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,25 +6,15 @@ import (
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
type localData struct {
|
|
||||||
LocalId int
|
|
||||||
LocalUpdate *task.LocalUpdate
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memory is an in memory implementation of LocalRepository
|
// Memory is an in memory implementation of LocalRepository
|
||||||
//
|
|
||||||
// It is meant for testing and does not make an attempt to
|
|
||||||
// keep local state between consecutive calls to SetTasks()
|
|
||||||
type Memory struct {
|
type Memory struct {
|
||||||
tasks []*task.Task
|
tasks map[string]*task.LocalTask
|
||||||
latestSync time.Time
|
latestSync time.Time
|
||||||
localData map[string]localData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMemory() *Memory {
|
func NewMemory() *Memory {
|
||||||
return &Memory{
|
return &Memory{
|
||||||
tasks: []*task.Task{},
|
tasks: map[string]*task.LocalTask{},
|
||||||
localData: map[string]localData{},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,56 +23,26 @@ func (m *Memory) LatestSync() (time.Time, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) SetTasks(tasks []*task.Task) error {
|
func (m *Memory) SetTasks(tasks []*task.Task) error {
|
||||||
nTasks := []*task.Task{}
|
var oldTasks []*task.LocalTask
|
||||||
for _, t := range tasks {
|
for _, ot := range m.tasks {
|
||||||
nt := *t
|
oldTasks = append(oldTasks, ot)
|
||||||
nt.Message = nil
|
}
|
||||||
nTasks = append(nTasks, &nt)
|
|
||||||
m.setLocalId(t.Id)
|
newTasks := MergeNewTaskSet(oldTasks, tasks)
|
||||||
|
|
||||||
|
m.tasks = map[string]*task.LocalTask{}
|
||||||
|
for _, nt := range newTasks {
|
||||||
|
m.tasks[nt.Id] = nt
|
||||||
}
|
}
|
||||||
m.tasks = nTasks
|
|
||||||
m.latestSync = time.Now()
|
m.latestSync = time.Now()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) setLocalId(id string) {
|
func (m *Memory) FindAll() ([]*task.LocalTask, error) {
|
||||||
used := []int{}
|
|
||||||
for _, ld := range m.localData {
|
|
||||||
used = append(used, ld.LocalId)
|
|
||||||
}
|
|
||||||
|
|
||||||
next := NextLocalId(used)
|
|
||||||
m.localData[id] = localData{
|
|
||||||
LocalId: next,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) FindAllInFolder(folder string) ([]*task.LocalTask, error) {
|
|
||||||
tasks := []*task.LocalTask{}
|
tasks := []*task.LocalTask{}
|
||||||
for _, t := range m.tasks {
|
for _, t := range m.tasks {
|
||||||
if t.Folder == folder {
|
tasks = append(tasks, t)
|
||||||
tasks = append(tasks, &task.LocalTask{
|
|
||||||
Task: *t,
|
|
||||||
LocalId: m.localData[t.Id].LocalId,
|
|
||||||
LocalUpdate: m.localData[t.Id].LocalUpdate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) FindAllInProject(project string) ([]*task.LocalTask, error) {
|
|
||||||
tasks := []*task.LocalTask{}
|
|
||||||
for _, t := range m.tasks {
|
|
||||||
if t.Project == project {
|
|
||||||
tasks = append(tasks, &task.LocalTask{
|
|
||||||
Task: *t,
|
|
||||||
LocalId: m.localData[t.Id].LocalId,
|
|
||||||
LocalUpdate: m.localData[t.Id].LocalUpdate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks, nil
|
return tasks, nil
|
||||||
|
@ -91,11 +51,7 @@ func (m *Memory) FindAllInProject(project string) ([]*task.LocalTask, error) {
|
||||||
func (m *Memory) FindById(id string) (*task.LocalTask, error) {
|
func (m *Memory) FindById(id string) (*task.LocalTask, error) {
|
||||||
for _, t := range m.tasks {
|
for _, t := range m.tasks {
|
||||||
if t.Id == id {
|
if t.Id == id {
|
||||||
return &task.LocalTask{
|
return t, nil
|
||||||
Task: *t,
|
|
||||||
LocalId: m.localData[t.Id].LocalId,
|
|
||||||
LocalUpdate: m.localData[t.Id].LocalUpdate,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,12 +60,8 @@ func (m *Memory) FindById(id string) (*task.LocalTask, error) {
|
||||||
|
|
||||||
func (m *Memory) FindByLocalId(localId int) (*task.LocalTask, error) {
|
func (m *Memory) FindByLocalId(localId int) (*task.LocalTask, error) {
|
||||||
for _, t := range m.tasks {
|
for _, t := range m.tasks {
|
||||||
if m.localData[t.Id].LocalId == localId {
|
if t.LocalId == localId {
|
||||||
return &task.LocalTask{
|
return t, nil
|
||||||
Task: *t,
|
|
||||||
LocalId: localId,
|
|
||||||
LocalUpdate: m.localData[t.Id].LocalUpdate,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,10 +69,7 @@ func (m *Memory) FindByLocalId(localId int) (*task.LocalTask, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) SetLocalUpdate(tsk *task.LocalTask) error {
|
func (m *Memory) SetLocalUpdate(tsk *task.LocalTask) error {
|
||||||
m.localData[tsk.Id] = localData{
|
m.tasks[tsk.Id] = tsk
|
||||||
LocalId: tsk.LocalId,
|
|
||||||
LocalUpdate: tsk.LocalUpdate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package storage_test
|
package storage_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -41,9 +42,10 @@ func TestMemory(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
tasks := []*task.Task{task1, task2, task3}
|
tasks := []*task.Task{task1, task2, task3}
|
||||||
localTask1 := &task.LocalTask{Task: *task1, LocalId: 1}
|
emptyUpdate := &task.LocalUpdate{}
|
||||||
localTask2 := &task.LocalTask{Task: *task2, LocalId: 2}
|
localTask1 := &task.LocalTask{Task: *task1, LocalUpdate: emptyUpdate}
|
||||||
localTask3 := &task.LocalTask{Task: *task3, LocalId: 3}
|
localTask2 := &task.LocalTask{Task: *task2, LocalUpdate: emptyUpdate}
|
||||||
|
localTask3 := &task.LocalTask{Task: *task3, LocalUpdate: emptyUpdate}
|
||||||
|
|
||||||
t.Run("sync", func(t *testing.T) {
|
t.Run("sync", func(t *testing.T) {
|
||||||
mem := storage.NewMemory()
|
mem := storage.NewMemory()
|
||||||
|
@ -58,28 +60,20 @@ func TestMemory(t *testing.T) {
|
||||||
test.Assert(t, latest.After(start), "latest was not after start")
|
test.Assert(t, latest.After(start), "latest was not after start")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("findallinfolder", func(t *testing.T) {
|
t.Run("findallin", func(t *testing.T) {
|
||||||
mem := storage.NewMemory()
|
mem := storage.NewMemory()
|
||||||
test.OK(t, mem.SetTasks(tasks))
|
test.OK(t, mem.SetTasks(tasks))
|
||||||
act, err := mem.FindAllInFolder(folder1)
|
act, err := mem.FindAll()
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
exp := []*task.LocalTask{localTask1, localTask2}
|
exp := []*task.LocalTask{localTask1, localTask2, localTask3}
|
||||||
for _, tsk := range exp {
|
for _, tsk := range act {
|
||||||
tsk.Message = nil
|
tsk.LocalId = 0
|
||||||
}
|
}
|
||||||
test.Equals(t, exp, act)
|
sExp := task.ById(exp)
|
||||||
})
|
sAct := task.ById(act)
|
||||||
|
sort.Sort(sExp)
|
||||||
t.Run("findallinproject", func(t *testing.T) {
|
sort.Sort(sAct)
|
||||||
mem := storage.NewMemory()
|
test.Equals(t, sExp, sAct)
|
||||||
test.OK(t, mem.SetTasks(tasks))
|
|
||||||
act, err := mem.FindAllInProject(project1)
|
|
||||||
test.OK(t, err)
|
|
||||||
exp := []*task.LocalTask{localTask1, localTask3}
|
|
||||||
for _, tsk := range exp {
|
|
||||||
tsk.Message = nil
|
|
||||||
}
|
|
||||||
test.Equals(t, exp, act)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("findbyid", func(t *testing.T) {
|
t.Run("findbyid", func(t *testing.T) {
|
||||||
|
@ -87,15 +81,17 @@ func TestMemory(t *testing.T) {
|
||||||
test.OK(t, mem.SetTasks(tasks))
|
test.OK(t, mem.SetTasks(tasks))
|
||||||
act, err := mem.FindById("id-2")
|
act, err := mem.FindById("id-2")
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
|
act.LocalId = 0
|
||||||
test.Equals(t, localTask2, act)
|
test.Equals(t, localTask2, act)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("findbylocalid", func(t *testing.T) {
|
t.Run("findbylocalid", func(t *testing.T) {
|
||||||
mem := storage.NewMemory()
|
mem := storage.NewMemory()
|
||||||
test.OK(t, mem.SetTasks(tasks))
|
test.OK(t, mem.SetTasks([]*task.Task{task1}))
|
||||||
act, err := mem.FindByLocalId(2)
|
act, err := mem.FindByLocalId(1)
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
test.Equals(t, localTask2, act)
|
act.LocalId = 0
|
||||||
|
test.Equals(t, localTask1, act)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("setlocalupdate", func(t *testing.T) {
|
t.Run("setlocalupdate", func(t *testing.T) {
|
||||||
|
|
|
@ -19,6 +19,11 @@ var sqliteMigrations = []sqliteMigration{
|
||||||
`CREATE TABLE local_id ("id" TEXT UNIQUE, "local_id" INTEGER UNIQUE)`,
|
`CREATE TABLE local_id ("id" TEXT UNIQUE, "local_id" INTEGER UNIQUE)`,
|
||||||
`ALTER TABLE local_id RENAME TO local_task`,
|
`ALTER TABLE local_id RENAME TO local_task`,
|
||||||
`ALTER TABLE local_task ADD COLUMN local_update TEXT`,
|
`ALTER TABLE local_task ADD COLUMN local_update TEXT`,
|
||||||
|
`ALTER TABLE task ADD COLUMN local_id INTEGER`,
|
||||||
|
`ALTER TABLE task ADD COLUMN local_update TEXT`,
|
||||||
|
`UPDATE task SET local_id = (SELECT local_id FROM local_task WHERE local_task.id=task.id)`,
|
||||||
|
`UPDATE task SET local_update = (SELECT local_update FROM local_task WHERE local_task.id=task.id)`,
|
||||||
|
`DROP TABLE local_task`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -71,134 +76,40 @@ func (s *Sqlite) LatestSync() (time.Time, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sqlite) SetTasks(tasks []*task.Task) error {
|
func (s *Sqlite) SetTasks(tasks []*task.Task) error {
|
||||||
// set tasks
|
oldTasks, err := s.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newTasks := MergeNewTaskSet(oldTasks, tasks)
|
||||||
|
|
||||||
if _, err := s.db.Exec(`DELETE FROM task`); err != nil {
|
if _, err := s.db.Exec(`DELETE FROM task`); err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
for _, t := range newTasks {
|
||||||
type localTaskInfo struct {
|
|
||||||
TaskId string
|
|
||||||
TaskVersion int
|
|
||||||
LocalId int
|
|
||||||
LocalUpdate task.LocalUpdate
|
|
||||||
}
|
|
||||||
localIdMap := map[string]localTaskInfo{}
|
|
||||||
for _, t := range tasks {
|
|
||||||
var recurStr string
|
var recurStr string
|
||||||
if t.Recur != nil {
|
if t.Recur != nil {
|
||||||
recurStr = t.Recur.String()
|
recurStr = t.Recur.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.db.Exec(`
|
_, err := s.db.Exec(`
|
||||||
INSERT INTO task
|
INSERT INTO task
|
||||||
(id, version, folder, action, project, due, recur)
|
(id, local_id, version, folder, action, project, due, recur, local_update)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, ?, ?, ?, ?)`,
|
(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
t.Id, t.Version, t.Folder, t.Action, t.Project, t.Due.String(), recurStr)
|
t.Id, t.LocalId, t.Version, t.Folder, t.Action, t.Project, t.Due.String(), recurStr, t.LocalUpdate)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
localIdMap[t.Id] = localTaskInfo{
|
|
||||||
TaskId: t.Id,
|
|
||||||
TaskVersion: t.Version,
|
|
||||||
LocalId: 0,
|
|
||||||
LocalUpdate: task.LocalUpdate{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set local_ids and local_updates:
|
|
||||||
// 1 - find existing
|
|
||||||
rows, err := s.db.Query(`SELECT id, local_id, local_update FROM local_task`)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var id string
|
|
||||||
var localId int
|
|
||||||
var localUpdate task.LocalUpdate
|
|
||||||
if err := rows.Scan(&id, &localId, &localUpdate); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
if oldInfo, ok := localIdMap[id]; ok {
|
|
||||||
newInfo := localTaskInfo{
|
|
||||||
TaskId: oldInfo.TaskId,
|
|
||||||
TaskVersion: oldInfo.TaskVersion,
|
|
||||||
LocalId: localId,
|
|
||||||
LocalUpdate: localUpdate,
|
|
||||||
}
|
|
||||||
localIdMap[id] = newInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2 - remove old values
|
|
||||||
if _, err := s.db.Exec(`DELETE FROM local_task`); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3 - figure out new values
|
|
||||||
var used []int
|
|
||||||
for _, info := range localIdMap {
|
|
||||||
if info.LocalId != 0 {
|
|
||||||
used = append(used, info.LocalId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for id, info := range localIdMap {
|
|
||||||
newInfo := info
|
|
||||||
// find new local_id when needed
|
|
||||||
if info.LocalId == 0 {
|
|
||||||
newLocalId := NextLocalId(used)
|
|
||||||
used = append(used, newLocalId)
|
|
||||||
newInfo.LocalId = newLocalId
|
|
||||||
}
|
|
||||||
// remove local_update when outdated
|
|
||||||
if info.LocalUpdate.ForVersion < info.TaskVersion {
|
|
||||||
newInfo.LocalUpdate = task.LocalUpdate{}
|
|
||||||
}
|
|
||||||
localIdMap[id] = newInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4 - store new values
|
|
||||||
for id, info := range localIdMap {
|
|
||||||
if _, err := s.db.Exec(`
|
|
||||||
INSERT INTO local_task
|
|
||||||
(id, local_id, local_update)
|
|
||||||
VALUES
|
|
||||||
(?, ?, ?)`, id, info.LocalId, info.LocalUpdate); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update system
|
|
||||||
if _, err := s.db.Exec(`
|
|
||||||
UPDATE system
|
|
||||||
SET latest_sync = ?`,
|
|
||||||
time.Now().Unix()); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sqlite) FindAllInFolder(folder string) ([]*task.LocalTask, error) {
|
func (s *Sqlite) FindAll() ([]*task.LocalTask, error) {
|
||||||
rows, err := s.db.Query(`
|
rows, err := s.db.Query(`
|
||||||
SELECT task.id, local_task.local_id, version, folder, action, project, due, recur, local_task.local_update
|
SELECT id, local_id, version, folder, action, project, due, recur, local_update
|
||||||
FROM task
|
FROM task`)
|
||||||
LEFT JOIN local_task ON task.id = local_task.id
|
|
||||||
WHERE folder = ?`, folder)
|
|
||||||
if err != nil {
|
|
||||||
return []*task.LocalTask{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasksFromRows(rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sqlite) FindAllInProject(project string) ([]*task.LocalTask, error) {
|
|
||||||
rows, err := s.db.Query(`
|
|
||||||
SELECT task.id, local_task.local_id, version, folder, action, project, due, recur, local_task.local_update
|
|
||||||
FROM task
|
|
||||||
LEFT JOIN local_task ON task.id = local_task.id
|
|
||||||
WHERE project = ?`, project)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []*task.LocalTask{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return []*task.LocalTask{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
@ -211,9 +122,8 @@ func (s *Sqlite) FindById(id string) (*task.LocalTask, error) {
|
||||||
var localId, version int
|
var localId, version int
|
||||||
var localUpdate task.LocalUpdate
|
var localUpdate task.LocalUpdate
|
||||||
row := s.db.QueryRow(`
|
row := s.db.QueryRow(`
|
||||||
SELECT local_task.local_id, version, folder, action, project, due, recur, local_task.local_update
|
SELECT local_id, version, folder, action, project, due, recur, local_update
|
||||||
FROM task
|
FROM task
|
||||||
LEFT JOIN local_task ON task.id = local_task.id
|
|
||||||
WHERE task.id = ?
|
WHERE task.id = ?
|
||||||
LIMIT 1`, id)
|
LIMIT 1`, id)
|
||||||
if err := row.Scan(&localId, &version, &folder, &action, &project, &due, &recur, &localUpdate); err != nil {
|
if err := row.Scan(&localId, &version, &folder, &action, &project, &due, &recur, &localUpdate); err != nil {
|
||||||
|
@ -237,7 +147,7 @@ LIMIT 1`, id)
|
||||||
|
|
||||||
func (s *Sqlite) FindByLocalId(localId int) (*task.LocalTask, error) {
|
func (s *Sqlite) FindByLocalId(localId int) (*task.LocalTask, error) {
|
||||||
var id string
|
var id string
|
||||||
row := s.db.QueryRow(`SELECT id FROM local_task WHERE local_id = ?`, localId)
|
row := s.db.QueryRow(`SELECT id FROM task WHERE local_id = ?`, localId)
|
||||||
if err := row.Scan(&id); err != nil {
|
if err := row.Scan(&id); err != nil {
|
||||||
return &task.LocalTask{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return &task.LocalTask{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
@ -252,7 +162,7 @@ func (s *Sqlite) FindByLocalId(localId int) (*task.LocalTask, error) {
|
||||||
|
|
||||||
func (s *Sqlite) SetLocalUpdate(tsk *task.LocalTask) error {
|
func (s *Sqlite) SetLocalUpdate(tsk *task.LocalTask) error {
|
||||||
if _, err := s.db.Exec(`
|
if _, err := s.db.Exec(`
|
||||||
UPDATE local_task
|
UPDATE task
|
||||||
SET local_update = ?
|
SET local_update = ?
|
||||||
WHERE local_id = ?`, tsk.LocalUpdate, tsk.LocalId); err != nil {
|
WHERE local_id = ?`, tsk.LocalUpdate, tsk.LocalId); err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
|
|
@ -53,6 +53,12 @@ func (lt *LocalTask) ApplyUpdate() {
|
||||||
lt.LocalUpdate = &LocalUpdate{}
|
lt.LocalUpdate = &LocalUpdate{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ById []*LocalTask
|
||||||
|
|
||||||
|
func (lt ById) Len() int { return len(lt) }
|
||||||
|
func (lt ById) Swap(i, j int) { lt[i], lt[j] = lt[j], lt[i] }
|
||||||
|
func (lt ById) Less(i, j int) bool { return lt[i].Id < lt[j].Id }
|
||||||
|
|
||||||
type ByDue []*LocalTask
|
type ByDue []*LocalTask
|
||||||
|
|
||||||
func (lt ByDue) Len() int { return len(lt) }
|
func (lt ByDue) Len() int { return len(lt) }
|
||||||
|
|
Loading…
Reference in New Issue