github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/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/Redstoneguy129/cli/internal/utils" 13 "github.com/charmbracelet/glamour" 14 "github.com/jackc/pgx/v4" 15 "github.com/spf13/afero" 16 ) 17 18 const LIST_MIGRATION_VERSION = "SELECT version FROM supabase_migrations.schema_migrations ORDER BY version" 19 20 var initSchemaPattern = regexp.MustCompile(`([0-9]{14})_init\.sql`) 21 22 func Run(ctx context.Context, username, password, database, host string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { 23 remoteVersions, err := loadRemoteVersions(ctx, username, password, database, host, options...) 24 if err != nil { 25 return err 26 } 27 localVersions, err := loadLocalVersions(fsys) 28 if err != nil { 29 return err 30 } 31 return RenderTable(remoteVersions, localVersions) 32 } 33 34 func loadRemoteVersions(ctx context.Context, username, password, database, host string, options ...func(*pgx.ConnConfig)) ([]string, error) { 35 conn, err := utils.ConnectRemotePostgres(ctx, username, password, database, host, options...) 36 if err != nil { 37 return nil, err 38 } 39 defer conn.Close(context.Background()) 40 return LoadRemoteMigrations(ctx, conn) 41 } 42 43 func LoadRemoteMigrations(ctx context.Context, conn *pgx.Conn) ([]string, error) { 44 rows, err := conn.Query(ctx, LIST_MIGRATION_VERSION) 45 if err != nil { 46 return nil, err 47 } 48 versions := []string{} 49 for rows.Next() { 50 var version string 51 if err := rows.Scan(&version); err != nil { 52 return nil, err 53 } 54 versions = append(versions, version) 55 } 56 return versions, nil 57 } 58 59 const ( 60 layoutVersion = "20060102150405" 61 layoutHuman = "2006-01-02 15:04:05" 62 ) 63 64 func formatTimestamp(version string) string { 65 timestamp, err := time.Parse(layoutVersion, version) 66 if err != nil { 67 return version 68 } 69 return timestamp.Format(layoutHuman) 70 } 71 72 func makeTable(remoteMigrations, localMigrations []string) string { 73 var err error 74 table := "|Local|Remote|Time (UTC)|\n|-|-|-|\n" 75 for i, j := 0, 0; i < len(remoteMigrations) || j < len(localMigrations); { 76 remoteTimestamp := math.MaxInt 77 if i < len(remoteMigrations) { 78 if remoteTimestamp, err = strconv.Atoi(remoteMigrations[i]); err != nil { 79 i++ 80 continue 81 } 82 } 83 localTimestamp := math.MaxInt 84 if j < len(localMigrations) { 85 if localTimestamp, err = strconv.Atoi(localMigrations[j]); err != nil { 86 j++ 87 continue 88 } 89 } 90 // Top to bottom chronological order 91 if localTimestamp < remoteTimestamp { 92 table += fmt.Sprintf("|`%s`|` `|`%s`|\n", localMigrations[j], formatTimestamp(localMigrations[j])) 93 j++ 94 } else if remoteTimestamp < localTimestamp { 95 table += fmt.Sprintf("|` `|`%s`|`%s`|\n", remoteMigrations[i], formatTimestamp(remoteMigrations[i])) 96 i++ 97 } else { 98 table += fmt.Sprintf("|`%s`|`%s`|`%s`|\n", localMigrations[j], remoteMigrations[i], formatTimestamp(remoteMigrations[i])) 99 i++ 100 j++ 101 } 102 } 103 return table 104 } 105 106 func RenderTable(remoteVersions, localVersions []string) error { 107 table := makeTable(remoteVersions, localVersions) 108 r, err := glamour.NewTermRenderer( 109 glamour.WithAutoStyle(), 110 glamour.WithWordWrap(-1), 111 ) 112 if err != nil { 113 return err 114 } 115 out, err := r.Render(table) 116 if err != nil { 117 return err 118 } 119 fmt.Print(out) 120 return nil 121 } 122 123 func loadLocalVersions(fsys afero.Fs) ([]string, error) { 124 names, err := LoadLocalMigrations(fsys) 125 if err != nil { 126 return nil, err 127 } 128 var versions []string 129 for _, filename := range names { 130 // LoadLocalMigrations guarantees we always have a match 131 verion := utils.MigrateFilePattern.FindStringSubmatch(filename)[1] 132 versions = append(versions, verion) 133 } 134 return versions, nil 135 } 136 137 func LoadLocalMigrations(fsys afero.Fs) ([]string, error) { 138 if err := utils.MkdirIfNotExistFS(fsys, utils.MigrationsDir); err != nil { 139 return nil, err 140 } 141 localMigrations, err := afero.ReadDir(fsys, utils.MigrationsDir) 142 if err != nil { 143 return nil, err 144 } 145 var names []string 146 for i, migration := range localMigrations { 147 filename := migration.Name() 148 if i == 0 && shouldSkip(filename) { 149 fmt.Fprintln(os.Stderr, "Skipping migration "+utils.Bold(filename)+`... (replace "init" with a different file name to apply this migration)`) 150 continue 151 } 152 matches := utils.MigrateFilePattern.FindStringSubmatch(filename) 153 if len(matches) == 0 { 154 fmt.Fprintln(os.Stderr, "Skipping migration "+utils.Bold(filename)+`... (file name must match pattern "<timestamp>_name.sql")`) 155 continue 156 } 157 names = append(names, filename) 158 } 159 return names, nil 160 } 161 162 func shouldSkip(name string) bool { 163 // NOTE: To handle backward-compatibility. `<timestamp>_init.sql` as 164 // the first migration (prev versions of the CLI) is deprecated. 165 matches := initSchemaPattern.FindStringSubmatch(name) 166 if len(matches) == 2 { 167 if timestamp, err := strconv.ParseUint(matches[1], 10, 64); err == nil && timestamp < 20211209000000 { 168 return true 169 } 170 } 171 return false 172 }