split off sync merge func

This commit is contained in:
Erik Winter 2021-08-25 06:52:48 +02:00
parent 41a27a4799
commit a2a0e9bb7b
10 changed files with 287 additions and 240 deletions

View File

@ -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 {

View File

@ -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)
}) })
} }
} }

View File

@ -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{}

View File

@ -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)
} }

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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
} }

View File

@ -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) {

View File

@ -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)

View File

@ -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) }