github.com/supabase/cli@v1.168.1/internal/migration/squash/squash.go (about) 1 package squash 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "strconv" 12 "time" 13 14 "github.com/go-errors/errors" 15 "github.com/jackc/pgconn" 16 "github.com/jackc/pgx/v4" 17 "github.com/spf13/afero" 18 "github.com/supabase/cli/internal/db/diff" 19 "github.com/supabase/cli/internal/db/dump" 20 "github.com/supabase/cli/internal/db/start" 21 "github.com/supabase/cli/internal/migration/apply" 22 "github.com/supabase/cli/internal/migration/history" 23 "github.com/supabase/cli/internal/migration/list" 24 "github.com/supabase/cli/internal/migration/repair" 25 "github.com/supabase/cli/internal/utils" 26 ) 27 28 var ErrMissingVersion = errors.New("version not found") 29 30 func Run(ctx context.Context, version string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { 31 if len(version) > 0 { 32 if _, err := strconv.Atoi(version); err != nil { 33 return errors.New(repair.ErrInvalidVersion) 34 } 35 if _, err := repair.GetMigrationFile(version, fsys); err != nil { 36 return err 37 } 38 } 39 if err := utils.LoadConfigFS(fsys); err != nil { 40 return err 41 } 42 // 1. Squash local migrations 43 if err := squashToVersion(ctx, version, fsys, options...); err != nil { 44 return err 45 } 46 // 2. Update migration history 47 if utils.IsLocalDatabase(config) || !utils.NewConsole().PromptYesNo("Update remote migration history table?", true) { 48 return nil 49 } 50 return baselineMigrations(ctx, config, version, fsys, options...) 51 } 52 53 func squashToVersion(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { 54 migrations, err := list.LoadPartialMigrations(version, fsys) 55 if err != nil { 56 return err 57 } 58 if len(migrations) == 0 { 59 return errors.New(ErrMissingVersion) 60 } 61 // Migrate to target version and dump 62 path := filepath.Join(utils.MigrationsDir, migrations[len(migrations)-1]) 63 if len(migrations) == 1 { 64 fmt.Fprintln(os.Stderr, utils.Bold(path), "is already the earliest migration.") 65 return nil 66 } 67 if err := squashMigrations(ctx, migrations, fsys, options...); err != nil { 68 return err 69 } 70 fmt.Fprintln(os.Stderr, "Squashed local migrations to", utils.Bold(path)) 71 // Remove merged files 72 for _, name := range migrations[:len(migrations)-1] { 73 path := filepath.Join(utils.MigrationsDir, name) 74 if err := fsys.Remove(path); err != nil { 75 fmt.Fprintln(os.Stderr, err) 76 } 77 } 78 return nil 79 } 80 81 func squashMigrations(ctx context.Context, migrations []string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { 82 // 1. Start shadow database 83 shadow, err := diff.CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort) 84 if err != nil { 85 return err 86 } 87 defer utils.DockerRemove(shadow) 88 if !start.WaitForHealthyService(ctx, shadow, start.HealthTimeout) { 89 return errors.New(start.ErrDatabase) 90 } 91 conn, err := diff.ConnectShadowDatabase(ctx, 10*time.Second, options...) 92 if err != nil { 93 return err 94 } 95 defer conn.Close(context.Background()) 96 if err := start.SetupDatabase(ctx, conn, shadow[:12], os.Stderr, fsys); err != nil { 97 return err 98 } 99 // Assuming entities in managed schemas are not altered, we can simply diff the dumps before and after migrations. 100 schemas := []string{"auth", "storage"} 101 config := pgconn.Config{ 102 Host: utils.Config.Hostname, 103 Port: utils.Config.Db.ShadowPort, 104 User: "postgres", 105 Password: utils.Config.Db.Password, 106 Database: "postgres", 107 } 108 var before, after bytes.Buffer 109 if err := dump.DumpSchema(ctx, config, schemas, false, false, &before); err != nil { 110 return err 111 } 112 // 2. Migrate to target version 113 if err := apply.MigrateUp(ctx, conn, migrations, fsys); err != nil { 114 return err 115 } 116 if err := dump.DumpSchema(ctx, config, schemas, false, false, &after); err != nil { 117 return err 118 } 119 // 3. Dump migrated schema 120 path := filepath.Join(utils.MigrationsDir, migrations[len(migrations)-1]) 121 f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 122 if err != nil { 123 return errors.Errorf("failed to open migration file: %w", err) 124 } 125 defer f.Close() 126 if err := dump.DumpSchema(ctx, config, nil, false, false, f); err != nil { 127 return err 128 } 129 // 4. Append managed schema diffs 130 fmt.Fprint(f, separatorComment) 131 return lineByLineDiff(&before, &after, f) 132 } 133 134 const separatorComment = ` 135 -- 136 -- Dumped schema changes for auth and storage 137 -- 138 139 ` 140 141 func lineByLineDiff(before, after io.Reader, f io.Writer) error { 142 anchor := bufio.NewScanner(before) 143 anchor.Scan() 144 // Assuming before is always a subset of after 145 scanner := bufio.NewScanner(after) 146 for scanner.Scan() { 147 line := scanner.Text() 148 if line == anchor.Text() { 149 anchor.Scan() 150 continue 151 } 152 if _, err := fmt.Fprintln(f, line); err != nil { 153 return errors.Errorf("failed to write line: %w", err) 154 } 155 } 156 return nil 157 } 158 159 func baselineMigrations(ctx context.Context, config pgconn.Config, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { 160 if len(version) == 0 { 161 // Expecting no errors here because the caller should have handled them 162 if migrations, err := list.LoadPartialMigrations(version, fsys); len(migrations) > 0 { 163 if matches := utils.MigrateFilePattern.FindStringSubmatch(migrations[0]); len(matches) > 1 { 164 version = matches[1] 165 } 166 } else if err != nil { 167 logger := utils.GetDebugLogger() 168 fmt.Fprintln(logger, err) 169 } 170 } 171 fmt.Fprintln(os.Stderr, "Baselining migration history to", version) 172 conn, err := utils.ConnectByConfig(ctx, config, options...) 173 if err != nil { 174 return err 175 } 176 defer conn.Close(context.Background()) 177 if err := history.CreateMigrationTable(ctx, conn); err != nil { 178 return err 179 } 180 m, err := repair.NewMigrationFromVersion(version, fsys) 181 if err != nil { 182 return err 183 } 184 // Data statements don't mutate schemas, safe to use statement cache 185 batch := pgx.Batch{} 186 batch.Queue(history.DELETE_MIGRATION_BEFORE, m.Version) 187 batch.Queue(history.INSERT_MIGRATION_VERSION, m.Version, m.Name, m.Lines) 188 if err := conn.SendBatch(ctx, &batch).Close(); err != nil { 189 return errors.Errorf("failed to update migration history: %w", err) 190 } 191 return nil 192 }