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  }