• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 18403653021

10 Oct 2025 10:20AM UTC coverage: 54.315% (-0.4%) from 54.696%
18403653021

Pull #4283

github

web-flow
Merge d880548e0 into a250719cb
Pull Request #4283: feat: add command to clone project to local

14 of 107 new or added lines in 4 files covered. (13.08%)

23 existing lines in 2 files now uncovered.

6419 of 11818 relevant lines covered (54.32%)

6.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.59
/internal/db/pull/pull.go
1
package pull
2

3
import (
4
        "context"
5
        _ "embed"
6
        "fmt"
7
        "math"
8
        "os"
9
        "path/filepath"
10
        "strconv"
11
        "strings"
12

13
        "github.com/go-errors/errors"
14
        "github.com/jackc/pgconn"
15
        "github.com/jackc/pgx/v4"
16
        "github.com/spf13/afero"
17
        "github.com/supabase/cli/internal/db/diff"
18
        "github.com/supabase/cli/internal/db/dump"
19
        "github.com/supabase/cli/internal/db/start"
20
        "github.com/supabase/cli/internal/migration/list"
21
        "github.com/supabase/cli/internal/migration/new"
22
        "github.com/supabase/cli/internal/migration/repair"
23
        "github.com/supabase/cli/internal/utils"
24
        "github.com/supabase/cli/pkg/migration"
25
)
26

27
var (
28
        errMissing     = errors.New("No migrations found")
29
        errInSync      = errors.New("No schema changes found")
30
        errConflict    = errors.Errorf("The remote database's migration history does not match local files in %s directory.", utils.MigrationsDir)
31
        managedSchemas = []string{"auth", "storage", "realtime"}
32
)
33

34
func Run(ctx context.Context, schema []string, config pgconn.Config, name string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
2✔
35
        // 1. Check postgres connection
2✔
36
        conn, err := utils.ConnectByConfig(ctx, config, options...)
2✔
37
        if err != nil {
3✔
38
                return err
1✔
39
        }
1✔
40
        defer conn.Close(context.Background())
1✔
41
        // 2. Pull schema
1✔
42
        timestamp := utils.GetCurrentTimestamp()
1✔
43
        path := new.GetMigrationPath(timestamp, name)
1✔
44
        if err := run(ctx, schema, path, conn, fsys); err != nil {
2✔
45
                return err
1✔
46
        }
1✔
47
        // 3. Insert a row to `schema_migrations`
48
        fmt.Fprintln(os.Stderr, "Schema written to "+utils.Bold(path))
×
49
        if shouldUpdate, err := utils.NewConsole().PromptYesNo(ctx, "Update remote migration history table?", true); err != nil {
×
50
                return err
×
51
        } else if shouldUpdate {
×
52
                return repair.UpdateMigrationTable(ctx, conn, []string{timestamp}, repair.Applied, false, fsys)
×
53
        }
×
54
        return nil
×
55
}
56

57
func run(ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys afero.Fs) error {
4✔
58
        config := conn.Config().Config
4✔
59
        // 1. Assert `supabase/migrations` and `schema_migrations` are in sync.
4✔
60
        if err := assertRemoteInSync(ctx, conn, fsys); errors.Is(err, errMissing) {
5✔
61
                return CloneRemoteSchema(ctx, path, config, fsys)
1✔
62
        } else if err != nil {
5✔
63
                return err
1✔
64
        }
1✔
65
        // 2. Fetch user defined schemas
66
        if len(schema) == 0 {
3✔
67
                var err error
1✔
68
                if schema, err = migration.ListUserSchemas(ctx, conn); err != nil {
2✔
69
                        return err
1✔
70
                }
1✔
71
                schema = append(schema, managedSchemas...)
×
72
        }
73
        // 3. Fetch remote schema changes
74
        return diffUserSchemas(ctx, schema, path, config, fsys)
1✔
75
}
76

77
func CloneRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
1✔
78
        // Ignore schemas flag when working on the initial pull
1✔
79
        if err := dumpRemoteSchema(ctx, path, config, fsys); err != nil {
1✔
NEW
80
                return err
×
NEW
81
        }
×
82
        // Pull changes in managed schemas automatically
83
        if err := diffRemoteSchema(ctx, managedSchemas, path, config, fsys); err != nil && !errors.Is(err, errInSync) {
2✔
84
                return err
1✔
85
        }
1✔
NEW
86
        return nil
×
87
}
88

89
func dumpRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
1✔
90
        // Special case if this is the first migration
1✔
91
        fmt.Fprintln(os.Stderr, "Dumping schema from remote database...")
1✔
92
        if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil {
1✔
93
                return err
×
94
        }
×
95
        f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
1✔
96
        if err != nil {
1✔
97
                return errors.Errorf("failed to open dump file: %w", err)
×
98
        }
×
99
        defer f.Close()
1✔
100
        return migration.DumpSchema(ctx, config, f, dump.DockerExec)
1✔
101
}
102

103
func diffRemoteSchema(ctx context.Context, schema []string, path string, config pgconn.Config, fsys afero.Fs) error {
1✔
104
        // Diff remote db (source) & shadow db (target) and write it as a new migration.
1✔
105
        output, err := diff.DiffDatabase(ctx, schema, config, os.Stderr, fsys, diff.DiffSchemaMigra)
1✔
106
        if err != nil {
2✔
107
                return err
1✔
108
        }
1✔
109
        if len(output) == 0 {
×
110
                return errors.New(errInSync)
×
111
        }
×
112
        // Append to existing migration file since we run this after dump
113
        f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
×
114
        if err != nil {
×
115
                return errors.Errorf("failed to open migration file: %w", err)
×
116
        }
×
117
        defer f.Close()
×
118
        if _, err := f.WriteString(output); err != nil {
×
119
                return errors.Errorf("failed to write migration file: %w", err)
×
120
        }
×
121
        return nil
×
122
}
123

124
func diffUserSchemas(ctx context.Context, schema []string, path string, config pgconn.Config, fsys afero.Fs) error {
1✔
125
        var managed, user []string
1✔
126
        for _, s := range schema {
2✔
127
                if utils.SliceContains(managedSchemas, s) {
1✔
128
                        managed = append(managed, s)
×
129
                } else {
1✔
130
                        user = append(user, s)
1✔
131
                }
1✔
132
        }
133
        fmt.Fprintln(os.Stderr, "Creating shadow database...")
1✔
134
        shadow, err := diff.CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort)
1✔
135
        if err != nil {
2✔
136
                return err
1✔
137
        }
1✔
138
        defer utils.DockerRemove(shadow)
×
139
        if err := start.WaitForHealthyService(ctx, start.HealthTimeout, shadow); err != nil {
×
140
                return err
×
141
        }
×
142
        if err := diff.MigrateShadowDatabase(ctx, shadow, fsys); err != nil {
×
143
                return err
×
144
        }
×
145
        shadowConfig := pgconn.Config{
×
146
                Host:     utils.Config.Hostname,
×
147
                Port:     utils.Config.Db.ShadowPort,
×
148
                User:     "postgres",
×
149
                Password: utils.Config.Db.Password,
×
150
                Database: "postgres",
×
151
        }
×
152
        // Diff managed and user defined schemas separately
×
153
        var output string
×
154
        if len(user) > 0 {
×
155
                fmt.Fprintln(os.Stderr, "Diffing schemas:", strings.Join(user, ","))
×
156
                if output, err = diff.DiffSchemaMigraBash(ctx, shadowConfig, config, user); err != nil {
×
157
                        return err
×
158
                }
×
159
        }
160
        if len(managed) > 0 {
×
161
                fmt.Fprintln(os.Stderr, "Diffing schemas:", strings.Join(managed, ","))
×
162
                if result, err := diff.DiffSchemaMigra(ctx, shadowConfig, config, managed); err != nil {
×
163
                        return err
×
164
                } else {
×
165
                        output += result
×
166
                }
×
167
        }
168
        if len(output) == 0 {
×
169
                return errors.New(errInSync)
×
170
        }
×
171
        if err := utils.WriteFile(path, []byte(output), fsys); err != nil {
×
172
                return errors.Errorf("failed to write dump file: %w", err)
×
173
        }
×
174
        return nil
×
175
}
176

177
func assertRemoteInSync(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
8✔
178
        remoteMigrations, err := migration.ListRemoteMigrations(ctx, conn)
8✔
179
        if err != nil {
9✔
180
                return err
1✔
181
        }
1✔
182
        localMigrations, err := list.LoadLocalVersions(fsys)
7✔
183
        if err != nil {
8✔
184
                return err
1✔
185
        }
1✔
186
        // Find any mismatch between local and remote migrations
187
        var extraRemote, extraLocal []string
6✔
188
        for i, j := 0, 0; i < len(remoteMigrations) || j < len(localMigrations); {
11✔
189
                remoteTimestamp := math.MaxInt
5✔
190
                if i < len(remoteMigrations) {
9✔
191
                        if remoteTimestamp, err = strconv.Atoi(remoteMigrations[i]); err != nil {
4✔
192
                                i++
×
193
                                continue
×
194
                        }
195
                }
196
                localTimestamp := math.MaxInt
5✔
197
                if j < len(localMigrations) {
9✔
198
                        if localTimestamp, err = strconv.Atoi(localMigrations[j]); err != nil {
4✔
199
                                j++
×
200
                                continue
×
201
                        }
202
                }
203
                // Top to bottom chronological order
204
                if localTimestamp < remoteTimestamp {
7✔
205
                        extraLocal = append(extraLocal, localMigrations[j])
2✔
206
                        j++
2✔
207
                } else if remoteTimestamp < localTimestamp {
6✔
208
                        extraRemote = append(extraRemote, remoteMigrations[i])
1✔
209
                        i++
1✔
210
                } else {
3✔
211
                        i++
2✔
212
                        j++
2✔
213
                }
2✔
214
        }
215
        // Suggest delete local migrations / reset migration history
216
        if len(extraRemote)+len(extraLocal) > 0 {
8✔
217
                utils.CmdSuggestion = suggestMigrationRepair(extraRemote, extraLocal)
2✔
218
                return errors.New(errConflict)
2✔
219
        }
2✔
220
        if len(localMigrations) == 0 {
6✔
221
                return errors.New(errMissing)
2✔
222
        }
2✔
223
        return nil
2✔
224
}
225

226
func suggestMigrationRepair(extraRemote, extraLocal []string) string {
2✔
227
        result := fmt.Sprintln("\nMake sure your local git repo is up-to-date. If the error persists, try repairing the migration history table:")
2✔
228
        for _, version := range extraRemote {
3✔
229
                result += fmt.Sprintln(utils.Bold("supabase migration repair --status reverted " + version))
1✔
230
        }
1✔
231
        for _, version := range extraLocal {
4✔
232
                result += fmt.Sprintln(utils.Bold("supabase migration repair --status applied " + version))
2✔
233
        }
2✔
234
        return result
2✔
235
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc