diff --git a/README.md b/README.md index d9a865b..c408b6e 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Run plan Shows migration files which can be applied redo Rerun last applied migration from db run Applies all new migrations + skip Marks migrations done without actually running them. verify Checks and shows invalid migrations Flags: @@ -155,17 +156,21 @@ That is, you can call `pgmigrator --config docs/patches/pgmigrator.toml plan` an * If there is a NONTR - do not let dryrun run (only up to a certain filename) * `StatementTimeout` setting is ignored +### Skip + +Like `Run`, but without actually running sql migration, only adding migration success record + ### Last Shows the latest database migrations from a table. **Output** - Showing last migrations in public.pgMigrations: - 34 - 2022-08-30 22:25:03 (ERR) > 2022-07-30-compilations-NONTR.sql - 33 - 2022-08-30 22:25:03 (3s) > 2022-07-30-compilations-fix.sql - 32 - 2022-08-30 22:25:34 (1s) > 2022-07-28-jwlinks.sql - 31 - 2022-08-30 22:23:12 (5m 4s) > 2022-07-18-movieComments.sql + Showing last migrations in public.pgMigrations: + 34 - 2022-08-30 22:25:03 (ERR) > 2022-07-30-compilations-NONTR.sql + 33 - 2022-08-30 22:25:03 (3s) > 2022-07-30-compilations-fix.sql + 32 - 2022-08-30 22:25:34 (1s) > 2022-07-28-jwlinks.sql + 31 - 2022-08-30 22:23:12 (5m 4s) > 2022-07-18-movieComments.sql ### Verify diff --git a/README.ru.md b/README.ru.md index d48a8a6..28c4481 100644 --- a/README.ru.md +++ b/README.ru.md @@ -86,6 +86,7 @@ A: Цель - простая утилита, которая работает с plan Shows migration files which can be applied redo Rerun last applied migration from db run Applies all new migrations + skip Marks migrations done without actually running them. verify Checks and shows invalid migrations Flags: @@ -157,6 +158,10 @@ A: Цель - простая утилита, которая работает с Как в `Run`, только в конце выводим сообщение о ROLLBACK. +### Skip + +Как и `Run`, но без выполнения sql миграции. Только добавление записи о том, что миграция применена + ### Last Показываем последние транзакции. diff --git a/pkg/app/app.go b/pkg/app/app.go index b4b668d..880286f 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -46,7 +46,7 @@ func New(rootCmd *cobra.Command, mg *migrator.Migrator, cfg Config) App { } func (a App) Run(ctx context.Context) error { - a.rootCmd.AddCommand(a.initCmd(), a.dryRunCmd(ctx), a.lastCmd(ctx), a.planCmd(ctx), a.redoCmd(ctx), a.runCmd(ctx), a.verifyCmd(ctx)) + a.rootCmd.AddCommand(a.initCmd(), a.dryRunCmd(ctx), a.lastCmd(ctx), a.planCmd(ctx), a.redoCmd(ctx), a.runCmd(ctx), a.verifyCmd(ctx), a.skipCmd(ctx)) a.rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { if cmd.Name() == "init" || cmd.Name() == "help" { return @@ -262,6 +262,46 @@ If applied, runs only migrations. By default: 5`, } } +// skipCmd marks migrations done without actually running them +func (a App) skipCmd(ctx context.Context) *cobra.Command { + return &cobra.Command{ + Use: "skip []", + Short: "Marks migrations done without actually running them", + Long: `Marks migrations done without actually running them. +If applied, marks only first migrations displayed in plan. Default = 5.`, + Run: func(cmd *cobra.Command, args []string) { + // get list of migrations + mm, err := a.mg.Plan(ctx) + if err != nil { + log.Fatalf("Execute command failed: %v\n", err) + } else if len(mm) == 0 { + fmt.Println("No new migrations were found.") + return + } + + // calculate count + cnt, err := count(args) + if err != nil { + log.Fatal("invalid argument") + } else if cnt > len(mm) { + cnt = len(mm) + } + + // skip migrations + ch := make(chan string) + wg := &sync.WaitGroup{} + go readCh(ch, wg) + fmt.Println("Skipping migrations...") + if err = a.mg.Skip(ctx, mm[:cnt], ch); err != nil { + log.Fatalf("Skip migration error: %v", err) + return + } + wg.Wait() + fmt.Println("Done") + }, + } +} + // redoCmd rerun last migration func (a App) redoCmd(ctx context.Context) *cobra.Command { return &cobra.Command{ diff --git a/pkg/migrator/migrator.go b/pkg/migrator/migrator.go index 1f7ce82..df2384f 100644 --- a/pkg/migrator/migrator.go +++ b/pkg/migrator/migrator.go @@ -34,6 +34,19 @@ func NewMigrator(db *pg.DB, cfg Config, rootDir string) *Migrator { return m } +// writeMigrationToDB inserts log that migration was completed in postgres +func writeMigrationToDB(ctx context.Context, mg Migration, tx *pg.Tx, start time.Time) error { + finish := time.Now() + pm := mg.ToDB() + pm.StartedAt = start + pm.FinishedAt = &finish + + if _, err := tx.ModelContext(ctx, pm).Insert(); err != nil { + return fmt.Errorf(`add new migration "%s" failed: %w`, mg.Filename, err) + } + return nil +} + // readAllFiles read files from migrator root dir and return its filenames func (m *Migrator) readAllFiles() ([]string, error) { dir, err := os.Open(m.rootDir) @@ -191,17 +204,7 @@ func (m *Migrator) applyMigration(ctx context.Context, mg Migration) (err error) return fmt.Errorf(`apply migration failed: %w`, err) } - // insert into pgMigrations - finish := time.Now() - pm := mg.ToDB() - pm.StartedAt = start - pm.FinishedAt = &finish - - if _, err = tx.ModelContext(ctx, pm).Insert(); err != nil { - return fmt.Errorf(`add new migration failed: %w`, err) - } - - return nil + return writeMigrationToDB(ctx, mg, tx, start) } // setStatementTimeout set statement timeout to transaction connection @@ -293,14 +296,52 @@ func (m *Migrator) dryRunMigrations(ctx context.Context, mm Migrations, chCurren return fmt.Errorf(`apply migration "%s" failed: %w`, mg.Filename, err) } - // insert into pgMigrations - finish := time.Now() - pm := mg.ToDB() - pm.StartedAt = start - pm.FinishedAt = &finish + if err = writeMigrationToDB(ctx, mg, tx, start); err != nil { + return err + } + } - if _, err = tx.ModelContext(ctx, pm).Insert(); err != nil { - return fmt.Errorf(`add new migration "%s" failed: %w`, mg.Filename, err) + return nil +} + +// Skip marks migrations as completed +func (m *Migrator) Skip(ctx context.Context, filenames []string, chCurrentFile chan string) error { + defer close(chCurrentFile) + + // create migration table if not exists + if err := m.createMigratorTable(ctx); err != nil { + return err + } + + // prepare migrations + mm, err := m.newMigrations(filenames) + if err != nil { + return fmt.Errorf("prepare migrations failed: %w", err) + } + + // skip migrations + if err := m.skipMigrations(ctx, mm, chCurrentFile); err != nil { + return fmt.Errorf("skip migrations failed: %w", err) + } + return nil +} + +func (m *Migrator) skipMigrations(ctx context.Context, mm Migrations, chCurrentFile chan string) (err error) { + var tx *pg.Tx + tx, err = m.db.Begin() + if err != nil { + return fmt.Errorf(`begin transaction failed: %w`, err) + } + + defer func() { + err = finishTxOnErr(tx, err) + }() + + // write migrations to pgMigrations table + for _, mg := range mm { + chCurrentFile <- mg.Filename + if err = writeMigrationToDB(ctx, mg, tx, time.Now()); err != nil { + return err } } diff --git a/pkg/migrator/migrator_test.go b/pkg/migrator/migrator_test.go index e72f767..0824827 100644 --- a/pkg/migrator/migrator_test.go +++ b/pkg/migrator/migrator_test.go @@ -356,6 +356,50 @@ func TestMigrator_DryRun(t *testing.T) { }) } +func TestMigrator_skipMigrations(t *testing.T) { + ctx := context.Background() + Convey("TestMigrator_skipMigrations", t, func() { + err := recreateSchema() + So(err, ShouldBeNil) + err = testMigrator.createMigratorTable(ctx) + So(err, ShouldBeNil) + + dirFiles := []string{ + "2022-12-12-01-create-table-statuses.sql", + "2022-12-12-02-create-table-news.sql", + } + mm, err := testMigrator.newMigrations(dirFiles) + So(err, ShouldBeNil) + + ch := make(chan string) + go readFromCh(ch, t) + err = testMigrator.skipMigrations(ctx, mm, ch) + So(err, ShouldBeNil) + + for _, mg := range mm { + var pm PgMigration + err = testMigrator.db.ModelContext(ctx, &pm).Where(`"filename" = ?`, mg.Filename).Select() + So(err, ShouldBeNil) + So(pm, ShouldNotBeNil) + So(pm.FinishedAt, ShouldNotBeEmpty) + } + }) +} + +func TestMigrator_Skip(t *testing.T) { + ctx := context.Background() + Convey("TestMigrator_Skip", t, func() { + err := recreateSchema() + So(err, ShouldBeNil) + filenames, err := testMigrator.Plan(ctx) + So(err, ShouldBeNil) + ch := make(chan string) + go readFromCh(ch, t) + err = testMigrator.Skip(ctx, filenames, ch) + So(err, ShouldBeNil) + }) +} + func TestMigrator_compareMD5Sum(t *testing.T) { Convey("TestMigrator_compareMD5Sum", t, func() { Convey("check correct", func() {