320 lines
7.7 KiB
Go
320 lines
7.7 KiB
Go
|
package goose
|
||
|
|
||
|
import (
|
||
|
"database/sql"
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"sort"
|
||
|
"time"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// ErrNoCurrentVersion when a current migration version is not found.
|
||
|
ErrNoCurrentVersion = errors.New("no current version found")
|
||
|
// ErrNoNextVersion when the next migration version is not found.
|
||
|
ErrNoNextVersion = errors.New("no next version found")
|
||
|
// MaxVersion is the maximum allowed version.
|
||
|
MaxVersion int64 = 9223372036854775807 // max(int64)
|
||
|
|
||
|
registeredGoMigrations = map[int64]*Migration{}
|
||
|
)
|
||
|
|
||
|
// Migrations slice.
|
||
|
type Migrations []*Migration
|
||
|
|
||
|
// helpers so we can use pkg sort
|
||
|
func (ms Migrations) Len() int { return len(ms) }
|
||
|
func (ms Migrations) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }
|
||
|
func (ms Migrations) Less(i, j int) bool {
|
||
|
if ms[i].Version == ms[j].Version {
|
||
|
panic(fmt.Sprintf("goose: duplicate version %v detected:\n%v\n%v", ms[i].Version, ms[i].Source, ms[j].Source))
|
||
|
}
|
||
|
return ms[i].Version < ms[j].Version
|
||
|
}
|
||
|
|
||
|
// Current gets the current migration.
|
||
|
func (ms Migrations) Current(current int64) (*Migration, error) {
|
||
|
for i, migration := range ms {
|
||
|
if migration.Version == current {
|
||
|
return ms[i], nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil, ErrNoCurrentVersion
|
||
|
}
|
||
|
|
||
|
// Next gets the next migration.
|
||
|
func (ms Migrations) Next(current int64) (*Migration, error) {
|
||
|
for i, migration := range ms {
|
||
|
if migration.Version > current {
|
||
|
return ms[i], nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil, ErrNoNextVersion
|
||
|
}
|
||
|
|
||
|
// Previous : Get the previous migration.
|
||
|
func (ms Migrations) Previous(current int64) (*Migration, error) {
|
||
|
for i := len(ms) - 1; i >= 0; i-- {
|
||
|
if ms[i].Version < current {
|
||
|
return ms[i], nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil, ErrNoNextVersion
|
||
|
}
|
||
|
|
||
|
// Last gets the last migration.
|
||
|
func (ms Migrations) Last() (*Migration, error) {
|
||
|
if len(ms) == 0 {
|
||
|
return nil, ErrNoNextVersion
|
||
|
}
|
||
|
|
||
|
return ms[len(ms)-1], nil
|
||
|
}
|
||
|
|
||
|
// Versioned gets versioned migrations.
|
||
|
func (ms Migrations) versioned() (Migrations, error) {
|
||
|
var migrations Migrations
|
||
|
|
||
|
// assume that the user will never have more than 19700101000000 migrations
|
||
|
for _, m := range ms {
|
||
|
// parse version as timestmap
|
||
|
versionTime, err := time.Parse(timestampFormat, fmt.Sprintf("%d", m.Version))
|
||
|
|
||
|
if versionTime.Before(time.Unix(0, 0)) || err != nil {
|
||
|
migrations = append(migrations, m)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return migrations, nil
|
||
|
}
|
||
|
|
||
|
// Timestamped gets the timestamped migrations.
|
||
|
func (ms Migrations) timestamped() (Migrations, error) {
|
||
|
var migrations Migrations
|
||
|
|
||
|
// assume that the user will never have more than 19700101000000 migrations
|
||
|
for _, m := range ms {
|
||
|
// parse version as timestmap
|
||
|
versionTime, err := time.Parse(timestampFormat, fmt.Sprintf("%d", m.Version))
|
||
|
if err != nil {
|
||
|
// probably not a timestamp
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if versionTime.After(time.Unix(0, 0)) {
|
||
|
migrations = append(migrations, m)
|
||
|
}
|
||
|
}
|
||
|
return migrations, nil
|
||
|
}
|
||
|
|
||
|
func (ms Migrations) String() string {
|
||
|
str := ""
|
||
|
for _, m := range ms {
|
||
|
str += fmt.Sprintln(m)
|
||
|
}
|
||
|
return str
|
||
|
}
|
||
|
|
||
|
// AddMigration adds a migration.
|
||
|
func AddMigration(up func(*sql.Tx) error, down func(*sql.Tx) error) {
|
||
|
_, filename, _, _ := runtime.Caller(1)
|
||
|
AddNamedMigration(filename, up, down)
|
||
|
}
|
||
|
|
||
|
// AddNamedMigration : Add a named migration.
|
||
|
func AddNamedMigration(filename string, up func(*sql.Tx) error, down func(*sql.Tx) error) {
|
||
|
v, _ := NumericComponent(filename)
|
||
|
migration := &Migration{Version: v, Next: -1, Previous: -1, Registered: true, UpFn: up, DownFn: down, Source: filename}
|
||
|
|
||
|
if existing, ok := registeredGoMigrations[v]; ok {
|
||
|
panic(fmt.Sprintf("failed to add migration %q: version conflicts with %q", filename, existing.Source))
|
||
|
}
|
||
|
|
||
|
registeredGoMigrations[v] = migration
|
||
|
}
|
||
|
|
||
|
// CollectMigrations returns all the valid looking migration scripts in the
|
||
|
// migrations folder and go func registry, and key them by version.
|
||
|
func CollectMigrations(dirpath string, current, target int64) (Migrations, error) {
|
||
|
if _, err := os.Stat(dirpath); os.IsNotExist(err) {
|
||
|
return nil, fmt.Errorf("%s directory does not exists", dirpath)
|
||
|
}
|
||
|
|
||
|
var migrations Migrations
|
||
|
|
||
|
// SQL migration files.
|
||
|
sqlMigrationFiles, err := filepath.Glob(dirpath + "/**.sql")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
for _, file := range sqlMigrationFiles {
|
||
|
v, err := NumericComponent(file)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if versionFilter(v, current, target) {
|
||
|
migration := &Migration{Version: v, Next: -1, Previous: -1, Source: file}
|
||
|
migrations = append(migrations, migration)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Go migrations registered via goose.AddMigration().
|
||
|
for _, migration := range registeredGoMigrations {
|
||
|
v, err := NumericComponent(migration.Source)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if versionFilter(v, current, target) {
|
||
|
migrations = append(migrations, migration)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Go migration files
|
||
|
goMigrationFiles, err := filepath.Glob(dirpath + "/**.go")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
for _, file := range goMigrationFiles {
|
||
|
v, err := NumericComponent(file)
|
||
|
if err != nil {
|
||
|
continue // Skip any files that don't have version prefix.
|
||
|
}
|
||
|
|
||
|
// Skip migrations already existing migrations registered via goose.AddMigration().
|
||
|
if _, ok := registeredGoMigrations[v]; ok {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if versionFilter(v, current, target) {
|
||
|
migration := &Migration{Version: v, Next: -1, Previous: -1, Source: file, Registered: false}
|
||
|
migrations = append(migrations, migration)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
migrations = sortAndConnectMigrations(migrations)
|
||
|
|
||
|
return migrations, nil
|
||
|
}
|
||
|
|
||
|
func sortAndConnectMigrations(migrations Migrations) Migrations {
|
||
|
sort.Sort(migrations)
|
||
|
|
||
|
// now that we're sorted in the appropriate direction,
|
||
|
// populate next and previous for each migration
|
||
|
for i, m := range migrations {
|
||
|
prev := int64(-1)
|
||
|
if i > 0 {
|
||
|
prev = migrations[i-1].Version
|
||
|
migrations[i-1].Next = m.Version
|
||
|
}
|
||
|
migrations[i].Previous = prev
|
||
|
}
|
||
|
|
||
|
return migrations
|
||
|
}
|
||
|
|
||
|
func versionFilter(v, current, target int64) bool {
|
||
|
|
||
|
if target > current {
|
||
|
return v > current && v <= target
|
||
|
}
|
||
|
|
||
|
if target < current {
|
||
|
return v <= current && v > target
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// EnsureDBVersion retrieves the current version for this DB.
|
||
|
// Create and initialize the DB version table if it doesn't exist.
|
||
|
func EnsureDBVersion(db *sql.DB) (int64, error) {
|
||
|
rows, err := GetDialect().dbVersionQuery(db)
|
||
|
if err != nil {
|
||
|
return 0, createVersionTable(db)
|
||
|
}
|
||
|
defer rows.Close()
|
||
|
|
||
|
// The most recent record for each migration specifies
|
||
|
// whether it has been applied or rolled back.
|
||
|
// The first version we find that has been applied is the current version.
|
||
|
|
||
|
toSkip := make([]int64, 0)
|
||
|
|
||
|
for rows.Next() {
|
||
|
var row MigrationRecord
|
||
|
if err = rows.Scan(&row.VersionID, &row.IsApplied); err != nil {
|
||
|
return 0, errors.Wrap(err, "failed to scan row")
|
||
|
}
|
||
|
|
||
|
// have we already marked this version to be skipped?
|
||
|
skip := false
|
||
|
for _, v := range toSkip {
|
||
|
if v == row.VersionID {
|
||
|
skip = true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if skip {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// if version has been applied we're done
|
||
|
if row.IsApplied {
|
||
|
return row.VersionID, nil
|
||
|
}
|
||
|
|
||
|
// latest version of migration has not been applied.
|
||
|
toSkip = append(toSkip, row.VersionID)
|
||
|
}
|
||
|
if err := rows.Err(); err != nil {
|
||
|
return 0, errors.Wrap(err, "failed to get next row")
|
||
|
}
|
||
|
|
||
|
return 0, ErrNoNextVersion
|
||
|
}
|
||
|
|
||
|
// Create the db version table
|
||
|
// and insert the initial 0 value into it
|
||
|
func createVersionTable(db *sql.DB) error {
|
||
|
txn, err := db.Begin()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
d := GetDialect()
|
||
|
|
||
|
if _, err := txn.Exec(d.createVersionTableSQL()); err != nil {
|
||
|
txn.Rollback()
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
version := 0
|
||
|
applied := true
|
||
|
if _, err := txn.Exec(d.insertVersionSQL(), version, applied); err != nil {
|
||
|
txn.Rollback()
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return txn.Commit()
|
||
|
}
|
||
|
|
||
|
// GetDBVersion is an alias for EnsureDBVersion, but returns -1 in error.
|
||
|
func GetDBVersion(db *sql.DB) (int64, error) {
|
||
|
version, err := EnsureDBVersion(db)
|
||
|
if err != nil {
|
||
|
return -1, err
|
||
|
}
|
||
|
|
||
|
return version, nil
|
||
|
}
|