github.com/supabase/cli@v1.168.1/internal/db/diff/diff.go (about)

     1  package diff
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/cenkalti/backoff/v4"
    17  	"github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/network"
    19  	"github.com/docker/go-connections/nat"
    20  	"github.com/go-errors/errors"
    21  	"github.com/jackc/pgconn"
    22  	"github.com/jackc/pgx/v4"
    23  	"github.com/spf13/afero"
    24  	"github.com/supabase/cli/internal/db/reset"
    25  	"github.com/supabase/cli/internal/db/start"
    26  	"github.com/supabase/cli/internal/gen/keys"
    27  	"github.com/supabase/cli/internal/migration/apply"
    28  	"github.com/supabase/cli/internal/migration/list"
    29  	"github.com/supabase/cli/internal/migration/repair"
    30  	"github.com/supabase/cli/internal/utils"
    31  	"github.com/supabase/cli/internal/utils/parser"
    32  )
    33  
    34  type DiffFunc func(context.Context, string, string, []string) (string, error)
    35  
    36  func Run(ctx context.Context, schema []string, file string, config pgconn.Config, differ DiffFunc, fsys afero.Fs, options ...func(*pgx.ConnConfig)) (err error) {
    37  	// Sanity checks.
    38  	if err := utils.LoadConfigFS(fsys); err != nil {
    39  		return err
    40  	}
    41  	if utils.IsLocalDatabase(config) {
    42  		if container, err := createShadowIfNotExists(ctx, fsys); err != nil {
    43  			return err
    44  		} else if len(container) > 0 {
    45  			defer utils.DockerRemove(container)
    46  			if !start.WaitForHealthyService(ctx, container, start.HealthTimeout) {
    47  				return errors.New(start.ErrDatabase)
    48  			}
    49  			if err := migrateBaseDatabase(ctx, container, fsys, options...); err != nil {
    50  				return err
    51  			}
    52  		}
    53  	}
    54  	// 1. Load all user defined schemas
    55  	if len(schema) == 0 {
    56  		schema, err = loadSchema(ctx, config, options...)
    57  		if err != nil {
    58  			return err
    59  		}
    60  	}
    61  	// 3. Run migra to diff schema
    62  	out, err := DiffDatabase(ctx, schema, config, os.Stderr, fsys, differ, options...)
    63  	if err != nil {
    64  		return err
    65  	}
    66  	branch := keys.GetGitBranch(fsys)
    67  	fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db diff")+" on branch "+utils.Aqua(branch)+".\n")
    68  	if err := SaveDiff(out, file, fsys); err != nil {
    69  		return err
    70  	}
    71  	drops := findDropStatements(out)
    72  	if len(drops) > 0 {
    73  		fmt.Fprintln(os.Stderr, "Found drop statements in schema diff. Please double check if these are expected:")
    74  		fmt.Fprintln(os.Stderr, utils.Yellow(strings.Join(drops, "\n")))
    75  	}
    76  	return nil
    77  }
    78  
    79  func createShadowIfNotExists(ctx context.Context, fsys afero.Fs) (string, error) {
    80  	if exists, err := afero.DirExists(fsys, utils.SchemasDir); err != nil {
    81  		return "", errors.Errorf("failed to check schemas: %w", err)
    82  	} else if !exists {
    83  		return "", nil
    84  	}
    85  	if err := utils.AssertSupabaseDbIsRunning(); !errors.Is(err, utils.ErrNotRunning) {
    86  		return "", err
    87  	}
    88  	fmt.Fprintf(os.Stderr, "Creating local database from %s...\n", utils.Bold(utils.SchemasDir))
    89  	return CreateShadowDatabase(ctx, utils.Config.Db.Port)
    90  }
    91  
    92  func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) {
    93  	var declared []string
    94  	if err := afero.Walk(fsys, utils.SchemasDir, func(path string, info fs.FileInfo, err error) error {
    95  		if err != nil {
    96  			return err
    97  		}
    98  		if info.Mode().IsRegular() && filepath.Ext(info.Name()) == ".sql" {
    99  			declared = append(declared, path)
   100  		}
   101  		return nil
   102  	}); err != nil {
   103  		return nil, errors.Errorf("failed to walk dir: %w", err)
   104  	}
   105  	return declared, nil
   106  }
   107  
   108  // https://github.com/djrobstep/migra/blob/master/migra/statements.py#L6
   109  var dropStatementPattern = regexp.MustCompile(`(?i)drop\s+`)
   110  
   111  func findDropStatements(out string) []string {
   112  	lines, err := parser.SplitAndTrim(strings.NewReader(out))
   113  	if err != nil {
   114  		return nil
   115  	}
   116  	var drops []string
   117  	for _, line := range lines {
   118  		if dropStatementPattern.MatchString(line) {
   119  			drops = append(drops, line)
   120  		}
   121  	}
   122  	return drops
   123  }
   124  
   125  func loadSchema(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) ([]string, error) {
   126  	conn, err := utils.ConnectByConfig(ctx, config, options...)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	defer conn.Close(context.Background())
   131  	// RLS policies in auth and storage schemas can be included with -s flag
   132  	return reset.LoadUserSchemas(ctx, conn)
   133  }
   134  
   135  func CreateShadowDatabase(ctx context.Context, port uint16) (string, error) {
   136  	config := start.NewContainerConfig()
   137  	hostPort := strconv.FormatUint(uint64(port), 10)
   138  	hostConfig := container.HostConfig{
   139  		PortBindings: nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}},
   140  		AutoRemove:   true,
   141  	}
   142  	networkingConfig := network.NetworkingConfig{}
   143  	if utils.Config.Db.MajorVersion <= 14 {
   144  		config.Entrypoint = nil
   145  		hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""}
   146  	}
   147  	return utils.DockerStart(ctx, config, hostConfig, networkingConfig, "")
   148  }
   149  
   150  func ConnectShadowDatabase(ctx context.Context, timeout time.Duration, options ...func(*pgx.ConnConfig)) (conn *pgx.Conn, err error) {
   151  	// Retry until connected, cancelled, or timeout
   152  	policy := backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Second), uint64(timeout.Seconds()))
   153  	config := pgconn.Config{Port: utils.Config.Db.ShadowPort}
   154  	connect := func() (*pgx.Conn, error) {
   155  		return utils.ConnectLocalPostgres(ctx, config, options...)
   156  	}
   157  	return backoff.RetryWithData(connect, backoff.WithContext(policy, ctx))
   158  }
   159  
   160  func MigrateShadowDatabase(ctx context.Context, container string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
   161  	migrations, err := list.LoadLocalMigrations(fsys)
   162  	if err != nil {
   163  		return err
   164  	}
   165  	conn, err := ConnectShadowDatabase(ctx, 10*time.Second, options...)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	defer conn.Close(context.Background())
   170  	if err := start.SetupDatabase(ctx, conn, container[:12], os.Stderr, fsys); err != nil {
   171  		return err
   172  	}
   173  	return apply.MigrateUp(ctx, conn, migrations, fsys)
   174  }
   175  
   176  func migrateBaseDatabase(ctx context.Context, container string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
   177  	migrations, err := loadDeclaredSchemas(fsys)
   178  	if err != nil {
   179  		return err
   180  	}
   181  	conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	defer conn.Close(context.Background())
   186  	if err := start.SetupDatabase(ctx, conn, container[:12], os.Stderr, fsys); err != nil {
   187  		return err
   188  	}
   189  	for _, path := range migrations {
   190  		fmt.Fprintln(os.Stderr, "Applying schema "+utils.Bold(path)+"...")
   191  		migration, err := repair.NewMigrationFromFile(path, fsys)
   192  		if err != nil {
   193  			return err
   194  		}
   195  		if err := migration.ExecBatch(ctx, conn); err != nil {
   196  			return err
   197  		}
   198  	}
   199  	return nil
   200  }
   201  
   202  func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w io.Writer, fsys afero.Fs, differ func(context.Context, string, string, []string) (string, error), options ...func(*pgx.ConnConfig)) (string, error) {
   203  	fmt.Fprintln(w, "Creating shadow database...")
   204  	shadow, err := CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort)
   205  	if err != nil {
   206  		return "", err
   207  	}
   208  	defer utils.DockerRemove(shadow)
   209  	if !start.WaitForHealthyService(ctx, shadow, start.HealthTimeout) {
   210  		return "", errors.New(start.ErrDatabase)
   211  	}
   212  	if err := MigrateShadowDatabase(ctx, shadow, fsys, options...); err != nil {
   213  		return "", err
   214  	}
   215  	fmt.Fprintln(w, "Diffing schemas:", strings.Join(schema, ","))
   216  	source := utils.ToPostgresURL(pgconn.Config{
   217  		Host:     utils.Config.Hostname,
   218  		Port:     utils.Config.Db.ShadowPort,
   219  		User:     "postgres",
   220  		Password: utils.Config.Db.Password,
   221  		Database: "postgres",
   222  	})
   223  	target := utils.ToPostgresURL(config)
   224  	return differ(ctx, source, target, schema)
   225  }