resolve.go

  1// Package gooseutil provides utility functions we want to keep internal to the package. It's
  2// intended to be a collection of well-tested helper functions.
  3package gooseutil
  4
  5import (
  6	"fmt"
  7	"math"
  8	"sort"
  9	"strconv"
 10	"strings"
 11)
 12
 13// UpVersions returns a list of migrations to apply based on the versions in the filesystem and the
 14// versions in the database. The target version can be used to specify a target version. In most
 15// cases this will be math.MaxInt64.
 16//
 17// The allowMissing flag can be used to allow missing migrations as part of the list of migrations
 18// to apply. Otherwise, an error will be returned if there are missing migrations in the database.
 19func UpVersions(
 20	fsysVersions []int64,
 21	dbVersions []int64,
 22	target int64,
 23	allowMissing bool,
 24) ([]int64, error) {
 25	// Sort the list of versions in the filesystem. This should already be sorted, but we do this
 26	// just in case.
 27	sortAscending(fsysVersions)
 28
 29	// dbAppliedVersions is a map of all applied migrations in the database.
 30	dbAppliedVersions := make(map[int64]bool, len(dbVersions))
 31	var dbMaxVersion int64
 32	for _, v := range dbVersions {
 33		dbAppliedVersions[v] = true
 34		if v > dbMaxVersion {
 35			dbMaxVersion = v
 36		}
 37	}
 38
 39	// Get a list of migrations that are missing from the database. A missing migration is one that
 40	// has a version less than the max version in the database and has not been applied.
 41	//
 42	// In most cases the target version is math.MaxInt64, but it can be used to specify a target
 43	// version. In which case we respect the target version and only surface migrations up to and
 44	// including that target.
 45	var missing []int64
 46	for _, v := range fsysVersions {
 47		if dbAppliedVersions[v] {
 48			continue
 49		}
 50		if v < dbMaxVersion && v <= target {
 51			missing = append(missing, v)
 52		}
 53	}
 54
 55	// feat(mf): It is very possible someone may want to apply ONLY new migrations and skip missing
 56	// migrations entirely. At the moment this is not supported, but leaving this comment because
 57	// that's where that logic would be handled.
 58	//
 59	// For example, if database has 1,4 already applied and 2,3,5 are new, we would apply only 5 and
 60	// skip 2,3. Not sure if this is a common use case, but it's possible someone may want to do
 61	// this.
 62	if len(missing) > 0 && !allowMissing {
 63		return nil, newMissingError(missing, dbMaxVersion, target)
 64	}
 65
 66	var out []int64
 67
 68	// 1. Add missing migrations to the list of migrations to apply, if any.
 69	out = append(out, missing...)
 70
 71	// 2. Add new migrations to the list of migrations to apply, if any.
 72	for _, v := range fsysVersions {
 73		if dbAppliedVersions[v] {
 74			continue
 75		}
 76		if v > dbMaxVersion && v <= target {
 77			out = append(out, v)
 78		}
 79	}
 80	// 3. Sort the list of migrations to apply.
 81	sortAscending(out)
 82
 83	return out, nil
 84}
 85
 86func newMissingError(
 87	missing []int64,
 88	dbMaxVersion int64,
 89	target int64,
 90) error {
 91	sortAscending(missing)
 92
 93	collected := make([]string, 0, len(missing))
 94	for _, v := range missing {
 95		collected = append(collected, strconv.FormatInt(v, 10))
 96	}
 97
 98	msg := "migration"
 99	if len(collected) > 1 {
100		msg += "s"
101	}
102
103	var versionsMsg string
104	if len(collected) > 1 {
105		versionsMsg = "versions " + strings.Join(collected, ",")
106	} else {
107		versionsMsg = "version " + collected[0]
108	}
109
110	desiredMsg := fmt.Sprintf("database version (%d)", dbMaxVersion)
111	if target != math.MaxInt64 {
112		desiredMsg += fmt.Sprintf(", with target version (%d)", target)
113	}
114
115	return fmt.Errorf("detected %d missing (out-of-order) %s lower than %s: %s",
116		len(missing), msg, desiredMsg, versionsMsg,
117	)
118}
119
120func sortAscending(versions []int64) {
121	sort.Slice(versions, func(i, j int) bool {
122		return versions[i] < versions[j]
123	})
124}