Repository: supabase/cli
Pull request #666 (2022-12-05 09:40:51)

feat: add migration repair command
## What kind of change does this PR introduce?

feature

## What is the current behavior?

need to manually delete version history when remote commit fails

## What is the new behavior?

- supports `migration repair <version> --status applied`
- supports `migration repair <version> --status reverted`

## Additional context

References
- https://www.prisma.io/docs/reference/api-reference/command-reference#migrate-resolve
- https://flywaydb.org/documentation/usage/commandline/repair

coveralls: ## Pull Request Test Coverage Report for [Build 3618783464](https://coveralls.io/builds/54833598)

* **51** of **58**   **(87.93%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.2%**) to **57.965%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/db.go](https://coveralls.io/builds/54833598/source?filename=cmd%2Fdb.go#L114) | 1 | 2 | 50.0%
| [internal/db/push/push.go](https://coveralls.io/builds/54833598/source?filename=internal%2Fdb%2Fpush%2Fpush.go#L76) | 35 | 37 | 94.59%
| [internal/migration/list/list.go](https://coveralls.io/builds/54833598/source?filename=internal%2Fmigration%2Flist%2Flist.go#L113) | 15 | 19 | 78.95%
<!-- | **Total:** | **51** | **58** | **87.93%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/54833598/badge)](https://coveralls.io/builds/54833598) |
| :-- | --: |
| Change from base [Build 3612839365](https://coveralls.io/builds/54816574): |  0.2% |
| Covered Lines: | 3264 |
| Relevant Lines: | 5631 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/cmd/db.go b/cmd/db.go
index 3398b391..196c6304 100644
--- a/cmd/db.go
+++ b/cmd/db.go
@@ -79,15 +79,17 @@ var (
 	dbDiffCmd = &cobra.Command{
 		Use:   "diff",
 		Short: "Diffs the local database for schema changes",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			fsys := afero.NewOsFs()
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			if linked {
-				projectRef, err := utils.LoadProjectRef(fsys)
-				if err != nil {
+				fsys := afero.NewOsFs()
+				if err := loadLinkedProject(fsys); err != nil {
 					return err
 				}
-				dbPassword = getPassword(projectRef)
 			}
+			return cmd.Root().PersistentPreRunE(cmd, args)
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			fsys := afero.NewOsFs()
 			if useMigra {
 				ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
 				return diff.RunMigra(ctx, schema, file, dbPassword, fsys)
@@ -101,22 +103,31 @@ var (
 	dbPushCmd = &cobra.Command{
 		Use:   "push",
 		Short: "Push new migrations to the remote database",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			fsys := afero.NewOsFs()
-			projectRef, err := utils.LoadProjectRef(fsys)
-			if err != nil {
+			if err := loadLinkedProject(fsys); err != nil {
 				return err
 			}
-			password := getPassword(projectRef)
+			return cmd.Root().PersistentPreRunE(cmd, args)
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			fsys := afero.NewOsFs()
 			host := utils.GetSupabaseDbHost(projectRef)
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return push.Run(ctx, dryRun, username, password, database, host, fsys)
+			return push.Run(ctx, dryRun, username, dbPassword, database, host, fsys)
 		},
 	}
 
 	dbRemoteCmd = &cobra.Command{
 		Use:   "remote",
 		Short: "Manage remote databases",
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+			fsys := afero.NewOsFs()
+			if err := loadLinkedProject(fsys); err != nil {
+				return err
+			}
+			return cmd.Root().PersistentPreRunE(cmd, args)
+		},
 	}
 
 	dbRemoteChangesCmd = &cobra.Command{
@@ -126,12 +137,7 @@ var (
 		Long:       "Show changes on the remote database since last migration.",
 		RunE: func(cmd *cobra.Command, args []string) error {
 			fsys := afero.NewOsFs()
-			projectRef, err := utils.LoadProjectRef(fsys)
-			if err != nil {
-				return err
-			}
-			password := getPassword(projectRef)
-			return changes.Run(cmd.Context(), username, password, database, fsys)
+			return changes.Run(cmd.Context(), username, dbPassword, database, fsys)
 		},
 	}
 
@@ -140,13 +146,8 @@ var (
 		Short: "Commit remote changes as a new migration",
 		RunE: func(cmd *cobra.Command, args []string) error {
 			fsys := afero.NewOsFs()
-			projectRef, err := utils.LoadProjectRef(fsys)
-			if err != nil {
-				return err
-			}
-			password := getPassword(projectRef)
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return commit.Run(ctx, username, password, database, fsys)
+			return commit.Run(ctx, username, dbPassword, database, fsys)
 		},
 	}
 
diff --git a/cmd/migration.go b/cmd/migration.go
index d9e3bdb1..b0ff1c5a 100644
--- a/cmd/migration.go
+++ b/cmd/migration.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	"os"
+	"os/signal"
 
 	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
@@ -9,6 +10,7 @@ import (
 	"github.com/supabase/cli/internal/link"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/migration/new"
+	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/internal/utils/credentials"
 )
@@ -23,15 +25,18 @@ var (
 	migrationListCmd = &cobra.Command{
 		Use:   "list",
 		Short: "List local and remote migrations",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			fsys := afero.NewOsFs()
-			projectRef, err := utils.LoadProjectRef(fsys)
-			if err != nil {
+			if err := loadLinkedProject(fsys); err != nil {
 				return err
 			}
-			password := getPassword(projectRef)
+			return cmd.Root().PersistentPreRunE(cmd, args)
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			fsys := afero.NewOsFs()
 			host := utils.GetSupabaseDbHost(projectRef)
-			return list.Run(cmd.Context(), username, password, database, host, fsys)
+			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
+			return list.Run(ctx, username, dbPassword, database, host, fsys)
 		},
 	}
 
@@ -43,17 +48,58 @@ var (
 			return new.Run(args[0], os.Stdin, afero.NewOsFs())
 		},
 	}
+
+	targetStatus = utils.EnumFlag{
+		Allowed: []string{
+			repair.Applied,
+			repair.Reverted,
+		},
+	}
+
+	migrationRepairCmd = &cobra.Command{
+		Use:   "repair <version>",
+		Short: "Repairs the migration history table",
+		Args:  cobra.ExactArgs(1),
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
+			fsys := afero.NewOsFs()
+			if err := loadLinkedProject(fsys); err != nil {
+				return err
+			}
+			return cmd.Root().PersistentPreRunE(cmd, args)
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			host := utils.GetSupabaseDbHost(projectRef)
+			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
+			return repair.Run(ctx, username, dbPassword, database, host, args[0], targetStatus.Value)
+		},
+	}
 )
 
 func init() {
-	flags := migrationListCmd.Flags()
-	flags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.")
-	cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", flags.Lookup("password")))
+	// Build list command
+	listFlags := migrationListCmd.Flags()
+	listFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.")
+	cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", listFlags.Lookup("password")))
 	migrationCmd.AddCommand(migrationListCmd)
+	// Build repair command
+	repairFlags := migrationRepairCmd.Flags()
+	repairFlags.Var(&targetStatus, "status", "Version status to update.")
+	cobra.CheckErr(migrationRepairCmd.MarkFlagRequired("status"))
+	migrationCmd.AddCommand(migrationRepairCmd)
+	// Build new command
 	migrationCmd.AddCommand(migrationNewCmd)
 	rootCmd.AddCommand(migrationCmd)
 }
 
+func loadLinkedProject(fsys afero.Fs) (err error) {
+	projectRef, err = utils.LoadProjectRef(fsys)
+	if err != nil {
+		return err
+	}
+	dbPassword = getPassword(projectRef)
+	return nil
+}
+
 func getPassword(projectRef string) string {
 	if password := viper.GetString("DB_PASSWORD"); len(password) > 0 {
 		return password
diff --git a/cmd/projects.go b/cmd/projects.go
index 81bb2c32..a6a490d8 100644
--- a/cmd/projects.go
+++ b/cmd/projects.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 	"os/signal"
+	"sort"
 	"strings"
 
 	"github.com/spf13/afero"
@@ -83,6 +84,7 @@ func init() {
 		region.Allowed[i] = k
 		i++
 	}
+	sort.Strings(region.Allowed)
 	// Add flags to cobra command
 	createFlags := projectsCreateCmd.Flags()
 	createFlags.BoolVarP(&interactive, "interactive", "i", false, "Enables interactive mode.")
diff --git a/internal/db/push/push.go b/internal/db/push/push.go
index 7ec26402..a6849b4d 100644
--- a/internal/db/push/push.go
+++ b/internal/db/push/push.go
@@ -8,11 +8,10 @@ import (
 	"path/filepath"
 
 	"github.com/jackc/pgconn"
-	"github.com/jackc/pgtype"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
-	"github.com/supabase/cli/internal/db/remote/commit"
 	"github.com/supabase/cli/internal/migration/list"
+	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/internal/utils/parser"
 )
@@ -91,15 +90,9 @@ func pushMigration(ctx context.Context, conn *pgx.Conn, filename string, fsys af
 		batch.ExecParams(line, nil, nil, nil, nil)
 	}
 	// Insert into migration history
-	lines = append(lines, commit.INSERT_MIGRATION_VERSION)
+	lines = append(lines, repair.INSERT_MIGRATION_VERSION)
 	version := utils.MigrateFilePattern.FindStringSubmatch(filename)[1]
-	batch.ExecParams(
-		commit.INSERT_MIGRATION_VERSION,
-		[][]byte{[]byte(version)},
-		[]uint32{pgtype.TextOID},
-		[]int16{pgtype.TextFormatCode},
-		nil,
-	)
+	repair.InsertVersionSQL(&batch, version)
 	// ExecBatch is implicitly transactional
 	if result, err := conn.PgConn().ExecBatch(ctx, &batch).ReadAll(); err != nil {
 		i := len(result)
diff --git a/internal/db/remote/commit/commit.go b/internal/db/remote/commit/commit.go
index adf89b22..dc326067 100644
--- a/internal/db/remote/commit/commit.go
+++ b/internal/db/remote/commit/commit.go
@@ -23,17 +23,10 @@ import (
 	"github.com/muesli/reflow/wrap"
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
+	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
 )
 
-const (
-	CHECK_MIGRATION_EXISTS = "SELECT 1 FROM supabase_migrations.schema_migrations LIMIT 1"
-	CREATE_MIGRATION_TABLE = `CREATE SCHEMA IF NOT EXISTS supabase_migrations;
-CREATE TABLE supabase_migrations.schema_migrations (version text NOT NULL PRIMARY KEY);
-`
-	INSERT_MIGRATION_VERSION = "INSERT INTO supabase_migrations.schema_migrations(version) VALUES($1)"
-)
-
 var (
 	//go:embed templates/dump_initial_migration.sh
 	dumpInitialMigrationScript string
@@ -123,7 +116,7 @@ func run(p utils.Program, ctx context.Context, username, password, database stri
 		}
 
 		// Insert a row to `schema_migrations`
-		if _, err := conn.Query(ctx, INSERT_MIGRATION_VERSION, timestamp); err != nil {
+		if _, err := conn.Exec(ctx, repair.INSERT_MIGRATION_VERSION, timestamp); err != nil {
 			return err
 		}
 
@@ -309,7 +302,7 @@ EOSQL
 	}
 
 	// 5. Insert a row to `schema_migrations`
-	if _, err := conn.Exec(ctx, INSERT_MIGRATION_VERSION, timestamp); err != nil {
+	if _, err := conn.Exec(ctx, repair.INSERT_MIGRATION_VERSION, timestamp); err != nil {
 		return err
 	}
 
diff --git a/internal/link/link.go b/internal/link/link.go
index 744195ee..c30af9ca 100644
--- a/internal/link/link.go
+++ b/internal/link/link.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/db/remote/commit"
+	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/internal/utils/credentials"
 	"golang.org/x/term"
@@ -35,10 +36,8 @@ func Run(ctx context.Context, projectRef, username, password, database string, f
 			return err
 		}
 		// If `schema_migrations` doesn't exist on the remote database, create it.
-		if _, err := conn.Exec(ctx, commit.CHECK_MIGRATION_EXISTS); err != nil {
-			if _, err := conn.Exec(ctx, commit.CREATE_MIGRATION_TABLE); err != nil {
-				return err
-			}
+		if _, err := conn.Exec(ctx, repair.CREATE_MIGRATION_TABLE); err != nil {
+			return err
 		}
 		// Save database password
 		if err := credentials.Set(projectRef, password); err != nil {
diff --git a/internal/migration/list/list.go b/internal/migration/list/list.go
index dacd8f15..9b58f0ec 100644
--- a/internal/migration/list/list.go
+++ b/internal/migration/list/list.go
@@ -20,32 +20,18 @@ const LIST_MIGRATION_VERSION = "SELECT version FROM supabase_migrations.schema_m
 var initSchemaPattern = regexp.MustCompile(`([0-9]{14})_init\.sql`)
 
 func Run(ctx context.Context, username, password, database, host string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
-	remoteVersions, err := loadRemoteMigrations(ctx, username, password, database, host, options...)
+	remoteVersions, err := loadRemoteVersions(ctx, username, password, database, host, options...)
 	if err != nil {
 		return err
 	}
-	localVersions, err := loadLocalMigrations(fsys)
+	localVersions, err := loadLocalVersions(fsys)
 	if err != nil {
 		return err
 	}
-	// Render table
-	table := makeTable(remoteVersions, localVersions)
-	r, err := glamour.NewTermRenderer(
-		glamour.WithAutoStyle(),
-		glamour.WithWordWrap(-1),
-	)
-	if err != nil {
-		return err
-	}
-	out, err := r.Render(table)
-	if err != nil {
-		return err
-	}
-	fmt.Print(out)
-	return nil
+	return RenderTable(remoteVersions, localVersions)
 }
 
-func loadRemoteMigrations(ctx context.Context, username, password, database, host string, options ...func(*pgx.ConnConfig)) ([]string, error) {
+func loadRemoteVersions(ctx context.Context, username, password, database, host string, options ...func(*pgx.ConnConfig)) ([]string, error) {
 	conn, err := utils.ConnectRemotePostgres(ctx, username, password, database, host, options...)
 	if err != nil {
 		return nil, err
@@ -83,7 +69,7 @@ func formatTimestamp(version string) string {
 	return timestamp.Format(layoutHuman)
 }
 
-func makeTable(remoteMigrations []string, localMigrations []string) string {
+func makeTable(remoteMigrations, localMigrations []string) string {
 	var err error
 	table := "|Local|Remote|Time (UTC)|\n|-|-|-|\n"
 	for i, j := 0, 0; i < len(remoteMigrations) || j < len(localMigrations); {
@@ -117,7 +103,24 @@ func makeTable(remoteMigrations []string, localMigrations []string) string {
 	return table
 }
 
-func loadLocalMigrations(fsys afero.Fs) ([]string, error) {
+func RenderTable(remoteVersions, localVersions []string) error {
+	table := makeTable(remoteVersions, localVersions)
+	r, err := glamour.NewTermRenderer(
+		glamour.WithAutoStyle(),
+		glamour.WithWordWrap(-1),
+	)
+	if err != nil {
+		return err
+	}
+	out, err := r.Render(table)
+	if err != nil {
+		return err
+	}
+	fmt.Print(out)
+	return nil
+}
+
+func loadLocalVersions(fsys afero.Fs) ([]string, error) {
 	names, err := LoadLocalMigrations(fsys)
 	if err != nil {
 		return nil, err
diff --git a/internal/migration/repair/repair.go b/internal/migration/repair/repair.go
new file mode 100644
index 00000000..52a34b2b
--- /dev/null
+++ b/internal/migration/repair/repair.go
@@ -0,0 +1,67 @@
+package repair
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/jackc/pgconn"
+	"github.com/jackc/pgtype"
+	"github.com/jackc/pgx/v4"
+	"github.com/supabase/cli/internal/utils"
+)
+
+const (
+	Applied  = "applied"
+	Reverted = "reverted"
+)
+
+const (
+	CREATE_VERSION_SCHEMA    = "CREATE SCHEMA IF NOT EXISTS supabase_migrations"
+	CREATE_VERSION_TABLE     = "CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations (version text NOT NULL PRIMARY KEY)"
+	INSERT_MIGRATION_VERSION = "INSERT INTO supabase_migrations.schema_migrations(version) VALUES($1)"
+	DELETE_MIGRATION_VERSION = "DELETE FROM supabase_migrations.schema_migrations WHERE version = $1"
+	CREATE_MIGRATION_TABLE   = CREATE_VERSION_SCHEMA + ";" + CREATE_VERSION_TABLE
+)
+
+func Run(ctx context.Context, username, password, database, host, version, status string, options ...func(*pgx.ConnConfig)) error {
+	conn, err := utils.ConnectRemotePostgres(ctx, username, password, database, host, options...)
+	if err != nil {
+		return err
+	}
+	defer conn.Close(context.Background())
+	// Create history table if not exists
+	batch := pgconn.Batch{}
+	batch.ExecParams(CREATE_VERSION_SCHEMA, nil, nil, nil, nil)
+	batch.ExecParams(CREATE_VERSION_TABLE, nil, nil, nil, nil)
+	// Update migration history
+	switch status {
+	case Applied:
+		InsertVersionSQL(&batch, version)
+	case Reverted:
+		DeleteVersionSQL(&batch, version)
+	}
+	if _, err = conn.PgConn().ExecBatch(ctx, &batch).ReadAll(); err != nil {
+		return err
+	}
+	fmt.Fprintln(os.Stderr, "Repaired migration history:", version, "=>", status)
+	return nil
+}
+
+func InsertVersionSQL(batch *pgconn.Batch, version string) {
+	updateVersionSQL(batch, INSERT_MIGRATION_VERSION, version)
+}
+
+func DeleteVersionSQL(batch *pgconn.Batch, version string) {
+	updateVersionSQL(batch, DELETE_MIGRATION_VERSION, version)
+}
+
+func updateVersionSQL(batch *pgconn.Batch, sql, version string) {
+	batch.ExecParams(
+		sql,
+		[][]byte{[]byte(version)},
+		[]uint32{pgtype.TextOID},
+		[]int16{pgtype.TextFormatCode},
+		nil,
+	)
+}
diff --git a/internal/start/start.go b/internal/start/start.go
index 5578d021..b61f3a7f 100644
--- a/internal/start/start.go
+++ b/internal/start/start.go
@@ -84,8 +84,8 @@ func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string) error
 var (
 	// TODO: Unhardcode keys
 	//go:embed templates/kong_config
-	kongConfigEmbed       string
-	kongConfigTemplate, _ = template.New("kongConfig").Parse(kongConfigEmbed)
+	kongConfigEmbed    string
+	kongConfigTemplate = template.Must(template.New("kongConfig").Parse(kongConfigEmbed))
 )
 
 func pullImage(p utils.Program, ctx context.Context, image string) error {
diff --git a/internal/utils/api.go b/internal/utils/api.go
index 4d51a5d6..58512908 100644
--- a/internal/utils/api.go
+++ b/internal/utils/api.go
@@ -209,6 +209,7 @@ var RegionMap = map[string]string{
 	"eu-central-1":   "Central EU (Frankfurt)",
 	"eu-west-1":      "West EU (Ireland)",
 	"eu-west-2":      "West EU (London)",
+	"eu-west-3":      "West EU (Paris)",
 	"sa-east-1":      "South America (São Paulo)",
 	"us-east-1":      "East US (North Virginia)",
 	"us-west-1":      "West US (North California)",
diff --git a/internal/utils/enum.go b/internal/utils/enum.go
index e756c6aa..1245c3c8 100644
--- a/internal/utils/enum.go
+++ b/internal/utils/enum.go
@@ -32,5 +32,9 @@ func (a *EnumFlag) Set(p string) error {
 }
 
 func (a *EnumFlag) Type() string {
+	values := strings.Join(a.Allowed, " | ")
+	if len(values) < 40 {
+		return fmt.Sprintf("[ %s ]", values)
+	}
 	return "string"
 }


Test patch:
diff --git a/internal/db/push/push_test.go b/internal/db/push/push_test.go
index 0a0141de..4bda2324 100644
--- a/internal/db/push/push_test.go
+++ b/internal/db/push/push_test.go
@@ -10,8 +10,8 @@ import (
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"github.com/supabase/cli/internal/db/remote/commit"
 	"github.com/supabase/cli/internal/migration/list"
+	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/testing/pgtest"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/internal/utils/parser"
@@ -88,13 +88,13 @@ func TestMigrationPush(t *testing.T) {
 		defer conn.Close(t)
 		conn.Query(list.LIST_MIGRATION_VERSION).
 			Reply("SELECT 0").
-			Query(commit.INSERT_MIGRATION_VERSION, "0").
+			Query(repair.INSERT_MIGRATION_VERSION, "0").
 			ReplyError(pgerrcode.NotNullViolation, `null value in column "version" of relation "schema_migrations"`)
 		// Run test
 		err := Run(context.Background(), false, user, pass, database, host, fsys, conn.Intercept)
 		// Check error
 		assert.ErrorContains(t, err, `ERROR: null value in column "version" of relation "schema_migrations" (SQLSTATE 23502)`)
-		assert.ErrorContains(t, err, "At statement 0: "+commit.INSERT_MIGRATION_VERSION)
+		assert.ErrorContains(t, err, "At statement 0: "+repair.INSERT_MIGRATION_VERSION)
 	})
 }
 
@@ -201,7 +201,7 @@ func TestPushLocal(t *testing.T) {
 		defer conn.Close(t)
 		conn.Query(sql).
 			Reply("CREATE SCHEMA").
-			Query(commit.INSERT_MIGRATION_VERSION, "0").
+			Query(repair.INSERT_MIGRATION_VERSION, "0").
 			Reply("INSERT 0 1")
 		// Connect to mock
 		ctx := context.Background()
@@ -243,7 +243,7 @@ func TestPushLocal(t *testing.T) {
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
-		conn.Query(commit.INSERT_MIGRATION_VERSION, "0").
+		conn.Query(repair.INSERT_MIGRATION_VERSION, "0").
 			ReplyError(pgerrcode.NotNullViolation, `null value in column "version" of relation "schema_migrations"`)
 		// Connect to mock
 		ctx := context.Background()
@@ -254,6 +254,6 @@ func TestPushLocal(t *testing.T) {
 		err = pushMigration(ctx, mock, "0_test.sql", fsys)
 		// Check error
 		assert.ErrorContains(t, err, `ERROR: null value in column "version" of relation "schema_migrations" (SQLSTATE 23502)`)
-		assert.ErrorContains(t, err, "At statement 0: "+commit.INSERT_MIGRATION_VERSION)
+		assert.ErrorContains(t, err, "At statement 0: "+repair.INSERT_MIGRATION_VERSION)
 	})
 }
diff --git a/internal/migration/list/list_test.go b/internal/migration/list/list_test.go
index 66cf44bf..3fc83439 100644
--- a/internal/migration/list/list_test.go
+++ b/internal/migration/list/list_test.go
@@ -69,7 +69,7 @@ func TestRemoteMigrations(t *testing.T) {
 		conn.Query(LIST_MIGRATION_VERSION).
 			Reply("SELECT 1", []interface{}{"20220727064247"})
 		// Run test
-		versions, err := loadRemoteMigrations(context.Background(), user, pass, db, host, conn.Intercept)
+		versions, err := loadRemoteVersions(context.Background(), user, pass, db, host, conn.Intercept)
 		// Check error
 		assert.NoError(t, err)
 		assert.ElementsMatch(t, []string{"20220727064247"}, versions)
@@ -77,7 +77,7 @@ func TestRemoteMigrations(t *testing.T) {
 
 	t.Run("throws error on connect failure", func(t *testing.T) {
 		// Run test
-		_, err := loadRemoteMigrations(context.Background(), user, pass, db, "0")
+		_, err := loadRemoteVersions(context.Background(), user, pass, db, "0")
 		// Check error
 		assert.ErrorContains(t, err, "dial error (dial tcp 0.0.0.0:6543: connect: connection refused)")
 	})
@@ -89,7 +89,7 @@ func TestRemoteMigrations(t *testing.T) {
 		conn.Query(LIST_MIGRATION_VERSION).
 			ReplyError(pgerrcode.UndefinedTable, "relation \"supabase_migrations.schema_migrations\" does not exist")
 		// Run test
-		_, err := loadRemoteMigrations(context.Background(), user, pass, db, host, conn.Intercept)
+		_, err := loadRemoteVersions(context.Background(), user, pass, db, host, conn.Intercept)
 		// Check error
 		assert.ErrorContains(t, err, `ERROR: relation "supabase_migrations.schema_migrations" does not exist (SQLSTATE 42P01)`)
 	})
@@ -101,7 +101,7 @@ func TestRemoteMigrations(t *testing.T) {
 		conn.Query(LIST_MIGRATION_VERSION).
 			Reply("SELECT 1", nil)
 		// Run test
-		_, err := loadRemoteMigrations(context.Background(), user, pass, db, host, conn.Intercept)
+		_, err := loadRemoteVersions(context.Background(), user, pass, db, host, conn.Intercept)
 		// Check error
 		assert.ErrorContains(t, err, "number of field descriptions must equal number of destinations, got 0 and 1")
 	})
@@ -128,7 +128,7 @@ func TestLocalMigrations(t *testing.T) {
 		path = filepath.Join(utils.MigrationsDir, "20220727064248_test.sql")
 		require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644))
 		// Run test
-		versions, err := loadLocalMigrations(fsys)
+		versions, err := loadLocalVersions(fsys)
 		// Check error
 		assert.NoError(t, err)
 		assert.ElementsMatch(t, []string{"20220727064246", "20220727064248"}, versions)
@@ -142,7 +142,7 @@ func TestLocalMigrations(t *testing.T) {
 		path = filepath.Join(utils.MigrationsDir, "20211208000001_invalid.ts")
 		require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644))
 		// Run test
-		versions, err := loadLocalMigrations(fsys)
+		versions, err := loadLocalVersions(fsys)
 		// Check error
 		assert.NoError(t, err)
 		assert.Empty(t, versions)
@@ -152,7 +152,7 @@ func TestLocalMigrations(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Run test
-		_, err := loadLocalMigrations(afero.NewReadOnlyFs(fsys))
+		_, err := loadLocalVersions(afero.NewReadOnlyFs(fsys))
 		// Check error
 		assert.ErrorContains(t, err, "operation not permitted")
 	})
@@ -161,7 +161,7 @@ func TestLocalMigrations(t *testing.T) {
 		// Setup in-memory fs
 		fsys := MockFs{DenyPath: utils.MigrationsDir}
 		// Run test
-		_, err := loadLocalMigrations(&fsys)
+		_, err := loadLocalVersions(&fsys)
 		// Check error
 		assert.ErrorContains(t, err, "permission denied")
 	})
diff --git a/internal/migration/repair/repair_test.go b/internal/migration/repair/repair_test.go
new file mode 100644
index 00000000..923c676c
--- /dev/null
+++ b/internal/migration/repair/repair_test.go
@@ -0,0 +1,74 @@
+package repair
+
+import (
+	"context"
+	"testing"
+
+	"github.com/jackc/pgerrcode"
+	"github.com/stretchr/testify/assert"
+	"github.com/supabase/cli/internal/testing/pgtest"
+)
+
+const (
+	user     = "admin"
+	pass     = "password"
+	database = "postgres"
+	host     = "localhost"
+)
+
+func TestRepairCommand(t *testing.T) {
+	t.Run("applies new version", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(CREATE_VERSION_SCHEMA).
+			Reply("CREATE SCHEMA").
+			Query(CREATE_VERSION_TABLE).
+			Reply("CREATE TABLE").
+			Query(INSERT_MIGRATION_VERSION, "0").
+			Reply("INSERT 0 1")
+		// Run test
+		err := Run(context.Background(), user, pass, database, host, "0", Applied, conn.Intercept)
+		// Check error
+		assert.NoError(t, err)
+	})
+
+	t.Run("reverts old version", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(CREATE_VERSION_SCHEMA).
+			Reply("CREATE SCHEMA").
+			Query(CREATE_VERSION_TABLE).
+			Reply("CREATE TABLE").
+			Query(DELETE_MIGRATION_VERSION, "0").
+			Reply("DELETE 1")
+		// Run test
+		err := Run(context.Background(), user, pass, database, host, "0", Reverted, conn.Intercept)
+		// Check error
+		assert.NoError(t, err)
+	})
+
+	t.Run("throws error on connect failure", func(t *testing.T) {
+		// Run test
+		err := Run(context.Background(), user, pass, database, "0", "0", Applied)
+		// Check error
+		assert.ErrorContains(t, err, "dial error (dial tcp 0.0.0.0:6543: connect: connection refused)")
+	})
+
+	t.Run("throws error on insert failure", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(CREATE_VERSION_SCHEMA).
+			Reply("CREATE SCHEMA").
+			Query(CREATE_VERSION_TABLE).
+			Reply("CREATE TABLE").
+			Query(INSERT_MIGRATION_VERSION, "0").
+			ReplyError(pgerrcode.DuplicateObject, `relation "supabase_migrations.schema_migrations" does not exist`)
+		// Run test
+		err := Run(context.Background(), user, pass, database, host, "0", Applied, conn.Intercept)
+		// Check error
+		assert.ErrorContains(t, err, `ERROR: relation "supabase_migrations.schema_migrations" does not exist (SQLSTATE 42710)`)
+	})
+}