1package goose
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "sort"
8 "strings"
9)
10
11type options struct {
12 allowMissing bool
13 applyUpByOne bool
14 noVersioning bool
15}
16
17type OptionsFunc func(o *options)
18
19func WithAllowMissing() OptionsFunc {
20 return func(o *options) { o.allowMissing = true }
21}
22
23func WithNoVersioning() OptionsFunc {
24 return func(o *options) { o.noVersioning = true }
25}
26
27func WithNoColor(b bool) OptionsFunc {
28 return func(o *options) { noColor = b }
29}
30
31func withApplyUpByOne() OptionsFunc {
32 return func(o *options) { o.applyUpByOne = true }
33}
34
35// UpTo migrates up to a specific version.
36func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
37 ctx := context.Background()
38 return UpToContext(ctx, db, dir, version, opts...)
39}
40
41func UpToContext(ctx context.Context, db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
42 option := &options{}
43 for _, f := range opts {
44 f(option)
45 }
46 foundMigrations, err := CollectMigrations(dir, minVersion, version)
47 if err != nil {
48 return err
49 }
50
51 if option.noVersioning {
52 if len(foundMigrations) == 0 {
53 return nil
54 }
55 if option.applyUpByOne {
56 // For up-by-one this means keep re-applying the first
57 // migration over and over.
58 version = foundMigrations[0].Version
59 }
60 return upToNoVersioning(ctx, db, foundMigrations, version)
61 }
62
63 if _, err := EnsureDBVersionContext(ctx, db); err != nil {
64 return err
65 }
66 dbMigrations, err := listAllDBVersions(ctx, db)
67 if err != nil {
68 return err
69 }
70 dbMaxVersion := dbMigrations[len(dbMigrations)-1].Version
71 // lookupAppliedInDB is a map of all applied migrations in the database.
72 lookupAppliedInDB := make(map[int64]bool)
73 for _, m := range dbMigrations {
74 lookupAppliedInDB[m.Version] = true
75 }
76
77 missingMigrations := findMissingMigrations(dbMigrations, foundMigrations, dbMaxVersion)
78
79 // feature(mf): It is very possible someone may want to apply ONLY new migrations
80 // and skip missing migrations altogether. At the moment this is not supported,
81 // but leaving this comment because that's where that logic will be handled.
82 if len(missingMigrations) > 0 && !option.allowMissing {
83 var collected []string
84 for _, m := range missingMigrations {
85 output := fmt.Sprintf("version %d: %s", m.Version, m.Source)
86 collected = append(collected, output)
87 }
88 return fmt.Errorf("error: found %d missing migrations before current version %d:\n\t%s",
89 len(missingMigrations), dbMaxVersion, strings.Join(collected, "\n\t"))
90 }
91 var migrationsToApply Migrations
92 if option.allowMissing {
93 migrationsToApply = missingMigrations
94 }
95 // filter all migrations with a version greater than the supplied version (min) and less than or
96 // equal to the requested version (max). Note, we do not need to filter out missing migrations
97 // because we are only appending "new" migrations that have a higher version than the current
98 // database max version, which inevitably means they are not "missing".
99 for _, m := range foundMigrations {
100 if lookupAppliedInDB[m.Version] {
101 continue
102 }
103 if m.Version > dbMaxVersion && m.Version <= version {
104 migrationsToApply = append(migrationsToApply, m)
105 }
106 }
107
108 var current int64
109 for _, m := range migrationsToApply {
110 if err := m.UpContext(ctx, db); err != nil {
111 return err
112 }
113 if option.applyUpByOne {
114 return nil
115 }
116 current = m.Version
117 }
118
119 if len(migrationsToApply) == 0 {
120 current, err = GetDBVersionContext(ctx, db)
121 if err != nil {
122 return err
123 }
124
125 log.Printf("goose: no migrations to run. current version: %d", current)
126 } else {
127 log.Printf("goose: successfully migrated database to version: %d", current)
128 }
129
130 // At this point there are no more migrations to apply. But we need to maintain
131 // the following behaviour:
132 // UpByOne returns an error to signifying there are no more migrations.
133 // Up and UpTo return nil
134
135 if option.applyUpByOne {
136 return ErrNoNextVersion
137 }
138
139 return nil
140}
141
142// upToNoVersioning applies up migrations up to, and including, the
143// target version.
144func upToNoVersioning(ctx context.Context, db *sql.DB, migrations Migrations, version int64) error {
145 var finalVersion int64
146 for _, current := range migrations {
147 if current.Version > version {
148 break
149 }
150 current.noVersioning = true
151 if err := current.UpContext(ctx, db); err != nil {
152 return err
153 }
154 finalVersion = current.Version
155 }
156 log.Printf("goose: up to current file version: %d", finalVersion)
157 return nil
158}
159
160// Up applies all available migrations.
161func Up(db *sql.DB, dir string, opts ...OptionsFunc) error {
162 ctx := context.Background()
163 return UpContext(ctx, db, dir, opts...)
164}
165
166// UpContext applies all available migrations.
167func UpContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
168 return UpToContext(ctx, db, dir, maxVersion, opts...)
169}
170
171// UpByOne migrates up by a single version.
172func UpByOne(db *sql.DB, dir string, opts ...OptionsFunc) error {
173 ctx := context.Background()
174 return UpByOneContext(ctx, db, dir, opts...)
175}
176
177// UpByOneContext migrates up by a single version.
178func UpByOneContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
179 opts = append(opts, withApplyUpByOne())
180 return UpToContext(ctx, db, dir, maxVersion, opts...)
181}
182
183// listAllDBVersions returns a list of all migrations, ordered ascending.
184func listAllDBVersions(ctx context.Context, db *sql.DB) (Migrations, error) {
185 dbMigrations, err := store.ListMigrations(ctx, db, TableName())
186 if err != nil {
187 return nil, err
188 }
189 all := make(Migrations, 0, len(dbMigrations))
190 for _, m := range dbMigrations {
191 all = append(all, &Migration{
192 Version: m.VersionID,
193 })
194 }
195 // ListMigrations returns migrations in descending order by id.
196 // But we want to return them in ascending order by version_id, so we re-sort.
197 sort.SliceStable(all, func(i, j int) bool {
198 return all[i].Version < all[j].Version
199 })
200 return all, nil
201}
202
203// findMissingMigrations migrations returns all missing migrations.
204// A migrations is considered missing if it has a version less than the
205// current known max version.
206func findMissingMigrations(knownMigrations, newMigrations Migrations, dbMaxVersion int64) Migrations {
207 existing := make(map[int64]bool)
208 for _, known := range knownMigrations {
209 existing[known.Version] = true
210 }
211 var missing Migrations
212 for _, new := range newMigrations {
213 if !existing[new.Version] && new.Version < dbMaxVersion {
214 missing = append(missing, new)
215 }
216 }
217 sort.SliceStable(missing, func(i, j int) bool {
218 return missing[i].Version < missing[j].Version
219 })
220 return missing
221}