128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package db_utils
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/golang-migrate/migrate/v4"
|
|
pgx "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
|
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
)
|
|
|
|
var (
|
|
OnNewInstance = errors.New("creating new instance")
|
|
OnVersionCheck = errors.New("checking DB version")
|
|
OnDrop = errors.New("dropping DB")
|
|
)
|
|
|
|
type MigrationConfig[InstanceCfgT any] struct {
|
|
// File path
|
|
// format: path or /absolutepath
|
|
MigrationsPath string
|
|
// format: sqlite3://path/to/db
|
|
// postgresql://user:password@ip:port/dbname?conn_opts
|
|
DB *sql.DB
|
|
// Put -1 for no limit, 0 to down database
|
|
// and any number > 0 to limit version
|
|
VersionLimit int
|
|
// Drop DB before applying migrations
|
|
Drop bool
|
|
// Config used for migration connects
|
|
DriverCfg InstanceCfgT
|
|
}
|
|
|
|
// NewMigrateSQLiteInstance is a newInstance function for sqlite3 database driver
|
|
func NewMigrateSQLiteInstance(db *sql.DB, sourceURL string, cfg *sqlite3.Config) (*migrate.Migrate, error) {
|
|
driver, err := sqlite3.WithInstance(db, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return migrate.NewWithDatabaseInstance(sourceURL, "sqlite3", driver)
|
|
}
|
|
|
|
// NewMigratePgxInstance is a newInstance function for pgx/v5 database driver
|
|
func NewMigratePgxInstance(db *sql.DB, sourceURL string, cfg *pgx.Config) (*migrate.Migrate, error) {
|
|
driver, err := pgx.WithInstance(db, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return migrate.NewWithDatabaseInstance(sourceURL, "pgx/v5", driver)
|
|
}
|
|
|
|
// DoMigrate applies migration to a database
|
|
func DoMigrate[InstanceCfgT any](cfg MigrationConfig[InstanceCfgT],
|
|
newInstance func(db *sql.DB, sourceURL string, cfg InstanceCfgT) (*migrate.Migrate, error)) (uint, bool, error) {
|
|
|
|
var ver uint
|
|
var dirty bool
|
|
var sourceURL string = fmt.Sprintf("file://%s", cfg.MigrationsPath)
|
|
|
|
m, err := newInstance(cfg.DB, sourceURL, cfg.DriverCfg)
|
|
if err != nil {
|
|
return 0, false, errors.Join(OnNewInstance, err)
|
|
}
|
|
|
|
// Drop db if needed, get current db
|
|
if cfg.Drop {
|
|
if err = m.Drop(); err != nil {
|
|
return 0, false, errors.Join(OnDrop, err)
|
|
}
|
|
// After drop, we have to create new migrate instance
|
|
m, err = newInstance(cfg.DB, sourceURL, cfg.DriverCfg)
|
|
if err != nil {
|
|
return 0, false, errors.Join(OnNewInstance, err)
|
|
}
|
|
} else {
|
|
// It's strange to check DB version after we drop it
|
|
// So I put version checking into else statement
|
|
ver, dirty, err = m.Version()
|
|
if err != nil && !errors.Is(err, migrate.ErrNilVersion) {
|
|
return 0, false, errors.Join(OnVersionCheck, err)
|
|
}
|
|
if dirty {
|
|
if err = m.Drop(); err != nil {
|
|
return ver, dirty, errors.Join(OnDrop, err)
|
|
}
|
|
// After drop, we have to create new migrate instance
|
|
m, err = newInstance(cfg.DB, sourceURL, cfg.DriverCfg)
|
|
if err != nil {
|
|
return ver, dirty, errors.Join(OnNewInstance, err)
|
|
}
|
|
// As we dropped DB
|
|
ver = 0
|
|
dirty = false
|
|
}
|
|
}
|
|
|
|
if cfg.VersionLimit == int(ver) {
|
|
return ver, dirty, nil
|
|
}
|
|
|
|
migratingUp := true
|
|
if cfg.VersionLimit > 0 {
|
|
if cfg.VersionLimit < int(ver) {
|
|
migratingUp = false
|
|
}
|
|
err = m.Migrate(uint(cfg.VersionLimit))
|
|
} else if cfg.VersionLimit == 0 {
|
|
migratingUp = false
|
|
err = m.Down()
|
|
} else {
|
|
err = m.Up()
|
|
}
|
|
if err != nil {
|
|
if !errors.Is(err, migrate.ErrNoChange) {
|
|
return 0, false, errors.Join(OnNewInstance, err,
|
|
fmt.Errorf("version limit: %d; migrating up: %v", cfg.VersionLimit, migratingUp))
|
|
}
|
|
return ver, dirty, nil
|
|
}
|
|
|
|
ver, dirty, err = m.Version()
|
|
if err != nil {
|
|
return ver, dirty, errors.Join(OnVersionCheck, err)
|
|
}
|
|
return ver, dirty, nil
|
|
}
|