This commit is contained in:
Erik Winter 2024-10-03 07:32:48 +02:00 committed by Erik Winter
parent cd784f999d
commit 5af427c23b
14 changed files with 618 additions and 243 deletions

View File

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

View File

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

View File

@ -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 {
return fmt.Errorf("could not get local ids: %v", err)
}
all, err := eventRepo.FindAll()
if err != nil { if err != nil {
return err return err
} }
for _, e := range all { for _, e := range all {
fmt.Printf("%s\t%s\t%s\t%s\n", e.ID, e.Title, e.Start.Format(time.DateTime), e.Duration.String()) lid, ok := localIDs[e.ID]
if !ok {
return fmt.Errorf("could not find local id for %s", e.ID)
}
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())
} }
return nil return nil
} }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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