Skip to content

Commit

Permalink
Add a pgroll latest command (#469)
Browse files Browse the repository at this point in the history
Add a `pgroll latest` command to show the latest version in either the
target database or a local directory of migration files.

## Documentation

`pgroll latest` prints the latest schema version in either the target
database or a local directory of migration files.

By default, `pgroll latest` prints the latest version in the target
database. Use the `--local` flag to print the latest version in a local
directory of migration files instead.

In both cases, the `--with-schema` flag can be used to prefix the latest
version with the schema name.

#### Database

Assuming that the [example
migrations](https://github.com/xataio/pgroll/tree/main/examples) have
been applied to the `public` schema in the target database, running:

```
$ pgroll latest 
```

will print the latest version in the target database:

```
45_add_table_check_constraint
```

The exact output will vary as the `examples/` directory is updated.

#### Local

Assuming that the [example
migrations](https://github.com/xataio/pgroll/tree/main/examples) are on
disk in a directory called `examples`, running:

```
$ pgroll latest --local examples/
```

will print the latest migration in the directory:

```
45_add_table_check_constraint
```

The exact output will vary as the `examples/` directory is updated.

---
Part of #446
  • Loading branch information
andrew-farries authored Nov 25, 2024
1 parent 6d48386 commit eaaabf9
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 0 deletions.
66 changes: 66 additions & 0 deletions cmd/latest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

func latestCmd() *cobra.Command {
var withSchema bool
var migrationsDir string

latestCmd := &cobra.Command{
Use: "latest <directory>",
Short: "Print the name of the latest schema version, either in the target database or a local directory",
Example: "latest --local ./migrations",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

m, err := NewRoll(ctx)
if err != nil {
return err
}
defer m.Close()

var latestVersion string
if migrationsDir != "" {
info, err := os.Stat(migrationsDir)
if err != nil {
return fmt.Errorf("failed to stat directory: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("migrations directory %q is not a directory", migrationsDir)
}

latestVersion, err = m.LatestVersionLocal(ctx, os.DirFS(migrationsDir))
if err != nil {
return fmt.Errorf("failed to get latest version from directory %q: %w", migrationsDir, err)
}
} else {
latestVersion, err = m.LatestVersionRemote(ctx)
if err != nil {
return fmt.Errorf("failed to get latest version from database: %w", err)
}
}

var prefix string
if withSchema {
prefix = m.Schema() + "_"
}

fmt.Printf("%s%s\n", prefix, latestVersion)

return nil
},
}

latestCmd.Flags().BoolVarP(&withSchema, "with-schema", "s", false, "prefix the version with the schema name")
latestCmd.Flags().StringVarP(&migrationsDir, "local", "l", "", "retrieve the latest version from a local migration directory")

return latestCmd
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func Execute() error {
rootCmd.AddCommand(statusCmd)
rootCmd.AddCommand(migrateCmd())
rootCmd.AddCommand(pullCmd())
rootCmd.AddCommand(latestCmd())

return rootCmd.Execute()
}
44 changes: 44 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* [complete](#complete)
* [rollback](#rollback)
* [status](#status)
* [migrate](#migrate)
* [latest](#latest)
* [pull](#pull)
* [Operations reference](#operations-reference)
* [Add column](#add-column)
Expand Down Expand Up @@ -537,6 +539,8 @@ The `pgroll` CLI offers the following subcommands:
* [complete](#complete)
* [rollback](#rollback)
* [status](#status)
* [migrate](#migrate)
* [latest](#latest)
* [pull](#pull)

The `pgroll` CLI has the following top-level flags:
Expand Down Expand Up @@ -638,6 +642,46 @@ 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).

### Latest

`pgroll latest` prints the latest schema version in either the target database or a local directory of migration files.

By default, `pgroll latest` prints the latest version in the target database. Use the `--local` flag to print the latest version in a local directory of migration files instead.

In both cases, the `--with-schema` flag can be used to prefix the latest version with the schema name.

#### Database

Assuming that the [example migrations](https://github.com/xataio/pgroll/tree/main/examples) have been applied to the `public` schema in the target database, running:

```
$ pgroll latest
```

will print the latest version in the target database:

```
45_add_table_check_constraint
```

The exact output will vary as the `examples/` directory is updated.

#### Local

Assuming that the [example migrations](https://github.com/xataio/pgroll/tree/main/examples) are on disk in a directory called `examples`, running:

```
$ pgroll latest --local examples/
```

will print the latest migration in the directory:

```
45_add_table_check_constraint
```

The exact output will vary as the `examples/` directory is updated.

### Status

`pgroll status` shows the current status of `pgroll` within a given schema:
Expand Down
51 changes: 51 additions & 0 deletions pkg/roll/latest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: Apache-2.0

package roll

import (
"context"
"fmt"
"io/fs"
)

var (
ErrNoMigrationFiles = fmt.Errorf("no migration files found")
ErrNoMigrationApplied = fmt.Errorf("no migrations applied")
)

// LatestVersionLocal returns the name of the last migration in `dir`, where the
// migration files are lexicographically ordered by filename.
func (m *Roll) LatestVersionLocal(ctx context.Context, dir fs.FS) (string, error) {
files, err := fs.Glob(dir, "*.json")
if err != nil {
return "", fmt.Errorf("reading directory: %w", err)
}

if len(files) == 0 {
return "", ErrNoMigrationFiles
}

latest := files[len(files)-1]

migration, err := openAndReadMigrationFile(dir, latest)
if err != nil {
return "", fmt.Errorf("reading migration file %q: %w", latest, err)
}

return migration.Name, nil
}

// LatestVersionRemote returns the name of the last migration to have been
// applied to the target schema.
func (m *Roll) LatestVersionRemote(ctx context.Context) (string, error) {
latestVersion, err := m.State().LatestVersion(ctx, m.Schema())
if err != nil {
return "", fmt.Errorf("failed to get latest version: %w", err)
}

if latestVersion == nil {
return "", ErrNoMigrationApplied
}

return *latestVersion, nil
}
93 changes: 93 additions & 0 deletions pkg/roll/latest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0

package roll_test

import (
"context"
"database/sql"
"testing"
"testing/fstest"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/xataio/pgroll/internal/testutils"
"github.com/xataio/pgroll/pkg/migrations"
"github.com/xataio/pgroll/pkg/roll"
)

func TestLatestVersionLocal(t *testing.T) {
t.Parallel()

t.Run("returns the name of the last migration in the directory", func(t *testing.T) {
fs := fstest.MapFS{
"01_migration_1.json": &fstest.MapFile{Data: exampleMigration(t, "01_migration_1")},
"02_migration_2.json": &fstest.MapFile{Data: exampleMigration(t, "02_migration_2")},
"03_migration_3.json": &fstest.MapFile{Data: exampleMigration(t, "03_migration_3")},
}

testutils.WithMigratorAndConnectionToContainer(t, func(roll *roll.Roll, _ *sql.DB) {
ctx := context.Background()

// Get the latest migration in the directory
latest, err := roll.LatestVersionLocal(ctx, fs)
require.NoError(t, err)

// Assert last migration name
assert.Equal(t, "03_migration_3", latest)
})
})

t.Run("returns an error if the directory is empty", func(t *testing.T) {
fs := fstest.MapFS{}

testutils.WithMigratorAndConnectionToContainer(t, func(m *roll.Roll, _ *sql.DB) {
ctx := context.Background()

// Get the latest migration in the directory
_, err := m.LatestVersionLocal(ctx, fs)

// Assert expected error
assert.ErrorIs(t, err, roll.ErrNoMigrationFiles)
})
})
}

func TestLatestVersionRemote(t *testing.T) {
t.Parallel()

t.Run("returns the name of the latest version in the target schema", func(t *testing.T) {
testutils.WithMigratorAndConnectionToContainer(t, func(m *roll.Roll, _ *sql.DB) {
ctx := context.Background()

// Start and complete a migration
err := m.Start(ctx, &migrations.Migration{
Name: "01_first_migration",
Operations: migrations.Operations{
&migrations.OpRawSQL{Up: "SELECT 1"},
},
})
require.NoError(t, err)
err = m.Complete(ctx)
require.NoError(t, err)

// Get the latest version in the target schema
latestVersion, err := m.LatestVersionRemote(ctx)
require.NoError(t, err)

// Assert latest migration name
assert.Equal(t, "01_first_migration", latestVersion)
})
})

t.Run("returns an error if no migrations have been applied", func(t *testing.T) {
testutils.WithMigratorAndConnectionToContainer(t, func(m *roll.Roll, _ *sql.DB) {
ctx := context.Background()

// Get the latest migration in the directory
_, err := m.LatestVersionRemote(ctx)

// Assert expected error
assert.ErrorIs(t, err, roll.ErrNoMigrationApplied)
})
})
}
1 change: 1 addition & 0 deletions pkg/roll/unapplied.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func openAndReadMigrationFile(dir fs.FS, filename string) (*migrations.Migration
if err != nil {
return nil, err
}
defer file.Close()

migration, err := migrations.ReadMigration(file)
if err != nil {
Expand Down

0 comments on commit eaaabf9

Please sign in to comment.