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 }