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

     1  package dump
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/docker/docker/api/types/container"
    12  	"github.com/docker/docker/api/types/network"
    13  	"github.com/go-errors/errors"
    14  	"github.com/jackc/pgconn"
    15  	"github.com/spf13/afero"
    16  	"github.com/supabase/cli/internal/utils"
    17  )
    18  
    19  var (
    20  	//go:embed templates/dump_schema.sh
    21  	dumpSchemaScript string
    22  	//go:embed templates/dump_data.sh
    23  	dumpDataScript string
    24  	//go:embed templates/dump_role.sh
    25  	dumpRoleScript string
    26  )
    27  
    28  func Run(ctx context.Context, path string, config pgconn.Config, schema, excludeTable []string, dataOnly, roleOnly, keepComments, useCopy, dryRun bool, fsys afero.Fs) error {
    29  	// Initialize output stream
    30  	var outStream afero.File
    31  	if len(path) > 0 {
    32  		f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
    33  		if err != nil {
    34  			return errors.Errorf("failed to open dump file: %w", err)
    35  		}
    36  		defer f.Close()
    37  		outStream = f
    38  	} else {
    39  		outStream = os.Stdout
    40  	}
    41  	// Load the requested script
    42  	if dryRun {
    43  		fmt.Fprintln(os.Stderr, "DRY RUN: *only* printing the pg_dump script to console.")
    44  	}
    45  	db := "remote"
    46  	if utils.IsLocalDatabase(config) {
    47  		db = "local"
    48  	}
    49  	if dataOnly {
    50  		fmt.Fprintf(os.Stderr, "Dumping data from %s database...\n", db)
    51  		return dumpData(ctx, config, schema, excludeTable, useCopy, dryRun, outStream)
    52  	} else if roleOnly {
    53  		fmt.Fprintf(os.Stderr, "Dumping roles from %s database...\n", db)
    54  		return dumpRole(ctx, config, keepComments, dryRun, outStream)
    55  	}
    56  	fmt.Fprintf(os.Stderr, "Dumping schemas from %s database...\n", db)
    57  	return DumpSchema(ctx, config, schema, keepComments, dryRun, outStream)
    58  }
    59  
    60  func DumpSchema(ctx context.Context, config pgconn.Config, schema []string, keepComments, dryRun bool, stdout io.Writer) error {
    61  	var env []string
    62  	if len(schema) > 0 {
    63  		// Must append flag because empty string results in error
    64  		env = append(env, "EXTRA_FLAGS=--schema="+strings.Join(schema, "|"))
    65  	} else {
    66  		env = append(env, "EXCLUDED_SCHEMAS="+strings.Join(utils.InternalSchemas, "|"))
    67  	}
    68  	if !keepComments {
    69  		env = append(env, "EXTRA_SED=/^--/d")
    70  	}
    71  	return dump(ctx, config, dumpSchemaScript, env, dryRun, stdout)
    72  }
    73  
    74  func dumpData(ctx context.Context, config pgconn.Config, schema, excludeTable []string, useCopy, dryRun bool, stdout io.Writer) error {
    75  	// We want to dump user data in auth, storage, etc. for migrating to new project
    76  	excludedSchemas := []string{
    77  		"information_schema",
    78  		"pg_*", // Wildcard pattern follows pg_dump
    79  		// Owned by extensions
    80  		// "cron",
    81  		"graphql",
    82  		"graphql_public",
    83  		// "net",
    84  		// "pgsodium",
    85  		// "pgsodium_masks",
    86  		"pgtle",
    87  		"repack",
    88  		"tiger",
    89  		"tiger_data",
    90  		"timescaledb_*",
    91  		"_timescaledb_*",
    92  		"topology",
    93  		// "vault",
    94  		// Managed by Supabase
    95  		// "auth",
    96  		"extensions",
    97  		"pgbouncer",
    98  		"realtime",
    99  		"_realtime",
   100  		// "storage",
   101  		"_analytics",
   102  		// "supabase_functions",
   103  		"supabase_migrations",
   104  	}
   105  	var env []string
   106  	if len(schema) > 0 {
   107  		env = append(env, "INCLUDED_SCHEMAS="+strings.Join(schema, "|"))
   108  	} else {
   109  		env = append(env, "INCLUDED_SCHEMAS=*", "EXCLUDED_SCHEMAS="+strings.Join(excludedSchemas, "|"))
   110  	}
   111  	var extraFlags []string
   112  	if !useCopy {
   113  		extraFlags = append(extraFlags, "--column-inserts", "--rows-per-insert 100000")
   114  	}
   115  	for _, table := range excludeTable {
   116  		escaped := quoteUpperCase(table)
   117  		// Use separate flags to avoid error: too many dotted names
   118  		extraFlags = append(extraFlags, "--exclude-table "+escaped)
   119  	}
   120  	if len(extraFlags) > 0 {
   121  		env = append(env, "EXTRA_FLAGS="+strings.Join(extraFlags, " "))
   122  	}
   123  	return dump(ctx, config, dumpDataScript, env, dryRun, stdout)
   124  }
   125  
   126  func quoteUpperCase(table string) string {
   127  	escaped := strings.ReplaceAll(table, ".", `"."`)
   128  	return fmt.Sprintf(`"%s"`, escaped)
   129  }
   130  
   131  func dumpRole(ctx context.Context, config pgconn.Config, keepComments, dryRun bool, stdout io.Writer) error {
   132  	env := []string{}
   133  	if !keepComments {
   134  		env = append(env, "EXTRA_SED=/^--/d")
   135  	}
   136  	return dump(ctx, config, dumpRoleScript, env, dryRun, stdout)
   137  }
   138  
   139  func dump(ctx context.Context, config pgconn.Config, script string, env []string, dryRun bool, stdout io.Writer) error {
   140  	allEnvs := append(env,
   141  		"PGHOST="+config.Host,
   142  		fmt.Sprintf("PGPORT=%d", config.Port),
   143  		"PGUSER="+config.User,
   144  		"PGPASSWORD="+config.Password,
   145  		"PGDATABASE="+config.Database,
   146  		"RESERVED_ROLES="+strings.Join(utils.ReservedRoles, "|"),
   147  		"ALLOWED_CONFIGS="+strings.Join(utils.AllowedConfigs, "|"),
   148  	)
   149  	if dryRun {
   150  		envMap := make(map[string]string, len(allEnvs))
   151  		for _, e := range allEnvs {
   152  			index := strings.IndexByte(e, '=')
   153  			if index < 0 {
   154  				continue
   155  			}
   156  			envMap[e[:index]] = e[index+1:]
   157  		}
   158  		expanded := os.Expand(script, func(key string) string {
   159  			// Bash variable expansion is unsupported:
   160  			// https://github.com/golang/go/issues/47187
   161  			parts := strings.Split(key, ":")
   162  			value := envMap[parts[0]]
   163  			// Escape double quotes in env vars
   164  			return strings.ReplaceAll(value, `"`, `\"`)
   165  		})
   166  		fmt.Println(expanded)
   167  		return nil
   168  	}
   169  	return utils.DockerRunOnceWithConfig(
   170  		ctx,
   171  		container.Config{
   172  			Image: utils.Pg15Image,
   173  			Env:   allEnvs,
   174  			Cmd:   []string{"bash", "-c", script, "--"},
   175  		},
   176  		container.HostConfig{
   177  			NetworkMode: container.NetworkMode("host"),
   178  		},
   179  		network.NetworkingConfig{},
   180  		"",
   181  		stdout,
   182  		os.Stderr,
   183  	)
   184  }