github.com/supabase/cli@v1.168.1/internal/db/pull/pull.go (about)

     1  package pull
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"math"
     8  	"os"
     9  	"strconv"
    10  
    11  	"github.com/go-errors/errors"
    12  	"github.com/jackc/pgconn"
    13  	"github.com/jackc/pgx/v4"
    14  	"github.com/spf13/afero"
    15  	"github.com/supabase/cli/internal/db/diff"
    16  	"github.com/supabase/cli/internal/db/dump"
    17  	"github.com/supabase/cli/internal/db/reset"
    18  	"github.com/supabase/cli/internal/migration/list"
    19  	"github.com/supabase/cli/internal/migration/new"
    20  	"github.com/supabase/cli/internal/migration/repair"
    21  	"github.com/supabase/cli/internal/utils"
    22  )
    23  
    24  var (
    25  	errMissing       = errors.New("No migrations found")
    26  	errInSync        = errors.New("No schema changes found")
    27  	errConflict      = errors.Errorf("The remote database's migration history does not match local files in %s directory.", utils.MigrationsDir)
    28  	suggestExtraPull = fmt.Sprintf(
    29  		"The %s and %s schemas are excluded. Run %s again to diff them.",
    30  		utils.Bold("auth"),
    31  		utils.Bold("storage"),
    32  		utils.Aqua("supabase db pull --schema auth,storage"),
    33  	)
    34  )
    35  
    36  func Run(ctx context.Context, schema []string, config pgconn.Config, name string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
    37  	// 1. Sanity checks.
    38  	if err := utils.LoadConfigFS(fsys); err != nil {
    39  		return err
    40  	}
    41  	// 2. Check postgres connection
    42  	conn, err := utils.ConnectByConfig(ctx, config, options...)
    43  	if err != nil {
    44  		return err
    45  	}
    46  	defer conn.Close(context.Background())
    47  	// 3. Pull schema
    48  	timestamp := utils.GetCurrentTimestamp()
    49  	path := new.GetMigrationPath(timestamp, name)
    50  	if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error {
    51  		return run(p, ctx, schema, path, conn, fsys)
    52  	}); err != nil {
    53  		return err
    54  	}
    55  	// 4. Insert a row to `schema_migrations`
    56  	fmt.Fprintln(os.Stderr, "Schema written to "+utils.Bold(path))
    57  	if shouldUpdate := utils.NewConsole().PromptYesNo("Update remote migration history table?", true); shouldUpdate {
    58  		return repair.UpdateMigrationTable(ctx, conn, []string{timestamp}, repair.Applied, false, fsys)
    59  	}
    60  	return nil
    61  }
    62  
    63  func run(p utils.Program, ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys afero.Fs) error {
    64  	config := conn.Config().Config
    65  	// 1. Assert `supabase/migrations` and `schema_migrations` are in sync.
    66  	if err := assertRemoteInSync(ctx, conn, fsys); errors.Is(err, errMissing) {
    67  		// Not passing down schemas to avoid pulling in managed schemas
    68  		if err = dumpRemoteSchema(p, ctx, path, config, fsys); err == nil {
    69  			utils.CmdSuggestion = suggestExtraPull
    70  		}
    71  		return err
    72  	} else if err != nil {
    73  		return err
    74  	}
    75  	// 2. Fetch remote schema changes
    76  	defaultSchema := len(schema) == 0
    77  	if defaultSchema {
    78  		var err error
    79  		schema, err = reset.LoadUserSchemas(ctx, conn)
    80  		if err != nil {
    81  			return err
    82  		}
    83  	}
    84  	err := diffRemoteSchema(p, ctx, schema, path, config, fsys)
    85  	if defaultSchema && (err == nil || errors.Is(err, errInSync)) {
    86  		utils.CmdSuggestion = suggestExtraPull
    87  	}
    88  	return err
    89  }
    90  
    91  func dumpRemoteSchema(p utils.Program, ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
    92  	// Special case if this is the first migration
    93  	p.Send(utils.StatusMsg("Dumping schema from remote database..."))
    94  	if err := utils.MkdirIfNotExistFS(fsys, utils.MigrationsDir); err != nil {
    95  		return err
    96  	}
    97  	f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
    98  	if err != nil {
    99  		return errors.Errorf("failed to open dump file: %w", err)
   100  	}
   101  	defer f.Close()
   102  	return dump.DumpSchema(ctx, config, nil, false, false, f)
   103  }
   104  
   105  func diffRemoteSchema(p utils.Program, ctx context.Context, schema []string, path string, config pgconn.Config, fsys afero.Fs) error {
   106  	w := utils.StatusWriter{Program: p}
   107  	// Diff remote db (source) & shadow db (target) and write it as a new migration.
   108  	output, err := diff.DiffDatabase(ctx, schema, config, w, fsys, diff.DiffSchemaMigra)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	if len(output) == 0 {
   113  		return errors.New(errInSync)
   114  	}
   115  	if err := utils.WriteFile(path, []byte(output), fsys); err != nil {
   116  		return errors.Errorf("failed to write dump file: %w", err)
   117  	}
   118  	return nil
   119  }
   120  
   121  func assertRemoteInSync(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
   122  	remoteMigrations, err := list.LoadRemoteMigrations(ctx, conn)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	localMigrations, err := list.LoadLocalVersions(fsys)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	// Find any mismatch between local and remote migrations
   131  	var extraRemote, extraLocal []string
   132  	for i, j := 0, 0; i < len(remoteMigrations) || j < len(localMigrations); {
   133  		remoteTimestamp := math.MaxInt
   134  		if i < len(remoteMigrations) {
   135  			if remoteTimestamp, err = strconv.Atoi(remoteMigrations[i]); err != nil {
   136  				i++
   137  				continue
   138  			}
   139  		}
   140  		localTimestamp := math.MaxInt
   141  		if j < len(localMigrations) {
   142  			if localTimestamp, err = strconv.Atoi(localMigrations[j]); err != nil {
   143  				j++
   144  				continue
   145  			}
   146  		}
   147  		// Top to bottom chronological order
   148  		if localTimestamp < remoteTimestamp {
   149  			extraLocal = append(extraLocal, localMigrations[j])
   150  			j++
   151  		} else if remoteTimestamp < localTimestamp {
   152  			extraRemote = append(extraRemote, remoteMigrations[i])
   153  			i++
   154  		} else {
   155  			i++
   156  			j++
   157  		}
   158  	}
   159  	// Suggest delete local migrations / reset migration history
   160  	if len(extraRemote)+len(extraLocal) > 0 {
   161  		utils.CmdSuggestion = suggestMigrationRepair(extraRemote, extraLocal)
   162  		return errors.New(errConflict)
   163  	}
   164  	if len(localMigrations) == 0 {
   165  		return errors.New(errMissing)
   166  	}
   167  	return nil
   168  }
   169  
   170  func suggestMigrationRepair(extraRemote, extraLocal []string) string {
   171  	result := fmt.Sprintln("\nMake sure your local git repo is up-to-date. If the error persists, try repairing the migration history table:")
   172  	for _, version := range extraRemote {
   173  		result += fmt.Sprintln(utils.Bold("supabase migration repair --status reverted " + version))
   174  	}
   175  	for _, version := range extraLocal {
   176  		result += fmt.Sprintln(utils.Bold("supabase migration repair --status applied " + version))
   177  	}
   178  	return result
   179  }