From cd02d545dc910ebcf43b3f88a3c40c4267879a21 Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Fri, 16 Jun 2023 13:16:57 +0300 Subject: [PATCH] feat: psql with (maybe) working migration system --- db/db.go | 43 ++++++++++++++++++++++++++++++ db/migrations.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 ++++ go.sum | 10 +++++++ tixe.go | 5 ++++ util/env.go | 14 ++++++++++ 6 files changed, 145 insertions(+) create mode 100644 db/db.go create mode 100644 db/migrations.go create mode 100644 util/env.go diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..2ab758d --- /dev/null +++ b/db/db.go @@ -0,0 +1,43 @@ +package db + +import ( + "context" + "fmt" + "log" + "time" + "tixe/util" + + "github.com/jackc/pgx/v5/pgxpool" +) + +var PgPool *pgxpool.Pool + +func NewPgPool() { + var err error + PgPool, err = pgxpool.New(context.Background(), pgConnectionString()) + if err != nil { + log.Fatalf("[tixe/db] Unable to create psql connection pool: '%s'", err) + } + log.Print("[tixe/db] Created psql pool") + + for { + err = PgPool.Ping(context.Background()) + if err != nil { + log.Print("[tixe/db] Database not ready, retrying in 5") + time.Sleep(5 * time.Second) + } else { + log.Print("[tixe/db] Database connection verified") + return + } + } +} + +func pgConnectionString() string { + user := util.LoadVar("TIXE_PSQL_USER") + pwd := util.LoadVar("TIXE_PSQL_PASSWORD") + host := util.LoadVar("TIXE_PSQL_HOST") + port := util.LoadVar("TIXE_PSQL_PORT") + db := util.LoadVar("TIXE_PSQL_DB") + + return fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", user, pwd, host, port, db) +} diff --git a/db/migrations.go b/db/migrations.go new file mode 100644 index 0000000..b22bdc5 --- /dev/null +++ b/db/migrations.go @@ -0,0 +1,68 @@ +package db + +import ( + "context" + "fmt" + "log" + + "github.com/jackc/pgx/v5/pgtype" +) + +var migrationsTable string = "schema_migrations" + +func RunMigrations() { + log.Print("[tixe/db] Running migrations") + + var ranTimestamp pgtype.Timestamptz + var schemaVersion int = 0 + + schemaVersionQuery := fmt.Sprintf( + `SELECT ran_timestamp, schema_version + FROM %s + ORDER BY schema_version + DESC LIMIT 1;`, + migrationsTable) + err := PgPool.QueryRow(context.Background(), schemaVersionQuery).Scan(&ranTimestamp, &schemaVersion) + if err != nil { + log.Print("[tixe/db] No schema version found, running all migrations") + } else { + log.Printf("[tixe/db] Last migration ran on %s, with schema version %d", ranTimestamp.Time, schemaVersion) + } + + migrations := migrations() + if schemaVersion != len(migrations) { + for i := 0; i < len(migrations); i++ { + if i <= schemaVersion { + log.Printf("[tixe/db] Running migration %d", i) + _, err := PgPool.Exec(context.Background(), migrations[i]) + if err != nil { + log.Fatalf("[tixe/db] Migration %d failed to run!", i) + } + } + } + + // We are now up to date + schemaVersion = len(migrations) + // Create a new entry in the migrations table + schemaMigrationInsertQuery := fmt.Sprintf( + `INSERT INTO %s(ran_timestamp, schema_version) VALUES(CURRENT_TIMESTAMP, %d);`, + migrationsTable, schemaVersion) + _, err = PgPool.Exec(context.Background(), schemaMigrationInsertQuery) + if err != nil { + log.Fatal("[tixe/db] Migrations ran, but was not able to create migration entry") + } else { + log.Print("[tixe/db] Migrations ran successfully") + } + } else { + log.Printf("[tixe/db] Already on schema version %d, no migrations to run", schemaVersion) + } +} + +func migrations() []string { + return []string{ + fmt.Sprintf(`CREATE TABLE %s ( + ran_timestamp timestamp, + schema_version integer + )`,migrationsTable), + } +} diff --git a/go.mod b/go.mod index 3ed952f..0e00eaf 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,10 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.1 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.0 // indirect + github.com/jackc/puddle/v2 v2.2.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/leodido/go-urn v1.2.4 // indirect @@ -25,6 +29,7 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/go.sum b/go.sum index 8cabed1..72ca3fb 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,14 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.0 h1:BSr+GCm4N6QcgIwv0DyTFHK9ugfEFF9DzSbbzxOiXU0= +github.com/jackc/pgx/v5 v5.4.0/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY= +github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk= +github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -61,6 +69,8 @@ golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= diff --git a/tixe.go b/tixe.go index 45e0917..38e7445 100644 --- a/tixe.go +++ b/tixe.go @@ -4,6 +4,7 @@ import ( "log" "net/http" "tixe/api" + "tixe/db" "tixe/template" "github.com/gin-gonic/gin" @@ -43,6 +44,10 @@ func setupRouter() *gin.Engine { func main() { log.Print("[tixe] Starting up...") + db.NewPgPool() + defer db.PgPool.Close() + db.RunMigrations() + err := template.NewTemplateEngine() if err != nil { log.Fatalf("[tixe] Creating a new TemplateEngine failed, '%s'", err) diff --git a/util/env.go b/util/env.go new file mode 100644 index 0000000..2594e85 --- /dev/null +++ b/util/env.go @@ -0,0 +1,14 @@ +package util + +import ( + "log" + "os" +) + +func LoadVar(key string) string { + value := os.Getenv(key) + if key == "" { + log.Fatalf("[tixe/util] Environment variable %s is empty!", key) + } + return value +} -- 2.44.1