Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions down.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func DownContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFun
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
migrations, err := CollectMigrations(dir, sentinelVersion(), maxVersion)
if err != nil {
return err
}
Expand Down Expand Up @@ -53,7 +53,7 @@ func DownToContext(ctx context.Context, db *sql.DB, dir string, version int64, o
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
migrations, err := CollectMigrations(dir, sentinelVersion(), maxVersion)
if err != nil {
return err
}
Expand All @@ -67,7 +67,7 @@ func DownToContext(ctx context.Context, db *sql.DB, dir string, version int64, o
return err
}

if currentVersion == 0 {
if currentVersion == sentinelVersion() {
log.Printf("goose: no migrations to run. current version: %d", currentVersion)
return nil
}
Expand Down
21 changes: 20 additions & 1 deletion globals.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

var (
registeredGoMigrations = make(map[int64]*Migration)
globalAllowZeroVersion bool
)

// ResetGlobalMigrations resets the global Go migrations registry.
Expand All @@ -17,6 +18,24 @@ func ResetGlobalMigrations() {
registeredGoMigrations = make(map[int64]*Migration)
}

// SetAllowZeroVersion sets the global allow zero version flag. When enabled, version 0 is accepted
// in migration filenames and Go migrations registered via [SetGlobalMigrations]. This is useful for
// tools like Drizzle ORM that generate zero-prefixed migrations such as 0000_sticky_sunset_bain.sql.
//
// Not safe for concurrent use.
func SetAllowZeroVersion(b bool) {
globalAllowZeroVersion = b
}

// sentinelVersion returns the version used as the sentinel row in the version table. Normally 0,
// but -1 when [SetAllowZeroVersion] is enabled.
func sentinelVersion() int64 {
if globalAllowZeroVersion {
return -1
}
return 0
}

// SetGlobalMigrations registers Go migrations globally. It returns an error if a migration with the
// same version has already been registered. Go migrations must be constructed using the
// [NewGoMigration] function.
Expand Down Expand Up @@ -45,7 +64,7 @@ func checkGoMigration(m *Migration) error {
if m.Type != TypeGo {
return fmt.Errorf("type must be %q", TypeGo)
}
if m.Version < 1 {
if m.Version < 1 && (!globalAllowZeroVersion || m.Version != 0) {
return errors.New("version must be greater than zero")
}
if m.Source != "" {
Expand Down
3 changes: 2 additions & 1 deletion internal/gooseutil/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ func UpVersions(
dbVersions []int64,
target int64,
allowMissing bool,
sentinelVersion int64,
) ([]int64, error) {
// Sort the list of versions in the filesystem. This should already be sorted, but we do this
// just in case.
slices.Sort(fsysVersions)

// dbAppliedVersions is a map of all applied migrations in the database.
dbAppliedVersions := make(map[int64]bool, len(dbVersions))
var dbMaxVersion int64
dbMaxVersion := sentinelVersion
for _, v := range dbVersions {
dbAppliedVersions[v] = true
if v > dbMaxVersion {
Expand Down
60 changes: 30 additions & 30 deletions internal/gooseutil/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,58 +12,58 @@ func TestResolveVersions(t *testing.T) {
t.Parallel()
t.Run("not_allow_missing", func(t *testing.T) {
// Nothing to apply nil
got, err := UpVersions(nil, nil, math.MaxInt64, false)
got, err := UpVersions(nil, nil, math.MaxInt64, false, 0)
require.NoError(t, err)
require.Empty(t, got)
// Nothing to apply empty
got, err = UpVersions([]int64{}, []int64{}, math.MaxInt64, false)
got, err = UpVersions([]int64{}, []int64{}, math.MaxInt64, false, 0)
require.NoError(t, err)
require.Empty(t, got)

// Nothing new
got, err = UpVersions([]int64{1, 2, 3}, []int64{1, 2, 3}, math.MaxInt64, false)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1, 2, 3}, math.MaxInt64, false, 0)
require.NoError(t, err)
require.Empty(t, got)

// All new
got, err = UpVersions([]int64{1, 2, 3}, []int64{}, math.MaxInt64, false)
got, err = UpVersions([]int64{1, 2, 3}, []int64{}, math.MaxInt64, false, 0)
require.NoError(t, err)
require.Len(t, got, 3)
require.Equal(t, int64(1), got[0])
require.Equal(t, int64(2), got[1])
require.Equal(t, int64(3), got[2])

// Squashed, no new
got, err = UpVersions([]int64{3}, []int64{3}, math.MaxInt64, false)
got, err = UpVersions([]int64{3}, []int64{3}, math.MaxInt64, false, 0)
require.NoError(t, err)
require.Empty(t, got)
// Squashed, 1 new
got, err = UpVersions([]int64{3, 4}, []int64{3}, math.MaxInt64, false)
got, err = UpVersions([]int64{3, 4}, []int64{3}, math.MaxInt64, false, 0)
require.NoError(t, err)
require.Len(t, got, 1)
require.Equal(t, int64(4), got[0])

// Some new with target
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 4, false)
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 4, false, 0)
require.NoError(t, err)
require.Len(t, got, 2)
require.Equal(t, int64(3), got[0])
require.Equal(t, int64(4), got[1]) // up to and including target
// Some new with zero target
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 0, false)
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 0, false, 0)
require.NoError(t, err)
require.Empty(t, got)

// Error: one missing migrations with max target
_, err = UpVersions([]int64{1, 2, 3, 4}, []int64{1 /* 2*/, 3}, math.MaxInt64, false)
_, err = UpVersions([]int64{1, 2, 3, 4}, []int64{1 /* 2*/, 3}, math.MaxInt64, false, 0)
require.Error(t, err)
require.Equal(t,
"detected 1 missing (out-of-order) migration lower than database version (3): version 2",
err.Error(),
)

// Error: multiple missing migrations with max target
_, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{ /* 1 */ 2 /* 3 */, 4, 5}, math.MaxInt64, false)
_, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{ /* 1 */ 2 /* 3 */, 4, 5}, math.MaxInt64, false, 0)
require.Error(t, err)
require.Equal(t,
"detected 2 missing (out-of-order) migrations lower than database version (5): versions 1,3",
Expand All @@ -84,29 +84,29 @@ func TestResolveVersions(t *testing.T) {
// error if there are missing migrations below the target version. This is because the
// user has explicitly requested a target version and we should respect that.

got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 1, false)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 1, false, 0)
require.NoError(t, err)
require.Empty(t, got)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 2, false)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 2, false, 0)
require.Error(t, err)
require.Equal(t,
"detected 1 missing (out-of-order) migration lower than database version (3), with target version (2): version 2",
err.Error(),
)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 3, false)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 3, false, 0)
require.Error(t, err)
require.Equal(t,
"detected 1 missing (out-of-order) migration lower than database version (3), with target version (3): version 2",
err.Error(),
)

_, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 4, false)
_, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 4, false, 0)
require.Error(t, err)
require.Equal(t,
"detected 1 missing (out-of-order) migration lower than database version (6), with target version (4): version 2",
err.Error(),
)
_, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 6, false)
_, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 6, false, 0)
require.Error(t, err)
require.Equal(t,
"detected 2 missing (out-of-order) migrations lower than database version (6), with target version (6): versions 2,5",
Expand All @@ -117,80 +117,80 @@ func TestResolveVersions(t *testing.T) {

t.Run("allow_missing", func(t *testing.T) {
// Nothing to apply nil
got, err := UpVersions(nil, nil, math.MaxInt64, true)
got, err := UpVersions(nil, nil, math.MaxInt64, true, 0)
require.NoError(t, err)
require.Empty(t, got)
// Nothing to apply empty
got, err = UpVersions([]int64{}, []int64{}, math.MaxInt64, true)
got, err = UpVersions([]int64{}, []int64{}, math.MaxInt64, true, 0)
require.NoError(t, err)
require.Empty(t, got)

// Nothing new
got, err = UpVersions([]int64{1, 2, 3}, []int64{1, 2, 3}, math.MaxInt64, true)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1, 2, 3}, math.MaxInt64, true, 0)
require.NoError(t, err)
require.Empty(t, got)

// All new
got, err = UpVersions([]int64{1, 2, 3}, []int64{}, math.MaxInt64, true)
got, err = UpVersions([]int64{1, 2, 3}, []int64{}, math.MaxInt64, true, 0)
require.NoError(t, err)
require.Len(t, got, 3)
require.Equal(t, int64(1), got[0])
require.Equal(t, int64(2), got[1])
require.Equal(t, int64(3), got[2])

// Squashed, no new
got, err = UpVersions([]int64{3}, []int64{3}, math.MaxInt64, true)
got, err = UpVersions([]int64{3}, []int64{3}, math.MaxInt64, true, 0)
require.NoError(t, err)
require.Empty(t, got)
// Squashed, 1 new
got, err = UpVersions([]int64{3, 4}, []int64{3}, math.MaxInt64, true)
got, err = UpVersions([]int64{3, 4}, []int64{3}, math.MaxInt64, true, 0)
require.NoError(t, err)
require.Len(t, got, 1)
require.Equal(t, int64(4), got[0])

// Some new with target
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 4, true)
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 4, true, 0)
require.NoError(t, err)
require.Len(t, got, 2)
require.Equal(t, int64(3), got[0])
require.Equal(t, int64(4), got[1]) // up to and including target
// Some new with zero target
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 0, true)
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 0, true, 0)
require.NoError(t, err)
require.Empty(t, got)

// No error: one missing
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2*/, 3}, math.MaxInt64, true)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2*/, 3}, math.MaxInt64, true, 0)
require.NoError(t, err)
require.Len(t, got, 1)
require.Equal(t, int64(2), got[0]) // missing

// No error: multiple missing and new with max target
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{ /* 1 */ 2 /* 3 */, 4}, math.MaxInt64, true)
got, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{ /* 1 */ 2 /* 3 */, 4}, math.MaxInt64, true, 0)
require.NoError(t, err)
require.Len(t, got, 3)
require.Equal(t, int64(1), got[0]) // missing
require.Equal(t, int64(3), got[1]) // missing
require.Equal(t, int64(5), got[2])

t.Run("target_lower_than_max", func(t *testing.T) {
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 1, true)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 1, true, 0)
require.NoError(t, err)
require.Empty(t, got)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 2, true)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 2, true, 0)
require.NoError(t, err)
require.Len(t, got, 1)
require.Equal(t, int64(2), got[0]) // missing
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 3, true)
got, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 3, true, 0)
require.NoError(t, err)
require.Len(t, got, 1)
require.Equal(t, int64(2), got[0]) // missing

got, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 4, true)
got, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 4, true, 0)
require.NoError(t, err)
require.Len(t, got, 1)
require.Equal(t, int64(2), got[0]) // missing
got, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 6, true)
got, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 6, true, 0)
require.NoError(t, err)
require.Len(t, got, 2)
require.Equal(t, int64(2), got[0]) // missing
Expand Down
6 changes: 3 additions & 3 deletions migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func EnsureDBVersionContext(ctx context.Context, db *sql.DB) (int64, error) {
if createErr != nil {
return 0, multierr.Append(err, createErr)
}
return 0, nil
return sentinelVersion(), nil
}
// The most recent record for each migration specifies
// whether it has been applied or rolled back.
Expand Down Expand Up @@ -251,7 +251,7 @@ func EnsureDBVersionContext(ctx context.Context, db *sql.DB) (int64, error) {
}

// createVersionTable creates the db version table and inserts the
// initial 0 value into it.
// initial sentinel value into it.
func createVersionTable(ctx context.Context, db *sql.DB) error {
txn, err := db.BeginTx(ctx, nil)
if err != nil {
Expand All @@ -261,7 +261,7 @@ func createVersionTable(ctx context.Context, db *sql.DB) error {
_ = txn.Rollback()
return err
}
if err := store.InsertVersion(ctx, txn, TableName(), 0); err != nil {
if err := store.InsertVersion(ctx, txn, TableName(), sentinelVersion()); err != nil {
_ = txn.Rollback()
return err
}
Expand Down
7 changes: 6 additions & 1 deletion migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ func insertOrDeleteVersionNoTx(ctx context.Context, db *sql.DB, version int64, d
//
// XXX_descriptivename.ext where XXX specifies the version number and ext specifies the type of
// migration, either .sql or .go.
//
// By default, version must be greater than zero. Use [SetAllowZeroVersion] to accept version 0.
func NumericComponent(filename string) (int64, error) {
base := filepath.Base(filename)
if ext := filepath.Ext(base); ext != ".go" && ext != ".sql" {
Expand All @@ -373,7 +375,10 @@ func NumericComponent(filename string) (int64, error) {
if err != nil {
return 0, fmt.Errorf("failed to parse version from migration file: %s: %w", base, err)
}
if n < 1 {
if n < 0 {
return 0, errors.New("migration version must be a non-negative number")
}
if n == 0 && !globalAllowZeroVersion {
return 0, errors.New("migration version must be greater than zero")
}
return n, nil
Expand Down
Loading
Loading