From fcd6477ec35d7aff7cdb9367490e19aa44c1017a Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 13 Feb 2023 23:41:45 -0600 Subject: [PATCH] Migration implementation, auto migrate when starting program --- database/migrate.go | 105 +++++++++++++++++++++++++++++++++++++++++++ main.go | 10 ++++- models/migrations.go | 21 +++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 database/migrate.go create mode 100644 models/migrations.go diff --git a/database/migrate.go b/database/migrate.go new file mode 100644 index 0000000..489fe49 --- /dev/null +++ b/database/migrate.go @@ -0,0 +1,105 @@ +package database + +import ( + "GoWeb/app" + "errors" + "fmt" + "github.com/lib/pq" + "log" + "reflect" +) + +// Migrate given a dummy object of any type, it will create a table with the same name as the type and create columns with the same name as the fields of the object +func Migrate(app *app.App, anyStruct interface{}) error { + valueOfStruct := reflect.ValueOf(anyStruct) + typeOfStruct := valueOfStruct.Type() + + tableName := typeOfStruct.Name() + err := createTable(app, tableName) + if err != nil { + return err + } + + for i := 0; i < valueOfStruct.NumField(); i++ { + fieldType := typeOfStruct.Field(i) + fieldName := fieldType.Name + if fieldName != "Id" && fieldName != "id" { + err := createColumn(app, tableName, fieldName, fieldType.Type.Name()) + if err != nil { + return err + } + } + } + + return nil +} + +// createTable creates a table with the given name if it doesn't exist, it is assumed that id will be the primary key +func createTable(app *app.App, tableName string) error { + sanitizedTableQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS \"%s\" (\"Id\" serial primary key)", tableName) + + _, err := app.Db.Query(sanitizedTableQuery) + if err != nil { + log.Println("Error creating table: " + tableName) + return err + } + + log.Println("Table created successfully (or already exists): " + tableName) + return nil +} + +// createColumn creates a column with the given name and type if it doesn't exist +func createColumn(app *app.App, tableName, columnName, columnType string) error { + postgresType, err := getPostgresType(columnType) + if err != nil { + log.Println("Error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType) + return err + } + + sanitizedTableName := pq.QuoteIdentifier(tableName) + query := fmt.Sprintf("ALTER TABLE %s ADD COLUMN IF NOT EXISTS \"%s\" %s", sanitizedTableName, columnName, postgresType) + + _, err = app.Db.Query(query) + if err != nil { + log.Println("Error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType) + return err + } + + log.Println("Column created successfully (or already exists):", columnName) + + return nil +} + +// Given a type in Go, return the corresponding type in Postgres +func getPostgresType(goType string) (string, error) { + switch goType { + case "int": + case "int32": + case "uint": + case "uint32": + return "integer", nil + case "int64": + case "uint64": + return "bigint", nil + case "int16": + case "int8": + case "uint16": + case "uint8": + case "byte": + return "smallint", nil + case "string": + return "text", nil + case "float64": + return "double precision", nil + case "bool": + return "boolean", nil + case "time.Time": + return "timestamp", nil + case "[]byte": + return "bytea", nil + default: + return "text", nil + } + + return "", errors.New("Unknown type: " + goType) +} diff --git a/main.go b/main.go index 9b91ef6..5ea320c 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "GoWeb/app" "GoWeb/config" "GoWeb/database" + "GoWeb/models" "GoWeb/routes" "embed" "log" @@ -37,8 +38,15 @@ func main() { file, err := os.OpenFile("logs/"+time.Now().Format("2006-01-02")+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) log.SetOutput(file) - // Connect to database + // Connect to database and run migrations appLoaded.Db = database.ConnectDB(&appLoaded) + if appLoaded.Config.Db.AutoMigrate { + err = models.RunAllMigrations(&appLoaded) + if err != nil { + log.Println(err) + return + } + } // Define Routes routes.GetRoutes(&appLoaded) diff --git a/models/migrations.go b/models/migrations.go new file mode 100644 index 0000000..c76fce4 --- /dev/null +++ b/models/migrations.go @@ -0,0 +1,21 @@ +package models + +import ( + "GoWeb/app" + "GoWeb/database" +) + +// RunAllMigrations defines the structs that should be represented in the database +func RunAllMigrations(app *app.App) error { + // Declare new dummy user for reflection + user := User{ + Id: 1, // Id is handled automatically, but it is added here to show it will be skipped during column creation + Username: "migrate", + Password: "migrate", + AuthToken: "migrate", + CreatedAt: "2021-01-01 00:00:00", + UpdatedAt: "2021-01-01 00:00:00", + } + + return database.Migrate(app, user) +}