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  }