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 }