178 lines
5.7 KiB
Go
178 lines
5.7 KiB
Go
package pull
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/jackc/pgconn"
|
|
"github.com/jackc/pgx/v4"
|
|
"github.com/spf13/afero"
|
|
"github.com/supabase/cli/internal/db/diff"
|
|
"github.com/supabase/cli/internal/db/dump"
|
|
"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/pkg/migration"
|
|
)
|
|
|
|
var (
|
|
errMissing = errors.New("No migrations found")
|
|
errInSync = errors.New("No schema changes found")
|
|
errConflict = errors.Errorf("The remote database's migration history does not match local files in %s directory.", utils.MigrationsDir)
|
|
suggestExtraPull = fmt.Sprintf(
|
|
"The %s and %s schemas are excluded. Run %s again to diff them.",
|
|
utils.Bold("auth"),
|
|
utils.Bold("storage"),
|
|
utils.Aqua("supabase db pull --schema auth,storage"),
|
|
)
|
|
)
|
|
|
|
func Run(ctx context.Context, schema []string, config pgconn.Config, name string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
|
|
// 1. Check postgres connection
|
|
conn, err := utils.ConnectByConfig(ctx, config, options...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close(context.Background())
|
|
// 2. Pull schema
|
|
timestamp := utils.GetCurrentTimestamp()
|
|
path := new.GetMigrationPath(timestamp, name)
|
|
if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error {
|
|
return run(p, ctx, schema, path, conn, fsys)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
// 3. Insert a row to `schema_migrations`
|
|
fmt.Fprintln(os.Stderr, "Schema written to "+utils.Bold(path))
|
|
if shouldUpdate, err := utils.NewConsole().PromptYesNo(ctx, "Update remote migration history table?", true); err != nil {
|
|
return err
|
|
} else if shouldUpdate {
|
|
return repair.UpdateMigrationTable(ctx, conn, []string{timestamp}, repair.Applied, false, fsys)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func run(p utils.Program, ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys afero.Fs) error {
|
|
config := conn.Config().Config
|
|
// 1. Assert `supabase/migrations` and `schema_migrations` are in sync.
|
|
if err := assertRemoteInSync(ctx, conn, fsys); errors.Is(err, errMissing) {
|
|
// Not passing down schemas to avoid pulling in managed schemas
|
|
if err = dumpRemoteSchema(p, ctx, path, config, fsys); err == nil {
|
|
utils.CmdSuggestion = suggestExtraPull
|
|
}
|
|
return err
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
// 2. Fetch remote schema changes
|
|
defaultSchema := len(schema) == 0
|
|
if defaultSchema {
|
|
var err error
|
|
schema, err = migration.ListUserSchemas(ctx, conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err := diffRemoteSchema(p, ctx, schema, path, config, fsys)
|
|
if defaultSchema && (err == nil || errors.Is(err, errInSync)) {
|
|
utils.CmdSuggestion = suggestExtraPull
|
|
}
|
|
return err
|
|
}
|
|
|
|
func dumpRemoteSchema(p utils.Program, ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
|
|
// Special case if this is the first migration
|
|
p.Send(utils.StatusMsg("Dumping schema from remote database..."))
|
|
if err := utils.MkdirIfNotExistFS(fsys, utils.MigrationsDir); err != nil {
|
|
return err
|
|
}
|
|
f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return errors.Errorf("failed to open dump file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
return dump.DumpSchema(ctx, config, nil, false, false, f)
|
|
}
|
|
|
|
func diffRemoteSchema(p utils.Program, ctx context.Context, schema []string, path string, config pgconn.Config, fsys afero.Fs) error {
|
|
w := utils.StatusWriter{Program: p}
|
|
// Diff remote db (source) & shadow db (target) and write it as a new migration.
|
|
output, err := diff.DiffDatabase(ctx, schema, config, w, fsys, diff.DiffSchemaMigra)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(output) == 0 {
|
|
return errors.New(errInSync)
|
|
}
|
|
if err := utils.WriteFile(path, []byte(output), fsys); err != nil {
|
|
return errors.Errorf("failed to write dump file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func assertRemoteInSync(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
|
|
remoteMigrations, err := migration.ListRemoteMigrations(ctx, conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
localMigrations, err := list.LoadLocalVersions(fsys)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Find any mismatch between local and remote migrations
|
|
var extraRemote, extraLocal []string
|
|
for i, j := 0, 0; i < len(remoteMigrations) || j < len(localMigrations); {
|
|
remoteTimestamp := math.MaxInt
|
|
if i < len(remoteMigrations) {
|
|
if remoteTimestamp, err = strconv.Atoi(remoteMigrations[i]); err != nil {
|
|
i++
|
|
continue
|
|
}
|
|
}
|
|
localTimestamp := math.MaxInt
|
|
if j < len(localMigrations) {
|
|
if localTimestamp, err = strconv.Atoi(localMigrations[j]); err != nil {
|
|
j++
|
|
continue
|
|
}
|
|
}
|
|
// Top to bottom chronological order
|
|
if localTimestamp < remoteTimestamp {
|
|
extraLocal = append(extraLocal, localMigrations[j])
|
|
j++
|
|
} else if remoteTimestamp < localTimestamp {
|
|
extraRemote = append(extraRemote, remoteMigrations[i])
|
|
i++
|
|
} else {
|
|
i++
|
|
j++
|
|
}
|
|
}
|
|
// Suggest delete local migrations / reset migration history
|
|
if len(extraRemote)+len(extraLocal) > 0 {
|
|
utils.CmdSuggestion = suggestMigrationRepair(extraRemote, extraLocal)
|
|
return errors.New(errConflict)
|
|
}
|
|
if len(localMigrations) == 0 {
|
|
return errors.New(errMissing)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func suggestMigrationRepair(extraRemote, extraLocal []string) string {
|
|
result := fmt.Sprintln("\nMake sure your local git repo is up-to-date. If the error persists, try repairing the migration history table:")
|
|
for _, version := range extraRemote {
|
|
result += fmt.Sprintln(utils.Bold("supabase migration repair --status reverted " + version))
|
|
}
|
|
for _, version := range extraLocal {
|
|
result += fmt.Sprintln(utils.Bold("supabase migration repair --status applied " + version))
|
|
}
|
|
return result
|
|
}
|