From 8522dbae201f4222971908f74a35e05f06ac1606 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sun, 22 Jun 2025 14:32:49 -0400 Subject: [PATCH] Adding migrations --- cmd/api/main.go | 32 ++++++++++----- go.mod | 14 +++++-- go.sum | 22 ++++++++++ internal/database/database.go | 37 +++++++++++++++++ .../000001_create_movies_table.down.sql | 2 + migrations/000001_create_movies_table.up.sql | 10 +++++ migrations/migrate.go | 41 +++++++++++++++++++ 7 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 internal/database/database.go create mode 100644 migrations/000001_create_movies_table.down.sql create mode 100644 migrations/000001_create_movies_table.up.sql create mode 100644 migrations/migrate.go diff --git a/cmd/api/main.go b/cmd/api/main.go index d95e766..954f1dc 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "database/sql" "errors" // "flag" @@ -15,8 +16,9 @@ import ( "time" "git.runcible.io/learning/pulley/internal/config" + "git.runcible.io/learning/pulley/internal/database" "git.runcible.io/learning/pulley/internal/logging" - "github.com/jackc/pgx/v5/pgxpool" + "git.runcible.io/learning/pulley/migrations" ) const Version = "1.0.0" @@ -62,23 +64,31 @@ func run(ctx context.Context, w io.Writer, args []string) error { appCtx, cancel := context.WithCancel(signalCtx) defer cancel() - pool, err := pgxpool.New(appCtx, cfg.DatabaseUri) + //run migrations + db, err := sql.Open("pgx", cfg.DatabaseUri) if err != nil { - slog.Error("Error encountered in creating pgx postgres connection pool", "error", err.Error()) + logger.Error("Error opening migrations connection") return err } - defer func() { - slog.Info("Closing database connection pool") - pool.Close() - }() + defer db.Close() - timoutCtx, close := context.WithTimeout(appCtx, 5*time.Second) - defer close() + err = migrations.Migrate(db) + if err != nil { + logger.Error("Error in migrations", "error", err.Error()) + return err + } - if err := pool.Ping(timoutCtx); err != nil { - slog.Error("Error in attempting first postgres database connection") + logger.Info("Migrations applied.") + db.Close() + + pool, err := database.OpenPgPool(ctx, cfg) + if err != nil { return err } + defer func() { + slog.Info("Closing database connection pool") + pool.Close() + }() app := application{config: cfg, logger: logger} diff --git a/go.mod b/go.mod index 0ada839..10326ff 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,22 @@ module git.runcible.io/learning/pulley go 1.24.1 -require github.com/julienschmidt/httprouter v1.3.0 +require ( + github.com/golang-migrate/migrate v3.5.4+incompatible + github.com/golang-migrate/migrate/v4 v4.18.3 + github.com/jackc/pgx/v5 v5.7.5 + github.com/julienschmidt/httprouter v1.3.0 + github.com/kelseyhightower/envconfig v1.4.0 +) require ( + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.5 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/kelseyhightower/envconfig v1.4.0 // indirect + go.uber.org/atomic v1.7.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/text v0.24.0 // indirect diff --git a/go.sum b/go.sum index 74f1c13..bd86198 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,17 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= +github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= +github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= +github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -11,10 +24,17 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= @@ -23,3 +43,5 @@ golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..13e7f2c --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,37 @@ +package database + +import ( + "context" + "log/slog" + "time" + + "git.runcible.io/learning/pulley/internal/config" + "github.com/jackc/pgx/v5/pgxpool" +) + +// https://medium.com/@neelkanthsingh.jr/understanding-database-connection-pools-and-the-pgx-library-in-go-3087f3c5a0c + +// pgxpool.New implicitly creates a dbConfig from the database uri params which can include connection params + +// newPgxConfig creates a pgxpool Config from the application service config +// func newPgxConfig(cfg config.ServiceConfig) (pgxpool.Config, error) { +// dbconfig, err := pgxpool.ParseConfig(cfg.DatabaseUri) +// } + +// OpenPgPool creates a db pool and tests connectivity +func OpenPgPool(ctx context.Context, cfg config.ServiceConfig) (*pgxpool.Pool, error) { + pool, err := pgxpool.New(ctx, cfg.DatabaseUri) + if err != nil { + return nil, err + } + + timoutCtx, close := context.WithTimeout(ctx, 5*time.Second) + defer close() + + if err := pool.Ping(timoutCtx); err != nil { + slog.Error("Error in attempting first postgres database connection") + pool.Close() + return nil, err + } + return pool, nil +} diff --git a/migrations/000001_create_movies_table.down.sql b/migrations/000001_create_movies_table.down.sql new file mode 100644 index 0000000..9ff717f --- /dev/null +++ b/migrations/000001_create_movies_table.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS movies; + diff --git a/migrations/000001_create_movies_table.up.sql b/migrations/000001_create_movies_table.up.sql new file mode 100644 index 0000000..312223d --- /dev/null +++ b/migrations/000001_create_movies_table.up.sql @@ -0,0 +1,10 @@ + CREATE TABLE IF NOT EXISTS movies ( + id bigserial PRIMARY KEY, + created_at timestamp(0) with time zone NOT NULL DEFAULT NOW(), + title text NOT NULL, + year integer NOT NULL, + runtime integer NOT NULL, + genres text[] NOT NULL, + version integer NOT NULL DEFAULT 1 +); + diff --git a/migrations/migrate.go b/migrations/migrate.go new file mode 100644 index 0000000..de7fd01 --- /dev/null +++ b/migrations/migrate.go @@ -0,0 +1,41 @@ +package migrations + +import ( + "database/sql" + "embed" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/pgx/v5" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed *.sql +var migrationFiles embed.FS + +func Migrate(db *sql.DB) error { + + //create database driver + driver, err := pgx.WithInstance(db, &pgx.Config{}) + if err != nil { + + } + + // Create IFS source from embedded files + source, err := iofs.New(migrationFiles, ".") + if err != nil { + return err + } + + //creat new migrations instance + m, err := migrate.NewWithInstance("iofs", source, "pgx", driver) + if err != nil { + return err + } + + // Run Migrations + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + return err + } + + return nil +}