Files
kjol/cmd/migrate/engine.go

261 lines
5.1 KiB
Go

package main
import (
"errors"
"fmt"
"os"
)
var (
errNoChange = errors.New("no change")
)
type engine struct {
source *migrationSource
db *migrateDB
}
func newEngine(migrationsDir string, connStr string, schemaName string) (*engine, error) {
source, err := newMigrationSource(migrationsDir)
if err != nil {
return nil, err
}
db, err := openDatabase(connStr, schemaName)
if err != nil {
return nil, err
}
return &engine{source: source, db: db}, nil
}
func (e *engine) close() error {
return e.db.close()
}
func (e *engine) up() error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
curVersion, dirty, err := e.db.version()
if err != nil {
return err
}
if dirty {
return fmt.Errorf("Dirty database version %d. Fix and force version.", curVersion)
}
// determine where to start
var version uint
if curVersion == nilVersion {
first, err := e.source.first()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errNoChange
}
return err
}
version = first
} else {
next, err := e.source.next(uint(curVersion))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errNoChange
}
return err
}
version = next
}
// apply from starting version forward
for {
if err := e.applyUp(version); err != nil {
return err
}
next, err := e.source.next(version)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil // done, applied all
}
return err
}
version = next
}
}
func (e *engine) down() error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
curVersion, dirty, err := e.db.version()
if err != nil {
return err
}
if dirty {
return fmt.Errorf("Dirty database version %d. Fix and force version.", curVersion)
}
if curVersion == nilVersion {
return errNoChange
}
// determine target version after rollback
targetVersion := nilVersion
prev, err := e.source.prev(uint(curVersion))
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
// at the first migration, target is nilVersion (-1)
} else {
targetVersion = int(prev)
}
return e.applyDown(uint(curVersion), targetVersion)
}
func (e *engine) goTo(target uint) error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
curVersion, dirty, err := e.db.version()
if err != nil {
return err
}
if dirty {
return fmt.Errorf("Dirty database version %d. Fix and force version.", curVersion)
}
if !e.source.versionExists(target) {
return fmt.Errorf("version %d not found in migration source", target)
}
intTarget := int(target)
if intTarget == curVersion {
return errNoChange
}
if intTarget > curVersion {
// going up
var version uint
if curVersion == nilVersion {
first, err := e.source.first()
if err != nil {
return err
}
version = first
} else {
next, err := e.source.next(uint(curVersion))
if err != nil {
return err
}
version = next
}
for {
if err := e.applyUp(version); err != nil {
return err
}
if version == target {
return nil
}
next, err := e.source.next(version)
if err != nil {
return err
}
version = next
}
}
// going down
version := uint(curVersion)
for int(version) > intTarget {
prev, err := e.source.prev(version)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// we're at the first version, can't go lower
return fmt.Errorf("cannot migrate down from version %d to %d", version, target)
}
return err
}
if err := e.applyDown(version, int(prev)); err != nil {
return err
}
version = prev
}
return nil
}
func (e *engine) force(version int) error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
return e.db.setVersion(version, false)
}
func (e *engine) dropAll() error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
return e.db.drop()
}
// applyUp applies a single up migration for the given version.
func (e *engine) applyUp(version uint) error {
// mark dirty before execution
if err := e.db.setVersion(int(version), true); err != nil {
return err
}
filePath, err := e.source.readUp(version)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// no up file for this version, just set clean
return e.db.setVersion(int(version), false)
}
return err
}
if err := e.db.run(filePath); err != nil {
return err
}
// mark clean after successful execution
return e.db.setVersion(int(version), false)
}
// applyDown applies a single down migration from fromVersion, setting targetVersion afterward.
func (e *engine) applyDown(fromVersion uint, targetVersion int) error {
// mark dirty with target version before execution
if err := e.db.setVersion(targetVersion, true); err != nil {
return err
}
filePath, err := e.source.readDown(fromVersion)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// no down file for this version, just set clean
return e.db.setVersion(targetVersion, false)
}
return err
}
if err := e.db.run(filePath); err != nil {
return err
}
// mark clean after successful execution
return e.db.setVersion(targetVersion, false)
}