github.com/supabase/cli@v1.168.1/internal/migration/list/list.go (about) 1 package list 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "os" 8 "regexp" 9 "strconv" 10 "time" 11 12 "github.com/charmbracelet/glamour" 13 "github.com/go-errors/errors" 14 "github.com/jackc/pgconn" 15 "github.com/jackc/pgerrcode" 16 "github.com/jackc/pgx/v4" 17 "github.com/spf13/afero" 18 "github.com/spf13/viper" 19 "github.com/supabase/cli/internal/utils" 20 "github.com/supabase/cli/internal/utils/pgxv5" 21 ) 22 23 const LIST_MIGRATION_VERSION = "SELECT version FROM supabase_migrations.schema_migrations ORDER BY version" 24 25 var initSchemaPattern = regexp.MustCompile(`([0-9]{14})_init\.sql`) 26 27 func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { 28 remoteVersions, err := loadRemoteVersions(ctx, config, options...) 29 if err != nil { 30 return err 31 } 32 localVersions, err := LoadLocalVersions(fsys) 33 if err != nil { 34 return err 35 } 36 table := makeTable(remoteVersions, localVersions) 37 return RenderTable(table) 38 } 39 40 func loadRemoteVersions(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) ([]string, error) { 41 conn, err := utils.ConnectByConfig(ctx, config, options...) 42 if err != nil { 43 return nil, err 44 } 45 defer conn.Close(context.Background()) 46 return LoadRemoteMigrations(ctx, conn) 47 } 48 49 func LoadRemoteMigrations(ctx context.Context, conn *pgx.Conn) ([]string, error) { 50 versions, err := listMigrationVersions(ctx, conn) 51 if err != nil { 52 var pgErr *pgconn.PgError 53 if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UndefinedTable { 54 // If migration history table is undefined, the remote project has no migrations 55 return nil, nil 56 } 57 } 58 return versions, err 59 } 60 61 func listMigrationVersions(ctx context.Context, conn *pgx.Conn) ([]string, error) { 62 rows, err := conn.Query(ctx, LIST_MIGRATION_VERSION) 63 if err != nil { 64 return nil, errors.Errorf("failed to query rows: %w", err) 65 } 66 return pgxv5.CollectStrings(rows) 67 } 68 69 const ( 70 layoutVersion = "20060102150405" 71 layoutHuman = "2006-01-02 15:04:05" 72 ) 73 74 func formatTimestamp(version string) string { 75 timestamp, err := time.Parse(layoutVersion, version) 76 if err != nil { 77 if viper.GetBool("DEBUG") { 78 fmt.Fprintln(os.Stderr, err) 79 } 80 return version 81 } 82 return timestamp.Format(layoutHuman) 83 } 84 85 func makeTable(remoteMigrations, localMigrations []string) string { 86 var err error 87 table := "|Local|Remote|Time (UTC)|\n|-|-|-|\n" 88 for i, j := 0, 0; i < len(remoteMigrations) || j < len(localMigrations); { 89 remoteTimestamp := math.MaxInt 90 if i < len(remoteMigrations) { 91 if remoteTimestamp, err = strconv.Atoi(remoteMigrations[i]); err != nil { 92 i++ 93 continue 94 } 95 } 96 localTimestamp := math.MaxInt 97 if j < len(localMigrations) { 98 if localTimestamp, err = strconv.Atoi(localMigrations[j]); err != nil { 99 j++ 100 continue 101 } 102 } 103 // Top to bottom chronological order 104 if localTimestamp < remoteTimestamp { 105 table += fmt.Sprintf("|`%s`|` `|`%s`|\n", localMigrations[j], formatTimestamp(localMigrations[j])) 106 j++ 107 } else if remoteTimestamp < localTimestamp { 108 table += fmt.Sprintf("|` `|`%s`|`%s`|\n", remoteMigrations[i], formatTimestamp(remoteMigrations[i])) 109 i++ 110 } else { 111 table += fmt.Sprintf("|`%s`|`%s`|`%s`|\n", localMigrations[j], remoteMigrations[i], formatTimestamp(remoteMigrations[i])) 112 i++ 113 j++ 114 } 115 } 116 return table 117 } 118 119 func RenderTable(markdown string) error { 120 r, err := glamour.NewTermRenderer( 121 glamour.WithAutoStyle(), 122 glamour.WithWordWrap(-1), 123 ) 124 if err != nil { 125 return errors.Errorf("failed to initialise terminal renderer: %w", err) 126 } 127 out, err := r.Render(markdown) 128 if err != nil { 129 return errors.Errorf("failed to render markdown: %w", err) 130 } 131 fmt.Print(out) 132 return nil 133 } 134 135 func LoadLocalVersions(fsys afero.Fs) ([]string, error) { 136 names, err := LoadLocalMigrations(fsys) 137 if err != nil { 138 return nil, err 139 } 140 var versions []string 141 for _, filename := range names { 142 // LoadLocalMigrations guarantees we always have a match 143 version := utils.MigrateFilePattern.FindStringSubmatch(filename)[1] 144 versions = append(versions, version) 145 } 146 return versions, nil 147 } 148 149 func LoadLocalMigrations(fsys afero.Fs) ([]string, error) { 150 return LoadPartialMigrations("", fsys) 151 } 152 153 func LoadPartialMigrations(version string, fsys afero.Fs) ([]string, error) { 154 localMigrations, err := afero.ReadDir(fsys, utils.MigrationsDir) 155 if err != nil && !errors.Is(err, os.ErrNotExist) { 156 return nil, errors.Errorf("failed to read directory: %w", err) 157 } 158 var names []string 159 for i, migration := range localMigrations { 160 filename := migration.Name() 161 if i == 0 && shouldSkip(filename) { 162 fmt.Fprintln(os.Stderr, "Skipping migration "+utils.Bold(filename)+`... (replace "init" with a different file name to apply this migration)`) 163 continue 164 } 165 matches := utils.MigrateFilePattern.FindStringSubmatch(filename) 166 if len(matches) == 0 { 167 fmt.Fprintln(os.Stderr, "Skipping migration "+utils.Bold(filename)+`... (file name must match pattern "<timestamp>_name.sql")`) 168 continue 169 } 170 names = append(names, filename) 171 if matches[1] == version { 172 break 173 } 174 } 175 return names, nil 176 } 177 178 func shouldSkip(name string) bool { 179 // NOTE: To handle backward-compatibility. `<timestamp>_init.sql` as 180 // the first migration (prev versions of the CLI) is deprecated. 181 matches := initSchemaPattern.FindStringSubmatch(name) 182 if len(matches) == 2 { 183 if timestamp, err := strconv.ParseUint(matches[1], 10, 64); err == nil && timestamp < 20211209000000 { 184 return true 185 } 186 } 187 return false 188 }