261 lines
5.1 KiB
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)
|
|
}
|