nettools/db/migrate.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
}