From d381528db182b5797e0f0b807851c14d48cc322a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Z=C3=ADpek?= Date: Tue, 23 Aug 2022 21:26:42 +0200 Subject: [PATCH] Added proper db migrations --- server/app/database.go | 44 -------- server/app/server.go | 10 +- server/database/database.go | 52 +++++++++ server/database/migrations.go | 100 ++++++++++++++++++ .../migrations/1661277568662_initial.sql | 18 ++++ 5 files changed, 179 insertions(+), 45 deletions(-) delete mode 100644 server/app/database.go create mode 100644 server/database/database.go create mode 100644 server/database/migrations.go create mode 100644 server/database/migrations/1661277568662_initial.sql diff --git a/server/app/database.go b/server/app/database.go deleted file mode 100644 index 67d9f86..0000000 --- a/server/app/database.go +++ /dev/null @@ -1,44 +0,0 @@ -package app - -import "database/sql" - -func initializeDb(databaseUrl string) *sql.DB { - db, err := sql.Open("sqlite3", databaseUrl) - - if err != nil { - panic(err) - } - - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS sensor_values ( - timestamp INTEGER NOT NULL, - sensor TEXT NOT NULL, - value REAL NOT NULL - );`) - - if err != nil { - panic(err) - } - - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS sensor_config ( - sensor TEXT NOT NULL, - key TEXT NOT NULL, - value REAL NOT NULL, - PRIMARY KEY (sensor, key) - );`) - - if err != nil { - panic(err) - } - - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS sessions ( - id TEXT NOT NULL, - expires_at INTEGER NOT NULL, - PRIMARY KEY (id) - );`) - - if err != nil { - panic(err) - } - - return db -} diff --git a/server/app/server.go b/server/app/server.go index 0388186..9af696b 100644 --- a/server/app/server.go +++ b/server/app/server.go @@ -2,6 +2,7 @@ package app import ( "basic-sensor-receiver/config" + "basic-sensor-receiver/database" "basic-sensor-receiver/services" "database/sql" ) @@ -15,7 +16,14 @@ type Server struct { func InitializeServer() *Server { server := Server{} server.Config = config.LoadConfig() - server.DB = initializeDb(server.Config.DatabaseUrl) + + db, err := database.Initialize(server.Config.DatabaseUrl) + + if err != nil { + panic(err) + } + + server.DB = db ctx := services.Context{DB: server.DB, Config: server.Config} diff --git a/server/database/database.go b/server/database/database.go new file mode 100644 index 0000000..8c1e290 --- /dev/null +++ b/server/database/database.go @@ -0,0 +1,52 @@ +package database + +import ( + "database/sql" + "fmt" + "time" +) + +func Initialize(databaseUrl string) (*sql.DB, error) { + db, err := sql.Open("sqlite3", databaseUrl) + + if err != nil { + return nil, fmt.Errorf("failed to open database connection: %w", err) + } + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS migrations ( + id TEXT NOT NULL, + run_at INTEGER NOT NULL + );`) + + if err != nil { + return nil, fmt.Errorf("failed to initialize migrations table: %w", err) + } + + pendingMigrations, err := getPendingMigrations(db) + + if err != nil { + return nil, fmt.Errorf("failed to get pending migrations: %w", err) + } + + for _, migrationId := range pendingMigrations { + migrationSource, err := getMigrationContent(migrationId) + + if err != nil { + return nil, err + } + + _, err = db.Exec(migrationSource) + + if err != nil { + return nil, fmt.Errorf("failed to run migration %s: %w", migrationId, err) + } + + _, err = db.Exec("INSERT INTO migrations (id, run_at) VALUES (?, ?)", migrationId, time.Now().Unix()) + + if err != nil { + return nil, fmt.Errorf("failed to store run migration: %w", err) + } + } + + return db, nil +} diff --git a/server/database/migrations.go b/server/database/migrations.go new file mode 100644 index 0000000..fab0c82 --- /dev/null +++ b/server/database/migrations.go @@ -0,0 +1,100 @@ +package database + +import ( + "database/sql" + "embed" + "fmt" + "io/fs" + "sort" + "strings" +) + +//go:embed migrations/*.sql +var migrationStorage embed.FS + +func getAppliedMigrationsSet(db *sql.DB) (map[string]bool, error) { + set := make(map[string]bool) + + rows, err := db.Query("SELECT id FROM migrations") + + if err != nil { + return nil, err + } + + defer rows.Close() + + for rows.Next() { + var id string + + err := rows.Scan(&id) + if err != nil { + return nil, err + } + + set[id] = true + } + + err = rows.Err() + if err != nil { + return nil, err + } + + return set, nil +} + +func getMigrations() ([]string, error) { + var items []string + + entries, err := migrationStorage.ReadDir("migrations") + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".sql") { + items = append(items, strings.TrimSuffix(entry.Name(), ".sql")) + } + } + + if err != nil { + return nil, err + } + + sort.Strings(items) + + return items, nil +} + +func getMigrationContent(id string) (string, error) { + content, err := fs.ReadFile(migrationStorage, "migrations/"+id+".sql") + + if err != nil { + return "", err + } + + return string(content), nil +} + +func getPendingMigrations(db *sql.DB) ([]string, error) { + var migrationsToRun []string + + migrationSet, err := getAppliedMigrationsSet(db) + + if err != nil { + return nil, err + } + + storedMigrations, err := getMigrations() + + if err != nil { + return nil, fmt.Errorf("failed to load stored migrations: %w", err) + } + + for _, storedMigration := range storedMigrations { + if !migrationSet[storedMigration] { + migrationsToRun = append(migrationsToRun, storedMigration) + } + } + + return migrationsToRun, nil +} diff --git a/server/database/migrations/1661277568662_initial.sql b/server/database/migrations/1661277568662_initial.sql new file mode 100644 index 0000000..8c09155 --- /dev/null +++ b/server/database/migrations/1661277568662_initial.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS sensor_values ( + timestamp INTEGER NOT NULL, + sensor TEXT NOT NULL, + value REAL NOT NULL +); + +CREATE TABLE IF NOT EXISTS sensor_config ( + sensor TEXT NOT NULL, + key TEXT NOT NULL, + value REAL NOT NULL, + PRIMARY KEY (sensor, key) +); + +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT NOT NULL, + expires_at INTEGER NOT NULL, + PRIMARY KEY (id) +);