├── .travis.yml ├── Dockerfile ├── Makefile ├── .gitignore ├── create.go ├── go.mod ├── docker-compose.yml ├── cmd └── migrate │ ├── main_test.go │ ├── create.go │ ├── status.go │ ├── up.go │ └── main.go ├── db ├── lock.go └── db.go ├── status.go ├── LICENSE ├── source ├── read.go ├── source_test.go ├── read_test.go └── source.go ├── up.go ├── README.md ├── migrate_test.go └── go.sum /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | services: 4 | - docker 5 | 6 | before_install: 7 | - sudo service postgresql stop 8 | 9 | script: 10 | - make test-all 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14 2 | 3 | RUN mkdir -p /go/src/github.com/johngibb/migrate 4 | WORKDIR /go/src/github.com/johngibb/migrate 5 | COPY . . 6 | 7 | RUN go install ./... 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: install test-all 2 | 3 | install: 4 | go install ./... 5 | 6 | test: 7 | go test ./... 8 | 9 | test-all: 10 | docker-compose rm -f postgres # discard state from prior runs 11 | docker-compose up --build --abort-on-container-exit --force-recreate 12 | 13 | .PHONY: all test test-all 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /create.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/johngibb/migrate/source" 7 | ) 8 | 9 | // Create generates an empty migration file. 10 | func Create(src *source.Source, name string) error { 11 | path, err := src.Create(name) 12 | if err != nil { 13 | return err 14 | } 15 | log.Printf("Created %s\n", path) 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/johngibb/migrate 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gofrs/uuid v4.2.0+incompatible // indirect 7 | github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315 8 | github.com/jackc/pgx/v4 v4.17.0 9 | github.com/lib/pq v1.10.6 // indirect 10 | github.com/pkg/errors v0.9.1 11 | github.com/shopspring/decimal v1.3.1 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | test: 5 | build: . 6 | command: ["go", "test", "-v", "./..."] 7 | depends_on: 8 | - postgres 9 | environment: 10 | DATABASE_URL: postgres://migrate:migrate@postgres:5432/test_migrations 11 | RUN_MIGRATIONS: "YES" 12 | postgres: 13 | image: postgres 14 | restart: always 15 | ports: 16 | - "5432:5432" 17 | environment: 18 | POSTGRES_USER: migrate 19 | POSTGRES_PASSWORD: migrate 20 | POSTGRES_DB: test_migrations 21 | -------------------------------------------------------------------------------- /cmd/migrate/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestTranslateLegacyArgs(t *testing.T) { 9 | tests := []struct { 10 | args []string 11 | want []string 12 | }{ 13 | { 14 | []string{"migrate", "--src", "./dir", "up"}, 15 | []string{"migrate", "up", "--src", "./dir"}, 16 | }, 17 | { 18 | []string{"migrate", "-src", "./migs", "-quiet", "up"}, 19 | []string{"migrate", "up", "-src", "./migs", "-quiet"}, 20 | }, 21 | { 22 | []string{"migrate", "-src", "./migs", "-conn", "myconn", "create"}, 23 | []string{"migrate", "create", "-src", "./migs"}, 24 | }, 25 | } 26 | 27 | for _, tt := range tests { 28 | got := translateLegacyArgs(tt.args) 29 | if !reflect.DeepEqual(got, tt.want) { 30 | t.Errorf("got %v, want %v", got, tt.want) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/migrate/create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/google/subcommands" 10 | "github.com/johngibb/migrate" 11 | "github.com/johngibb/migrate/source" 12 | ) 13 | 14 | type Create struct { 15 | srcPath string 16 | } 17 | 18 | func (*Create) Name() string { return "create" } 19 | func (*Create) Synopsis() string { return "create new migration file" } 20 | func (*Create) Usage() string { 21 | return `migrate create -src : 22 | Creates a new migration file. 23 | ` 24 | } 25 | 26 | func (cmd *Create) SetFlags(f *flag.FlagSet) { 27 | f.StringVar(&cmd.srcPath, "src", ".", "directory containing migration files") 28 | } 29 | 30 | func (cmd *Create) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { 31 | if len(f.Args()) < 1 { 32 | fmt.Fprint(os.Stderr, "error: missing migration name\n") 33 | f.Usage() 34 | return subcommands.ExitUsageError 35 | } 36 | src, err := source.New(cmd.srcPath) 37 | must(err) 38 | must(migrate.Create(src, f.Arg(0))) 39 | return subcommands.ExitSuccess 40 | } 41 | -------------------------------------------------------------------------------- /db/lock.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "hash/fnv" 6 | ) 7 | 8 | func generateAdvisoryLockID(database string) int { 9 | h := fnv.New32a() 10 | h.Write([]byte(database)) 11 | h.Write([]byte("migrate")) 12 | return int(h.Sum32()) 13 | } 14 | 15 | // TryLock attempts to acquire an exclusive lock for running migrations 16 | // on this database. 17 | func (c *Client) TryLock(ctx context.Context) (bool, error) { 18 | id := generateAdvisoryLockID(c.databaseName) 19 | var success bool 20 | err := c.conn.QueryRow(ctx, `select pg_try_advisory_lock($1);`, id).Scan(&success) 21 | if err != nil { 22 | return false, err 23 | } 24 | if success { 25 | c.locked = true 26 | } 27 | return success, nil 28 | } 29 | 30 | // Unlock unlocks the exclusive migration lock. 31 | func (c *Client) Unlock(ctx context.Context) (bool, error) { 32 | id := generateAdvisoryLockID(c.databaseName) 33 | var success bool 34 | err := c.conn.QueryRow(ctx, `select pg_advisory_unlock($1);`, id).Scan(&success) 35 | if err != nil { 36 | return false, err 37 | } 38 | if success { 39 | c.locked = false 40 | } 41 | return success, nil 42 | } 43 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strconv" 7 | 8 | "github.com/johngibb/migrate/db" 9 | "github.com/johngibb/migrate/source" 10 | ) 11 | 12 | // Status displays every migration, and whether it's been applied yet. 13 | func Status(ctx context.Context, src *source.Source, db *db.Client) error { 14 | migrations, err := src.FindMigrations() 15 | if err != nil { 16 | return err 17 | } 18 | applied, err := db.GetMigrations(ctx) 19 | if err != nil { 20 | return err 21 | } 22 | isApplied := func(name string) bool { 23 | for _, a := range applied { 24 | if a.Name == name { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | 31 | w := maxNameWidth(migrations) 32 | for _, m := range migrations { 33 | status := "pending" 34 | if isApplied(m.Name) { 35 | status = "applied" 36 | } 37 | 38 | log.Printf("%-"+strconv.Itoa(w)+"s %s\n", m.Name, status) 39 | } 40 | return nil 41 | } 42 | 43 | func maxNameWidth(mm []*source.Migration) int { 44 | w := 0 45 | for _, m := range mm { 46 | if n := len(m.Name); n > w { 47 | w = n 48 | } 49 | } 50 | return w 51 | } 52 | -------------------------------------------------------------------------------- /cmd/migrate/status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | 7 | "github.com/google/subcommands" 8 | 9 | "github.com/johngibb/migrate" 10 | "github.com/johngibb/migrate/db" 11 | "github.com/johngibb/migrate/source" 12 | ) 13 | 14 | type Status struct { 15 | conn string 16 | srcPath string 17 | } 18 | 19 | func (*Status) Name() string { return "status" } 20 | func (*Status) Synopsis() string { return "display the current status of the migrations" } 21 | func (*Status) Usage() string { 22 | return `migrate status: 23 | Display a list of pending and applied migrations. 24 | ` 25 | } 26 | 27 | func (cmd *Status) SetFlags(f *flag.FlagSet) { 28 | f.StringVar(&cmd.conn, "conn", "", "postgres connection string") 29 | f.StringVar(&cmd.srcPath, "src", ".", "directory containing migration files") 30 | } 31 | 32 | func (cmd *Status) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { 33 | src, err := source.New(cmd.srcPath) 34 | must(err) 35 | db, err := db.Connect(ctx, cmd.conn) 36 | must(err) 37 | defer db.Close(ctx) 38 | must(migrate.Status(ctx, src, db)) 39 | return subcommands.ExitSuccess 40 | } 41 | -------------------------------------------------------------------------------- /cmd/migrate/up.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | 7 | "github.com/google/subcommands" 8 | 9 | "github.com/johngibb/migrate" 10 | "github.com/johngibb/migrate/db" 11 | "github.com/johngibb/migrate/source" 12 | ) 13 | 14 | type Up struct { 15 | conn string 16 | srcPath string 17 | quiet bool 18 | } 19 | 20 | func (*Up) Name() string { return "up" } 21 | func (*Up) Synopsis() string { return "apply all pending migrations to the db" } 22 | func (*Up) Usage() string { 23 | return `migrate up -src -conn [-quiet]: 24 | Apply all pending migrations. 25 | ` 26 | } 27 | 28 | func (cmd *Up) SetFlags(f *flag.FlagSet) { 29 | f.StringVar(&cmd.conn, "conn", "", "postgres connection string") 30 | f.StringVar(&cmd.srcPath, "src", ".", "directory containing migration files") 31 | f.BoolVar(&cmd.quiet, "quiet", false, "only print errors") 32 | } 33 | 34 | func (cmd *Up) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { 35 | src, err := source.New(cmd.srcPath) 36 | must(err) 37 | db, err := db.Connect(ctx, cmd.conn) 38 | must(err) 39 | defer db.Close(ctx) 40 | must(migrate.Up(ctx, src, db, cmd.quiet)) 41 | return subcommands.ExitSuccess 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /source/read.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // ReadStatements reads the migration file and parses it into individual 14 | // statements. 15 | func (m *Migration) ReadStatements() ([]string, error) { 16 | f, err := os.Open(m.Path) 17 | if err != nil { 18 | return nil, errors.Wrap(err, "could not open file") 19 | } 20 | defer f.Close() 21 | return splitStatements(f), nil 22 | } 23 | 24 | const ( 25 | apostrophe = "'" 26 | dollarSign = "$" 27 | semicolon = ";" 28 | ) 29 | 30 | // splitStatements splits the given file into separate statements. 31 | func splitStatements(r io.Reader) []string { 32 | var ( 33 | buf bytes.Buffer 34 | inBlock bool 35 | inString bool 36 | result []string 37 | scanner = bufio.NewScanner(r) 38 | ) 39 | 40 | scanner.Split(bufio.ScanBytes) 41 | last := "" 42 | 43 | for scanner.Scan() { 44 | curr := scanner.Text() 45 | _, _ = buf.WriteString(curr) 46 | 47 | switch last { 48 | case apostrophe: 49 | inString = !inString 50 | case dollarSign: 51 | if curr == dollarSign && !inString { 52 | inBlock = !inBlock 53 | } 54 | } 55 | 56 | if curr == semicolon && !(inBlock || inString) { 57 | result = append(result, buf.String()) 58 | buf.Reset() 59 | } 60 | 61 | last = curr 62 | } 63 | 64 | if buf.Len() > 0 { 65 | if s := strings.TrimSpace(buf.String()); s != "" { 66 | result = append(result, s) 67 | } 68 | } 69 | 70 | return result 71 | } 72 | -------------------------------------------------------------------------------- /source/source_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | func TestParseMigration(t *testing.T) { 10 | tests := []struct { 11 | path string 12 | want *Migration 13 | }{{ 14 | path: "./source/123_add_tables_to_db.sql", 15 | want: &Migration{ 16 | Path: "./source/123_add_tables_to_db.sql", 17 | Version: 123, 18 | Name: "123_add_tables_to_db", 19 | }, 20 | }} 21 | for i, tt := range tests { 22 | got, err := parseMigration(tt.path) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | if !reflect.DeepEqual(got, tt.want) { 27 | t.Errorf("%d: got %v, want %v", i, got, tt.want) 28 | } 29 | } 30 | } 31 | 32 | func TestSort(t *testing.T) { 33 | tests := []struct { 34 | paths []string 35 | want []string 36 | }{{ 37 | paths: []string{ 38 | "./source/10_tenth.sql", 39 | "./source/1_first.sql", 40 | }, 41 | want: []string{ 42 | "./source/1_first.sql", 43 | "./source/10_tenth.sql", 44 | }, 45 | }} 46 | for i, tt := range tests { 47 | // Parse migrations. 48 | var migrations []*Migration 49 | for _, s := range tt.paths { 50 | m, err := parseMigration(s) 51 | if err != nil { 52 | t.Fatal("could not parse migration") 53 | } 54 | migrations = append(migrations, m) 55 | } 56 | 57 | // Sort migrations. 58 | sort.Sort(ByVersion(migrations)) 59 | 60 | // Extract paths 61 | var got []string 62 | for _, m := range migrations { 63 | got = append(got, m.Path) 64 | } 65 | 66 | if !reflect.DeepEqual(got, tt.want) { 67 | t.Errorf("%d: got %v, want %v", i, got, tt.want) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cmd/migrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/google/subcommands" 11 | ) 12 | 13 | func main() { 14 | log.SetFlags(0) 15 | subcommands.Register(&Status{}, "") 16 | subcommands.Register(&Up{}, "") 17 | subcommands.Register(&Create{}, "") 18 | subcommands.Register(subcommands.HelpCommand(), "") 19 | 20 | os.Args = translateLegacyArgs(os.Args) 21 | 22 | flag.Parse() 23 | os.Exit(int( 24 | subcommands.Execute(context.Background()), 25 | )) 26 | } 27 | 28 | // must calls log.Fatal if the error is non-nil. 29 | func must(err error) { 30 | if err != nil { 31 | log.Fatalf("migrate: %v\n", err) 32 | } 33 | } 34 | 35 | // translateLegacyArgs translates legacy command-line arguments to the newer 36 | // format used by subcommands. Formerly, flags were specified before the 37 | // command. 38 | // 39 | // Example: 40 | // 41 | // before: [migrate -src ./migrations create add_table] 42 | // after: [migrate create -src ./migrations add_table] 43 | func translateLegacyArgs(args []string) []string { 44 | if len(args) < 2 { 45 | return args 46 | } 47 | var ( 48 | cmd string 49 | flags []string 50 | ) 51 | if !isFlag(args[1]) { 52 | return args 53 | } 54 | for _, arg := range args[1:] { 55 | if isCommand(arg) { 56 | cmd = arg 57 | } else { 58 | flags = append(flags, arg) 59 | } 60 | } 61 | 62 | // Special case: "create" no longer accepts a "conn" flag. 63 | if cmd == "create" { 64 | for i, s := range flags { 65 | switch { 66 | case strings.HasPrefix(s, "-conn="), strings.HasPrefix(s, "--conn="): 67 | flags = append(flags[0:i], flags[i+1:]...) 68 | case strings.HasPrefix(s, "-conn"), strings.HasPrefix(s, "--conn"): 69 | flags = append(flags[0:i], flags[i+2:]...) 70 | } 71 | } 72 | } 73 | return append([]string{args[0], cmd}, flags...) 74 | } 75 | 76 | func isFlag(s string) bool { 77 | return strings.HasPrefix(s, "-conn") || strings.HasPrefix(s, "--conn") || 78 | strings.HasPrefix(s, "-src") || strings.HasPrefix(s, "--src") || 79 | strings.HasPrefix(s, "-quiet") || strings.HasPrefix(s, "--quiet") 80 | } 81 | 82 | func isCommand(s string) bool { 83 | return s == "create" || 84 | s == "status" || 85 | s == "up" 86 | } 87 | -------------------------------------------------------------------------------- /source/read_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestSplitStatements(t *testing.T) { 11 | tests := []struct { 12 | src string 13 | statements []string 14 | }{ 15 | { 16 | `create table test(id int)`, 17 | []string{`create table test(id int)`}, 18 | }, 19 | { 20 | ` 21 | create table test1(id int); 22 | create table test2(id int); 23 | `, 24 | []string{ 25 | `create table test1(id int);`, 26 | `create table test2(id int);`, 27 | }, 28 | }, 29 | { 30 | ` 31 | create table test1( 32 | id int, 33 | name text 34 | ); 35 | create table test2(id int); 36 | `, 37 | []string{ 38 | `create table test1( 39 | id int, 40 | name text 41 | );`, 42 | `create table test2(id int);`, 43 | }, 44 | }, 45 | { 46 | `insert into test select ';'`, 47 | []string{`insert into test select ';'`}, 48 | }, 49 | { 50 | `create function update_trigger() returns trigger as $$ 51 | begin 52 | new.tsv := 53 | to_tsvector(coalesce(new.alpha, 'foo''s')) || 54 | to_tsvector(coalesce(new.bravo, '$$')); 55 | return new; 56 | end 57 | $$ language plpgsql;`, 58 | nil, 59 | }, 60 | { 61 | `select * from table where thing not in (';''', ''); 62 | select * from table where thing not in ('', '''');`, 63 | []string{ 64 | `select * from table where thing not in (';''', '');`, 65 | `select * from table where thing not in ('', '''');`, 66 | }, 67 | }, 68 | } 69 | for i, tt := range tests { 70 | want := tt.statements 71 | if len(want) == 0 { 72 | want = []string{tt.src} 73 | } 74 | if got := trimAll(splitStatements(strings.NewReader(tt.src))); !reflect.DeepEqual(got, want) { 75 | t.Errorf("read failed: %d, got: \n%s\nwant: \n%s", i, toJSON(got), toJSON(tt.statements)) 76 | } 77 | } 78 | } 79 | 80 | func trimAll(ss []string) []string { 81 | result := make([]string, len(ss)) 82 | for i, s := range ss { 83 | result[i] = strings.TrimSpace(s) 84 | } 85 | return result 86 | } 87 | 88 | func toJSON(v interface{}) string { 89 | b, err := json.Marshal(v) 90 | if err != nil { 91 | panic(err) 92 | } 93 | return string(b) 94 | } 95 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | // Package db implements a Postgres client for applying, querying, and 2 | // recording migrations. 3 | package db 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/jackc/pgx/v4" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Migration is a migration that's been applied to the database. 13 | type Migration struct { 14 | Name string 15 | } 16 | 17 | // Client is a migration database connection. 18 | type Client struct { 19 | conn *pgx.Conn 20 | databaseName string 21 | locked bool 22 | ensured bool 23 | } 24 | 25 | // Connect connects to the Postgres database at the given uri. 26 | func Connect(ctx context.Context, uri string) (*Client, error) { 27 | cfg, err := pgx.ParseConfig(uri) 28 | if err != nil { 29 | return nil, errors.Wrapf(err, "could not parse uri: %s", uri) 30 | } 31 | conn, err := pgx.ConnectConfig(ctx, cfg) 32 | if err != nil { 33 | return nil, errors.Wrap(err, "could not connect to database") 34 | } 35 | c := &Client{ 36 | conn: conn, 37 | databaseName: cfg.Database, 38 | } 39 | return c, nil 40 | } 41 | 42 | // Close closes the underlying database connection. 43 | func (c *Client) Close(ctx context.Context) error { 44 | return c.conn.Close(ctx) 45 | } 46 | 47 | // ensureMigrationsTable ensures that the migrations table exists. 48 | func (c *Client) ensureMigrationsTable(ctx context.Context) error { 49 | if c.ensured { // only need to run the full check once 50 | return nil 51 | } 52 | _, err := c.conn.Exec(ctx, ` 53 | create table if not exists migrations ( 54 | name text 55 | ); 56 | `) 57 | if err == nil { 58 | c.ensured = true 59 | } 60 | return err 61 | } 62 | 63 | // ApplyMigration executes the given statements against the database, 64 | // and then records that the migration has been applied. 65 | func (c *Client) Exec(ctx context.Context, sql string) error { 66 | if err := c.ensureMigrationsTable(ctx); err != nil { 67 | return err 68 | } 69 | _, err := c.conn.Exec(ctx, sql) 70 | return err 71 | } 72 | 73 | func (c *Client) LogCompletedMigration(ctx context.Context, name string) error { 74 | _, err := c.conn.Exec(ctx, `insert into migrations values ($1);`, name) 75 | return err 76 | } 77 | 78 | // GetMigrations returns all migrations that have been applied to the 79 | // database. 80 | func (c *Client) GetMigrations(ctx context.Context) ([]*Migration, error) { 81 | if err := c.ensureMigrationsTable(ctx); err != nil { 82 | return nil, err 83 | } 84 | rows, err := c.conn.Query(ctx, `select name from migrations;`) 85 | if err != nil { 86 | return nil, errors.Wrap(err, "could not query migrations") 87 | } 88 | var result []*Migration 89 | for rows.Next() { 90 | var m Migration 91 | if err := rows.Scan(&m.Name); err != nil { 92 | return nil, errors.Wrap(err, "error scanning migration") 93 | } 94 | result = append(result, &m) 95 | } 96 | return result, nil 97 | } 98 | -------------------------------------------------------------------------------- /up.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/johngibb/migrate/db" 13 | "github.com/johngibb/migrate/source" 14 | ) 15 | 16 | var DefaultLogger = log.New(os.Stderr, "", 0) 17 | 18 | // Up applies all pending migrations from src to the db. 19 | func Up(ctx context.Context, src *source.Source, db *db.Client, quiet bool) (err error) { 20 | logger := DefaultLogger 21 | 22 | // If we're running in quiet mode, buffer all log messages, and only print 23 | // them if an error occurs. 24 | if quiet { 25 | var buf strings.Builder 26 | logger = log.New(&buf, "", 0) 27 | defer func() { 28 | if quiet && err != nil { 29 | DefaultLogger.Print(buf.String()) 30 | } 31 | }() 32 | } 33 | 34 | migrations, err := src.FindMigrations() 35 | if err != nil { 36 | return errors.Wrap(err, "error reading migration files") 37 | } 38 | 39 | // Acquire an exclusive lock. 40 | locked, err := db.TryLock(ctx) 41 | if err != nil { 42 | return errors.Wrap(err, "error acquiring lock") 43 | } 44 | if !locked { 45 | return errors.New("could not acquire lock") 46 | } 47 | 48 | // Release the lock after running all migrations. 49 | defer func() { 50 | _, e := db.Unlock(ctx) 51 | if err != nil && e != nil { 52 | err = e 53 | } 54 | }() 55 | 56 | applied, err := db.GetMigrations(ctx) 57 | if err != nil { 58 | return errors.Wrap(err, "error fetching migrations") 59 | } 60 | isApplied := func(name string) bool { 61 | for _, a := range applied { 62 | if a.Name == name { 63 | return true 64 | } 65 | } 66 | return false 67 | } 68 | 69 | var pending []*source.Migration 70 | for _, m := range migrations { 71 | if !isApplied(m.Name) { 72 | pending = append(pending, m) 73 | } 74 | } 75 | 76 | if len(pending) == 0 { 77 | logger.Println("nothing to do") 78 | return nil 79 | } 80 | 81 | for _, m := range pending { 82 | logger.Printf("Running %s:", m.Name) 83 | stmts, err := m.ReadStatements() 84 | if err != nil { 85 | return errors.Wrap(err, "error reading migration") 86 | } 87 | for _, stmt := range stmts { 88 | logger.Println(prefixAll("> ", stmt)) 89 | start := time.Now() 90 | err := db.Exec(ctx, stmt) 91 | elapsed := time.Since(start) 92 | if err != nil { 93 | logger.Printf("=> FAIL (%s)", elapsed) 94 | return err 95 | } 96 | logger.Printf("=> OK (%v)", elapsed) 97 | } 98 | if err := db.LogCompletedMigration(ctx, m.Name); err != nil { 99 | return errors.Wrap(err, "error completing migration") 100 | } 101 | } 102 | return nil 103 | } 104 | 105 | // prefixAll prefixes every line in the string. 106 | func prefixAll(prefix, stmt string) string { 107 | ss := strings.Split(strings.TrimSpace(stmt), "\n") 108 | for i, s := range ss { 109 | ss[i] = prefix + s 110 | } 111 | return strings.Join(ss, "\n") 112 | } 113 | -------------------------------------------------------------------------------- /source/source.go: -------------------------------------------------------------------------------- 1 | // Package source facilitates reading (and generating) migration source 2 | // files. 3 | package source 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // Source is handle to a directory containing migration source files. 18 | type Source struct { 19 | path string 20 | } 21 | 22 | // New creates a new Source, or returns an error if the path does not 23 | // exist. 24 | func New(path string) (*Source, error) { 25 | if _, err := os.Stat(path); os.IsNotExist(err) { 26 | return nil, errors.Errorf("directory does not exist: %s", path) 27 | } 28 | return &Source{path: path}, nil 29 | } 30 | 31 | // Migration is a handle to a migration source file. 32 | type Migration struct { 33 | // Path is the path of the file. 34 | Path string 35 | 36 | // Name is the name of the migration, derived from the file name. 37 | Name string 38 | 39 | // Version is the numeric version of the migration, derived from the 40 | // file name and used to sort the migrations. 41 | Version int 42 | } 43 | 44 | // parseMigration parses a path into a Migration. 45 | func parseMigration(path string) (*Migration, error) { 46 | base := filepath.Base(path) 47 | sep := strings.Index(base, "_") 48 | if sep == -1 { 49 | return nil, errors.Errorf("invalid file name: %s", base) 50 | } 51 | version, err := strconv.Atoi(base[:sep]) 52 | if err != nil { 53 | return nil, errors.Errorf("invalid file name: %s", base) 54 | } 55 | name := strings.TrimSuffix(base, ".sql") 56 | m := &Migration{ 57 | Path: path, 58 | Name: name, 59 | Version: version, 60 | } 61 | return m, nil 62 | } 63 | 64 | // FindMigrations finds all migrations under the source path. 65 | func (s *Source) FindMigrations() ([]*Migration, error) { 66 | paths, err := filepath.Glob(filepath.Join(s.path, "*.sql")) 67 | if err != nil { 68 | return nil, errors.Wrap(err, "could not glob path") 69 | } 70 | result := make([]*Migration, len(paths)) 71 | for i, p := range paths { 72 | m, err := parseMigration(p) 73 | if err != nil { 74 | return nil, err 75 | } 76 | result[i] = m 77 | } 78 | sort.Sort(ByVersion(result)) 79 | return result, nil 80 | } 81 | 82 | // Create generates a new migration source file under the source path. 83 | func (s *Source) Create(name string) (string, error) { 84 | timestamp := time.Now().UTC().Format("20060102150405") 85 | filename := fmt.Sprintf("%s_%s.sql", timestamp, name) 86 | path := filepath.Join(s.path, filename) 87 | f, err := os.Create(path) 88 | if err != nil { 89 | return "", err 90 | } 91 | if err := f.Close(); err != nil { 92 | return "", err 93 | } 94 | return path, nil 95 | } 96 | 97 | // ByVersion sorts migrations by their version numbers. 98 | type ByVersion []*Migration 99 | 100 | func (ms ByVersion) Len() int { return len(ms) } 101 | func (ms ByVersion) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] } 102 | func (ms ByVersion) Less(i, j int) bool { 103 | return ms[i].Version < ms[j].Version 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/johngibb/migrate.svg?branch=master)](https://travis-ci.org/johngibb/migrate) 2 | [![GoDoc](https://godoc.org/github.com/johngibb/migrate?status.svg)](https://godoc.org/github.com/johngibb/migrate) 3 | 4 | # migrate 5 | 6 | Migrate is an exceedingly simple Postgres database migration tool. 7 | 8 | The simplicity is manifested in the following principles: 9 | 10 | * There are no "down" migrations. Because migrations necessarily mutate 11 | state on a live server, [you can't have a safe rollback button](https://mcfunley.com/page/3/). 12 | * Migrations are *not* automatically executed within a transaction. 13 | Transactions are expensive, often unnecessary, and prevent certain 14 | operations (e.g. `create index concurrently`). If the migration 15 | warrants transactional semantics, simply include `begin; ... ; 16 | commit;` within the source of your migration. 17 | * Migrations are not skipped if they are added out of order with regard 18 | to their version number, unlike [some alternative tools](https://github.com/mattes/migrate/issues/237). 19 | This handles the case where a migration is added in a feature branch, 20 | and no longer has the highest version number when merged into master. 21 | 22 | ## Install 23 | 24 | ``` 25 | $ go get -u github.com/johngibb/migrate/cmd/migrate 26 | ``` 27 | 28 | This will install `migrate` to your $GOPATH/bin directory. 29 | 30 | ## Usage 31 | 32 | Create a migration: 33 | 34 | ``` 35 | $ migrate create -src : 36 | Creates a new migration file. 37 | -src string 38 | directory containing migration files (default ".") 39 | ``` 40 | 41 | View pending and applied migrations: 42 | 43 | ``` 44 | $ migrate status: 45 | Display a list of pending and applied migrations. 46 | -conn string 47 | postgres connection string 48 | -src string 49 | directory containing migration files (default ".") 50 | ``` 51 | 52 | Apply pending migrations: 53 | 54 | ``` 55 | $ migrate up -src -conn [-quiet]: 56 | Apply all pending migrations. 57 | -conn string 58 | postgres connection string 59 | -quiet 60 | only print errors 61 | -src string 62 | directory containing migration files (default ".") 63 | ``` 64 | 65 | ## Migrations 66 | 67 | Migrations are written as plain SQL scripts. All statements should be 68 | terminated with a semicolon, as `migrate` will execute the script one 69 | statement at a time. 70 | 71 | A simple migration to add a users table might look like: 72 | 73 | ```sql 74 | create table users (id int, name text); 75 | ``` 76 | 77 | A more complicated migration that uses a transaction to create multiple 78 | tables and build an index might look like: 79 | 80 | ```sql 81 | begin; 82 | create table users (id int, name text); 83 | create table groups (id int, name text); 84 | create table users_groups (user_id int, group_id int); 85 | commit; 86 | create index concurrently on users (id); 87 | ``` 88 | 89 | # Development 90 | 91 | To run the full integration tests, you'll need to have 92 | [Docker for Mac](https://www.docker.com/docker-mac) installed. 93 | 94 | ``` 95 | make install // compiles the entire project 96 | make test // runs the unit tests on your host machine 97 | make test-all // compiles and runs all tests (including integration 98 | // tests against a Postgres instance) using Docker 99 | ``` 100 | -------------------------------------------------------------------------------- /migrate_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | "sync" 13 | "testing" 14 | "time" 15 | 16 | "github.com/jackc/pgx/v4" 17 | ) 18 | 19 | var ( 20 | connectionString string // set in setup 21 | waitOnce sync.Once 22 | ) 23 | 24 | func TestMigrateUp(t *testing.T) { 25 | ctx := context.Background() 26 | setup(ctx, t) 27 | // Create two migrations./ 28 | createMigration(ctx, "1_add_users_table.sql", ` 29 | begin; 30 | create table users(id int); 31 | commit; 32 | create index concurrently on users(id); 33 | `) 34 | createMigration(ctx, "2_add_orders_table.sql", `create table orders(id int);`) 35 | 36 | // Run the migrations. 37 | out := mustRun("migrate up --src ./migrations --conn %s", connectionString) 38 | 39 | // Verify the output looks correct. 40 | want := regexp.MustCompile(`Running 1_add_users_table: 41 | > begin; 42 | => OK \(.*\) 43 | > create table users\(id int\); 44 | => OK \(.*\) 45 | > commit; 46 | => OK \(.*\) 47 | > create index concurrently on users\(id\); 48 | => OK \(.*\) 49 | Running 2_add_orders_table: 50 | > create table orders\(id int\); 51 | => OK \(.*\) 52 | `) 53 | if !want.MatchString(out) { 54 | t.Errorf("output: want:\n%v\n\ngot:\n%s", want, out) 55 | } 56 | } 57 | 58 | func TestMigrateUpQuietNoError(t *testing.T) { 59 | ctx := context.Background() 60 | setup(ctx, t) 61 | // Create a migration. 62 | createMigration(ctx, "1_add_users_table.sql", "create table users(id int);") 63 | 64 | // Run the migration. 65 | out := mustRun("migrate up --src ./migrations --conn %s --quiet", connectionString) 66 | 67 | // Verify the output was quiet. 68 | if out != "" { 69 | t.Errorf("output: want blank, got:\n%s", out) 70 | } 71 | } 72 | 73 | func TestMigrateUpQuietError(t *testing.T) { 74 | ctx := context.Background() 75 | setup(ctx, t) 76 | // Create a broken migration. 77 | createMigration(ctx, "1_add_users_table.sql", `invalid sql statement;`) 78 | 79 | // Run the migrations. 80 | out, err := run("migrate up --src ./migrations --conn %s --quiet", connectionString) 81 | if err == nil { 82 | t.Fatal("error was nil") 83 | } 84 | 85 | // Verify the output was printed even though quiet was specified. 86 | want := regexp.MustCompile(`Running 1_add_users_table: 87 | > invalid sql statement; 88 | => FAIL \(.*\) 89 | migrate: ERROR: syntax error at or near "invalid" \(SQLSTATE 42601\) 90 | `) 91 | if !want.MatchString(out) { 92 | t.Errorf("output: want:\n%v\n\ngot:\n%s", want, out) 93 | } 94 | } 95 | 96 | func TestMigrateCreate(t *testing.T) { 97 | ctx := context.Background() 98 | setup(ctx, t) 99 | // Create a new migration. 100 | mustRun("migrate create --src ./migrations add_users_table") 101 | 102 | // Confirm the new file exists. 103 | files, err := filepath.Glob("./migrations/*add_users_table.sql") 104 | must(err, "error globbing migrations") 105 | if len(files) == 0 { 106 | t.Error("file not found: *add_users_table.sql") 107 | } 108 | } 109 | 110 | func TestMigrateStatus(t *testing.T) { 111 | ctx := context.Background() 112 | setup(ctx, t) 113 | // Create a new migration. 114 | createMigration(ctx, "1_add_users_table.sql", `create table users(id int);`) 115 | 116 | // Run `migrate status`, ensuring the above migration is displayed 117 | // as pending. 118 | out := mustRun("migrate status --src ./migrations --conn %s", connectionString) 119 | if want := "1_add_users_table pending"; !strings.Contains(out, want) { 120 | t.Errorf("output missing: %q", want) 121 | } 122 | } 123 | 124 | func TestMigrateStatusAndUp(t *testing.T) { 125 | ctx := context.Background() 126 | setup(ctx, t) 127 | // Create two migrations. 128 | createMigration(ctx, "1_add_users_table.sql", "create table users(id int);") 129 | createMigration(ctx, "2_add_users_table.sql", "create table orders(id int);") 130 | 131 | // Confirm they are listed as pending. 132 | out := mustRun("migrate status --src ./migrations --conn %s", connectionString) 133 | if want := "1_add_users_table pending"; !strings.Contains(out, want) { 134 | t.Errorf("output missing: %q", want) 135 | } 136 | if want := "2_add_users_table pending"; !strings.Contains(out, want) { 137 | t.Errorf("output missing: %q", want) 138 | } 139 | 140 | // Apply them using "migrate up". 141 | mustRun("migrate up --src ./migrations --conn %s", connectionString) 142 | 143 | // Confirm they are listed as applied. 144 | out = mustRun("migrate status --src ./migrations --conn %s", connectionString) 145 | if want := "1_add_users_table applied"; !strings.Contains(out, want) { 146 | t.Errorf("output missing: %q", want) 147 | } 148 | if want := "2_add_users_table applied"; !strings.Contains(out, want) { 149 | t.Errorf("output missing: %q", want) 150 | } 151 | } 152 | 153 | func TestDSN(t *testing.T) { 154 | ctx := context.Background() 155 | setup(ctx, t) 156 | createMigration(ctx, "1_add_users_table.sql", "create table users(id int);") 157 | 158 | // Parse the connection URI. 159 | cfg, err := pgx.ParseConfig(connectionString) 160 | must(err, "parsing pg uri") 161 | 162 | // Convert the URI to a DSN. 163 | dsn := fmt.Sprintf( 164 | "user=%s password=%s host=%s port=%d dbname=%s", 165 | cfg.User, 166 | cfg.Password, 167 | cfg.Host, 168 | cfg.Port, 169 | cfg.Database, 170 | ) 171 | 172 | // Confirm the tool still works correctly with a DSN. 173 | out := mustRun("migrate --src ./migrations --conn '%s' status", dsn) 174 | if want := "1_add_users_table pending"; !strings.Contains(out, want) { 175 | t.Errorf("output missing: %q", want) 176 | } 177 | } 178 | 179 | func TestLocking(t *testing.T) { 180 | ctx := context.Background() 181 | setup(ctx, t) 182 | createMigration(ctx, "1_add_users_table.sql", "select pg_sleep(1);") 183 | 184 | // Run the migration twice in parallel. 185 | out := make([]string, 2) 186 | var wg sync.WaitGroup 187 | wg.Add(2) 188 | for i := 0; i < 2; i++ { 189 | go func(i int) { 190 | out[i], _ = run("migrate --src ./migrations --conn %s up", connectionString) 191 | wg.Done() 192 | }(i) 193 | } 194 | wg.Wait() 195 | 196 | // Look for one success and one failure. 197 | var foundSuccess, foundFailure bool 198 | for _, s := range out { 199 | if strings.Contains(s, "=> OK") { 200 | foundSuccess = true 201 | } 202 | if strings.Contains(s, "could not acquire lock") { 203 | foundFailure = true 204 | } 205 | } 206 | if !foundSuccess { 207 | t.Error("neither command succeeded") 208 | } 209 | if !foundFailure { 210 | t.Error("neither command failed") 211 | } 212 | } 213 | 214 | func TestLegacyCommandLineArgs(t *testing.T) { 215 | ctx := context.Background() 216 | setup(ctx, t) 217 | mustRun("migrate -src ./migrations -conn %s create add_table", connectionString) 218 | mustRun("migrate -src ./migrations -conn %s status", connectionString) 219 | mustRun("migrate -src ./migrations -conn %s up", connectionString) 220 | 221 | mustRun("migrate -src=./migrations -conn=%s create add_table2", connectionString) 222 | mustRun("migrate -src=./migrations -conn=%s status", connectionString) 223 | mustRun("migrate -src=./migrations -conn=%s up", connectionString) 224 | } 225 | 226 | // run runs the command, returning the output and an error. 227 | func run(cmd string, args ...interface{}) (string, error) { 228 | parts := splitCMD(fmt.Sprintf(cmd, args...)) 229 | out, err := exec.Command(parts[0], parts[1:]...).CombinedOutput() 230 | return string(out), err 231 | } 232 | 233 | // mustRun runs the command, calling log.Fatal if any error occurs. 234 | func mustRun(cmd string, args ...interface{}) string { 235 | out, err := run(cmd, args...) 236 | if err != nil { 237 | log.Fatalf("%s:\n%v\n\n%s", err, "migrate", out) 238 | } 239 | return out 240 | } 241 | 242 | // must calls log.Fatal if the err is not nil. 243 | func must(err error, msg string) { 244 | if err != nil { 245 | log.Fatalf(msg+": %v", err) 246 | } 247 | } 248 | 249 | // splitCMD splits apart a command by spaces, preserving strings within 250 | // single quotes. 251 | // 252 | // For example: "migrate 'user=migrate password=migrate'" 253 | // 254 | // => ["migrate", "user=migrate password=migrate"] 255 | func splitCMD(s string) []string { 256 | r := regexp.MustCompile("'.+'|\".+\"|\\S+") 257 | result := r.FindAllString(s, -1) 258 | for i, s := range result { 259 | result[i] = strings.Trim(s, "'") // remove surrounding single quotes 260 | } 261 | return result 262 | } 263 | 264 | // clearMigrations recreates an empty migrations folder for testing. 265 | func clearMigrations() { 266 | must(os.RemoveAll("./migrations"), "error deleting migrations directory") 267 | must(os.MkdirAll("./migrations", os.ModeDir), "error creating migrations directory") 268 | } 269 | 270 | // resetDB drops all tables in the database. 271 | func resetDB(ctx context.Context) { 272 | cfg, err := pgx.ParseConfig(connectionString) 273 | must(err, "error parsing connection uri") 274 | conn, err := pgx.ConnectConfig(ctx, cfg) 275 | must(err, "error connecting to database") 276 | defer conn.Close(ctx) 277 | rows, err := conn.Query(ctx, `select table_name from information_schema.tables where table_schema = current_schema();`) 278 | must(err, "error fetching tables") 279 | var tables []string 280 | for rows.Next() { 281 | var name string 282 | must(rows.Scan(&name), "error scanning row") 283 | tables = append(tables, name) 284 | } 285 | for _, table := range tables { 286 | _, err := conn.Exec(ctx, "drop table "+table) 287 | must(err, "error dropping table") 288 | } 289 | } 290 | 291 | // createMigration creates a new migration with the given name in the "migrations" folder. 292 | func createMigration(ctx context.Context, name, source string) { 293 | f, err := os.Create("./migrations/" + name) 294 | must(err, "error writing migration") 295 | defer f.Close() 296 | fmt.Fprintln(f, source) 297 | } 298 | 299 | // setup prepares the environment for testing: 300 | // - waits for the db to accept connections 301 | // - clears the migrations directory 302 | // - resets the database 303 | func setup(ctx context.Context, t *testing.T) { 304 | if os.Getenv("RUN_MIGRATIONS") != "YES" { 305 | t.SkipNow() 306 | } 307 | uri := os.Getenv("DATABASE_URL") 308 | if uri == "" { 309 | t.Fatal("DATABASE_URL env not set") 310 | } 311 | 312 | // Wait for the DB to become available, since the `depends_on` 313 | // configuration in the docker-compose file waits for the postgres 314 | // container to finish booting, but not for the actual service to be 315 | // ready to accept connections. 316 | waitOnce.Do(func() { 317 | cfg, err := pgx.ParseConfig(uri) 318 | must(err, "error parsing uri") 319 | for i := 0; i < 20; i++ { 320 | conn, connErr := pgx.ConnectConfig(ctx, cfg) 321 | if connErr == nil { 322 | conn.Close(ctx) 323 | return 324 | } 325 | err = connErr 326 | time.Sleep(200 * time.Millisecond) 327 | } 328 | must(err, "error connecting to database") 329 | }) 330 | connectionString = uri 331 | clearMigrations() 332 | resetDB(ctx) 333 | } 334 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 3 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 4 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 5 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 6 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 7 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 8 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 13 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 14 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 15 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 16 | github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= 17 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 18 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 19 | github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315 h1:WW91Hq2v0qDzoPME+TPD4En72+d2Ue3ZMKPYfwR9yBU= 20 | github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 21 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 22 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 23 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 24 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 25 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 26 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 27 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 28 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 29 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 30 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 31 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 32 | github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= 33 | github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= 34 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 35 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 36 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 37 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 38 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 39 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 40 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 41 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 42 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 43 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 44 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 45 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 46 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 47 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 48 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 49 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 50 | github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= 51 | github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 52 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 53 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 54 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 55 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 56 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 57 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 58 | github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= 59 | github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 60 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 61 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 62 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 63 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 64 | github.com/jackc/pgx/v4 v4.17.0 h1:Hsx+baY8/zU2WtPLQyZi8WbecgcsWEeyoK1jvg/WgIo= 65 | github.com/jackc/pgx/v4 v4.17.0/go.mod h1:Gd6RmOhtFLTu8cp/Fhq4kP195KrshxYJH3oW8AWJ1pw= 66 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 67 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 68 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 69 | github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 70 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 71 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 72 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 73 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 74 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 75 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 76 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 77 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 78 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 79 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 80 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 81 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 82 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 83 | github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= 84 | github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 85 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 86 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 87 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 88 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 89 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 90 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 91 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 92 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 93 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 94 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 95 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 96 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 97 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 98 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 99 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 100 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 101 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 102 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 103 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 104 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 105 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 106 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 107 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 108 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 109 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 110 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 111 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 112 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 113 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 114 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 115 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 116 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 117 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 118 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 119 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 120 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 121 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 122 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 123 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 124 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 125 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 126 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 127 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 128 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 129 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 130 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 131 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 132 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 133 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 134 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 135 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 136 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 137 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 138 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 139 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= 140 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 141 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 142 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 143 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 144 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 145 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 146 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 147 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 148 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 149 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 150 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 151 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 152 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 153 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 154 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 155 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 156 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 157 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 158 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 159 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 160 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 161 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 164 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 165 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 166 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 167 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 168 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 169 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 170 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 171 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 172 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 173 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 174 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 175 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 176 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 177 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 178 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 179 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 180 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 181 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 182 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 183 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 184 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 185 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 186 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 187 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 188 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 189 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 190 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 191 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 192 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 193 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 194 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 195 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 196 | --------------------------------------------------------------------------------