github.com/supabase/cli@v1.168.1/internal/db/diff/diff.go (about) 1 package diff 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 "io" 8 "io/fs" 9 "os" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/cenkalti/backoff/v4" 17 "github.com/docker/docker/api/types/container" 18 "github.com/docker/docker/api/types/network" 19 "github.com/docker/go-connections/nat" 20 "github.com/go-errors/errors" 21 "github.com/jackc/pgconn" 22 "github.com/jackc/pgx/v4" 23 "github.com/spf13/afero" 24 "github.com/supabase/cli/internal/db/reset" 25 "github.com/supabase/cli/internal/db/start" 26 "github.com/supabase/cli/internal/gen/keys" 27 "github.com/supabase/cli/internal/migration/apply" 28 "github.com/supabase/cli/internal/migration/list" 29 "github.com/supabase/cli/internal/migration/repair" 30 "github.com/supabase/cli/internal/utils" 31 "github.com/supabase/cli/internal/utils/parser" 32 ) 33 34 type DiffFunc func(context.Context, string, string, []string) (string, error) 35 36 func Run(ctx context.Context, schema []string, file string, config pgconn.Config, differ DiffFunc, fsys afero.Fs, options ...func(*pgx.ConnConfig)) (err error) { 37 // Sanity checks. 38 if err := utils.LoadConfigFS(fsys); err != nil { 39 return err 40 } 41 if utils.IsLocalDatabase(config) { 42 if container, err := createShadowIfNotExists(ctx, fsys); err != nil { 43 return err 44 } else if len(container) > 0 { 45 defer utils.DockerRemove(container) 46 if !start.WaitForHealthyService(ctx, container, start.HealthTimeout) { 47 return errors.New(start.ErrDatabase) 48 } 49 if err := migrateBaseDatabase(ctx, container, fsys, options...); err != nil { 50 return err 51 } 52 } 53 } 54 // 1. Load all user defined schemas 55 if len(schema) == 0 { 56 schema, err = loadSchema(ctx, config, options...) 57 if err != nil { 58 return err 59 } 60 } 61 // 3. Run migra to diff schema 62 out, err := DiffDatabase(ctx, schema, config, os.Stderr, fsys, differ, options...) 63 if err != nil { 64 return err 65 } 66 branch := keys.GetGitBranch(fsys) 67 fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db diff")+" on branch "+utils.Aqua(branch)+".\n") 68 if err := SaveDiff(out, file, fsys); err != nil { 69 return err 70 } 71 drops := findDropStatements(out) 72 if len(drops) > 0 { 73 fmt.Fprintln(os.Stderr, "Found drop statements in schema diff. Please double check if these are expected:") 74 fmt.Fprintln(os.Stderr, utils.Yellow(strings.Join(drops, "\n"))) 75 } 76 return nil 77 } 78 79 func createShadowIfNotExists(ctx context.Context, fsys afero.Fs) (string, error) { 80 if exists, err := afero.DirExists(fsys, utils.SchemasDir); err != nil { 81 return "", errors.Errorf("failed to check schemas: %w", err) 82 } else if !exists { 83 return "", nil 84 } 85 if err := utils.AssertSupabaseDbIsRunning(); !errors.Is(err, utils.ErrNotRunning) { 86 return "", err 87 } 88 fmt.Fprintf(os.Stderr, "Creating local database from %s...\n", utils.Bold(utils.SchemasDir)) 89 return CreateShadowDatabase(ctx, utils.Config.Db.Port) 90 } 91 92 func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) { 93 var declared []string 94 if err := afero.Walk(fsys, utils.SchemasDir, func(path string, info fs.FileInfo, err error) error { 95 if err != nil { 96 return err 97 } 98 if info.Mode().IsRegular() && filepath.Ext(info.Name()) == ".sql" { 99 declared = append(declared, path) 100 } 101 return nil 102 }); err != nil { 103 return nil, errors.Errorf("failed to walk dir: %w", err) 104 } 105 return declared, nil 106 } 107 108 // https://github.com/djrobstep/migra/blob/master/migra/statements.py#L6 109 var dropStatementPattern = regexp.MustCompile(`(?i)drop\s+`) 110 111 func findDropStatements(out string) []string { 112 lines, err := parser.SplitAndTrim(strings.NewReader(out)) 113 if err != nil { 114 return nil 115 } 116 var drops []string 117 for _, line := range lines { 118 if dropStatementPattern.MatchString(line) { 119 drops = append(drops, line) 120 } 121 } 122 return drops 123 } 124 125 func loadSchema(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) ([]string, error) { 126 conn, err := utils.ConnectByConfig(ctx, config, options...) 127 if err != nil { 128 return nil, err 129 } 130 defer conn.Close(context.Background()) 131 // RLS policies in auth and storage schemas can be included with -s flag 132 return reset.LoadUserSchemas(ctx, conn) 133 } 134 135 func CreateShadowDatabase(ctx context.Context, port uint16) (string, error) { 136 config := start.NewContainerConfig() 137 hostPort := strconv.FormatUint(uint64(port), 10) 138 hostConfig := container.HostConfig{ 139 PortBindings: nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}}, 140 AutoRemove: true, 141 } 142 networkingConfig := network.NetworkingConfig{} 143 if utils.Config.Db.MajorVersion <= 14 { 144 config.Entrypoint = nil 145 hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""} 146 } 147 return utils.DockerStart(ctx, config, hostConfig, networkingConfig, "") 148 } 149 150 func ConnectShadowDatabase(ctx context.Context, timeout time.Duration, options ...func(*pgx.ConnConfig)) (conn *pgx.Conn, err error) { 151 // Retry until connected, cancelled, or timeout 152 policy := backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Second), uint64(timeout.Seconds())) 153 config := pgconn.Config{Port: utils.Config.Db.ShadowPort} 154 connect := func() (*pgx.Conn, error) { 155 return utils.ConnectLocalPostgres(ctx, config, options...) 156 } 157 return backoff.RetryWithData(connect, backoff.WithContext(policy, ctx)) 158 } 159 160 func MigrateShadowDatabase(ctx context.Context, container string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { 161 migrations, err := list.LoadLocalMigrations(fsys) 162 if err != nil { 163 return err 164 } 165 conn, err := ConnectShadowDatabase(ctx, 10*time.Second, options...) 166 if err != nil { 167 return err 168 } 169 defer conn.Close(context.Background()) 170 if err := start.SetupDatabase(ctx, conn, container[:12], os.Stderr, fsys); err != nil { 171 return err 172 } 173 return apply.MigrateUp(ctx, conn, migrations, fsys) 174 } 175 176 func migrateBaseDatabase(ctx context.Context, container string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { 177 migrations, err := loadDeclaredSchemas(fsys) 178 if err != nil { 179 return err 180 } 181 conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...) 182 if err != nil { 183 return err 184 } 185 defer conn.Close(context.Background()) 186 if err := start.SetupDatabase(ctx, conn, container[:12], os.Stderr, fsys); err != nil { 187 return err 188 } 189 for _, path := range migrations { 190 fmt.Fprintln(os.Stderr, "Applying schema "+utils.Bold(path)+"...") 191 migration, err := repair.NewMigrationFromFile(path, fsys) 192 if err != nil { 193 return err 194 } 195 if err := migration.ExecBatch(ctx, conn); err != nil { 196 return err 197 } 198 } 199 return nil 200 } 201 202 func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w io.Writer, fsys afero.Fs, differ func(context.Context, string, string, []string) (string, error), options ...func(*pgx.ConnConfig)) (string, error) { 203 fmt.Fprintln(w, "Creating shadow database...") 204 shadow, err := CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort) 205 if err != nil { 206 return "", err 207 } 208 defer utils.DockerRemove(shadow) 209 if !start.WaitForHealthyService(ctx, shadow, start.HealthTimeout) { 210 return "", errors.New(start.ErrDatabase) 211 } 212 if err := MigrateShadowDatabase(ctx, shadow, fsys, options...); err != nil { 213 return "", err 214 } 215 fmt.Fprintln(w, "Diffing schemas:", strings.Join(schema, ",")) 216 source := utils.ToPostgresURL(pgconn.Config{ 217 Host: utils.Config.Hostname, 218 Port: utils.Config.Db.ShadowPort, 219 User: "postgres", 220 Password: utils.Config.Db.Password, 221 Database: "postgres", 222 }) 223 target := utils.ToPostgresURL(config) 224 return differ(ctx, source, target, schema) 225 }