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.
211 lines
5.1 KiB
Go
211 lines
5.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
|
|
"github.com/pressly/goose/v3"
|
|
)
|
|
|
|
// Each file holds both directions, marked with -- +goose Up / -- +goose Down.
|
|
// Create new ones with: goose -dir internal/store/migrations create NAME sql
|
|
//
|
|
//go:embed migrations/*.sql
|
|
var migrationsFS embed.FS
|
|
|
|
// ErrNeedsMigration is returned by Open when the database has pending
|
|
// migrations. Apply them with the migrate command, or pass
|
|
// WithAutoMigrate.
|
|
var ErrNeedsMigration = errors.New("store: database needs migrating")
|
|
|
|
// ErrSchemaTooNew is returned by Open when the database has migrations
|
|
// this build doesn't know about, i.e. it was migrated by a newer build.
|
|
var ErrSchemaTooNew = errors.New("store: database schema is newer than this build")
|
|
|
|
// ErrWouldEmptyDatabase is returned by StepDown when the migration would drop
|
|
// all of the tables and thus empty the database.
|
|
var ErrWouldEmptyDatabase = errors.New(
|
|
"migration cancelled; database would be emptied by undoing all migrations",
|
|
)
|
|
|
|
// NewMigrator returns a goose Provider over the store's embedded migrations,
|
|
// for applying, rolling back or inspecting them. Each migration runs in its
|
|
// own transaction, so a failed migration leaves the database at the previous
|
|
// version (goose has no "dirty" state).
|
|
func NewMigrator(db *sql.DB) (*goose.Provider, error) {
|
|
fsys, err := fs.Sub(migrationsFS, "migrations")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return goose.NewProvider(goose.DialectSQLite3, db, fsys)
|
|
}
|
|
|
|
// LatestVersion is the highest migration version in this build.
|
|
func LatestVersion(provider *goose.Provider) int64 {
|
|
sources := provider.ListSources() // sorted ascending
|
|
if len(sources) == 0 {
|
|
return 0
|
|
}
|
|
return sources[len(sources)-1].Version
|
|
}
|
|
|
|
// checkVersion returns ErrNeedsMigration or ErrSchemaTooNew unless the
|
|
// database is exactly at this build's schema.
|
|
func checkVersion(ctx context.Context, provider *goose.Provider) error {
|
|
current, err := provider.GetDBVersion(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
latest := LatestVersion(provider)
|
|
if current > latest {
|
|
return fmt.Errorf(
|
|
"%w (database is at version %d, this build knows up to %d)",
|
|
ErrSchemaTooNew,
|
|
current,
|
|
latest,
|
|
)
|
|
}
|
|
|
|
pending, err := provider.HasPending(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pending {
|
|
return fmt.Errorf(
|
|
"%w (database is at version %d, latest is %d)",
|
|
ErrNeedsMigration,
|
|
current,
|
|
latest,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkNotNewer refuses to work on a database migrated by a newer build:
|
|
// this build has neither the newer migrations nor their down steps.
|
|
func checkNotNewer(ctx context.Context, provider *goose.Provider) error {
|
|
current, err := provider.GetDBVersion(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if latest := LatestVersion(provider); current > latest {
|
|
return fmt.Errorf(
|
|
"database is at version %d but this build only knows up to %d; use a newer build",
|
|
current,
|
|
latest,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// prepareSchema migrates (with WithAutoMigrate) or verifies the schema
|
|
// version.
|
|
func prepareSchema(ctx context.Context, db *sql.DB, opts options) error {
|
|
provider, err := NewMigrator(db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.autoMigrate {
|
|
if _, err := provider.Up(ctx); err != nil {
|
|
return fmt.Errorf("store: migrating database: %w", err)
|
|
}
|
|
}
|
|
|
|
return checkVersion(ctx, provider)
|
|
}
|
|
|
|
// StepUp applies steps pending migrations, or all of them when steps is 0.
|
|
func StepUp(
|
|
ctx context.Context, provider *goose.Provider, steps int,
|
|
) ([]*goose.MigrationResult, error) {
|
|
var results []*goose.MigrationResult
|
|
|
|
if err := checkNotNewer(ctx, provider); err != nil {
|
|
return results, err
|
|
}
|
|
|
|
var err error
|
|
if steps == 0 {
|
|
results, err = provider.Up(ctx)
|
|
} else {
|
|
for range steps {
|
|
var result *goose.MigrationResult
|
|
result, err = provider.UpByOne(ctx)
|
|
if errors.Is(err, goose.ErrNoNextVersion) {
|
|
err = nil
|
|
break
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
|
|
return results, err
|
|
}
|
|
|
|
// StepDown rolls back steps applied migrations, or all of them when steps
|
|
// is 0. Rolling everything back deletes all data, so confirmDrop is checked
|
|
// first in that case.
|
|
func StepDown(
|
|
ctx context.Context, provider *goose.Provider, steps int, confirmDrop bool,
|
|
) ([]*goose.MigrationResult, error) {
|
|
var results []*goose.MigrationResult
|
|
|
|
if err := checkNotNewer(ctx, provider); err != nil {
|
|
return results, err
|
|
}
|
|
|
|
current, err := provider.GetDBVersion(ctx)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
if current == 0 {
|
|
return results, nil
|
|
}
|
|
|
|
statuses, err := provider.Status(ctx)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
applied := 0
|
|
for _, status := range statuses {
|
|
if status.State == goose.StateApplied {
|
|
applied++
|
|
}
|
|
}
|
|
all := steps == 0 || steps >= applied
|
|
if all && !confirmDrop {
|
|
return results, ErrWouldEmptyDatabase
|
|
}
|
|
|
|
if all {
|
|
results, err = provider.DownTo(ctx, 0)
|
|
} else {
|
|
for range steps {
|
|
var result *goose.MigrationResult
|
|
result, err = provider.Down(ctx)
|
|
if errors.Is(err, goose.ErrNoNextVersion) {
|
|
err = nil
|
|
break
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
|
|
return results, err
|
|
}
|