local id
This commit is contained in:
parent
cd784f999d
commit
5af427c23b
|
@ -44,14 +44,14 @@ var AddCmd = &cli.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCmd(repo storage.EventRepo) *cli.Command {
|
func NewAddCmd(localRepo storage.LocalID, eventRepo storage.Event) *cli.Command {
|
||||||
AddCmd.Action = func(cCtx *cli.Context) error {
|
AddCmd.Action = func(cCtx *cli.Context) error {
|
||||||
return Add(cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"), repo)
|
return Add(localRepo, eventRepo, cCtx.String("name"), cCtx.String("on"), cCtx.String("at"), cCtx.String("for"))
|
||||||
}
|
}
|
||||||
return AddCmd
|
return AddCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func Add(nameStr, onStr, atStr, frStr string, repo storage.EventRepo) error {
|
func Add(localIDRepo storage.LocalID, eventRepo storage.Event, nameStr, onStr, atStr, frStr string) error {
|
||||||
if nameStr == "" {
|
if nameStr == "" {
|
||||||
return fmt.Errorf("%w: name is required", ErrInvalidArg)
|
return fmt.Errorf("%w: name is required", ErrInvalidArg)
|
||||||
}
|
}
|
||||||
|
@ -91,9 +91,17 @@ func Add(nameStr, onStr, atStr, frStr string, repo storage.EventRepo) error {
|
||||||
}
|
}
|
||||||
e.Duration = fr
|
e.Duration = fr
|
||||||
}
|
}
|
||||||
if err := repo.Store(e); err != nil {
|
if err := eventRepo.Store(e); err != nil {
|
||||||
return fmt.Errorf("could not store event: %v", err)
|
return fmt.Errorf("could not store event: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localID, err := localIDRepo.Next()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create next local id: %v", err)
|
||||||
|
}
|
||||||
|
if err := localIDRepo.Store(e.ID, localID); err != nil {
|
||||||
|
return fmt.Errorf("could not store local id: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
"go-mod.ewintr.nl/planner/plan/command"
|
"go-mod.ewintr.nl/planner/plan/command"
|
||||||
"go-mod.ewintr.nl/planner/plan/storage"
|
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAdd(t *testing.T) {
|
func TestAdd(t *testing.T) {
|
||||||
|
@ -104,21 +104,34 @@ func TestAdd(t *testing.T) {
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
mem := storage.NewMemory()
|
eventRepo := memory.NewEvent()
|
||||||
actErr := command.Add(tc.args["name"], tc.args["on"], tc.args["at"], tc.args["for"], mem) != nil
|
localRepo := memory.NewLocalID()
|
||||||
|
actErr := command.Add(localRepo, eventRepo, tc.args["name"], tc.args["on"], tc.args["at"], tc.args["for"]) != nil
|
||||||
if tc.expErr != actErr {
|
if tc.expErr != actErr {
|
||||||
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||||
}
|
}
|
||||||
if tc.expErr {
|
if tc.expErr {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actEvents, err := mem.FindAll()
|
actEvents, err := eventRepo.FindAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
}
|
}
|
||||||
if len(actEvents) != 1 {
|
if len(actEvents) != 1 {
|
||||||
t.Errorf("exp 1, got %d", len(actEvents))
|
t.Errorf("exp 1, got %d", len(actEvents))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actLocalIDs, err := localRepo.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
}
|
||||||
|
if len(actLocalIDs) != 1 {
|
||||||
|
t.Errorf("exp 1, got %v", len(actLocalIDs))
|
||||||
|
}
|
||||||
|
if _, ok := actLocalIDs[actEvents[0].ID]; !ok {
|
||||||
|
t.Errorf("exp true, got %v", ok)
|
||||||
|
}
|
||||||
|
|
||||||
if actEvents[0].ID == "" {
|
if actEvents[0].ID == "" {
|
||||||
t.Errorf("exp string not te be empty")
|
t.Errorf("exp string not te be empty")
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,22 +13,29 @@ var ListCmd = &cli.Command{
|
||||||
Usage: "List everything",
|
Usage: "List everything",
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListCmd(repo storage.EventRepo) *cli.Command {
|
func NewListCmd(localRepo storage.LocalID, eventRepo storage.Event) *cli.Command {
|
||||||
ListCmd.Action = NewListAction(repo)
|
ListCmd.Action = func(cCtx *cli.Context) error {
|
||||||
|
return List(localRepo, eventRepo)
|
||||||
|
}
|
||||||
return ListCmd
|
return ListCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListAction(repo storage.EventRepo) func(*cli.Context) error {
|
func List(localRepo storage.LocalID, eventRepo storage.Event) error {
|
||||||
return func(cCtx *cli.Context) error {
|
localIDs, err := localRepo.FindAll()
|
||||||
all, err := repo.FindAll()
|
if err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("could not get local ids: %v", err)
|
||||||
return err
|
}
|
||||||
|
all, err := eventRepo.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, e := range all {
|
||||||
|
lid, ok := localIDs[e.ID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not find local id for %s", e.ID)
|
||||||
}
|
}
|
||||||
for _, e := range all {
|
fmt.Printf("%s\t%d\t%s\t%s\t%s\n", e.ID, lid, e.Title, e.Start.Format(time.DateTime), e.Duration.String())
|
||||||
fmt.Printf("%s\t%s\t%s\t%s\n", e.ID, e.Title, e.Start.Format(time.DateTime), e.Duration.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"go-mod.ewintr.nl/planner/plan/command"
|
"go-mod.ewintr.nl/planner/plan/command"
|
||||||
"go-mod.ewintr.nl/planner/plan/storage"
|
"go-mod.ewintr.nl/planner/plan/storage/sqlite"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := storage.NewSqlite(conf.DBPath)
|
localIDRepo, eventRepo, err := sqlite.NewSqlites(conf.DBPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("could not open db file: %s\n", err)
|
fmt.Printf("could not open db file: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -33,8 +33,8 @@ func main() {
|
||||||
Name: "plan",
|
Name: "plan",
|
||||||
Usage: "Plan your day with events",
|
Usage: "Plan your day with events",
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
command.NewAddCmd(repo),
|
command.NewAddCmd(localIDRepo, eventRepo),
|
||||||
command.NewListCmd(repo),
|
command.NewListCmd(localIDRepo, eventRepo),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package storage
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -8,18 +8,18 @@ import (
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Memory struct {
|
type Event struct {
|
||||||
events map[string]item.Event
|
events map[string]item.Event
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMemory() *Memory {
|
func NewEvent() *Event {
|
||||||
return &Memory{
|
return &Event{
|
||||||
events: make(map[string]item.Event),
|
events: make(map[string]item.Event),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Memory) Find(id string) (item.Event, error) {
|
func (r *Event) Find(id string) (item.Event, error) {
|
||||||
r.mutex.RLock()
|
r.mutex.RLock()
|
||||||
defer r.mutex.RUnlock()
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ func (r *Memory) Find(id string) (item.Event, error) {
|
||||||
return event, nil
|
return event, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Memory) FindAll() ([]item.Event, error) {
|
func (r *Event) FindAll() ([]item.Event, error) {
|
||||||
r.mutex.RLock()
|
r.mutex.RLock()
|
||||||
defer r.mutex.RUnlock()
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
|
@ -45,15 +45,16 @@ func (r *Memory) FindAll() ([]item.Event, error) {
|
||||||
return events, nil
|
return events, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Memory) Store(e item.Event) error {
|
func (r *Event) Store(e item.Event) error {
|
||||||
r.mutex.Lock()
|
r.mutex.Lock()
|
||||||
defer r.mutex.Unlock()
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
r.events[e.ID] = e
|
r.events[e.ID] = e
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Memory) Delete(id string) error {
|
func (r *Event) Delete(id string) error {
|
||||||
r.mutex.Lock()
|
r.mutex.Lock()
|
||||||
defer r.mutex.Unlock()
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
@ -61,5 +62,6 @@ func (r *Memory) Delete(id string) error {
|
||||||
return errors.New("event not found")
|
return errors.New("event not found")
|
||||||
}
|
}
|
||||||
delete(r.events, id)
|
delete(r.events, id)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package storage
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -7,10 +7,10 @@ import (
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMemory(t *testing.T) {
|
func TestEvent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
mem := NewMemory()
|
mem := NewEvent()
|
||||||
|
|
||||||
t.Log("empty")
|
t.Log("empty")
|
||||||
actEvents, actErr := mem.FindAll()
|
actEvents, actErr := mem.FindAll()
|
|
@ -0,0 +1,61 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalID struct {
|
||||||
|
ids map[string]int
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocalID() *LocalID {
|
||||||
|
return &LocalID{
|
||||||
|
ids: make(map[string]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *LocalID) FindAll() (map[string]int, error) {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
return ml.ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *LocalID) Next() (int, error) {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
cur := make([]int, 0, len(ml.ids))
|
||||||
|
for _, i := range ml.ids {
|
||||||
|
cur = append(cur, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
localID := storage.NextLocalID(cur)
|
||||||
|
|
||||||
|
return localID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *LocalID) Store(id string, localID int) error {
|
||||||
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
ml.ids[id] = localID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *LocalID) Delete(id string) error {
|
||||||
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, ok := ml.ids[id]; !ok {
|
||||||
|
return storage.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(ml.ids, id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package memory_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLocalID(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
repo := memory.NewLocalID()
|
||||||
|
|
||||||
|
t.Log("start empty")
|
||||||
|
actIDs, actErr := repo.FindAll()
|
||||||
|
if actErr != nil {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
if len(actIDs) != 0 {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("next id")
|
||||||
|
actNext, actErr := repo.Next()
|
||||||
|
if actErr != nil {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
if actNext != 1 {
|
||||||
|
t.Errorf("exp 1, got %v", actNext)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("store")
|
||||||
|
if actErr = repo.Store("test", 1); actErr != nil {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
actIDs, actErr = repo.FindAll()
|
||||||
|
if actErr != nil {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
expIDs := map[string]int{
|
||||||
|
"test": 1,
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(expIDs, actIDs); diff != "" {
|
||||||
|
t.Errorf("(exp +, got -)\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("delete")
|
||||||
|
if actErr = repo.Delete("test"); actErr != nil {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
actIDs, actErr = repo.FindAll()
|
||||||
|
if actErr != nil {
|
||||||
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
|
}
|
||||||
|
if len(actIDs) != 0 {
|
||||||
|
t.Errorf("exp 0, got %v", actErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("delete non-existing")
|
||||||
|
actErr = repo.Delete("non-existing")
|
||||||
|
if !errors.Is(actErr, storage.ErrNotFound) {
|
||||||
|
t.Errorf("exp %v, got %v", storage.ErrNotFound, actErr)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,206 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
timestampFormat = "2006-01-02 15:04:05"
|
|
||||||
)
|
|
||||||
|
|
||||||
var migrations = []string{
|
|
||||||
`CREATE TABLE events ("id" TEXT UNIQUE, "title" TEXT, "start" TIMESTAMP, "duration" TEXT)`,
|
|
||||||
`PRAGMA journal_mode=WAL`,
|
|
||||||
`PRAGMA synchronous=NORMAL`,
|
|
||||||
`PRAGMA cache_size=2000`,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidConfiguration = errors.New("invalid configuration")
|
|
||||||
ErrIncompatibleSQLMigration = errors.New("incompatible migration")
|
|
||||||
ErrNotEnoughSQLMigrations = errors.New("already more migrations than wanted")
|
|
||||||
ErrSqliteFailure = errors.New("sqlite returned an error")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Sqlite struct {
|
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSqlite(dbPath string) (*Sqlite, error) {
|
|
||||||
db, err := sql.Open("sqlite", dbPath)
|
|
||||||
if err != nil {
|
|
||||||
return &Sqlite{}, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Sqlite{
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.migrate(migrations); err != nil {
|
|
||||||
return &Sqlite{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sqlite) Store(event item.Event) error {
|
|
||||||
if _, err := s.db.Exec(`
|
|
||||||
INSERT INTO events
|
|
||||||
(id, title, start, duration)
|
|
||||||
VALUES
|
|
||||||
(?, ?, ?, ?)
|
|
||||||
ON CONFLICT(id) DO UPDATE
|
|
||||||
SET
|
|
||||||
title=?,
|
|
||||||
start=?,
|
|
||||||
duration=?`,
|
|
||||||
event.ID, event.Title, event.Start.Format(timestampFormat), event.Duration.String(),
|
|
||||||
event.Title, event.Start.Format(timestampFormat), event.Duration.String()); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sqlite) Find(id string) (item.Event, error) {
|
|
||||||
var event item.Event
|
|
||||||
var durStr string
|
|
||||||
err := s.db.QueryRow(`
|
|
||||||
SELECT id, title, start, duration
|
|
||||||
FROM events
|
|
||||||
WHERE id = ?`, id).Scan(&event.ID, &event.Title, &event.Start, &durStr)
|
|
||||||
switch {
|
|
||||||
case err == sql.ErrNoRows:
|
|
||||||
return item.Event{}, fmt.Errorf("event not found: %w", err)
|
|
||||||
case err != nil:
|
|
||||||
return item.Event{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
dur, err := time.ParseDuration(durStr)
|
|
||||||
if err != nil {
|
|
||||||
return item.Event{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
event.Duration = dur
|
|
||||||
|
|
||||||
return event, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sqlite) FindAll() ([]item.Event, error) {
|
|
||||||
rows, err := s.db.Query(`
|
|
||||||
SELECT id, title, start, duration
|
|
||||||
FROM events`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]item.Event, 0)
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var event item.Event
|
|
||||||
var durStr string
|
|
||||||
if err := rows.Scan(&event.ID, &event.Title, &event.Start, &durStr); err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
dur, err := time.ParseDuration(durStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
event.Duration = dur
|
|
||||||
result = append(result, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sqlite) Delete(id string) error {
|
|
||||||
result, err := s.db.Exec(`
|
|
||||||
DELETE FROM events
|
|
||||||
WHERE id = ?`, id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rowsAffected, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rowsAffected == 0 {
|
|
||||||
return fmt.Errorf("event not found: %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sqlite) migrate(wanted []string) error {
|
|
||||||
// admin table
|
|
||||||
if _, err := s.db.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS migration
|
|
||||||
("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT)
|
|
||||||
`); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// find existing
|
|
||||||
rows, err := s.db.Query(`SELECT query FROM migration ORDER BY id`)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
existing := []string{}
|
|
||||||
for rows.Next() {
|
|
||||||
var query string
|
|
||||||
if err := rows.Scan(&query); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
existing = append(existing, string(query))
|
|
||||||
}
|
|
||||||
rows.Close()
|
|
||||||
|
|
||||||
// compare
|
|
||||||
missing, err := compareMigrations(wanted, existing)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute missing
|
|
||||||
for _, query := range missing {
|
|
||||||
if _, err := s.db.Exec(string(query)); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// register
|
|
||||||
if _, err := s.db.Exec(`
|
|
||||||
INSERT INTO migration
|
|
||||||
(query) VALUES (?)
|
|
||||||
`, query); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareMigrations(wanted, existing []string) ([]string, error) {
|
|
||||||
needed := []string{}
|
|
||||||
if len(wanted) < len(existing) {
|
|
||||||
return []string{}, ErrNotEnoughSQLMigrations
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, want := range wanted {
|
|
||||||
switch {
|
|
||||||
case i >= len(existing):
|
|
||||||
needed = append(needed, want)
|
|
||||||
case want == existing[i]:
|
|
||||||
// do nothing
|
|
||||||
case want != existing[i]:
|
|
||||||
return []string{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return needed, nil
|
|
||||||
}
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqliteEvent struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqliteEvent) Store(event item.Event) error {
|
||||||
|
if _, err := s.db.Exec(`
|
||||||
|
INSERT INTO events
|
||||||
|
(id, title, start, duration)
|
||||||
|
VALUES
|
||||||
|
(?, ?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE
|
||||||
|
SET
|
||||||
|
title=?,
|
||||||
|
start=?,
|
||||||
|
duration=?`,
|
||||||
|
event.ID, event.Title, event.Start.Format(timestampFormat), event.Duration.String(),
|
||||||
|
event.Title, event.Start.Format(timestampFormat), event.Duration.String()); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqliteEvent) Find(id string) (item.Event, error) {
|
||||||
|
var event item.Event
|
||||||
|
var durStr string
|
||||||
|
err := s.db.QueryRow(`
|
||||||
|
SELECT id, title, start, duration
|
||||||
|
FROM events
|
||||||
|
WHERE id = ?`, id).Scan(&event.ID, &event.Title, &event.Start, &durStr)
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return item.Event{}, fmt.Errorf("event not found: %w", err)
|
||||||
|
case err != nil:
|
||||||
|
return item.Event{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
dur, err := time.ParseDuration(durStr)
|
||||||
|
if err != nil {
|
||||||
|
return item.Event{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
event.Duration = dur
|
||||||
|
|
||||||
|
return event, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqliteEvent) FindAll() ([]item.Event, error) {
|
||||||
|
rows, err := s.db.Query(`
|
||||||
|
SELECT id, title, start, duration
|
||||||
|
FROM events`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
result := make([]item.Event, 0)
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var event item.Event
|
||||||
|
var durStr string
|
||||||
|
if err := rows.Scan(&event.ID, &event.Title, &event.Start, &durStr); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
dur, err := time.ParseDuration(durStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
event.Duration = dur
|
||||||
|
result = append(result, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqliteEvent) Delete(id string) error {
|
||||||
|
result, err := s.db.Exec(`
|
||||||
|
DELETE FROM events
|
||||||
|
WHERE id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return storage.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalID struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalID) FindAll() (map[string]int, error) {
|
||||||
|
rows, err := l.db.Query(`
|
||||||
|
SELECT id, local_id
|
||||||
|
FROM localids
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
result := make(map[string]int)
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
var localID int
|
||||||
|
if err := rows.Scan(&id, &localID); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
result[id] = localID
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalID) Next() (int, error) {
|
||||||
|
idMap, err := l.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
cur := make([]int, 0, len(idMap))
|
||||||
|
for _, localID := range idMap {
|
||||||
|
cur = append(cur, localID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage.NextLocalID(cur), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalID) Store(id string, localID int) error {
|
||||||
|
if _, err := l.db.Exec(`
|
||||||
|
INSERT INTO localids
|
||||||
|
(id, local_id)
|
||||||
|
VALUES
|
||||||
|
(? ,?)`, id, localID); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalID) Delete(id string) error {
|
||||||
|
result, err := l.db.Exec(`
|
||||||
|
DELETE FROM localids
|
||||||
|
WHERE id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return storage.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
timestampFormat = "2006-01-02 15:04:05"
|
||||||
|
)
|
||||||
|
|
||||||
|
var migrations = []string{
|
||||||
|
`CREATE TABLE events ("id" TEXT UNIQUE, "title" TEXT, "start" TIMESTAMP, "duration" TEXT)`,
|
||||||
|
`PRAGMA journal_mode=WAL`,
|
||||||
|
`PRAGMA synchronous=NORMAL`,
|
||||||
|
`PRAGMA cache_size=2000`,
|
||||||
|
`CREATE TABLE localids ("id" TEXT UNIQUE, "local_id" INTEGER)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidConfiguration = errors.New("invalid configuration")
|
||||||
|
ErrIncompatibleSQLMigration = errors.New("incompatible migration")
|
||||||
|
ErrNotEnoughSQLMigrations = errors.New("already more migrations than wanted")
|
||||||
|
ErrSqliteFailure = errors.New("sqlite returned an error")
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSqlites(dbPath string) (*LocalID, *SqliteEvent, error) {
|
||||||
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sl := &LocalID{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
se := &SqliteEvent{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migrate(db, migrations); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sl, se, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrate(db *sql.DB, wanted []string) error {
|
||||||
|
// admin table
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS migration
|
||||||
|
("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT)
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// find existing
|
||||||
|
rows, err := db.Query(`SELECT query FROM migration ORDER BY id`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing := []string{}
|
||||||
|
for rows.Next() {
|
||||||
|
var query string
|
||||||
|
if err := rows.Scan(&query); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
existing = append(existing, string(query))
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
|
||||||
|
// compare
|
||||||
|
missing, err := compareMigrations(wanted, existing)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute missing
|
||||||
|
for _, query := range missing {
|
||||||
|
if _, err := db.Exec(string(query)); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// register
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
INSERT INTO migration
|
||||||
|
(query) VALUES (?)
|
||||||
|
`, query); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareMigrations(wanted, existing []string) ([]string, error) {
|
||||||
|
needed := []string{}
|
||||||
|
if len(wanted) < len(existing) {
|
||||||
|
return []string{}, ErrNotEnoughSQLMigrations
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, want := range wanted {
|
||||||
|
switch {
|
||||||
|
case i >= len(existing):
|
||||||
|
needed = append(needed, want)
|
||||||
|
case want == existing[i]:
|
||||||
|
// do nothing
|
||||||
|
case want != existing[i]:
|
||||||
|
return []string{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return needed, nil
|
||||||
|
}
|
|
@ -1,10 +1,62 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import "go-mod.ewintr.nl/planner/item"
|
import (
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
|
||||||
type EventRepo interface {
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalID interface {
|
||||||
|
FindAll() (map[string]int, error)
|
||||||
|
Next() (int, error)
|
||||||
|
Store(id string, localID int) error
|
||||||
|
Delete(id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event interface {
|
||||||
Store(event item.Event) error
|
Store(event item.Event) error
|
||||||
Find(id string) (item.Event, error)
|
Find(id string) (item.Event, error)
|
||||||
FindAll() ([]item.Event, error)
|
FindAll() ([]item.Event, error)
|
||||||
Delete(id string) error
|
Delete(id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NextLocalID(used []int) int {
|
||||||
|
if len(used) == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Ints(used)
|
||||||
|
usedMax := 1
|
||||||
|
for _, u := range used {
|
||||||
|
if u > usedMax {
|
||||||
|
usedMax = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var limit int
|
||||||
|
for limit = 1; limit <= len(used) || limit < usedMax; limit *= 10 {
|
||||||
|
}
|
||||||
|
|
||||||
|
newId := used[len(used)-1] + 1
|
||||||
|
if newId < limit {
|
||||||
|
return newId
|
||||||
|
}
|
||||||
|
|
||||||
|
usedMap := map[int]bool{}
|
||||||
|
for _, u := range used {
|
||||||
|
usedMap[u] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < limit; i++ {
|
||||||
|
if _, ok := usedMap[i]; !ok {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return limit
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package storage_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNextLocalId(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
used []int
|
||||||
|
exp int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
used: []int{},
|
||||||
|
exp: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not empty",
|
||||||
|
used: []int{5},
|
||||||
|
exp: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple",
|
||||||
|
used: []int{2, 3, 4},
|
||||||
|
exp: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "holes",
|
||||||
|
used: []int{1, 5, 8},
|
||||||
|
exp: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expand limit",
|
||||||
|
used: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
||||||
|
exp: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrap if possible",
|
||||||
|
used: []int{8, 9},
|
||||||
|
exp: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find hole",
|
||||||
|
used: []int{1, 2, 3, 4, 5, 7, 8, 9},
|
||||||
|
exp: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dont wrap if expanded before",
|
||||||
|
used: []int{15, 16},
|
||||||
|
exp: 17,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "do wrap if expanded limit is reached",
|
||||||
|
used: []int{99},
|
||||||
|
exp: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sync bug",
|
||||||
|
used: []int{151, 956, 955, 150, 154, 155, 145, 144,
|
||||||
|
136, 152, 148, 146, 934, 149, 937, 135, 140, 139,
|
||||||
|
143, 137, 153, 939, 138, 953, 147, 141, 938, 142,
|
||||||
|
},
|
||||||
|
exp: 957,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
act := storage.NextLocalID(tc.used)
|
||||||
|
if tc.exp != act {
|
||||||
|
t.Errorf("exp %v, got %v", tc.exp, act)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue