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}