Skip to content

Commit 85e917c

Browse files
Add pgroll migrate subcommand (#465)
Add a `pgroll migrate` subcommand. ## Documentation `pgroll migrate` applies all outstanding migrations from a source directory to the target database. Assuming that migrations up to and including migration `40_create_enum_type` from the [example migrations directory](https://github.com/xataio/pgroll/tree/main/examples) have been applied, running: ``` $ pgroll migrate examples/ ``` will apply migrations from `41_add_enum_column` onwards to the target database. If the `--complete` flag is passed to `pgroll migrate` the final migration to be applied will be completed. Otherwise the final migration will be left active (started but not completed). ## Notes: * If no migrations have yet been applied to the target database, `migrate` applies all of the migrations in the source directory. * This PR removes the `pgroll bootstrap` command (#414) as it is equivalent to running `pgroll migrate <directory> --complete` against a fresh database. Part of #446
1 parent e044c56 commit 85e917c

File tree

10 files changed

+355
-50
lines changed

10 files changed

+355
-50
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ ledger:
3131
examples: ledger
3232
@go build
3333
@./pgroll init
34-
@./pgroll bootstrap examples
34+
@./pgroll migrate examples --complete
3535
@go clean
3636

3737
test:

cmd/bootstrap.go

Lines changed: 0 additions & 41 deletions
This file was deleted.

cmd/migrate.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package cmd
4+
5+
import (
6+
"fmt"
7+
"os"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func migrateCmd() *cobra.Command {
13+
var complete bool
14+
15+
migrateCmd := &cobra.Command{
16+
Use: "migrate <directory>",
17+
Short: "Apply outstanding migrations from a directory to a database",
18+
Example: "migrate ./migrations",
19+
Args: cobra.ExactArgs(1),
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
ctx := cmd.Context()
22+
migrationsDir := args[0]
23+
24+
m, err := NewRoll(ctx)
25+
if err != nil {
26+
return err
27+
}
28+
defer m.Close()
29+
30+
latestVersion, err := m.State().LatestVersion(ctx, m.Schema())
31+
if err != nil {
32+
return fmt.Errorf("unable to determine latest version: %w", err)
33+
}
34+
35+
active, err := m.State().IsActiveMigrationPeriod(ctx, m.Schema())
36+
if err != nil {
37+
return fmt.Errorf("unable to determine active migration period: %w", err)
38+
}
39+
if active {
40+
return fmt.Errorf("migration %q is active and must be completed first", *latestVersion)
41+
}
42+
43+
info, err := os.Stat(migrationsDir)
44+
if err != nil {
45+
return fmt.Errorf("failed to stat directory: %w", err)
46+
}
47+
if !info.IsDir() {
48+
return fmt.Errorf("migrations directory %q is not a directory", migrationsDir)
49+
}
50+
51+
migs, err := m.UnappliedMigrations(ctx, os.DirFS(migrationsDir))
52+
if err != nil {
53+
return fmt.Errorf("failed to get migrations to apply: %w", err)
54+
}
55+
56+
if len(migs) == 0 {
57+
fmt.Println("database is up to date; no migrations to apply")
58+
return nil
59+
}
60+
61+
// Run all migrations after the latest version up to the final migration,
62+
// completing each one.
63+
for _, mig := range migs[:len(migs)-1] {
64+
if err := runMigration(ctx, m, mig, true); err != nil {
65+
return fmt.Errorf("failed to run migration file %q: %w", mig.Name, err)
66+
}
67+
}
68+
69+
// Run the final migration, completing it only if requested.
70+
return runMigration(ctx, m, migs[len(migs)-1], complete)
71+
},
72+
}
73+
74+
migrateCmd.Flags().BoolVarP(&complete, "complete", "c", false, "complete the final migration rather than leaving it active")
75+
76+
return migrateCmd
77+
}

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func Execute() error {
7676
rootCmd.AddCommand(analyzeCmd)
7777
rootCmd.AddCommand(initCmd)
7878
rootCmd.AddCommand(statusCmd)
79-
rootCmd.AddCommand(bootstrapCmd)
79+
rootCmd.AddCommand(migrateCmd())
8080
rootCmd.AddCommand(pullCmd())
8181

8282
return rootCmd.Execute()

cmd/start.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import (
66
"context"
77
"fmt"
88
"os"
9-
"path/filepath"
10-
"strings"
119

1210
"github.com/pterm/pterm"
1311
"github.com/spf13/cobra"
@@ -52,12 +50,16 @@ func runMigrationFromFile(ctx context.Context, m *roll.Roll, fileName string, co
5250
return err
5351
}
5452

53+
return runMigration(ctx, m, migration, complete)
54+
}
55+
56+
func runMigration(ctx context.Context, m *roll.Roll, migration *migrations.Migration, complete bool) error {
5557
sp, _ := pterm.DefaultSpinner.WithText("Starting migration...").Start()
5658
cb := func(n int64) {
5759
sp.UpdateText(fmt.Sprintf("%d records complete...", n))
5860
}
5961

60-
err = m.Start(ctx, migration, cb)
62+
err := m.Start(ctx, migration, cb)
6163
if err != nil {
6264
sp.Fail(fmt.Sprintf("Failed to start migration: %s", err))
6365
return err
@@ -71,9 +73,6 @@ func runMigrationFromFile(ctx context.Context, m *roll.Roll, fileName string, co
7173
}
7274

7375
version := migration.Name
74-
if version == "" {
75-
version = strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName))
76-
}
7776
viewName := roll.VersionedSchemaName(flags.Schema(), version)
7877
msg := fmt.Sprintf("New version of the schema available under the postgres %q schema", viewName)
7978
sp.Success(msg)

docs/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,21 @@ Migrations cannot be rolled back once completed. Attempting to roll back a migra
623623

624624
:warning: Before running `pgroll rollback` ensure that any new versions of applications that depend on the new database schema are no longer live. Prematurely running `pgroll rollback` can cause downtime of new application instances that depend on the new schema.
625625

626+
627+
### Migrate
628+
629+
`pgroll migrate` applies all outstanding migrations from a directory to the target database.
630+
631+
Assuming that migrations up to and including migration `40_create_enum_type` from the [example migrations](https://github.com/xataio/pgroll/tree/main/examples) directory have been applied, running:
632+
633+
```
634+
$ pgroll migrate examples/
635+
```
636+
637+
will apply migrations from `41_add_enum_column` onwards to the target database.
638+
639+
If the `--complete` flag is passed to `pgroll migrate` the final migration to be applied will be completed. Otherwise the final migration will be left active (started but not completed).
640+
626641
### Status
627642

628643
`pgroll status` shows the current status of `pgroll` within a given schema:

pkg/roll/roll.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const (
2424
DefaultBackfillDelay time.Duration = 0
2525
)
2626

27+
var ErrMismatchedMigration = fmt.Errorf("remote migration does not match local migration")
28+
2729
type Roll struct {
2830
pgConn db.DB
2931

@@ -144,6 +146,11 @@ func (m *Roll) PgConn() db.DB {
144146
return m.pgConn
145147
}
146148

149+
// State returns the state instance the Roll instance is acting on
150+
func (m *Roll) State() *state.State {
151+
return m.state
152+
}
153+
147154
// Schema returns the schema the Roll instance is acting on
148155
func (m *Roll) Schema() string {
149156
return m.schema

pkg/roll/unapplied.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package roll
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"io/fs"
9+
10+
"github.com/xataio/pgroll/pkg/migrations"
11+
)
12+
13+
// UnappliedMigrations returns a slice of unapplied migrations from `dir`,
14+
// lexicographically ordered by filename. Applying each of the returned
15+
// migrations in order will bring the database up to date with `dir`.
16+
//
17+
// If the local order of migrations does not match the order of migrations in
18+
// the schema history, an `ErrMismatchedMigration` error is returned.
19+
func (m *Roll) UnappliedMigrations(ctx context.Context, dir fs.FS) ([]*migrations.Migration, error) {
20+
latestVersion, err := m.State().LatestVersion(ctx, m.Schema())
21+
if err != nil {
22+
return nil, fmt.Errorf("determining latest version: %w", err)
23+
}
24+
25+
files, err := fs.Glob(dir, "*.json")
26+
if err != nil {
27+
return nil, fmt.Errorf("reading directory: %w", err)
28+
}
29+
30+
history, err := m.State().SchemaHistory(ctx, m.Schema())
31+
if err != nil {
32+
return nil, fmt.Errorf("reading schema history: %w", err)
33+
}
34+
35+
// Find the index of the first unapplied migration
36+
var idx int
37+
if latestVersion != nil {
38+
for _, file := range files {
39+
migration, err := openAndReadMigrationFile(dir, file)
40+
if err != nil {
41+
return nil, fmt.Errorf("reading migration file %q: %w", file, err)
42+
}
43+
44+
remoteMigration := history[idx].Migration
45+
if remoteMigration.Name != migration.Name {
46+
return nil, fmt.Errorf("%w: remote=%q, local=%q", ErrMismatchedMigration, remoteMigration.Name, migration.Name)
47+
}
48+
49+
idx++
50+
if migration.Name == *latestVersion {
51+
break
52+
}
53+
}
54+
}
55+
56+
// Return all unapplied migrations
57+
migs := make([]*migrations.Migration, 0, len(files))
58+
for _, file := range files[idx:] {
59+
migration, err := openAndReadMigrationFile(dir, file)
60+
if err != nil {
61+
return nil, fmt.Errorf("reading migration file %q: %w", file, err)
62+
}
63+
migs = append(migs, migration)
64+
}
65+
66+
return migs, nil
67+
}
68+
69+
func openAndReadMigrationFile(dir fs.FS, filename string) (*migrations.Migration, error) {
70+
file, err := dir.Open(filename)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
migration, err := migrations.ReadMigration(file)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
return migration, nil
81+
}

0 commit comments

Comments
 (0)