-
Notifications
You must be signed in to change notification settings - Fork 875
feat: Add reset-password command #1380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
b080572
18de0d2
bfa835d
907f457
f6f2310
cb36912
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,38 +4,40 @@ import ( | |
"database/sql" | ||
"embed" | ||
"errors" | ||
"os" | ||
|
||
"github.com/golang-migrate/migrate/v4" | ||
"github.com/golang-migrate/migrate/v4/database/postgres" | ||
"github.com/golang-migrate/migrate/v4/source" | ||
"github.com/golang-migrate/migrate/v4/source/iofs" | ||
"golang.org/x/xerrors" | ||
) | ||
|
||
//go:embed migrations/*.sql | ||
var migrations embed.FS | ||
|
||
func migrateSetup(db *sql.DB) (*migrate.Migrate, error) { | ||
func migrateSetup(db *sql.DB) (source.Driver, *migrate.Migrate, error) { | ||
sourceDriver, err := iofs.New(migrations, "migrations") | ||
if err != nil { | ||
return nil, xerrors.Errorf("create iofs: %w", err) | ||
return nil, nil, xerrors.Errorf("create iofs: %w", err) | ||
} | ||
|
||
dbDriver, err := postgres.WithInstance(db, &postgres.Config{}) | ||
if err != nil { | ||
return nil, xerrors.Errorf("wrap postgres connection: %w", err) | ||
return nil, nil, xerrors.Errorf("wrap postgres connection: %w", err) | ||
} | ||
|
||
m, err := migrate.NewWithInstance("", sourceDriver, "", dbDriver) | ||
if err != nil { | ||
return nil, xerrors.Errorf("new migrate instance: %w", err) | ||
return nil, nil, xerrors.Errorf("new migrate instance: %w", err) | ||
} | ||
|
||
return m, nil | ||
return sourceDriver, m, nil | ||
} | ||
|
||
// MigrateUp runs SQL migrations to ensure the database schema is up-to-date. | ||
func MigrateUp(db *sql.DB) error { | ||
m, err := migrateSetup(db) | ||
_, m, err := migrateSetup(db) | ||
if err != nil { | ||
return xerrors.Errorf("migrate setup: %w", err) | ||
} | ||
|
@@ -55,7 +57,7 @@ func MigrateUp(db *sql.DB) error { | |
|
||
// MigrateDown runs all down SQL migrations. | ||
func MigrateDown(db *sql.DB) error { | ||
m, err := migrateSetup(db) | ||
_, m, err := migrateSetup(db) | ||
if err != nil { | ||
return xerrors.Errorf("migrate setup: %w", err) | ||
} | ||
|
@@ -72,3 +74,68 @@ func MigrateDown(db *sql.DB) error { | |
|
||
return nil | ||
} | ||
|
||
// EnsureClean checks whether all migrations for the current version have been | ||
// applied, without making any changes to the database. If not, returns a | ||
// non-nil error. | ||
func EnsureClean(db *sql.DB) error { | ||
sourceDriver, m, err := migrateSetup(db) | ||
if err != nil { | ||
return xerrors.Errorf("migrate setup: %w", err) | ||
} | ||
|
||
version, dirty, err := m.Version() | ||
if err != nil { | ||
return xerrors.Errorf("get migration version: %w", err) | ||
} | ||
|
||
if dirty { | ||
return xerrors.Errorf("database has not been cleanly migrated") | ||
} | ||
|
||
// Verify that the database's migration version is "current" by checking | ||
// that a migration with that version exists, but there is no next version. | ||
err = CheckLatestVersion(sourceDriver, version) | ||
if err != nil { | ||
return xerrors.Errorf("database needs migration: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Returns nil if currentVersion corresponds to the latest available migration, | ||
// otherwise an error explaining why not. | ||
func CheckLatestVersion(sourceDriver source.Driver, currentVersion uint) error { | ||
Comment on lines
+106
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only thing I'm significantly unhappy about with this commit. I feel like this ought to be a private function, but that doesn't play nicely with the way we put tests in a separate package. The alternative would be to test the |
||
// This is ugly, but seems like the only way to do it with the public | ||
// interfaces provided by golang-migrate. | ||
|
||
// Check that there is no later version | ||
nextVersion, err := sourceDriver.Next(currentVersion) | ||
if err == nil { | ||
return xerrors.Errorf("current version is %d, but later version %d exists", currentVersion, nextVersion) | ||
} | ||
if !errors.Is(err, os.ErrNotExist) { | ||
return xerrors.Errorf("get next migration after %d: %w", currentVersion, err) | ||
} | ||
|
||
// Once we reach this point, we know that either currentVersion doesn't | ||
// exist, or it has no successor (the return value from | ||
// sourceDriver.Next() is the same in either case). So we need to check | ||
// that either it's the first version, or it has a predecessor. | ||
|
||
firstVersion, err := sourceDriver.First() | ||
if err != nil { | ||
// the total number of migrations should be non-zero, so this must be | ||
// an actual error, not just a missing file | ||
return xerrors.Errorf("get first migration: %w", err) | ||
} | ||
if firstVersion == currentVersion { | ||
return nil | ||
} | ||
|
||
_, err = sourceDriver.Prev(currentVersion) | ||
if err != nil { | ||
return xerrors.Errorf("get previous migration: %w", err) | ||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it helps, but if you want to test 99% of this function, you could write this as e.g.
func EnsureClean(db *sql.DB) error { return ensureClean(db, migrateSetup); }
.This would let you swap out the
migrateSetup
function in your test, if that helps with the global state (considering your comment here #1380 (comment)). It would still require an internal test package but I think that's fine for this use-case.