gci is configured with the sections standard, default and localmodule, and goimports with the module path as its local prefix, so both formatters agree.
141 lines
3.6 KiB
Go
141 lines
3.6 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
|
|
}
|
|
|
|
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
|
|
}
|