190 lines
5.6 KiB
Go
190 lines
5.6 KiB
Go
package squash
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/jackc/pgconn"
|
|
"github.com/jackc/pgx/v4"
|
|
"github.com/spf13/afero"
|
|
"github.com/supabase/cli/internal/db/diff"
|
|
"github.com/supabase/cli/internal/db/dump"
|
|
"github.com/supabase/cli/internal/db/start"
|
|
"github.com/supabase/cli/internal/migration/list"
|
|
"github.com/supabase/cli/internal/migration/repair"
|
|
"github.com/supabase/cli/internal/utils"
|
|
"github.com/supabase/cli/pkg/migration"
|
|
)
|
|
|
|
var ErrMissingVersion = errors.New("version not found")
|
|
|
|
func Run(ctx context.Context, version string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
|
|
if len(version) > 0 {
|
|
if _, err := strconv.Atoi(version); err != nil {
|
|
return errors.New(repair.ErrInvalidVersion)
|
|
}
|
|
if _, err := repair.GetMigrationFile(version, fsys); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// 1. Squash local migrations
|
|
if err := squashToVersion(ctx, version, fsys, options...); err != nil {
|
|
return err
|
|
}
|
|
// 2. Update migration history
|
|
if utils.IsLocalDatabase(config) {
|
|
return nil
|
|
}
|
|
if shouldUpdate, err := utils.NewConsole().PromptYesNo(ctx, "Update remote migration history table?", true); err != nil {
|
|
return err
|
|
} else if !shouldUpdate {
|
|
return nil
|
|
}
|
|
return baselineMigrations(ctx, config, version, fsys, options...)
|
|
}
|
|
|
|
func squashToVersion(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
|
|
migrations, err := list.LoadPartialMigrations(version, fsys)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(migrations) == 0 {
|
|
return errors.New(ErrMissingVersion)
|
|
}
|
|
// Migrate to target version and dump
|
|
local := migrations[len(migrations)-1]
|
|
if len(migrations) == 1 {
|
|
fmt.Fprintln(os.Stderr, utils.Bold(local), "is already the earliest migration.")
|
|
return nil
|
|
}
|
|
if err := squashMigrations(ctx, migrations, fsys, options...); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(os.Stderr, "Squashed local migrations to", utils.Bold(local))
|
|
// Remove merged files
|
|
for _, path := range migrations[:len(migrations)-1] {
|
|
if err := fsys.Remove(path); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func squashMigrations(ctx context.Context, migrations []string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
|
|
// 1. Start shadow database
|
|
shadow, err := diff.CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer utils.DockerRemove(shadow)
|
|
if err := start.WaitForHealthyService(ctx, start.HealthTimeout, shadow); err != nil {
|
|
return err
|
|
}
|
|
conn, err := diff.ConnectShadowDatabase(ctx, 10*time.Second, options...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close(context.Background())
|
|
if err := start.SetupDatabase(ctx, conn, shadow[:12], os.Stderr, fsys); err != nil {
|
|
return err
|
|
}
|
|
// Assuming entities in managed schemas are not altered, we can simply diff the dumps before and after migrations.
|
|
schemas := []string{"auth", "storage"}
|
|
config := pgconn.Config{
|
|
Host: utils.Config.Hostname,
|
|
Port: utils.Config.Db.ShadowPort,
|
|
User: "postgres",
|
|
Password: utils.Config.Db.Password,
|
|
Database: "postgres",
|
|
}
|
|
var before, after bytes.Buffer
|
|
if err := dump.DumpSchema(ctx, config, schemas, false, false, &before); err != nil {
|
|
return err
|
|
}
|
|
// 2. Migrate to target version
|
|
if err := migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys)); err != nil {
|
|
return err
|
|
}
|
|
if err := dump.DumpSchema(ctx, config, schemas, false, false, &after); err != nil {
|
|
return err
|
|
}
|
|
// 3. Dump migrated schema
|
|
path := migrations[len(migrations)-1]
|
|
f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return errors.Errorf("failed to open migration file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
if err := dump.DumpSchema(ctx, config, nil, false, false, f); err != nil {
|
|
return err
|
|
}
|
|
// 4. Append managed schema diffs
|
|
fmt.Fprint(f, separatorComment)
|
|
return lineByLineDiff(&before, &after, f)
|
|
}
|
|
|
|
const separatorComment = `
|
|
--
|
|
-- Dumped schema changes for auth and storage
|
|
--
|
|
|
|
`
|
|
|
|
func lineByLineDiff(before, after io.Reader, f io.Writer) error {
|
|
anchor := bufio.NewScanner(before)
|
|
anchor.Scan()
|
|
// Assuming before is always a subset of after
|
|
scanner := bufio.NewScanner(after)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if line == anchor.Text() {
|
|
anchor.Scan()
|
|
continue
|
|
}
|
|
if _, err := fmt.Fprintln(f, line); err != nil {
|
|
return errors.Errorf("failed to write line: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func baselineMigrations(ctx context.Context, config pgconn.Config, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
|
|
if len(version) == 0 {
|
|
// Expecting no errors here because the caller should have handled them
|
|
if localVersions, err := list.LoadLocalVersions(fsys); len(localVersions) > 0 {
|
|
version = localVersions[0]
|
|
} else if err != nil {
|
|
logger := utils.GetDebugLogger()
|
|
fmt.Fprintln(logger, err)
|
|
}
|
|
}
|
|
fmt.Fprintln(os.Stderr, "Baselining migration history to", version)
|
|
conn, err := utils.ConnectByConfig(ctx, config, options...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close(context.Background())
|
|
if err := migration.CreateMigrationTable(ctx, conn); err != nil {
|
|
return err
|
|
}
|
|
m, err := repair.NewMigrationFromVersion(version, fsys)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Data statements don't mutate schemas, safe to use statement cache
|
|
batch := pgx.Batch{}
|
|
batch.Queue(migration.DELETE_MIGRATION_BEFORE, m.Version)
|
|
batch.Queue(migration.INSERT_MIGRATION_VERSION, m.Version, m.Name, m.Statements)
|
|
if err := conn.SendBatch(ctx, &batch).Close(); err != nil {
|
|
return errors.Errorf("failed to update migration history: %w", err)
|
|
}
|
|
return nil
|
|
}
|