Short and single-letter names in application code become full words (tests keep theirs). Wrapped calls, signatures and composite literals put one item per line, and one-line function bodies are expanded. Exported names and groups of constants get doc comments, and comments refer to the renamed identifiers.
152 lines
3.8 KiB
Go
152 lines
3.8 KiB
Go
package migrate
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/pressly/goose/v3"
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.avsdev.uk/Go/job-search/internal/cli"
|
|
"git.avsdev.uk/Go/job-search/internal/store"
|
|
)
|
|
|
|
// openMigrator loads the configuration and returns a migrator over the
|
|
// store's embedded migrations for the configured database, opened with the
|
|
// store's connection settings. The caller must close the returned *sql.DB.
|
|
//
|
|
// If the database file doesn't exist, create decides between asking whether
|
|
// to create it (up) and failing (down, status: nothing to work on).
|
|
func openMigrator(
|
|
cmd *cobra.Command, opts *cli.Options, create bool, createConfirmed bool,
|
|
) (*goose.Provider, *sql.DB, error) {
|
|
conf, err := opts.RunSetup(cmd, true, true)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("no configuration loaded (%w)", err)
|
|
}
|
|
|
|
file := conf.Database.File
|
|
if len(file) == 0 {
|
|
return nil, nil, errors.New(
|
|
"no database file specified. Update your configuration",
|
|
)
|
|
}
|
|
|
|
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
|
|
if !create {
|
|
return nil, nil, fmt.Errorf("database file '%s' does not exist", file)
|
|
}
|
|
if !createConfirmed {
|
|
question := fmt.Sprintf(
|
|
"Database file '%s' does not exist. Would you like to create it?", file,
|
|
)
|
|
if !cli.Confirm(question) {
|
|
return nil, nil, errors.New("migrate: no database file")
|
|
}
|
|
}
|
|
} else if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
db, err := store.OpenDB(file)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
migrator, err := store.NewMigrator(db)
|
|
if err != nil {
|
|
return nil, nil, errors.Join(err, db.Close())
|
|
}
|
|
|
|
return migrator, db, nil
|
|
}
|
|
|
|
// closeDB closes db when deferred, adding any error to *err.
|
|
func closeDB(db *sql.DB, err *error) {
|
|
*err = errors.Join(*err, db.Close())
|
|
}
|
|
|
|
// errWriter writes until a write fails, then keeps the error, so a run of
|
|
// prints needs checking only once.
|
|
type errWriter struct {
|
|
w io.Writer
|
|
err error
|
|
}
|
|
|
|
// printf writes like fmt.Fprintf, unless an earlier write failed.
|
|
func (e *errWriter) printf(format string, args ...any) {
|
|
if e.err == nil {
|
|
_, e.err = fmt.Fprintf(e.w, format, args...)
|
|
}
|
|
}
|
|
|
|
// printResults lists migrations that ran, e.g.
|
|
//
|
|
// applied 00003_create_release_tags_table.sql (2ms)
|
|
func printResults(out io.Writer, results []*goose.MigrationResult) error {
|
|
output := &errWriter{
|
|
w: out,
|
|
}
|
|
for _, result := range results {
|
|
verb := "applied"
|
|
if result.Direction == "down" {
|
|
verb = "reverted"
|
|
}
|
|
output.printf(
|
|
" %-9s %s (%s)\n",
|
|
verb,
|
|
filepath.Base(result.Source.Path),
|
|
result.Duration.Round(time.Millisecond),
|
|
)
|
|
}
|
|
return output.err
|
|
}
|
|
|
|
// failure prints what ran before a failed migration and describes the
|
|
// failure. Each migration runs in a transaction, so the failed one has been
|
|
// rolled back and the database is at the last version that succeeded.
|
|
func failure(out io.Writer, err error) error {
|
|
var partialErr *goose.PartialError
|
|
if !errors.As(err, &partialErr) {
|
|
return err
|
|
}
|
|
|
|
printErr := printResults(out, partialErr.Applied)
|
|
|
|
name := "unknown migration"
|
|
if partialErr.Failed != nil && partialErr.Failed.Source != nil {
|
|
name = filepath.Base(partialErr.Failed.Source.Path)
|
|
}
|
|
|
|
return errors.Join(
|
|
fmt.Errorf(
|
|
"migration %s failed and was rolled back; the database is at the last "+
|
|
"version that succeeded.\nMigration error: %w",
|
|
name,
|
|
partialErr.Err,
|
|
),
|
|
printErr,
|
|
)
|
|
}
|
|
|
|
// printVersion reports where the database ended up.
|
|
func printVersion(ctx context.Context, out io.Writer, migrator *goose.Provider) error {
|
|
dbVersion, err := migrator.GetDBVersion(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if dbVersion == 0 {
|
|
_, err = fmt.Fprintln(out, "Database cleared")
|
|
} else {
|
|
_, err = fmt.Fprintf(out, "Database migrated to version %d\n", dbVersion)
|
|
}
|
|
return err
|
|
}
|