github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/db/remote/commit/commit.go (about)

     1  package commit
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"errors"
     7  	"fmt"
     8  	"net/url"
     9  	"path/filepath"
    10  
    11  	"github.com/Redstoneguy129/cli/internal/db/diff"
    12  	"github.com/Redstoneguy129/cli/internal/db/dump"
    13  	"github.com/Redstoneguy129/cli/internal/migration/list"
    14  	"github.com/Redstoneguy129/cli/internal/migration/repair"
    15  	"github.com/Redstoneguy129/cli/internal/utils"
    16  	"github.com/jackc/pgx/v4"
    17  	"github.com/spf13/afero"
    18  )
    19  
    20  func Run(ctx context.Context, schema []string, username, password, database string, fsys afero.Fs) error {
    21  	// Sanity checks.
    22  	{
    23  		if err := utils.AssertDockerIsRunning(ctx); err != nil {
    24  			return err
    25  		}
    26  		if err := utils.LoadConfigFS(fsys); err != nil {
    27  			return err
    28  		}
    29  	}
    30  
    31  	if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error {
    32  		return run(p, ctx, schema, username, password, database, fsys)
    33  	}); err != nil {
    34  		return err
    35  	}
    36  
    37  	fmt.Println("Finished " + utils.Aqua("supabase db remote commit") + ".")
    38  	return nil
    39  }
    40  
    41  func run(p utils.Program, ctx context.Context, schema []string, username, password, database string, fsys afero.Fs) error {
    42  	projectRef, err := utils.LoadProjectRef(fsys)
    43  	if err != nil {
    44  		return err
    45  	}
    46  	host := utils.GetSupabaseDbHost(projectRef)
    47  
    48  	// 1. Assert `supabase/migrations` and `schema_migrations` are in sync.
    49  	p.Send(utils.StatusMsg("Connecting to remote database..."))
    50  	conn, err := utils.ConnectRemotePostgres(ctx, username, password, database, host)
    51  	if err != nil {
    52  		return err
    53  	}
    54  	defer conn.Close(context.Background())
    55  	if err := AssertRemoteInSync(ctx, conn, fsys); err != nil {
    56  		return err
    57  	}
    58  
    59  	// 2. Fetch remote schema changes
    60  	if len(schema) == 0 {
    61  		schema, err = diff.LoadUserSchemas(ctx, conn)
    62  		if err != nil {
    63  			return err
    64  		}
    65  	}
    66  	timestamp := utils.GetCurrentTimestamp()
    67  	if err := fetchRemote(p, ctx, schema, timestamp, username, password, database, host, fsys); err != nil {
    68  		return err
    69  	}
    70  
    71  	// 3. Insert a row to `schema_migrations`
    72  	_, err = conn.Exec(ctx, repair.INSERT_MIGRATION_VERSION, timestamp)
    73  	return err
    74  }
    75  
    76  func fetchRemote(p utils.Program, ctx context.Context, schema []string, timestamp, username, password, database, host string, fsys afero.Fs) error {
    77  	path := filepath.Join(utils.MigrationsDir, timestamp+"_remote_commit.sql")
    78  	// Special case if this is the first migration
    79  	if migrations, err := list.LoadLocalMigrations(fsys); err != nil {
    80  		return err
    81  	} else if len(migrations) == 0 {
    82  		p.Send(utils.StatusMsg("Committing initial migration on remote database..."))
    83  		return dump.Run(ctx, path, username, password, database, host, false, fsys)
    84  	}
    85  
    86  	w := utils.StatusWriter{Program: p}
    87  	// Diff remote db (source) & shadow db (target) and write it as a new migration.
    88  	target := fmt.Sprintf("postgresql://%s@%s:6543/postgres", url.UserPassword(database, password), host)
    89  	output, err := diff.DiffDatabase(ctx, schema, target, w, fsys)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	if len(output) == 0 {
    94  		return errors.New("no changes found")
    95  	}
    96  	return afero.WriteFile(fsys, path, []byte(output), 0644)
    97  }
    98  
    99  func AssertRemoteInSync(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
   100  	remoteMigrations, err := list.LoadRemoteMigrations(ctx, conn)
   101  	if err != nil {
   102  		return err
   103  	}
   104  	localMigrations, err := list.LoadLocalMigrations(fsys)
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	conflictErr := errors.New("The remote database's migration history is not in sync with the contents of " + utils.Bold(utils.MigrationsDir) + `. Resolve this by:
   110  - Updating the project from version control to get the latest ` + utils.Bold(utils.MigrationsDir) + `,
   111  - Pushing unapplied migrations with ` + utils.Aqua("supabase db push") + `,
   112  - Or failing that, manually editing supabase_migrations.schema_migrations table with ` + utils.Aqua("supabase migration repair") + ".")
   113  	if len(remoteMigrations) != len(localMigrations) {
   114  		return conflictErr
   115  	}
   116  
   117  	for i, remoteTimestamp := range remoteMigrations {
   118  		// LoadLocalMigrations guarantees we always have a match
   119  		localTimestamp := utils.MigrateFilePattern.FindStringSubmatch(localMigrations[i])[1]
   120  		if localTimestamp != remoteTimestamp {
   121  			return conflictErr
   122  		}
   123  	}
   124  
   125  	return nil
   126  }