├── .gitignore ├── go.mod ├── LICENCE ├── go.sum ├── .travis.yml ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[op] 2 | pg_dump_sample 3 | .tags 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pg_dump_sample 2 | 3 | require ( 4 | github.com/cbroglie/mustache v1.0.1 5 | github.com/jessevdk/go-flags v1.4.0 6 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect 7 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 8 | gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a // indirect 9 | gopkg.in/pg.v4 v4.9.5 10 | gopkg.in/yaml.v2 v2.2.2 11 | ) 12 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 Dan Keder 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cbroglie/mustache v1.0.1 h1:ivMg8MguXq/rrz2eu3tw6g3b16+PQhoTn6EZAhst2mw= 2 | github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXNLPY/PO69g= 3 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 4 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 5 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= 6 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 7 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= 8 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 9 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 11 | gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a h1:stTHdEoWg1pQ8riaP5ROrjS6zy6wewH/Q2iwnLCQUXY= 12 | gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/pg.v4 v4.9.5 h1:bs21aaMPPPcUPhNtqGxN8EeYUFU10MsNrC7U9m/lJgU= 15 | gopkg.in/pg.v4 v4.9.5/go.mod h1:cSUPtzgofjgARAbFCE5u6WDHGPgbR1sjUYcWQlKvpec= 16 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 17 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # https://blog.questionable.services/article/build-go-binaries-travis-ci-github/ 3 | # 4 | language: go 5 | sudo: false 6 | matrix: 7 | include: 8 | # "1.x" always refers to the latest Go version, inc. the patch release. 9 | # e.g. "1.x" is 1.11 until 1.11.1 is available. 10 | - go: 1.x 11 | env: LATEST=true 12 | # - go: "1.7.x" 13 | # - go: "1.8.x" 14 | # - go: "1.9.x" 15 | # - go: "1.10.x" 16 | # - go: "1.11.x" 17 | # - go: "tip" 18 | 19 | before_install: 20 | # gox simplifies building for multiple architectures 21 | - go get github.com/mitchellh/gox 22 | 23 | install: 24 | - # skip 25 | 26 | script: 27 | - go get -t -v ./... 28 | - diff -u <(echo -n) <(gofmt -d .) 29 | - go vet $(go list ./... | grep -v /vendor/) 30 | - go test -v -race ./... 31 | # Only build binaries from the latest Go release. 32 | - if [ "${LATEST}" = "true" ]; then gox -os="linux darwin windows" -arch="amd64" -output="pg_dump_sample.{{.OS}}.{{.Arch}}" -ldflags "-X main.Rev=`git rev-parse --short HEAD`" -verbose ./...; fi 33 | - ls -l 34 | 35 | deploy: 36 | provider: releases 37 | skip_cleanup: true 38 | api_key: 39 | secure: EOXlAWik+XjZh9tgimsZId4MNKIEcqyBdweTrQBPOq/Ej2lzHuCGkC/u8Z9oRFbV5MvRMrpnbF1ZMvngGyn821OghDoPA9v18gtYCNEKvbYDQRIY+4rBsJUYR1v6QETj3kiNuZbgHsMHhK+ebv2FR+4syG0awuwJ+KVVlTWDofzQaupVJ5mNCHb+A1oAVNI74EmoI5KTfN3WFdvy/44UPQ3lafYXq+mBIO8HmRMYyB1dFPKI009m8iB3kX3rdQz2awEs1bYPPyLfr5LU2xMpOVPDGnaHNblZ6apYeiLyNeFjXCqe6fshnrEzKCOJG652XxVoSaJoIC28Bh2GurYzDIb1C1fj/xKPdsJm+vQgxkdiam/b73+tLhqYFXrtvdDZR+EQGwz+xQSuH8trqLZ6LeMaEMgApqOmyypJqMFgu2KktWzfGDbK6dRmiI8haDuNSwYPBLug+sMuxAIfzWa4TnM+QEvihryNB05tPtpArjXpHg5fOmKPKU5YycB89uf2qE13LDxWctuc0/pMlA70bIL65iLu8Gu1BxoRWx99HEO9imVHDRPxf6LJEB7rIk5Ln4tcWuXipKCKiINYM2cPpPuTkmhCoRWLv+AkVK+znsILVYa3csh71/nHggr+eMwfJ0NsKzWwV9i/KExFwmX2wF3gZBfLHR1bAiVI6SSGfm4= 40 | file: 41 | # The names of the binaries to output, based on the -output template passed to gox. 42 | - pg_dump_sample.darwin.amd64 43 | - pg_dump_sample.linux.amd64 44 | - pg_dump_sample.windows.amd64.exe 45 | on: 46 | # What to repository to build 47 | repo: dankeder/pg_dump_sample 48 | # Only build binaries for tagged commits 49 | tags: true 50 | condition: $LATEST = true 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pg_dump_sample 2 | 3 | [![Build Status](https://travis-ci.org/dankeder/pg_dump_sample.svg?branch=master)](https://travis-ci.org/dankeder/pg_dump_sample) 4 | 5 | This is a simple tool for dumping a sample of data from a PostgreSQL database. 6 | The resulting dump can be loaded back into an new database using standard tools 7 | (e.g. `psql(1)`). 8 | 9 | 10 | 11 | ## Why would I want this? 12 | 13 | It is useful if you have a huge PostgreSQL database and you need a 14 | database with a small dataset for testing or development. 15 | 16 | 17 | ## Features 18 | 19 | - Data sampling: Dump either all rows of a table or only rows matching the 20 | specified query. 21 | - Easy anonymization of data: You have full control over what's in the dump, 22 | it's very easy to create anonymized DB dumps. 23 | - Templated queries: You can extract common parts of queries into one place. 24 | That's especially useful if you have e.g. a list of IDs the dump should contain. 25 | - Table dependency awareness - Table A will be dumped before table B 26 | automatically if table B has foreign keys pointing to table A. 27 | - Standard SQL - The produced dump files contain just a bunch of SQL commands. 28 | They can be loaded by standard tools such as `psql(1)` or processed further. 29 | 30 | 31 | ## How to build 32 | 33 | The `pg_dump_sample` is written in Go. You need to [setup the Go compiler and 34 | setup environment](https://golang.org/doc/install) first to build it. 35 | 36 | go get github.com/dankeder/pg_dump_sample 37 | 38 | If it went well you should be able to run it: 39 | 40 | pg_dump_sample 41 | 42 | If not check that you have `$GOPATH/bin` in your `$PATH`. 43 | 44 | 45 | ## How to use 46 | 47 | A quick example: 48 | 49 | pg_dump_sample -f mydb.yaml -h mydbhost.dev -U postgres -o mydb_dump.sql mydb 50 | 51 | Available command-line options: 52 | 53 | Usage: 54 | pg_dump_sample [options] database 55 | 56 | Application Options: 57 | -h, --host= Database server host or socket directory (default: local socket) [$PGHOST] 58 | -p, --port= Database server port (default: 5432) [$PGPORT] 59 | -U, --username= Database user name (default: current user) [$PGUSER] 60 | -w, --no-password Don't prompt for password 61 | -f, --manifest-file= Path to manifest file 62 | -o, --output-file= Path to the output file 63 | -s, --tls Use SSL/TLS database connection 64 | --help Show help 65 | 66 | The available command-line options are heavily inspired by 67 | [`pg_dump(1)`](http://www.postgresql.org/docs/9.4/static/app-pgdump.html). 68 | Anyone familiar with it should feel right at home. 69 | 70 | It is also possible to use environmental variables to set the options. 71 | Note that the environmental variables have lower precenence than command-line 72 | options. The supported variables are: 73 | 74 | | Environmental variable | Description | 75 | | ------------------------ | ----------------------------------- | 76 | | `PGHOST` | `-h, --host` | 77 | | `PGPORT` | `-p, --port` | 78 | | `PGUSER` | `-U, --username` | 79 | | `PGPASSWORD` | Used to set the password. Use of this environment variable is not recommended for security reasons (some operating systems allow non-root users to see process environment variables via ps) 80 | | `PGDATABASE` | database | 81 | 82 | 83 | ### Manifest file 84 | 85 | The main difference between `pg_dump_sample` and `pg_dump(1)` is that 86 | `pg_dump_sample` requires a manifest file describing how to dump the database. 87 | The manifest file is a YAML file describing what tables to dump and how to dump 88 | them. 89 | 90 | A quick example: 91 | 92 | --- 93 | vars: 94 | # Condition to dump only certain users 95 | matching_user_id: "(users.id BETWEEN 1000 AND 2000)" 96 | 97 | tables: 98 | # Dump everything from table "consts" 99 | - table: consts 100 | 101 | # Dump only matching users 102 | - table: users 103 | query: "SELECT * FROM users WHERE {{matching_user_id}}" 104 | post_actions: 105 | - "SELECT pg_catalog.setval('users_id_seq', MAX(id) + 1, true) FROM users" 106 | 107 | # Dump only tickets that were bought by matching users 108 | - table: tickets 109 | query: > 110 | SELECT purchases.* FROM purchases, users 111 | WHERE 112 | purchases.buyer_id = users.id 113 | AND {{matching_user_id}} 114 | 115 | 116 | Currently these top-level keys are available: 117 | 118 | #### `vars` 119 | 120 | Definitions of variables which will be used to replace placeholders in queries. 121 | 122 | #### `tables` 123 | 124 | List of tables to dump. Tables are dumped in the order they are specified in the 125 | manifest file, with one exception: if the table contains foreign keys 126 | referencing another table, the referenced table will be dumped first. This is to 127 | ensure that the dump can be loaded later without errors. 128 | 129 | By default all rows of the table will be dumped. If you don't want to dump all 130 | the rows use the `query` to specify a SELECT SQL statement which returns the 131 | rows you want to dump. 132 | 133 | 134 | ## TODO 135 | 136 | - Use separate vars files to override vars from manifest? 137 | - Allow setting vars using command-line options? 138 | 139 | 140 | ## Contributing 141 | 142 | - If you find a new bug, a missing feature, etc. please create a ticket in Isuses. 143 | - Contributions are very welcome - just open a pull-request. 144 | 145 | 146 | # Licence 147 | 148 | MIT 149 | 150 | 151 | # Author 152 | 153 | Dan Keder 154 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "os/user" 9 | "strconv" 10 | "strings" 11 | "syscall" 12 | 13 | "github.com/cbroglie/mustache" 14 | flags "github.com/jessevdk/go-flags" 15 | "golang.org/x/crypto/ssh/terminal" 16 | pg "gopkg.in/pg.v4" 17 | yaml "gopkg.in/yaml.v2" 18 | ) 19 | 20 | const ( 21 | BEGIN_DUMP = ` 22 | -- 23 | -- PostgreSQL database dump 24 | -- 25 | 26 | BEGIN; 27 | 28 | SET statement_timeout = 0; 29 | SET lock_timeout = 0; 30 | SET client_encoding = 'UTF8'; 31 | SET standard_conforming_strings = on; 32 | SET check_function_bodies = false; 33 | SET client_min_messages = warning; 34 | 35 | SET search_path = public, pg_catalog; 36 | 37 | ` 38 | 39 | END_DUMP = ` 40 | COMMIT; 41 | 42 | -- 43 | -- PostgreSQL database dump complete 44 | -- 45 | ` 46 | 47 | BEGIN_TABLE_DUMP = ` 48 | -- 49 | -- Data for Name: %s; Type: TABLE DATA 50 | -- 51 | 52 | COPY %s (%s) FROM stdin; 53 | ` 54 | 55 | END_TABLE_DUMP = `\. 56 | ` 57 | 58 | SQL_CMD_DUMP = "\n%s;\n" 59 | ) 60 | 61 | type Options struct { 62 | Host string 63 | Port int 64 | Username string 65 | NoPasswordPrompt bool 66 | Password string 67 | ManifestFile string 68 | OutputFile string 69 | Database string 70 | UseTls bool 71 | } 72 | 73 | type ManifestItem struct { 74 | Table string `yaml:"table"` 75 | Query string `yaml:"query"` 76 | Columns []string `yaml:"columns,flow"` 77 | PostActions []string `yaml:"post_actions,flow"` 78 | } 79 | 80 | type Manifest struct { 81 | Vars map[string]string `yaml:"vars"` 82 | Tables []ManifestItem `yaml:"tables"` 83 | } 84 | 85 | type ManifestIterator struct { 86 | db *pg.DB 87 | manifest *Manifest 88 | todo map[string]ManifestItem 89 | done map[string]ManifestItem 90 | stack []string 91 | } 92 | 93 | func NewManifestIterator(db *pg.DB, manifest *Manifest) *ManifestIterator { 94 | m := ManifestIterator{ 95 | db, 96 | manifest, 97 | make(map[string]ManifestItem), 98 | make(map[string]ManifestItem), 99 | make([]string, 0), 100 | } 101 | 102 | for _, item := range m.manifest.Tables { 103 | m.stack = append(m.stack, item.Table) 104 | m.todo[item.Table] = item 105 | } 106 | 107 | return &m 108 | } 109 | 110 | func (m *ManifestIterator) Next() (*ManifestItem, error) { 111 | if len(m.stack) == 0 { 112 | return nil, nil 113 | } 114 | 115 | table := m.stack[0] 116 | m.stack = m.stack[1:] 117 | 118 | if _, ok := m.todo[table]; !ok { 119 | return m.Next() 120 | } 121 | 122 | deps, err := getTableDeps(m.db, table) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | todoDeps := make([]string, 0) 128 | for _, dep := range deps { 129 | _, is_todo := m.todo[dep] 130 | _, is_done := m.done[dep] 131 | if !is_todo && !is_done { 132 | // A new dependency table not present in the manifest file was 133 | // found, create a default entry for it 134 | m.todo[dep] = ManifestItem{Table: dep} 135 | } 136 | if _, ok := m.todo[dep]; ok && table != dep { 137 | todoDeps = append(todoDeps, dep) 138 | } 139 | } 140 | 141 | if len(todoDeps) > 0 { 142 | m.stack = append(todoDeps, append([]string{table}, m.stack...)...) 143 | return m.Next() 144 | } 145 | 146 | result := m.todo[table] 147 | m.done[table] = m.todo[table] 148 | delete(m.todo, table) 149 | 150 | return &result, nil 151 | } 152 | 153 | func parseArgs() (*Options, error) { 154 | var opts struct { 155 | Host string `short:"h" long:"host" default:"/tmp" default-mask:"local socket" env:"PGHOST" description:"Database server host or socket directory"` 156 | Port string `short:"p" long:"port" default:"5432" env:"PGPORT" description:"Database server port"` 157 | Username string `short:"U" long:"username" default-mask:"current user" env:"PGUSER" description:"Database user name"` 158 | NoPasswordPrompt bool `short:"w" long:"no-password" description:"Don't prompt for password"` 159 | ManifestFile string `short:"f" long:"manifest-file" description:"Path to manifest file"` 160 | OutputFile string `short:"o" long:"output-file" description:"Path to the output file"` 161 | UseTls bool `short:"s" long:"tls" description:"Use SSL/TLS database connection"` 162 | Help bool `long:"help" description:"Show help"` 163 | } 164 | 165 | parser := flags.NewParser(&opts, flags.None) 166 | parser.Usage = "[options] database" 167 | 168 | args, err := parser.Parse() 169 | if err != nil { 170 | parser.WriteHelp(os.Stderr) 171 | return nil, err 172 | } 173 | 174 | if opts.Help { 175 | parser.WriteHelp(os.Stdout) 176 | os.Exit(0) 177 | } 178 | 179 | // Manifest file 180 | if opts.ManifestFile == "" { 181 | parser.WriteHelp(os.Stderr) 182 | return nil, fmt.Errorf("required flag `-f, --manifest-file` not specified") 183 | } 184 | 185 | // Username 186 | if opts.Username == "" { 187 | currentUser, err := user.Current() 188 | if err != nil { 189 | return nil, fmt.Errorf("failed to get current user") 190 | } 191 | opts.Username = currentUser.Username 192 | } 193 | 194 | // Port 195 | port, err := strconv.Atoi(opts.Port) 196 | if err != nil { 197 | parser.WriteHelp(os.Stderr) 198 | return nil, fmt.Errorf("port must be a number 0-65535") 199 | } 200 | 201 | // Database 202 | Database := "" 203 | if len(args) == 0 { 204 | Database = os.Getenv("PGDATABASE") 205 | } else if len(args) == 1 { 206 | Database = args[0] 207 | } else if len(args) > 1 { 208 | parser.WriteHelp(os.Stderr) 209 | return nil, fmt.Errorf("only one database may be specified at a time") 210 | } 211 | 212 | // Password 213 | Password := os.Getenv("PGPASSWORD") 214 | 215 | return &Options{ 216 | Host: opts.Host, 217 | Port: port, 218 | Username: opts.Username, 219 | NoPasswordPrompt: opts.NoPasswordPrompt, 220 | Password: Password, 221 | ManifestFile: opts.ManifestFile, 222 | OutputFile: opts.OutputFile, 223 | UseTls: opts.UseTls, 224 | Database: Database, 225 | }, nil 226 | } 227 | 228 | func connectDB(opts *pg.Options) (*pg.DB, error) { 229 | db := pg.Connect(opts) 230 | var model []struct { 231 | X string 232 | } 233 | _, err := db.Query(&model, `SELECT 1 AS x`) 234 | if err != nil { 235 | return nil, err 236 | } 237 | return db, nil 238 | } 239 | 240 | func beginDump(w io.Writer) { 241 | fmt.Fprintf(w, BEGIN_DUMP) 242 | } 243 | 244 | func endDump(w io.Writer) { 245 | fmt.Fprintf(w, END_DUMP) 246 | } 247 | 248 | func beginTable(w io.Writer, table string, columns []string) { 249 | quoted := make([]string, 0) 250 | for _, v := range columns { 251 | quoted = append(quoted, strconv.Quote(v)) 252 | } 253 | colstr := strings.Join(quoted, ", ") 254 | fmt.Fprintf(w, BEGIN_TABLE_DUMP, table, table, colstr) 255 | } 256 | 257 | func endTable(w io.Writer) { 258 | fmt.Fprintf(w, END_TABLE_DUMP) 259 | } 260 | 261 | func dumpSqlCmd(w io.Writer, v string) { 262 | fmt.Fprintf(w, SQL_CMD_DUMP, v) 263 | } 264 | 265 | func dumpTable(w io.Writer, db *pg.DB, table string) error { 266 | sql := fmt.Sprintf(`COPY %s TO STDOUT`, table) 267 | 268 | _, err := db.CopyTo(w, sql) 269 | if err != nil { 270 | return err 271 | } 272 | 273 | return nil 274 | } 275 | 276 | func readPassword(username string) (string, error) { 277 | fmt.Fprintf(os.Stderr, "Password for %s: ", username) 278 | password, err := terminal.ReadPassword(int(syscall.Stdin)) 279 | fmt.Print("\n") 280 | return string(password), err 281 | } 282 | 283 | func readManifest(r io.Reader) (*Manifest, error) { 284 | data, err := ioutil.ReadAll(r) 285 | if err != nil { 286 | return nil, err 287 | } 288 | 289 | manifest := Manifest{} 290 | yaml.Unmarshal(data, &manifest) 291 | 292 | return &manifest, nil 293 | } 294 | 295 | func getTableCols(db *pg.DB, table string) ([]string, error) { 296 | var model []struct { 297 | Colname string 298 | } 299 | sql := ` 300 | SELECT attname as colname 301 | FROM pg_catalog.pg_attribute 302 | WHERE 303 | attrelid = ?::regclass 304 | AND attnum > 0 305 | AND attisdropped = FALSE 306 | ORDER BY attnum 307 | ` 308 | _, err := db.Query(&model, sql, table) 309 | if err != nil { 310 | return nil, err 311 | } 312 | 313 | var cols = make([]string, 0) 314 | for _, v := range model { 315 | cols = append(cols, v.Colname) 316 | } 317 | 318 | return cols, nil 319 | } 320 | 321 | func getTableDeps(db *pg.DB, table string) ([]string, error) { 322 | var model []struct { 323 | Tablename string 324 | } 325 | sql := ` 326 | SELECT confrelid::regclass AS tablename 327 | FROM pg_catalog.pg_constraint 328 | WHERE 329 | conrelid = ?::regclass 330 | AND contype = 'f' 331 | ` 332 | _, err := db.Query(&model, sql, table) 333 | if err != nil { 334 | return nil, err 335 | } 336 | 337 | var tables = make([]string, 0) 338 | for _, v := range model { 339 | tables = append(tables, v.Tablename) 340 | } 341 | 342 | return tables, nil 343 | } 344 | 345 | func makeDump(db *pg.DB, manifest *Manifest, w io.Writer) error { 346 | beginDump(w) 347 | 348 | iterator := NewManifestIterator(db, manifest) 349 | for { 350 | v, err := iterator.Next() 351 | if err != nil { 352 | return err 353 | } 354 | if v == nil { 355 | break 356 | } 357 | 358 | cols := v.Columns 359 | if len(cols) == 0 { 360 | cols, err = getTableCols(db, v.Table) 361 | if err != nil { 362 | return err 363 | } 364 | } 365 | 366 | beginTable(w, v.Table, cols) 367 | if v.Query == "" { 368 | err := dumpTable(w, db, v.Table) 369 | if err != nil { 370 | return err 371 | } 372 | } else { 373 | query, err := mustache.Render(v.Query, manifest.Vars) 374 | if err != nil { 375 | return err 376 | } 377 | 378 | err = dumpTable(w, db, fmt.Sprintf("(%s)", query)) 379 | if err != nil { 380 | return err 381 | } 382 | } 383 | endTable(w) 384 | 385 | for _, sql := range v.PostActions { 386 | dumpSqlCmd(w, sql) 387 | } 388 | } 389 | 390 | endDump(w) 391 | 392 | return nil 393 | } 394 | 395 | func main() { 396 | // Parse command-line arguments 397 | opts, err := parseArgs() 398 | if err != nil { 399 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 400 | os.Exit(1) 401 | } 402 | 403 | // Open manifest file 404 | manifestFile, err := os.Open(opts.ManifestFile) 405 | if err != nil { 406 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 407 | os.Exit(1) 408 | } 409 | 410 | // Read manifest 411 | manifest, err := readManifest(manifestFile) 412 | if err != nil { 413 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 414 | os.Exit(1) 415 | } 416 | 417 | // Open output file 418 | output := os.Stdout 419 | if opts.OutputFile != "" { 420 | output, err = os.OpenFile(opts.OutputFile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) 421 | if err != nil { 422 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 423 | os.Exit(1) 424 | } 425 | } 426 | 427 | // Connect to the DB 428 | db, err := connectDB(&pg.Options{ 429 | Addr: fmt.Sprintf("%s:%d", opts.Host, opts.Port), 430 | Database: opts.Database, 431 | SSL: opts.UseTls, 432 | User: opts.Username, 433 | Password: opts.Password, 434 | }) 435 | if err != nil { 436 | password := opts.Password 437 | if !opts.NoPasswordPrompt { 438 | // Read database password from the terminal 439 | password, err = readPassword(opts.Username) 440 | if err != nil { 441 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 442 | os.Exit(1) 443 | } 444 | } 445 | 446 | // Try again, this time with password 447 | db, err = connectDB(&pg.Options{ 448 | Addr: fmt.Sprintf("%s:%d", opts.Host, opts.Port), 449 | Database: opts.Database, 450 | SSL: opts.UseTls, 451 | User: opts.Username, 452 | Password: password, 453 | }) 454 | if err != nil { 455 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 456 | os.Exit(1) 457 | } 458 | } 459 | 460 | // Make the dump 461 | err = makeDump(db, manifest, output) 462 | if err != nil { 463 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 464 | os.Exit(1) 465 | } 466 | } 467 | --------------------------------------------------------------------------------