140 lines
3.6 KiB
Go
140 lines
3.6 KiB
Go
package migrate
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.avsdev.uk/Go/job-search/internal/cli"
|
|
"git.avsdev.uk/Go/job-search/internal/store"
|
|
"github.com/pressly/goose/v3"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
m, err := store.NewMigrator(db)
|
|
if err != nil {
|
|
return nil, nil, errors.Join(err, db.Close())
|
|
}
|
|
|
|
return m, 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, a ...any) {
|
|
if e.err == nil {
|
|
_, e.err = fmt.Fprintf(e.w, format, a...)
|
|
}
|
|
}
|
|
|
|
// printResults lists migrations that ran, e.g.
|
|
//
|
|
// applied 00003_create_release_tags_table.sql (2ms)
|
|
func printResults(w io.Writer, results []*goose.MigrationResult) error {
|
|
ew := &errWriter{w: w}
|
|
for _, r := range results {
|
|
verb := "applied"
|
|
if r.Direction == "down" {
|
|
verb = "reverted"
|
|
}
|
|
ew.printf(" %-9s %s (%s)\n",
|
|
verb, filepath.Base(r.Source.Path), r.Duration.Round(time.Millisecond))
|
|
}
|
|
return ew.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(w io.Writer, err error) error {
|
|
var pe *goose.PartialError
|
|
if !errors.As(err, &pe) {
|
|
return err
|
|
}
|
|
|
|
printErr := printResults(w, pe.Applied)
|
|
|
|
name := "unknown migration"
|
|
if pe.Failed != nil && pe.Failed.Source != nil {
|
|
name = filepath.Base(pe.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, pe.Err), printErr)
|
|
}
|
|
|
|
// printVersion reports where the database ended up.
|
|
func printVersion(ctx context.Context, w io.Writer, m *goose.Provider) error {
|
|
v, err := m.GetDBVersion(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v == 0 {
|
|
_, err = fmt.Fprintln(w, "Database cleared")
|
|
} else {
|
|
_, err = fmt.Fprintf(w, "Database migrated to version %d\n", v)
|
|
}
|
|
return err
|
|
}
|