1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-25 09:21:11 +02:00
pocketbase/tools/migrate/runner.go

274 lines
5.9 KiB
Go
Raw Normal View History

2022-07-06 23:19:05 +02:00
package migrate
import (
"fmt"
2023-01-07 22:25:56 +02:00
"strings"
2022-07-06 23:19:05 +02:00
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/pocketbase/dbx"
"github.com/spf13/cast"
)
2022-12-02 11:36:13 +02:00
const DefaultMigrationsTable = "_migrations"
2022-07-06 23:19:05 +02:00
// Runner defines a simple struct for managing the execution of db migrations.
type Runner struct {
db *dbx.DB
migrationsList MigrationsList
tableName string
}
// NewRunner creates and initializes a new db migrations Runner instance.
func NewRunner(db *dbx.DB, migrationsList MigrationsList) (*Runner, error) {
runner := &Runner{
db: db,
migrationsList: migrationsList,
2022-12-02 11:36:13 +02:00
tableName: DefaultMigrationsTable,
2022-07-06 23:19:05 +02:00
}
if err := runner.createMigrationsTable(); err != nil {
return nil, err
}
return runner, nil
}
// Run interactively executes the current runner with the provided args.
//
// The following commands are supported:
// - up - applies all migrations
// - down [n] - reverts the last n applied migrations
2022-07-06 23:19:05 +02:00
func (r *Runner) Run(args ...string) error {
cmd := "up"
if len(args) > 0 {
cmd = args[0]
}
switch cmd {
case "up":
applied, err := r.Up()
if err != nil {
return err
}
if len(applied) == 0 {
color.Green("No new migrations to apply.")
} else {
for _, file := range applied {
color.Green("Applied %s", file)
}
}
return nil
case "down":
toRevertCount := 1
if len(args) > 1 {
toRevertCount = cast.ToInt(args[1])
if toRevertCount < 0 {
// revert all applied migrations
toRevertCount = len(r.migrationsList.Items())
}
}
2023-01-07 22:25:56 +02:00
names, err := r.lastAppliedMigrations(toRevertCount)
if err != nil {
return err
}
2022-07-06 23:19:05 +02:00
confirm := false
prompt := &survey.Confirm{
2023-01-07 22:25:56 +02:00
Message: fmt.Sprintf(
"\n%v\nDo you really want to revert the last %d applied migration(s)?",
strings.Join(names, "\n"),
toRevertCount,
),
2022-07-06 23:19:05 +02:00
}
survey.AskOne(prompt, &confirm)
if !confirm {
fmt.Println("The command has been cancelled")
return nil
}
reverted, err := r.Down(toRevertCount)
if err != nil {
return err
}
if len(reverted) == 0 {
color.Green("No migrations to revert.")
} else {
for _, file := range reverted {
color.Green("Reverted %s", file)
}
}
2023-03-25 21:48:19 +02:00
return nil
case "history-sync":
if err := r.removeMissingAppliedMigrations(); err != nil {
return err
}
color.Green("The %s table was synced with the available migrations.", r.tableName)
2022-07-06 23:19:05 +02:00
return nil
default:
return fmt.Errorf("Unsupported command: %q\n", cmd)
}
}
// Up executes all unapplied migrations for the provided runner.
//
// On success returns list with the applied migrations file names.
func (r *Runner) Up() ([]string, error) {
applied := []string{}
err := r.db.Transactional(func(tx *dbx.Tx) error {
for _, m := range r.migrationsList.Items() {
// skip applied
if r.isMigrationApplied(tx, m.File) {
2022-07-06 23:19:05 +02:00
continue
}
// ignore empty Up action
if m.Up != nil {
if err := m.Up(tx); err != nil {
return fmt.Errorf("Failed to apply migration %s: %w", m.File, err)
}
2022-07-06 23:19:05 +02:00
}
if err := r.saveAppliedMigration(tx, m.File); err != nil {
return fmt.Errorf("Failed to save applied migration info for %s: %w", m.File, err)
2022-07-06 23:19:05 +02:00
}
applied = append(applied, m.File)
2022-07-06 23:19:05 +02:00
}
return nil
})
if err != nil {
return nil, err
}
return applied, nil
}
2023-01-07 22:25:56 +02:00
// Down reverts the last `toRevertCount` applied migrations
// (in the order they were applied).
2022-07-06 23:19:05 +02:00
//
// On success returns list with the reverted migrations file names.
func (r *Runner) Down(toRevertCount int) ([]string, error) {
2022-07-18 22:00:54 +02:00
reverted := make([]string, 0, toRevertCount)
2022-07-06 23:19:05 +02:00
2023-01-07 22:25:56 +02:00
names, appliedErr := r.lastAppliedMigrations(toRevertCount)
if appliedErr != nil {
return nil, appliedErr
}
2022-07-06 23:19:05 +02:00
err := r.db.Transactional(func(tx *dbx.Tx) error {
2023-01-07 22:25:56 +02:00
for _, name := range names {
for _, m := range r.migrationsList.Items() {
if m.File != name {
continue
}
2022-07-06 23:19:05 +02:00
2023-01-07 22:25:56 +02:00
// revert limit reached
if toRevertCount-len(reverted) <= 0 {
return nil
}
2022-07-06 23:19:05 +02:00
2023-01-07 22:25:56 +02:00
// ignore empty Down action
if m.Down != nil {
if err := m.Down(tx); err != nil {
return fmt.Errorf("Failed to revert migration %s: %w", m.File, err)
}
}
2022-07-06 23:19:05 +02:00
2023-01-07 22:25:56 +02:00
if err := r.saveRevertedMigration(tx, m.File); err != nil {
return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.File, err)
}
2022-07-06 23:19:05 +02:00
2023-01-07 22:25:56 +02:00
reverted = append(reverted, m.File)
2022-07-06 23:19:05 +02:00
}
}
return nil
})
if err != nil {
return nil, err
}
2023-01-07 22:25:56 +02:00
2022-07-18 22:00:54 +02:00
return reverted, nil
2022-07-06 23:19:05 +02:00
}
func (r *Runner) createMigrationsTable() error {
rawQuery := fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %v (file VARCHAR(255) PRIMARY KEY NOT NULL, applied INTEGER NOT NULL)",
r.db.QuoteTableName(r.tableName),
)
_, err := r.db.NewQuery(rawQuery).Execute()
return err
}
func (r *Runner) isMigrationApplied(tx dbx.Builder, file string) bool {
var exists bool
err := tx.Select("count(*)").
From(r.tableName).
Where(dbx.HashExp{"file": file}).
Limit(1).
Row(&exists)
return err == nil && exists
}
func (r *Runner) saveAppliedMigration(tx dbx.Builder, file string) error {
_, err := tx.Insert(r.tableName, dbx.Params{
"file": file,
2023-01-07 22:25:56 +02:00
"applied": time.Now().UnixMicro(),
2022-07-06 23:19:05 +02:00
}).Execute()
return err
}
func (r *Runner) saveRevertedMigration(tx dbx.Builder, file string) error {
_, err := tx.Delete(r.tableName, dbx.HashExp{"file": file}).Execute()
return err
}
2023-01-07 22:25:56 +02:00
func (r *Runner) lastAppliedMigrations(limit int) ([]string, error) {
var files = make([]string, 0, limit)
err := r.db.Select("file").
From(r.tableName).
Where(dbx.Not(dbx.HashExp{"applied": nil})).
OrderBy("applied DESC", "file DESC").
Limit(int64(limit)).
Column(&files)
if err != nil {
return nil, err
}
return files, nil
}
2023-03-25 21:48:19 +02:00
func (r *Runner) removeMissingAppliedMigrations() error {
loadedMigrations := r.migrationsList.Items()
names := make([]any, len(loadedMigrations))
for i, migration := range loadedMigrations {
names[i] = migration.File
}
_, err := r.db.Delete(r.tableName, dbx.Not(dbx.HashExp{
"file": names,
})).Execute()
return err
}