From 49d45970a45c0511b4cd9f6cbf582b4e963e70f9 Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Tue, 24 Sep 2024 07:44:00 +0200 Subject: [PATCH] cal sqlite --- cal/main.go | 52 ++++++++++---- cal/sqlite.go | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 cal/sqlite.go diff --git a/cal/main.go b/cal/main.go index 5f8ebdd..54faf9b 100644 --- a/cal/main.go +++ b/cal/main.go @@ -6,37 +6,63 @@ import ( "time" "go-mod.ewintr.nl/planner/item" - "go-mod.ewintr.nl/planner/sync/client" ) func main() { fmt.Println("cal") - c := client.NewClient("http://localhost:8092", "testKey") - items, err := c.Updated([]item.Kind{item.KindEvent}, time.Time{}) + repo, err := NewSqlite("test.db") if err != nil { fmt.Println(err) os.Exit(1) } - fmt.Printf("%+v\n", items) - - i := item.Item{ - ID: "id-1", - Kind: item.KindEvent, - Updated: time.Now(), - Body: "body", + one := item.Event{ + ID: "a", + EventBody: item.EventBody{ + Title: "title", + Start: time.Now(), + End: time.Now().Add(-5 * time.Second), + }, } - if err := c.Update([]item.Item{i}); err != nil { + if err := repo.Delete(one.ID); err != nil { fmt.Println(err) os.Exit(1) } - items, err = c.Updated([]item.Kind{item.KindEvent}, time.Time{}) + all, err := repo.FindAll() if err != nil { fmt.Println(err) os.Exit(1) } - fmt.Printf("%+v\n", items) + fmt.Printf("all: %+v\n", all) + + // c := client.NewClient("http://localhost:8092", "testKey") + // items, err := c.Updated([]item.Kind{item.KindEvent}, time.Time{}) + // if err != nil { + // fmt.Println(err) + // os.Exit(1) + // } + + // fmt.Printf("%+v\n", items) + + // i := item.Item{ + // ID: "id-1", + // Kind: item.KindEvent, + // Updated: time.Now(), + // Body: "body", + // } + // if err := c.Update([]item.Item{i}); err != nil { + // fmt.Println(err) + // os.Exit(1) + // } + + // items, err = c.Updated([]item.Kind{item.KindEvent}, time.Time{}) + // if err != nil { + // fmt.Println(err) + // os.Exit(1) + // } + + // fmt.Printf("%+v\n", items) } diff --git a/cal/sqlite.go b/cal/sqlite.go new file mode 100644 index 0000000..20b153b --- /dev/null +++ b/cal/sqlite.go @@ -0,0 +1,194 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + + "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, "end" TIMESTAMP)`, + `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, end) +VALUES +(?, ?, ?, ?) +ON CONFLICT(id) DO UPDATE +SET +title=?, +start=?, +end=?`, + event.ID, event.Title, event.Start.Format(timestampFormat), event.End.Format(timestampFormat), + event.Title, event.Start.Format(timestampFormat), event.End.Format(timestampFormat)); err != nil { + return fmt.Errorf("%w: %v", ErrSqliteFailure, err) + } + return nil +} + +func (s *Sqlite) Find(id string) (item.Event, error) { + var event item.Event + err := s.db.QueryRow(` +SELECT id, title, start, end +FROM events +WHERE id = ?`, id).Scan(&event.ID, &event.Title, &event.Start, &event.End) + + if err == sql.ErrNoRows { + return event, fmt.Errorf("event not found: %w", err) + } + if err != nil { + return event, fmt.Errorf("%w: %v", ErrSqliteFailure, err) + } + + return event, nil +} + +func (s *Sqlite) FindAll() ([]item.Event, error) { + rows, err := s.db.Query(` +SELECT id, title, start, end +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 + if err := rows.Scan(&event.ID, &event.Title, &event.Start, &event.End); err != nil { + return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) + } + 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 +}