189 lines
5.3 KiB
Go
189 lines
5.3 KiB
Go
package dump
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/network"
|
|
"github.com/go-errors/errors"
|
|
"github.com/jackc/pgconn"
|
|
"github.com/spf13/afero"
|
|
"github.com/supabase/cli/internal/utils"
|
|
cliConfig "github.com/supabase/cli/pkg/config"
|
|
)
|
|
|
|
var (
|
|
//go:embed templates/dump_schema.sh
|
|
dumpSchemaScript string
|
|
//go:embed templates/dump_data.sh
|
|
dumpDataScript string
|
|
//go:embed templates/dump_role.sh
|
|
dumpRoleScript string
|
|
)
|
|
|
|
func Run(ctx context.Context, path string, config pgconn.Config, schema, excludeTable []string, dataOnly, roleOnly, keepComments, useCopy, dryRun bool, fsys afero.Fs) error {
|
|
// Initialize output stream
|
|
var outStream afero.File
|
|
if len(path) > 0 {
|
|
f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return errors.Errorf("failed to open dump file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
outStream = f
|
|
} else {
|
|
outStream = os.Stdout
|
|
}
|
|
// Load the requested script
|
|
if dryRun {
|
|
fmt.Fprintln(os.Stderr, "DRY RUN: *only* printing the pg_dump script to console.")
|
|
}
|
|
db := "remote"
|
|
if utils.IsLocalDatabase(config) {
|
|
db = "local"
|
|
}
|
|
if dataOnly {
|
|
fmt.Fprintf(os.Stderr, "Dumping data from %s database...\n", db)
|
|
return dumpData(ctx, config, schema, excludeTable, useCopy, dryRun, outStream)
|
|
} else if roleOnly {
|
|
fmt.Fprintf(os.Stderr, "Dumping roles from %s database...\n", db)
|
|
return dumpRole(ctx, config, keepComments, dryRun, outStream)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Dumping schemas from %s database...\n", db)
|
|
return DumpSchema(ctx, config, schema, keepComments, dryRun, outStream)
|
|
}
|
|
|
|
func DumpSchema(ctx context.Context, config pgconn.Config, schema []string, keepComments, dryRun bool, stdout io.Writer) error {
|
|
var env []string
|
|
if len(schema) > 0 {
|
|
// Must append flag because empty string results in error
|
|
env = append(env, "EXTRA_FLAGS=--schema="+strings.Join(schema, "|"))
|
|
} else {
|
|
env = append(env, "EXCLUDED_SCHEMAS="+strings.Join(utils.InternalSchemas, "|"))
|
|
}
|
|
if !keepComments {
|
|
env = append(env, "EXTRA_SED=/^--/d")
|
|
}
|
|
return dump(ctx, config, dumpSchemaScript, env, dryRun, stdout)
|
|
}
|
|
|
|
func dumpData(ctx context.Context, config pgconn.Config, schema, excludeTable []string, useCopy, dryRun bool, stdout io.Writer) error {
|
|
// We want to dump user data in auth, storage, etc. for migrating to new project
|
|
excludedSchemas := []string{
|
|
"information_schema",
|
|
"pg_*", // Wildcard pattern follows pg_dump
|
|
// Owned by extensions
|
|
// "cron",
|
|
"graphql",
|
|
"graphql_public",
|
|
// "net",
|
|
// "pgmq",
|
|
// "pgsodium",
|
|
// "pgsodium_masks",
|
|
"pgtle",
|
|
"repack",
|
|
"tiger",
|
|
"tiger_data",
|
|
"timescaledb_*",
|
|
"_timescaledb_*",
|
|
"topology",
|
|
// "vault",
|
|
// Managed by Supabase
|
|
// "auth",
|
|
"extensions",
|
|
"pgbouncer",
|
|
"realtime",
|
|
// "storage",
|
|
// "supabase_functions",
|
|
"supabase_migrations",
|
|
// TODO: Remove in a few version in favor of _supabase internal db
|
|
"_analytics",
|
|
"_realtime",
|
|
"_supavisor",
|
|
}
|
|
var env []string
|
|
if len(schema) > 0 {
|
|
env = append(env, "INCLUDED_SCHEMAS="+strings.Join(schema, "|"))
|
|
} else {
|
|
env = append(env, "INCLUDED_SCHEMAS=*", "EXCLUDED_SCHEMAS="+strings.Join(excludedSchemas, "|"))
|
|
}
|
|
var extraFlags []string
|
|
if !useCopy {
|
|
extraFlags = append(extraFlags, "--column-inserts", "--rows-per-insert 100000")
|
|
}
|
|
for _, table := range excludeTable {
|
|
escaped := quoteUpperCase(table)
|
|
// Use separate flags to avoid error: too many dotted names
|
|
extraFlags = append(extraFlags, "--exclude-table "+escaped)
|
|
}
|
|
if len(extraFlags) > 0 {
|
|
env = append(env, "EXTRA_FLAGS="+strings.Join(extraFlags, " "))
|
|
}
|
|
return dump(ctx, config, dumpDataScript, env, dryRun, stdout)
|
|
}
|
|
|
|
func quoteUpperCase(table string) string {
|
|
escaped := strings.ReplaceAll(table, ".", `"."`)
|
|
return fmt.Sprintf(`"%s"`, escaped)
|
|
}
|
|
|
|
func dumpRole(ctx context.Context, config pgconn.Config, keepComments, dryRun bool, stdout io.Writer) error {
|
|
env := []string{}
|
|
if !keepComments {
|
|
env = append(env, "EXTRA_SED=/^--/d")
|
|
}
|
|
return dump(ctx, config, dumpRoleScript, env, dryRun, stdout)
|
|
}
|
|
|
|
func dump(ctx context.Context, config pgconn.Config, script string, env []string, dryRun bool, stdout io.Writer) error {
|
|
allEnvs := append(env,
|
|
"PGHOST="+config.Host,
|
|
fmt.Sprintf("PGPORT=%d", config.Port),
|
|
"PGUSER="+config.User,
|
|
"PGPASSWORD="+config.Password,
|
|
"PGDATABASE="+config.Database,
|
|
"RESERVED_ROLES="+strings.Join(utils.ReservedRoles, "|"),
|
|
"ALLOWED_CONFIGS="+strings.Join(utils.AllowedConfigs, "|"),
|
|
)
|
|
if dryRun {
|
|
envMap := make(map[string]string, len(allEnvs))
|
|
for _, e := range allEnvs {
|
|
index := strings.IndexByte(e, '=')
|
|
if index < 0 {
|
|
continue
|
|
}
|
|
envMap[e[:index]] = e[index+1:]
|
|
}
|
|
expanded := os.Expand(script, func(key string) string {
|
|
// Bash variable expansion is unsupported:
|
|
// https://github.com/golang/go/issues/47187
|
|
parts := strings.Split(key, ":")
|
|
value := envMap[parts[0]]
|
|
// Escape double quotes in env vars
|
|
return strings.ReplaceAll(value, `"`, `\"`)
|
|
})
|
|
fmt.Println(expanded)
|
|
return nil
|
|
}
|
|
return utils.DockerRunOnceWithConfig(
|
|
ctx,
|
|
container.Config{
|
|
Image: cliConfig.Images.Pg15,
|
|
Env: allEnvs,
|
|
Cmd: []string{"bash", "-c", script, "--"},
|
|
},
|
|
container.HostConfig{
|
|
NetworkMode: network.NetworkHost,
|
|
},
|
|
network.NetworkingConfig{},
|
|
"",
|
|
stdout,
|
|
os.Stderr,
|
|
)
|
|
}
|