up.go

  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}