2023-02-14 05:41:45 +00:00
package database
import (
2024-07-10 22:45:04 +00:00
"GoWeb/internal"
2023-02-14 05:41:45 +00:00
"errors"
"fmt"
"github.com/lib/pq"
2023-09-03 20:45:12 +00:00
"log/slog"
2023-02-14 05:41:45 +00:00
"reflect"
)
2024-01-20 22:32:07 +00:00
// 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
2024-07-02 02:19:48 +00:00
func Migrate ( app * app . Deps , anyStruct interface { } ) error {
2023-02-14 05:41:45 +00:00
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
2024-01-20 22:32:07 +00:00
// Create column if dummy for migration is NOT zero value
fieldValue := valueOfStruct . Field ( i ) . Interface ( )
if ! reflect . ValueOf ( fieldValue ) . IsZero ( ) {
if fieldName != "Id" && fieldName != "id" {
err := createColumn ( app , tableName , fieldName , fieldType . Type . Name ( ) )
if err != nil {
return err
}
2023-02-14 05:41:45 +00:00
}
}
}
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
2024-07-02 02:19:48 +00:00
func createTable ( app * app . Deps , tableName string ) error {
2023-02-18 01:01:59 +00:00
var tableExists bool
err := app . Db . QueryRow ( "SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname ~ $1 AND pg_catalog.pg_table_is_visible(c.oid))" , "^" + tableName + "$" ) . Scan ( & tableExists )
2023-02-14 05:41:45 +00:00
if err != nil {
2023-09-03 20:56:35 +00:00
slog . Error ( "error checking if table exists: " + tableName )
2023-02-14 05:41:45 +00:00
return err
}
2023-02-18 01:01:59 +00:00
if tableExists {
2023-09-03 20:56:35 +00:00
slog . Info ( "table already exists: " + tableName )
2023-02-18 01:01:59 +00:00
return nil
} else {
sanitizedTableQuery := fmt . Sprintf ( "CREATE TABLE IF NOT EXISTS \"%s\" (\"Id\" serial primary key)" , tableName )
_ , err := app . Db . Query ( sanitizedTableQuery )
if err != nil {
2023-09-03 20:56:35 +00:00
slog . Error ( "error creating table: " + tableName )
2023-02-18 01:01:59 +00:00
return err
}
2023-09-03 20:56:35 +00:00
slog . Info ( "table created successfully: " + tableName )
2023-02-18 01:01:59 +00:00
return nil
}
2023-02-14 05:41:45 +00:00
}
// createColumn creates a column with the given name and type if it doesn't exist
2024-07-02 02:19:48 +00:00
func createColumn ( app * app . Deps , tableName , columnName , columnType string ) error {
2023-02-18 01:01:59 +00:00
var columnExists bool
err := app . Db . QueryRow ( "SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = $2)" , tableName , columnName ) . Scan ( & columnExists )
2023-02-14 05:41:45 +00:00
if err != nil {
2023-09-03 20:56:35 +00:00
slog . Error ( "error checking if column exists: " + columnName + " in table: " + tableName )
2023-02-14 05:41:45 +00:00
return err
}
2023-02-18 01:01:59 +00:00
if columnExists {
2023-09-03 20:56:35 +00:00
slog . Info ( "column already exists: " + columnName + " in table: " + tableName )
2023-02-18 01:01:59 +00:00
return nil
} else {
postgresType , err := getPostgresType ( columnType )
if err != nil {
2023-09-03 20:56:35 +00:00
slog . Error ( "error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType )
2023-02-18 01:01:59 +00:00
return err
}
sanitizedTableName := pq . QuoteIdentifier ( tableName )
query := fmt . Sprintf ( "ALTER TABLE %s ADD COLUMN IF NOT EXISTS \"%s\" %s" , sanitizedTableName , columnName , postgresType )
2023-02-14 05:41:45 +00:00
2023-02-18 01:01:59 +00:00
_ , err = app . Db . Query ( query )
if err != nil {
2023-09-03 20:56:35 +00:00
slog . Error ( "error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType )
2023-02-18 01:01:59 +00:00
return err
}
2023-02-14 05:41:45 +00:00
2023-09-03 20:56:35 +00:00
slog . Info ( "column created successfully:" , columnName )
2023-02-14 05:41:45 +00:00
2023-02-18 01:01:59 +00:00
return nil
}
2023-02-14 05:41:45 +00:00
}
// Given a type in Go, return the corresponding type in Postgres
func getPostgresType ( goType string ) ( string , error ) {
switch goType {
2023-02-18 00:47:29 +00:00
case "int" , "int32" , "uint" , "uint32" :
2023-02-14 05:41:45 +00:00
return "integer" , nil
2023-02-18 00:47:29 +00:00
case "int64" , "uint64" :
2023-02-14 05:41:45 +00:00
return "bigint" , nil
2023-02-18 00:47:29 +00:00
case "int16" , "int8" , "uint16" , "uint8" , "byte" :
2023-02-14 05:41:45 +00:00
return "smallint" , nil
case "string" :
return "text" , nil
case "float64" :
return "double precision" , nil
case "bool" :
return "boolean" , nil
2023-02-18 00:52:15 +00:00
case "Time" :
2023-02-14 05:41:45 +00:00
return "timestamp" , nil
case "[]byte" :
return "bytea" , nil
}
return "" , errors . New ( "Unknown type: " + goType )
}