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) }