From c06d008a20876bed15537a2c3590778e08257a82 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Mon, 20 Jul 2026 20:55:04 +0200 Subject: [PATCH] feat: added database migration on startup and user+list db migrations --- cmd/api-server/main.go | 4 ++ go.mod | 2 + internal/database/migrate.go | 43 +++++++++++++++++++ .../migrations/000001_create_users.down.sql | 2 + .../migrations/000001_create_users.up.sql | 16 +++++++ .../migrations/000002_create_lists.down.sql | 3 ++ .../migrations/000002_create_lists.up.sql | 26 +++++++++++ 7 files changed, 96 insertions(+) create mode 100644 internal/database/migrate.go create mode 100644 internal/database/migrations/000001_create_users.down.sql create mode 100644 internal/database/migrations/000001_create_users.up.sql create mode 100644 internal/database/migrations/000002_create_lists.down.sql create mode 100644 internal/database/migrations/000002_create_lists.up.sql diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index 5d4988c..08a93ab 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -69,6 +69,10 @@ func run(serviceName string, serviceVersion string) error { } }() + if err := database.RunMigrations(db); err != nil { + slog.Error("Failed to run migrations", slog.Any("error", err)) + } + srv := makeServer(db, cfg.Port) go func() { slog.Info("Starting http server", "addr", srv.Addr) diff --git a/go.mod b/go.mod index d23146b..65d2c29 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/robindittmar/dttmr-api go 1.26 require ( + github.com/golang-migrate/migrate/v4 v4.19.1 github.com/jackc/pgx/v5 v5.10.0 github.com/joho/godotenv v1.5.1 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 @@ -25,6 +26,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/lib/pq v1.10.9 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect diff --git a/internal/database/migrate.go b/internal/database/migrate.go new file mode 100644 index 0000000..c251815 --- /dev/null +++ b/internal/database/migrate.go @@ -0,0 +1,43 @@ +package database + +import ( + "database/sql" + "embed" + "errors" + "fmt" + "log/slog" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed migrations/*.sql +var migrationFS embed.FS + +func RunMigrations(db *sql.DB) error { + sourceDriver, err := iofs.New(migrationFS, "migrations") + if err != nil { + return fmt.Errorf("failed to load embedded migrations: %w", err) + } + + dbDriver, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + return fmt.Errorf("failed to create migration db driver: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", sourceDriver, "postgres", dbDriver) + if err != nil { + return fmt.Errorf("failed to initialize migrator: %w", err) + } + + slog.Info("Running database migrations...") + err = m.Up() + + if err != nil && !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("failed to run database migrations: %w", err) + } + + slog.Info("Database migrations applied successfully") + return nil +} diff --git a/internal/database/migrations/000001_create_users.down.sql b/internal/database/migrations/000001_create_users.down.sql new file mode 100644 index 0000000..0e03010 --- /dev/null +++ b/internal/database/migrations/000001_create_users.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS sessions; +DROP TABLE IF EXISTS users; diff --git a/internal/database/migrations/000001_create_users.up.sql b/internal/database/migrations/000001_create_users.up.sql new file mode 100644 index 0000000..76b4892 --- /dev/null +++ b/internal/database/migrations/000001_create_users.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_id UUID UNIQUE NOT NULL, + is_revoked BOOLEAN NOT NULL DEFAULT FALSE, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/internal/database/migrations/000002_create_lists.down.sql b/internal/database/migrations/000002_create_lists.down.sql new file mode 100644 index 0000000..fbbcce8 --- /dev/null +++ b/internal/database/migrations/000002_create_lists.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS list_items; +DROP TABLE IF EXISTS list_users; +DROP TABLE IF EXISTS lists; diff --git a/internal/database/migrations/000002_create_lists.up.sql b/internal/database/migrations/000002_create_lists.up.sql new file mode 100644 index 0000000..ed8ecec --- /dev/null +++ b/internal/database/migrations/000002_create_lists.up.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS lists ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS list_users ( + list_id UUID REFERENCES lists(id) ON DELETE CASCADE, + user_id UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (list_id, user_id) +); + +CREATE INDEX idx_list_users_user_id ON list_users(user_id); + +CREATE TABLE IF NOT EXISTS list_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + list_id UUID REFERENCES lists(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + is_completed BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_list_items_list_id ON list_items(list_id);