├── .djinn.yml ├── .gitignore ├── LICENSE ├── cmd ├── internal │ ├── add.go │ ├── author.go │ ├── cat.go │ ├── cmd.go │ ├── db.go │ ├── log.go │ ├── ls.go │ ├── run.go │ ├── show.go │ └── sync.go └── mgrt │ └── main.go ├── db.go ├── db_sqlite3.go ├── go.mod ├── go.sum ├── make.sh ├── readme.md ├── revision.go └── revision_test.go /.djinn.yml: -------------------------------------------------------------------------------- 1 | driver: 2 | type: qemu 3 | image: debian/stable 4 | env: 5 | - LDFLAGS=-s -w 6 | sources: 7 | - https://github.com/andrewpillar/mgrt 8 | stages: 9 | - deps 10 | - test 11 | - make 12 | - make-sqlite3 13 | jobs: 14 | - stage: deps 15 | commands: 16 | - apt install -y curl 17 | - curl -sL https://golang.org/dl/go1.16.4.linux-amd64.tar.gz -o go.tar.gz 18 | - tar -xf go.tar.gz 19 | - mv go /usr/lib 20 | - ln -sf /usr/lib/go/bin/go /usr/bin/go 21 | - ln -sf /usr/lib/go/bin/gofmt /usr/bin/gofmt 22 | - stage: test 23 | commands: 24 | - cd mgrt 25 | - go test -cover -tags sqlite3 26 | - stage: make 27 | commands: 28 | - cd mgrt 29 | - ./make.sh 30 | artifacts: 31 | - mgrt/bin/mgrt 32 | - stage: make-sqlite3 33 | commands: 34 | - cd mgrt 35 | - TAGS="sqlite3" ./make.sh 36 | artifacts: 37 | - mgrt/bin/mgrt => mgrt-sqlite3 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | revisions/ 3 | *.db 4 | *.tar.gz 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Andrew Pillar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/internal/add.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | 11 | "github.com/andrewpillar/mgrt/v3" 12 | ) 13 | 14 | var ( 15 | revisionsDir = "revisions" 16 | 17 | AddCmd = &Command{ 18 | Usage: "add [comment]", 19 | Short: "add a new revision", 20 | Long: `Add will open up the editor specified via EDITOR for creating the new revision. 21 | The -c flag can be given to specify a category for the new revision.`, 22 | Run: addCmd, 23 | } 24 | ) 25 | 26 | func revisionPath(id string) string { 27 | return filepath.Join(revisionsDir, id+".sql") 28 | } 29 | 30 | func openInEditor(path string) error { 31 | editor := os.Getenv("EDITOR") 32 | 33 | if editor == "" { 34 | return errors.New("EDITOR not set") 35 | } 36 | 37 | cmd := exec.Command(editor, path) 38 | cmd.Stdin = os.Stdin 39 | cmd.Stdout = os.Stdout 40 | cmd.Stderr = os.Stderr 41 | return cmd.Run() 42 | } 43 | 44 | func addCmd(cmd *Command, args []string) { 45 | var category string 46 | 47 | fs := flag.NewFlagSet(cmd.Argv0, flag.ExitOnError) 48 | fs.StringVar(&category, "c", "", "the category to put the revision under") 49 | fs.Parse(args[1:]) 50 | 51 | args = fs.Args() 52 | 53 | var comment string 54 | 55 | if len(args) >= 1 { 56 | comment = args[0] 57 | } 58 | 59 | dir := revisionsDir 60 | 61 | if category != "" { 62 | dir = filepath.Join(revisionsDir, category) 63 | } 64 | 65 | if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { 66 | fmt.Fprintf(os.Stderr, "%s: failed to create %s directory: %s", cmd.Argv0, revisionsDir, err) 67 | os.Exit(1) 68 | } 69 | 70 | author, err := mgrtAuthor() 71 | 72 | if err != nil { 73 | fmt.Fprintf(os.Stderr, "%s: failed to get mgrt author: %s", cmd.Argv0, err) 74 | os.Exit(1) 75 | } 76 | 77 | var rev *mgrt.Revision 78 | 79 | if category != "" { 80 | rev = mgrt.NewRevisionCategory(category, author, comment) 81 | } else { 82 | rev = mgrt.NewRevision(author, comment) 83 | } 84 | 85 | path := filepath.Join(dir, rev.ID+".sql") 86 | 87 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, os.FileMode(0644)) 88 | 89 | if err != nil { 90 | fmt.Fprintf(os.Stderr, "%s: failed to create revision: %s", cmd.Argv0, err) 91 | os.Exit(1) 92 | } 93 | 94 | defer f.Close() 95 | 96 | f.WriteString(rev.String()) 97 | 98 | if err := openInEditor(path); err != nil { 99 | fmt.Fprintf(os.Stderr, "%s: failed to open revision file: %s", cmd.Argv0, err) 100 | os.Exit(1) 101 | } 102 | fmt.Println("revision created", rev.Slug()) 103 | } 104 | -------------------------------------------------------------------------------- /cmd/internal/author.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "os/user" 8 | "strings" 9 | ) 10 | 11 | func git(subcmd string, args ...string) (string, string, error) { 12 | var ( 13 | stdout bytes.Buffer 14 | stderr bytes.Buffer 15 | ) 16 | 17 | cmd := exec.Command("git", append([]string{subcmd}, args...)...) 18 | cmd.Stdin = os.Stdin 19 | cmd.Stdout = &stdout 20 | cmd.Stderr = &stderr 21 | 22 | err := cmd.Run() 23 | return stdout.String(), stderr.String(), err 24 | } 25 | 26 | // mgrtAuthor will attempt to get author information from git using the 27 | // config.name and config.email properties. If this fails, then it falls back 28 | // to getting the current user's username. 29 | func mgrtAuthor() (string, error) { 30 | stdout, _, err := git("config", "user.name") 31 | 32 | if err != nil { 33 | u, err := user.Current() 34 | 35 | if err != nil { 36 | return "", err 37 | } 38 | return u.Username, nil 39 | } 40 | 41 | name := strings.TrimSpace(stdout) 42 | 43 | stdout, _, err = git("config", "user.email") 44 | 45 | if err != nil { 46 | return name, nil 47 | } 48 | return name + " <" + strings.TrimSpace(stdout) + ">", nil 49 | } 50 | -------------------------------------------------------------------------------- /cmd/internal/cat.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/andrewpillar/mgrt/v3" 9 | ) 10 | 11 | var CatCmd = &Command{ 12 | Usage: "cat ", 13 | Short: "display the given revisions", 14 | Long: `Cat will print out the given revisions`, 15 | Run: catCmd, 16 | } 17 | 18 | func catCmd(cmd *Command, args []string) { 19 | var sql bool 20 | 21 | fs := flag.NewFlagSet(cmd.Argv0, flag.ExitOnError) 22 | fs.BoolVar(&sql, "sql", false, "only display the sql of the revision") 23 | fs.Parse(args[1:]) 24 | 25 | args = fs.Args() 26 | 27 | if len(args) < 1 { 28 | fmt.Fprintf(os.Stderr, "usage: %s \n", cmd.Argv0) 29 | os.Exit(1) 30 | } 31 | 32 | info, err := os.Stat(revisionsDir) 33 | 34 | if err != nil { 35 | if os.IsNotExist(err) { 36 | fmt.Fprintf(os.Stderr, "%s: no migrations created\n", cmd.Argv0) 37 | os.Exit(1) 38 | } 39 | fmt.Fprintf(os.Stderr, "%s: failed to cat revision(s): %s\n", cmd.Argv0, err) 40 | os.Exit(1) 41 | } 42 | 43 | if !info.IsDir() { 44 | fmt.Fprintf(os.Stderr, "%s: %s is not a directory\n", cmd.Argv0, revisionsDir) 45 | os.Exit(1) 46 | } 47 | 48 | for _, id := range args { 49 | r, err := mgrt.OpenRevision(revisionPath(id)) 50 | 51 | if err != nil { 52 | fmt.Fprintf(os.Stderr, "%s: failed to cat revision: %s\n", cmd.Argv0, err) 53 | os.Exit(1) 54 | } 55 | 56 | if sql { 57 | fmt.Println(r.SQL) 58 | return 59 | } 60 | fmt.Println(r.String()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cmd/internal/cmd.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | ) 8 | 9 | type Command struct { 10 | Argv0 string // Argv0 is the name of the process running the command. 11 | Usage string // Usage is the usage line of the command. 12 | Short string // Short is a short description of what the command does. 13 | Long string // Long is a longer more in depth description of the command. 14 | 15 | // Run is the function that will actually execute the command. This will 16 | // be passed a pointer to the Command itself, along with the arguments 17 | // given to it. The first item in the arguments list will be the command 18 | // name. 19 | Run func(*Command, []string) 20 | 21 | // Commands is the set of sub-commands the command could have. 22 | Commands *CommandSet 23 | } 24 | 25 | type CommandSet struct { 26 | longest int 27 | names []string 28 | cmds map[string]*Command 29 | 30 | Argv0 string 31 | Long string 32 | Usage func() 33 | } 34 | 35 | type ErrCommandNotFound string 36 | 37 | func helpCmd(cmd *Command, args []string) { 38 | if len(args) < 2 { 39 | cmd.Commands.usage() 40 | return 41 | } 42 | 43 | name := args[1] 44 | 45 | cmd1, ok := cmd.Commands.cmds[name] 46 | 47 | if !ok { 48 | fmt.Fprintf(os.Stderr, "%s %s: no such command. Run '%s %s'.\n", cmd.Argv0, args[0], cmd.Argv0, args[0]) 49 | os.Exit(1) 50 | } 51 | 52 | if cmd1.Long == "" && cmd1.Commands != nil { 53 | fmt.Printf("usage: %s %s\n", cmd1.Argv0, cmd1.Usage) 54 | cmd1.Commands.usage() 55 | return 56 | } 57 | 58 | fmt.Printf("usage: %s %s\n", cmd1.Argv0, cmd1.Usage) 59 | 60 | if cmd1.Long != "" { 61 | fmt.Println() 62 | fmt.Println(cmd1.Long) 63 | } 64 | } 65 | 66 | func HelpCmd(cmds *CommandSet) *Command { 67 | return &Command{ 68 | Usage: cmds.Argv0 + " help [command]", 69 | Short: "display usage and help information about a given command", 70 | Long: "", 71 | Run: helpCmd, 72 | Commands: cmds, 73 | } 74 | } 75 | 76 | func (c *CommandSet) defaultUsage() { 77 | sort.Strings(c.names) 78 | 79 | fmt.Println(c.Long) 80 | 81 | if len(c.names) > 0 { 82 | fmt.Println("The commands are:") 83 | fmt.Println() 84 | } 85 | 86 | printHelp := false 87 | 88 | for _, name := range c.names { 89 | if name == "help" { 90 | printHelp = true 91 | continue 92 | } 93 | 94 | fmt.Printf(" %s%*s%s\n", name, c.longest-len(name)+4, " ", c.cmds[name].Short) 95 | } 96 | 97 | if printHelp { 98 | fmt.Printf("\nUse '%s help [command]' for more information about that command.\n", c.Argv0) 99 | } 100 | } 101 | 102 | func (c *CommandSet) usage() { 103 | if c.Usage == nil { 104 | c.defaultUsage() 105 | return 106 | } 107 | c.Usage() 108 | } 109 | 110 | func (c *CommandSet) Add(name string, cmd *Command) { 111 | if c.cmds == nil { 112 | c.cmds = make(map[string]*Command) 113 | } 114 | 115 | if _, ok := c.cmds[name]; !ok { 116 | if l := len(name); l > c.longest { 117 | c.longest = l 118 | } 119 | 120 | cmd.Argv0 = c.Argv0 + " " + name 121 | 122 | c.names = append(c.names, name) 123 | c.cmds[name] = cmd 124 | } 125 | } 126 | 127 | func (c *CommandSet) Parse(args []string) error { 128 | if len(args) < 1 { 129 | c.usage() 130 | return nil 131 | } 132 | 133 | name := args[0] 134 | 135 | cmd, ok := c.cmds[name] 136 | 137 | if !ok { 138 | return ErrCommandNotFound(name) 139 | } 140 | 141 | cmd.Run(cmd, args) 142 | return nil 143 | } 144 | 145 | func (e ErrCommandNotFound) Error() string { return "command not found " + string(e) } 146 | -------------------------------------------------------------------------------- /cmd/internal/db.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type dbItem struct { 12 | Name string 13 | Type string 14 | DSN string 15 | } 16 | 17 | var ( 18 | DBLsCmd = &Command{ 19 | Usage: "ls", 20 | Short: "list the databases configured", 21 | Run: dbLsCmd, 22 | } 23 | 24 | DBSetCmd = &Command{ 25 | Usage: "set ", 26 | Short: "set the database connection", 27 | Run: dbSetCmd, 28 | } 29 | 30 | DBRmCmd = &Command{ 31 | Usage: "rm ", 32 | Short: "remove a database connection", 33 | Run: dbRmCmd, 34 | } 35 | ) 36 | 37 | func mgrtdir() (string, error) { 38 | cfgdir, err := os.UserConfigDir() 39 | 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | dir := filepath.Join(cfgdir, "mgrt") 45 | 46 | info, err := os.Stat(dir) 47 | 48 | if err != nil { 49 | if os.IsNotExist(err) { 50 | if err := os.MkdirAll(dir, os.FileMode(0700)); err != nil { 51 | return "", err 52 | } 53 | return dir, nil 54 | } 55 | return "", err 56 | } 57 | 58 | if !info.IsDir() { 59 | return "", errors.New("not a directory " + dir) 60 | } 61 | return dir, nil 62 | } 63 | 64 | func getdbitem(name string) (dbItem, error) { 65 | dir, err := mgrtdir() 66 | 67 | if err != nil { 68 | return dbItem{}, err 69 | } 70 | 71 | f, err := os.Open(filepath.Join(dir, name)) 72 | 73 | if err != nil { 74 | return dbItem{}, err 75 | } 76 | 77 | defer f.Close() 78 | 79 | it := dbItem{ 80 | Name: name, 81 | } 82 | 83 | if err := json.NewDecoder(f).Decode(&it); err != nil { 84 | return it, err 85 | } 86 | return it, nil 87 | } 88 | 89 | func DBCmd(argv0 string) *Command { 90 | cmd := &Command{ 91 | Usage: "db [arguments]", 92 | Short: "manage configured databases", 93 | Run: dbCmd, 94 | Commands: &CommandSet{ 95 | Argv0: argv0 + " db", 96 | }, 97 | } 98 | 99 | cmd.Commands.Add("ls", DBLsCmd) 100 | cmd.Commands.Add("rm", DBRmCmd) 101 | cmd.Commands.Add("set", DBSetCmd) 102 | return cmd 103 | } 104 | 105 | func dbCmd(cmd *Command, args []string) { 106 | if len(args[1:]) < 1 { 107 | fmt.Println("usage:", cmd.Argv0, cmd.Usage) 108 | } 109 | 110 | if err := cmd.Commands.Parse(args[1:]); err != nil { 111 | fmt.Fprintf(os.Stderr, "%s %s: %s\n", cmd.Argv0, args[0], err) 112 | os.Exit(1) 113 | } 114 | } 115 | 116 | func dbLsCmd(cmd *Command, args []string) { 117 | dir, err := mgrtdir() 118 | 119 | if err != nil { 120 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 121 | os.Exit(1) 122 | } 123 | 124 | err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 125 | if err != nil { 126 | return err 127 | } 128 | 129 | if info.IsDir() { 130 | return nil 131 | } 132 | 133 | b, err := os.ReadFile(path) 134 | 135 | if err != nil { 136 | return err 137 | } 138 | 139 | it := dbItem{ 140 | Name: filepath.Base(path), 141 | } 142 | 143 | if err := json.Unmarshal(b, &it); err != nil { 144 | return err 145 | } 146 | 147 | fmt.Println(it.Name) 148 | return nil 149 | }) 150 | 151 | if err != nil { 152 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 153 | os.Exit(1) 154 | } 155 | } 156 | 157 | func dbSetCmd(cmd *Command, args []string) { 158 | if len(args[1:]) != 3 { 159 | fmt.Fprintf(os.Stderr, "usage: %s \n", cmd.Argv0) 160 | os.Exit(1) 161 | } 162 | 163 | dir, err := mgrtdir() 164 | 165 | if err != nil { 166 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 167 | os.Exit(1) 168 | } 169 | 170 | it := dbItem{ 171 | Name: args[1], 172 | Type: args[2], 173 | DSN: args[3], 174 | } 175 | 176 | fname := filepath.Join(dir, it.Name) 177 | 178 | _, err = os.Stat(fname) 179 | 180 | if err != nil { 181 | if !os.IsNotExist(err) { 182 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 183 | os.Exit(1) 184 | } 185 | } 186 | 187 | if err == nil { 188 | if err := os.RemoveAll(fname); err != nil { 189 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 190 | os.Exit(1) 191 | } 192 | } 193 | 194 | f, err := os.OpenFile(fname, os.O_CREATE|os.O_WRONLY, os.FileMode(0400)) 195 | 196 | if err != nil { 197 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 198 | os.Exit(1) 199 | } 200 | 201 | defer f.Close() 202 | 203 | if err := json.NewEncoder(f).Encode(&it); err != nil { 204 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 205 | os.Exit(1) 206 | } 207 | } 208 | 209 | func dbRmCmd(cmd *Command, args []string) { 210 | if len(args[1:]) < 1 { 211 | fmt.Fprintf(os.Stderr, "usage: %s \n", cmd.Argv0) 212 | os.Exit(1) 213 | } 214 | 215 | dir, err := mgrtdir() 216 | 217 | if err != nil { 218 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 219 | os.Exit(1) 220 | } 221 | 222 | for _, name := range args[1:] { 223 | os.Remove(filepath.Join(dir, name)) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /cmd/internal/log.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/andrewpillar/mgrt/v3" 11 | ) 12 | 13 | var LogCmd = &Command{ 14 | Usage: "log", 15 | Short: "log the performed revisions", 16 | Long: `Log displays all of the revisions that have been performed in the given 17 | database. The -n flag can be given to limit the number of revisions that are 18 | shown in the log. The database to connect to is specified via the -type and 19 | -dsn flags, or via the -db flag if a database connection has been configured 20 | via the "mgrt db" command. 21 | 22 | The -type flag specifies the type of database to connect to, it will be one of, 23 | 24 | mysql 25 | postgresql 26 | sqlite3 27 | 28 | The -dsn flag specifies the data source name for the database. This will vary 29 | depending on the type of database you're connecting to. 30 | 31 | mysql and postgresql both allow for the URI connection string, such as, 32 | 33 | type://[user[:password]@][host]:[port][,...][/dbname][?param1=value1&...] 34 | 35 | where type would either be mysql or postgresql. The postgresql type also allows 36 | for the DSN string such as, 37 | 38 | host=localhost port=5432 dbname=mydb connect_timeout=10 39 | 40 | sqlite3 however will accept a filepath, or the :memory: string, for example, 41 | 42 | -dsn :memory:`, 43 | Run: logCmd, 44 | } 45 | 46 | func logCmd(cmd *Command, args []string) { 47 | var ( 48 | typ string 49 | dsn string 50 | dbname string 51 | n int 52 | ) 53 | 54 | fs := flag.NewFlagSet(cmd.Argv0, flag.ExitOnError) 55 | fs.StringVar(&typ, "type", "", "the database type one of postgresql, sqlite3") 56 | fs.StringVar(&dsn, "dsn", "", "the dsn for the database to run the revisions against") 57 | fs.StringVar(&dbname, "db", "", "the database to connect to") 58 | fs.IntVar(&n, "n", 0, "the number of entries to show") 59 | fs.Parse(args[1:]) 60 | 61 | if dbname != "" { 62 | it, err := getdbitem(dbname) 63 | 64 | if err != nil { 65 | if os.IsNotExist(err) { 66 | fmt.Fprintf(os.Stderr, "%s: database %s does not exist\n", cmd.Argv0, dbname) 67 | os.Exit(1) 68 | } 69 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 70 | os.Exit(1) 71 | } 72 | 73 | typ = it.Type 74 | dsn = it.DSN 75 | } 76 | 77 | if typ == "" { 78 | fmt.Fprintf(os.Stderr, "%s: database not specified\n", cmd.Argv0) 79 | os.Exit(1) 80 | } 81 | 82 | if dsn == "" { 83 | fmt.Fprintf(os.Stderr, "%s: database not specified\n", cmd.Argv0) 84 | os.Exit(1) 85 | } 86 | 87 | db, err := mgrt.Open(typ, dsn) 88 | 89 | if err != nil { 90 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 91 | os.Exit(1) 92 | } 93 | 94 | defer db.Close() 95 | 96 | revs, err := mgrt.GetRevisions(db, n) 97 | 98 | if err != nil { 99 | fmt.Fprintf(os.Stderr, "%s: failed to get revisions: %s\n", cmd.Argv0, err) 100 | os.Exit(1) 101 | } 102 | 103 | for _, rev := range revs { 104 | fmt.Println("revision", rev.Slug()) 105 | fmt.Println("Author: ", rev.Author) 106 | fmt.Println("Performed: ", rev.PerformedAt.Format(time.ANSIC)) 107 | fmt.Println() 108 | 109 | lines := strings.Split(rev.Comment, "\n") 110 | 111 | for _, line := range lines { 112 | fmt.Println(" ", line) 113 | } 114 | fmt.Println() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /cmd/internal/ls.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/andrewpillar/mgrt/v3" 9 | ) 10 | 11 | var LsCmd = &Command{ 12 | Usage: "ls", 13 | Short: "list revisions", 14 | Long: `List will display all of the revisions you have.`, 15 | Run: lsCmd, 16 | } 17 | 18 | func lsCmd(cmd *Command, args []string) { 19 | var category string 20 | 21 | fs := flag.NewFlagSet(cmd.Argv0, flag.ExitOnError) 22 | fs.StringVar(&category, "c", "", "the category to list the revisions of") 23 | fs.Parse(args[1:]) 24 | 25 | info, err := os.Stat(revisionsDir) 26 | 27 | if err != nil { 28 | if os.IsNotExist(err) { 29 | return 30 | } 31 | 32 | fmt.Fprintf(os.Stderr, "%s: failed to list revisions: %s\n", cmd.Argv0, err) 33 | os.Exit(1) 34 | } 35 | 36 | if !info.IsDir() { 37 | fmt.Fprintf(os.Stderr, "%s: %s is not a directory\n", cmd.Argv0, revisionsDir) 38 | os.Exit(1) 39 | } 40 | 41 | pad := 0 42 | 43 | revs, err := mgrt.LoadRevisions(revisionsDir) 44 | 45 | if err != nil { 46 | fmt.Fprintf(os.Stderr, "%s: failed to list revision: %s\n", cmd.Argv0, err) 47 | os.Exit(1) 48 | } 49 | 50 | for _, rev := range revs { 51 | if l := len(rev.Author); l > pad { 52 | pad = l 53 | } 54 | } 55 | 56 | show := true 57 | 58 | for _, r := range revs { 59 | if category != "" { 60 | show = r.Category == category 61 | } 62 | 63 | if show { 64 | if r.Comment != "" { 65 | fmt.Printf("%s: %-*s - %s\n", r.Slug(), pad, r.Author, r.Title()) 66 | continue 67 | } 68 | fmt.Printf("%s: %s\n", r.Slug(), r.Author) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/internal/run.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/andrewpillar/mgrt/v3" 10 | ) 11 | 12 | var RunCmd = &Command{ 13 | Usage: "run ", 14 | Short: "run the given revisions", 15 | Long: `Run will perform the given revisions against the given database. The database 16 | to connect to is specified via the -type and -dsn flags, or via the -db flag if a database 17 | connection has been configured via the "mgrt db" command. 18 | 19 | The -c flag specifies the category of revisions to run. If not given, then the 20 | default revisions will be run. 21 | 22 | The -type flag specifies the type of database to connect to, it will be one of, 23 | 24 | mysql 25 | postgresql 26 | sqlite3 27 | 28 | The -dsn flag specifies the data source name for the database. This will vary 29 | depending on the type of database you're connecting to. 30 | 31 | mysql and postgresql both allow for the URI connection string, such as, 32 | 33 | type://[user[:password]@][host]:[port][,...][/dbname][?param1=value1&...] 34 | 35 | where type would either be mysql or postgresql. The postgresql type also allows 36 | for the DSN string such as, 37 | 38 | host=localhost port=5432 dbname=mydb connect_timeout=10 39 | 40 | sqlite3 however will accept a filepath, or the :memory: string, for example, 41 | 42 | -dsn :memory:`, 43 | Run: runCmd, 44 | } 45 | 46 | func runCmd(cmd *Command, args []string) { 47 | info, err := os.Stat(revisionsDir) 48 | 49 | if err != nil { 50 | if os.IsNotExist(err) { 51 | fmt.Fprintf(os.Stderr, "%s: no revisions to run\n", cmd.Argv0) 52 | os.Exit(1) 53 | } 54 | 55 | fmt.Fprintf(os.Stderr, "%s: failed to run revisions: %s\n", cmd.Argv0, err) 56 | os.Exit(1) 57 | } 58 | 59 | if !info.IsDir() { 60 | fmt.Fprintf(os.Stderr, "%s: %s is not a directory\n", cmd.Argv0, revisionsDir) 61 | os.Exit(1) 62 | } 63 | 64 | var ( 65 | typ string 66 | dsn string 67 | category string 68 | dbname string 69 | verbose bool 70 | ) 71 | 72 | fs := flag.NewFlagSet(cmd.Argv0, flag.ExitOnError) 73 | fs.StringVar(&typ, "type", "", "the database type one of postgresql, sqlite3") 74 | fs.StringVar(&dsn, "dsn", "", "the dsn for the database to run the revisions against") 75 | fs.StringVar(&category, "c", "", "the category of revisions to run") 76 | fs.StringVar(&dbname, "db", "", "the database to connect to") 77 | fs.BoolVar(&verbose, "v", false, "display information about the revisions performed") 78 | fs.Parse(args[1:]) 79 | 80 | if dbname != "" { 81 | it, err := getdbitem(dbname) 82 | 83 | if err != nil { 84 | if os.IsNotExist(err) { 85 | fmt.Fprintf(os.Stderr, "%s: database %s does not exist\n", cmd.Argv0, dbname) 86 | os.Exit(1) 87 | } 88 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 89 | os.Exit(1) 90 | } 91 | 92 | typ = it.Type 93 | dsn = it.DSN 94 | } 95 | 96 | if typ == "" { 97 | fmt.Fprintf(os.Stderr, "%s: database not specified\n", cmd.Argv0) 98 | os.Exit(1) 99 | } 100 | 101 | if dsn == "" { 102 | fmt.Fprintf(os.Stderr, "%s: database not specified\n", cmd.Argv0) 103 | os.Exit(1) 104 | } 105 | 106 | revs := make([]*mgrt.Revision, 0) 107 | 108 | for _, id := range fs.Args() { 109 | rev, err := mgrt.OpenRevision(revisionPath(id)) 110 | 111 | if err != nil { 112 | fmt.Fprintf(os.Stderr, "%s: failed to open revision %s: %s\n", cmd.Argv0, id, err) 113 | os.Exit(1) 114 | } 115 | revs = append(revs, rev) 116 | } 117 | 118 | if len(revs) == 0 { 119 | dir := revisionsDir 120 | 121 | if category != "" { 122 | dir = filepath.Join(revisionsDir, category) 123 | } 124 | 125 | revs, err = mgrt.LoadRevisions(dir) 126 | 127 | if err != nil { 128 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 129 | os.Exit(1) 130 | } 131 | } 132 | 133 | db, err := mgrt.Open(typ, dsn) 134 | 135 | if err != nil { 136 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 137 | os.Exit(1) 138 | } 139 | 140 | defer db.Close() 141 | 142 | var c mgrt.Collection 143 | 144 | for _, rev := range revs { 145 | c.Put(rev) 146 | } 147 | 148 | code := 0 149 | 150 | for _, rev := range c.Slice() { 151 | if err := rev.Perform(db); err != nil { 152 | fmt.Fprintf(os.Stderr, "%s\n", err) 153 | code = 1 154 | continue 155 | } 156 | 157 | if verbose { 158 | fmt.Println(rev.ID, rev.Title()) 159 | } 160 | } 161 | 162 | if code != 0 { 163 | os.Exit(code) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /cmd/internal/show.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/andrewpillar/mgrt/v3" 11 | ) 12 | 13 | var ShowCmd = &Command{ 14 | Usage: "show [revision]", 15 | Short: "show the given revision", 16 | Long: `Show will show the SQL that was run in the given revision. If no revision is 17 | specified, then the latest revision will be shown, if any. The database to connect to is 18 | specified via the -type and -dsn flags, or via the -db flag if a database connection has 19 | been configured via the "mgrt db" command. 20 | 21 | The -type flag specifies the type of database to connect to, it will be one of, 22 | 23 | mysql 24 | postgresql 25 | sqlite3 26 | 27 | The -dsn flag specifies the data source name for the database. This will vary 28 | depending on the type of database you're connecting to. 29 | 30 | mysql and postgresql both allow for the URI connection string, such as, 31 | 32 | type://[user[:password]@][host]:[port][,...][/dbname][?param1=value1&...] 33 | 34 | where type would either be mysql or postgresql. The postgresql type also allows 35 | for the DSN string such as, 36 | 37 | host=localhost port=5432 dbname=mydb connect_timeout=10 38 | 39 | sqlite3 however will accept a filepath, or the :memory: string, for example, 40 | 41 | -dsn :memory:`, 42 | Run: showCmd, 43 | } 44 | 45 | func showCmd(cmd *Command, args []string) { 46 | var ( 47 | typ string 48 | dsn string 49 | dbname string 50 | ) 51 | 52 | fs := flag.NewFlagSet(cmd.Argv0, flag.ExitOnError) 53 | fs.StringVar(&typ, "type", "", "the database type one of postgresql, sqlite3") 54 | fs.StringVar(&dsn, "dsn", "", "the dsn for the database to run the revisions against") 55 | fs.StringVar(&dbname, "db", "", "the database to connect to") 56 | fs.Parse(args[1:]) 57 | 58 | if dbname != "" { 59 | it, err := getdbitem(dbname) 60 | 61 | if err != nil { 62 | if os.IsNotExist(err) { 63 | fmt.Fprintf(os.Stderr, "%s: database %s does not exist\n", cmd.Argv0, dbname) 64 | os.Exit(1) 65 | } 66 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 67 | os.Exit(1) 68 | } 69 | 70 | typ = it.Type 71 | dsn = it.DSN 72 | } 73 | 74 | if typ == "" { 75 | fmt.Fprintf(os.Stderr, "%s: database not specified\n", cmd.Argv0) 76 | os.Exit(1) 77 | } 78 | 79 | if dsn == "" { 80 | fmt.Fprintf(os.Stderr, "%s: database not specified\n", cmd.Argv0) 81 | os.Exit(1) 82 | } 83 | 84 | db, err := mgrt.Open(typ, dsn) 85 | 86 | if err != nil { 87 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd.Argv0, err) 88 | os.Exit(1) 89 | } 90 | 91 | defer db.Close() 92 | 93 | args = fs.Args() 94 | 95 | var rev *mgrt.Revision 96 | 97 | if len(args) >= 1 { 98 | rev, err = mgrt.GetRevision(db, args[0]) 99 | 100 | if err != nil { 101 | fmt.Fprintf(os.Stderr, "%s: failed to show revision: %s\n", cmd.Argv0, err) 102 | os.Exit(1) 103 | } 104 | } 105 | 106 | if rev == nil { 107 | revs, err := mgrt.GetRevisions(db, 1) 108 | 109 | if err != nil { 110 | fmt.Fprintf(os.Stderr, "%s: failed to show revision: %s\n", cmd.Argv0, err) 111 | os.Exit(1) 112 | } 113 | rev = revs[0] 114 | } 115 | 116 | fmt.Println("revision", rev.ID) 117 | fmt.Println("Author: ", rev.Author) 118 | fmt.Println("Performed: ", rev.PerformedAt.Format(time.ANSIC)) 119 | fmt.Println() 120 | 121 | lines := strings.Split(rev.Comment, "\n") 122 | 123 | for _, line := range lines { 124 | fmt.Println(" ", line) 125 | } 126 | fmt.Println() 127 | 128 | lines = strings.Split(rev.SQL, "\n") 129 | 130 | for _, line := range lines { 131 | fmt.Println(" ", line) 132 | } 133 | fmt.Println() 134 | } 135 | -------------------------------------------------------------------------------- /cmd/internal/sync.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/andrewpillar/mgrt/v3" 10 | ) 11 | 12 | var SyncCmd = &Command{ 13 | Usage: "sync <-type type> <-dsn dsn>", 14 | Short: "sync the performed revisions", 15 | Long: `Sync will update the local revisions with what has been performed in the 16 | database. Doing this will overwrite any pre-existing revisions you may have 17 | locally. The database to connect to is specified via the -type and -dsn flags, 18 | or via the -db flag if a database connection has been configured via the "mgrt db" 19 | command. 20 | 21 | The -type flag specifies the type of database to connect to, it will be one of, 22 | 23 | mysql 24 | postgresql 25 | sqlite3 26 | 27 | The -dsn flag specifies the data source name for the database. This will vary 28 | depending on the type of database you're connecting to. 29 | 30 | mysql and postgresql both allow for the URI connection string, such as, 31 | 32 | type://[user[:password]@][host]:[port][,...][/dbname][?param1=value1&...] 33 | 34 | where type would either be mysql or postgresql. The postgresql type also allows 35 | for the DSN string such as, 36 | 37 | host=localhost port=5432 dbname=mydb connect_timeout=10 38 | 39 | sqlite3 however will accept a filepath, or the :memory: string, for example, 40 | 41 | -dsn :memory:`, 42 | Run: syncCmd, 43 | } 44 | 45 | func syncCmd(cmd *Command, args []string) { 46 | argv0 := args[0] 47 | 48 | var ( 49 | typ string 50 | dsn string 51 | dbname string 52 | ) 53 | 54 | fs := flag.NewFlagSet(cmd.Argv0+" "+argv0, flag.ExitOnError) 55 | fs.StringVar(&typ, "type", "", "the database type one of postgresql, sqlite3") 56 | fs.StringVar(&dsn, "dsn", "", "the dsn for the database to run the revisions against") 57 | fs.StringVar(&dbname, "db", "", "the database to connect to") 58 | fs.Parse(args[1:]) 59 | 60 | if dbname != "" { 61 | it, err := getdbitem(dbname) 62 | 63 | if err != nil { 64 | if os.IsNotExist(err) { 65 | fmt.Fprintf(os.Stderr, "%s %s: database %s does not exist\n", cmd.Argv0, argv0, dbname) 66 | os.Exit(1) 67 | } 68 | fmt.Fprintf(os.Stderr, "%s %s: %s\n", cmd.Argv0, argv0, err) 69 | os.Exit(1) 70 | } 71 | 72 | typ = it.Type 73 | dsn = it.DSN 74 | } 75 | 76 | if typ == "" { 77 | fmt.Fprintf(os.Stderr, "%s %s: database not specified\n", cmd.Argv0, argv0) 78 | os.Exit(1) 79 | } 80 | 81 | if dsn == "" { 82 | fmt.Fprintf(os.Stderr, "%s %s: database not specified\n", cmd.Argv0, argv0) 83 | os.Exit(1) 84 | } 85 | 86 | db, err := mgrt.Open(typ, dsn) 87 | 88 | if err != nil { 89 | fmt.Fprintf(os.Stderr, "%s %s: %s\n", cmd.Argv0, argv0, err) 90 | os.Exit(1) 91 | } 92 | 93 | defer db.Close() 94 | 95 | if err := os.MkdirAll(revisionsDir, os.FileMode(0755)); err != nil { 96 | fmt.Fprintf(os.Stderr, "%s %s: %s\n", cmd.Argv0, argv0, err) 97 | os.Exit(1) 98 | } 99 | 100 | revs, err := mgrt.GetRevisions(db, -1) 101 | 102 | if err != nil { 103 | fmt.Fprintf(os.Stderr, "%s %s: failed to get revisions: %s\n", cmd.Argv0, argv0, err) 104 | os.Exit(1) 105 | } 106 | 107 | for _, rev := range revs { 108 | dir := filepath.Join(revisionsDir, rev.Category) 109 | 110 | err = func() error { 111 | if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { 112 | return err 113 | } 114 | 115 | f, err := os.OpenFile(filepath.Join(dir, rev.ID+".sql"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0644)) 116 | 117 | if err != nil { 118 | return err 119 | } 120 | 121 | defer f.Close() 122 | 123 | f.WriteString(rev.String()) 124 | return nil 125 | }() 126 | 127 | if err != nil { 128 | fmt.Fprintf(os.Stderr, "%s %s: failed to sync revisions: %s\n", cmd.Argv0, argv0, err) 129 | os.Exit(1) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /cmd/mgrt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/andrewpillar/mgrt/v3/cmd/internal" 9 | ) 10 | 11 | var Build string 12 | 13 | func run(args []string) error { 14 | cmds := &internal.CommandSet{ 15 | Argv0: args[0], 16 | Long: `mgrt is a simple migration tool. 17 | 18 | Usage: 19 | 20 | mgrt [-version] [arguments] 21 | `, 22 | } 23 | 24 | cmds.Add("add", internal.AddCmd) 25 | cmds.Add("cat", internal.CatCmd) 26 | cmds.Add("db", internal.DBCmd(cmds.Argv0)) 27 | cmds.Add("log", internal.LogCmd) 28 | cmds.Add("ls", internal.LsCmd) 29 | cmds.Add("run", internal.RunCmd) 30 | cmds.Add("show", internal.ShowCmd) 31 | cmds.Add("sync", internal.SyncCmd) 32 | cmds.Add("help", internal.HelpCmd(cmds)) 33 | 34 | var version bool 35 | 36 | fs := flag.NewFlagSet(args[0], flag.ExitOnError) 37 | fs.BoolVar(&version, "version", false, "display version information and exit") 38 | fs.Parse(args[1:]) 39 | 40 | if version { 41 | fmt.Println(Build) 42 | return nil 43 | } 44 | return cmds.Parse(fs.Args()) 45 | } 46 | 47 | func main() { 48 | if err := run(os.Args); err != nil { 49 | fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) 50 | os.Exit(1) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package mgrt 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | 10 | _ "github.com/go-sql-driver/mysql" 11 | _ "github.com/jackc/pgx/v4/stdlib" 12 | ) 13 | 14 | // DB is a thin abstraction over the *sql.DB struct from the stdlib. 15 | type DB struct { 16 | *sql.DB 17 | 18 | // Type is the type of database being connected to. This will be passed to 19 | // sql.Open when the connection is being opened. 20 | Type string 21 | 22 | // Init is the function to call to initialize the database for performing 23 | // revisions. 24 | Init func(*sql.DB) error 25 | 26 | // Parameterize is the function that is called to parameterize the query 27 | // that will be executed against the database. This will make sure the 28 | // correct SQL dialect is being used for the type of database. 29 | Parameterize func(string) string 30 | } 31 | 32 | var ( 33 | dbMu sync.RWMutex 34 | dbs = make(map[string]*DB) 35 | 36 | mysqlInit = `CREATE TABLE mgrt_revisions ( 37 | id VARCHAR NOT NULL UNIQUE, 38 | author VARCHAR NOT NULL, 39 | comment TEXT NOT NULL, 40 | sql TEXT NOT NULL, 41 | performed_at INT NOT NULL 42 | );` 43 | 44 | postgresInit = `CREATE TABLE mgrt_revisions ( 45 | id VARCHAR NOT NULL UNIQUE, 46 | author VARCHAR NOT NULL, 47 | comment TEXT NOT NULL, 48 | sql TEXT NOT NULL, 49 | performed_at INT NOT NULL 50 | );` 51 | ) 52 | 53 | func init() { 54 | Register("mysql", &DB{ 55 | Type: "mysql", 56 | Init: initMysql, 57 | Parameterize: parameterizeMysql, 58 | }) 59 | 60 | Register("postgresql", &DB{ 61 | Type: "pgx", 62 | Init: initPostgresql, 63 | Parameterize: parameterizePostgresql, 64 | }) 65 | } 66 | 67 | func initMysql(db *sql.DB) error { 68 | if _, err := db.Exec(mysqlInit); err != nil { 69 | if !strings.Contains(err.Error(), "already exists") { 70 | return err 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | func initPostgresql(db *sql.DB) error { 77 | if _, err := db.Exec(postgresInit); err != nil { 78 | if !strings.Contains(err.Error(), "already exists") { 79 | return err 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | func parameterizeMysql(s string) string { return s } 86 | 87 | func parameterizePostgresql(s string) string { 88 | q := make([]byte, 0, len(s)) 89 | n := int64(0) 90 | 91 | for i := strings.Index(s, "?"); i != -1; i = strings.Index(s, "?") { 92 | n++ 93 | 94 | q = append(q, s[:i]...) 95 | q = append(q, '$') 96 | q = strconv.AppendInt(q, n, 10) 97 | 98 | s = s[i+1:] 99 | } 100 | return string(append(q, []byte(s)...)) 101 | } 102 | 103 | // Register will register the given *DB for the given database type. If the 104 | // given type is a duplicate, then this panics. If the given *DB is nil, then 105 | // this panics. 106 | func Register(typ string, db *DB) { 107 | dbMu.Lock() 108 | defer dbMu.Unlock() 109 | 110 | if db == nil { 111 | panic("mgrt: nil database registered") 112 | } 113 | 114 | if _, ok := dbs[typ]; ok { 115 | panic("mgrt: database already registered for " + typ) 116 | } 117 | dbs[typ] = db 118 | } 119 | 120 | // Open is a utility function that will call sql.Open with the given typ and 121 | // dsn. The database connection returned from this will then be passed to Init 122 | // for initializing the database. 123 | func Open(typ, dsn string) (*DB, error) { 124 | dbMu.RLock() 125 | defer dbMu.RUnlock() 126 | 127 | db, ok := dbs[typ] 128 | 129 | if !ok { 130 | return nil, errors.New("unknown database type " + typ) 131 | } 132 | 133 | sqldb, err := sql.Open(db.Type, dsn) 134 | 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | if err := db.Init(sqldb); err != nil { 140 | return nil, err 141 | } 142 | 143 | db.DB = sqldb 144 | return db, nil 145 | } 146 | -------------------------------------------------------------------------------- /db_sqlite3.go: -------------------------------------------------------------------------------- 1 | //go:build sqlite3 2 | // +build sqlite3 3 | 4 | package mgrt 5 | 6 | import ( 7 | "database/sql" 8 | "strings" 9 | 10 | _ "github.com/mattn/go-sqlite3" 11 | ) 12 | 13 | var sqlite3Init = `CREATE TABLE mgrt_revisions ( 14 | id VARCHAR NOT NULL, 15 | author VARCHAR NOT NULL, 16 | comment TEXT NOT NULL, 17 | sql TEXT NOT NULL, 18 | performed_at INT NOT NULL 19 | );` 20 | 21 | func init() { 22 | Register("sqlite3", &DB{ 23 | Type: "sqlite3", 24 | Init: initSqlite3, 25 | Parameterize: func(s string) string { return s }, 26 | }) 27 | } 28 | 29 | func initSqlite3(db *sql.DB) error { 30 | if _, err := db.Exec(sqlite3Init); err != nil { 31 | if !strings.Contains(err.Error(), "already exists") { 32 | return err 33 | } 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/andrewpillar/mgrt/v3 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.6.0 // indirect 7 | github.com/jackc/pgx/v4 v4.11.0 // indirect 8 | github.com/mattn/go-sqlite3 v1.14.7 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 5 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 6 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 7 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 8 | github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= 9 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 10 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 12 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 14 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 15 | github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 16 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 17 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 18 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 19 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 20 | github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= 21 | github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 22 | github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= 23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 24 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 25 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 26 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 27 | github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= 28 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 29 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 30 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 31 | github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= 32 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 33 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 34 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= 35 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 36 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 37 | github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 38 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 39 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 40 | github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 41 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 42 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 43 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 46 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 47 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 48 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 49 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 50 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 51 | github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 52 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 54 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 55 | github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= 56 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= 57 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 58 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 59 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 60 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 61 | github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= 62 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 63 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 64 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 65 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 66 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 67 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 68 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 69 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 70 | github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 71 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 72 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 73 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 74 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 75 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 76 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 77 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 78 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 79 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 80 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 81 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 82 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 83 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 84 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 85 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 86 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 87 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 88 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 89 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 90 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 91 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 92 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 93 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 94 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 95 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 96 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 97 | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 98 | github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= 99 | github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 100 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 101 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 102 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 103 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 104 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 105 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 106 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 107 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 108 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 109 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 110 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 111 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 112 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 113 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 114 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 115 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 116 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 117 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 118 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 119 | github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= 120 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 121 | github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 122 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 123 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 124 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 125 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 126 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 127 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 128 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 129 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 130 | github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= 131 | github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= 132 | github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= 133 | github.com/jackc/pgconn v1.8.1 h1:ySBX7Q87vOMqKU2bbmKbUvtYhauDFclYbNDYIE1/h6s= 134 | github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= 135 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 136 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 137 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 138 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 139 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 140 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 141 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 142 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 143 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 144 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 145 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 146 | github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 147 | github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8= 148 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 149 | github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 150 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 151 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 152 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 153 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 154 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 155 | github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= 156 | github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= 157 | github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= 158 | github.com/jackc/pgtype v1.7.0 h1:6f4kVsW01QftE38ufBYxKciO6gyioXSC0ABIRLcZrGs= 159 | github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= 160 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 161 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 162 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 163 | github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= 164 | github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= 165 | github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= 166 | github.com/jackc/pgx/v4 v4.11.0 h1:J86tSWd3Y7nKjwT/43xZBvpi04keQWx8gNC2YkdJhZI= 167 | github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= 168 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 169 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 170 | github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 171 | github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 172 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 173 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 174 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 175 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 176 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 177 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 178 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 179 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 180 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 181 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 182 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 183 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 184 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 185 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 186 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 187 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 188 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 189 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 190 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 191 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 192 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 193 | github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= 194 | github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= 195 | github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= 196 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 197 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 198 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 199 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 200 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 201 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 202 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 203 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 204 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 205 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 206 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 207 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 208 | github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA= 209 | github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 210 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 211 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 212 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 213 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 214 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 215 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 216 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 217 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 218 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 219 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 220 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 221 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 222 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 223 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 224 | github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= 225 | github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= 226 | github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= 227 | github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= 228 | github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 229 | github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 230 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 231 | github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= 232 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 233 | github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 234 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 235 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 236 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 237 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 238 | github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= 239 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 240 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 241 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 242 | github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= 243 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 244 | github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 245 | github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 246 | github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= 247 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 248 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 249 | github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= 250 | github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= 251 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 252 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 253 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 254 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 255 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 256 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 257 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 258 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 259 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 260 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= 261 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 262 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 263 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 264 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 265 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 266 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 267 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 268 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 269 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 270 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 271 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 272 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 273 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 274 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 275 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 276 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 277 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 278 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 279 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 280 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 281 | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= 282 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 283 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 284 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 285 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 286 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 287 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 288 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 289 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 290 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 291 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 292 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 293 | github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 294 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 295 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 296 | github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 297 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 298 | github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 299 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 300 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 301 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 302 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 303 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 304 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 305 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 306 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 307 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 308 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 309 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 310 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 311 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 312 | go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= 313 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 314 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 315 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 316 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 317 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 318 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 319 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 320 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 321 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 322 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 323 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 324 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 325 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 326 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 327 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 328 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 329 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 330 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 331 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 332 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 333 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 334 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 335 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 336 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 337 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= 338 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 339 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 340 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 341 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 342 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 343 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 344 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 345 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 346 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 347 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 348 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 349 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 350 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 351 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 352 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 353 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 354 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 355 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 356 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 357 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 358 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 359 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 360 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 361 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 362 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 363 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 364 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 365 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 366 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 367 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 368 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 369 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 370 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 371 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 372 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 373 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 374 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 375 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 376 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 377 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 378 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 379 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 380 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 381 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 382 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 394 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 395 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 396 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 397 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 398 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 399 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 400 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 401 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 402 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 403 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 404 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 405 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 406 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 407 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 408 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 409 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 410 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 411 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 412 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 413 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 414 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 415 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 416 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 417 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 418 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 419 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 420 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 421 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 422 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 423 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 424 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 425 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 426 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 427 | google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 428 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 429 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 430 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 431 | google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= 432 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 433 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 434 | google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 435 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 436 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 437 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 438 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 439 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 440 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 441 | gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 442 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 443 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 444 | gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= 445 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 446 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 447 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 448 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 449 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 450 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 451 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 452 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 453 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 454 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 455 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 456 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 457 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 458 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | _version() { 6 | git log --decorate=full --format=format:%d | 7 | head -1 | 8 | tr ',' '\n' | 9 | grep tag: | 10 | cut -d / -f 3 | 11 | tr -d ',)' 12 | } 13 | 14 | [ ! -d bin ] && mkdir bin 15 | 16 | bin="$(basename $(pwd))" 17 | module="$(head -1 go.mod | awk '{ print $2 }')" 18 | version="$(_version)" 19 | 20 | [ "$version" = "" ] && { 21 | version="devel $(git log -n 1 --format='format: +%h %cd' HEAD)" 22 | } 23 | 24 | default_tags="netgo osusergo" 25 | default_ldflags=$(printf -- "-X 'main.Build=%s'" "$version") 26 | 27 | tags="$TAGS $default_tags" 28 | ldflags="$LDFLAGS $default_ldflags" 29 | 30 | set -x 31 | GOOS="$GOOS" GOARCH="$GOARCH" go build -ldflags "$ldflags" -tags "$tags" -o bin/"$bin" ./cmd/mgrt 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mgrt 2 | 3 | mgrt is a simple tool for managing revisions across SQL databases. It takes SQL 4 | scripts, runs them against the database, and keeps a log of them. 5 | 6 | * [Quick start](#quick-start) 7 | * [Database connection](#database-connection) 8 | * [Revisions](#revisions) 9 | * [Categories](#categories) 10 | * [Revision log](#revision-log) 11 | * [Viewing revisions](#viewing-revisions) 12 | * [Library usage](#library-usage) 13 | 14 | ## Quick start 15 | 16 | To install mgrt, clone the repository and run the `./make.sh` script, 17 | 18 | $ git clone https://github.com/andrewpillar/mgrt 19 | $ cd mgrt 20 | $ ./make.sh 21 | 22 | to build mgrt with SQLite3 support add `sqlite3` to the `TAGS` environment 23 | variable, 24 | 25 | $ TAGS="sqlite3" ./make.sh 26 | 27 | this will produce a binary at `bin/mgrt`, add this to your `PATH`. 28 | 29 | Once installed you can start using mgrt right away, there is nothing to 30 | initialize. To begin writing revisions simply invoke `mgrt add`, 31 | 32 | $ mgrt add "My first revision" 33 | 34 | this will create a new revision file in the `revisions` directory, and open 35 | it up for editting with the revision to write, 36 | 37 | /* 38 | Revision: 20060102150405 39 | Author: Andrew Pillar 40 | 41 | My first revision 42 | */ 43 | 44 | CREATE TABLE users ( 45 | id INT NOT NULL UNIQUE 46 | ); 47 | 48 | once you've saved the revision and quit the editor, you will see the revision ID 49 | printed out, 50 | 51 | $ mgrt add "My first revision" 52 | revision created 20060102150405 53 | 54 | local revisions can be viewed with `mgrt ls`. This will display the ID, the 55 | author of the revision, and its comment, if any, 56 | 57 | $ mgrt ls 58 | 20060102150405: Andrew Pillar - My first revision 59 | 60 | revisions can be applied to the database via `mgrt run`. This command takes two 61 | flags, `-type` and `-dsn` to specify the type of database to run the revision 62 | against, and the data source for that database. Let's run our revision against 63 | an SQLite3 database, 64 | 65 | $ mgrt run -type sqlite3 -dsn acme.db 66 | 67 | revisions can only be performed on a database once, and cannot be undone. We can 68 | view the revisions that have been run against the database with `mgrt log`. Just 69 | like `mgrt run`, we use the `-type` and `-dsn` flags to specify the database to 70 | connect to, 71 | 72 | $ mgrt log -type sqlite3 -dsn acme.db 73 | revision 20060102150405 74 | Author: Andrew Pillar 75 | Performed: Mon Jan 6 15:04:05 2006 76 | My first revision 77 | 78 | CREATE TABLE users ( 79 | id INT NOT NULL UNIQUE 80 | ); 81 | 82 | this will list out the revisions that have been performed, along with the SQL 83 | code that was executed as part of that revision. 84 | 85 | mgrt also offers the ability to sync the revisions that have been performed on 86 | a database against what you have locally. This is achieved with `mgrt sync`, and 87 | just like before, this also takes the `-type` and `-dsn` flags. Lets delete the 88 | `revisions` directory that was created for us and do a `mgrt sync`. 89 | 90 | $ rm -rf revisions 91 | $ mgrt ls 92 | $ mgrt sync -type sqlite3 -dsn acme.db 93 | $ mgrt ls 94 | 20060102150405: Andrew Pillar - My first revision 95 | 96 | with `mgrt sync` you can easily view the revisions that have been run against 97 | different databases. 98 | 99 | ## Database connection 100 | 101 | Database connections for mgrt can be managed via the `mgrt db` command. This 102 | allows you to set aliases for the different databases you can connect to, 103 | for example, 104 | 105 | $ mgrt db set local-db postgresql "host=localhost port=5432 dbname=dev user=admin password=secret" 106 | 107 | this can then be used via the `-db` flag for the commands that require a 108 | database connection. 109 | 110 | The `mgrt db set` command expects the type of the database, and the DSN for 111 | connecting to the database. The type will be one of, 112 | 113 | * mysql 114 | * postgresql 115 | * sqlite3 116 | 117 | the DSN will vary depending on the type of database being used. The mysql and 118 | postgresql you can use the URI connection string, such as, 119 | 120 | type://[user[:password]@][host]:[port][,...][/dbname][?param1=value1&...] 121 | 122 | where type would either be mysql or postgresql. The postgresql type also allows 123 | for the DSN string such as, 124 | 125 | host=localhost port=5432 dbname=mydb connect_timeout=10 126 | 127 | sqlite3 however will accept a filepath. 128 | 129 | You can also specify the `-type` and `-dsn` flags too. These take the same 130 | arguments as above. The `-db` flag however is more convenient to use. 131 | 132 | ## Revisions 133 | 134 | Revisions are SQL scripts that are performed against the given database. Each 135 | revision can only be performed once, and cannot be undone. If you wish to undo 136 | a revision, then it is recommended to write another revision that does the 137 | inverse of the prior. 138 | 139 | Revisions are stored in the `revisions` directory from where the `mgrt add` 140 | command was run. Each revision file is prefixed with a comment block header 141 | that contains metadata about the revision itself, such as the ID, the author and 142 | a short comment about the revision. 143 | 144 | ## Categories 145 | 146 | Revisions can be organized into categories via the command line. This is done 147 | by passing the `-c` flag to the `mgrt add` command and specifying the category 148 | for that revision. This will create a sub-directory in the `revisions` directory 149 | containing that revision. Revisions in a category will only be performed when 150 | the `-c` flag for that category is given to the `mgrt run` command. 151 | 152 | Organizing revisions into categories can be useful if you want to keep certain 153 | revision logic separate from other revision logic. For example, if you want to 154 | separate table creation from permission granting, you could do something like, 155 | 156 | $ mgrt add -c schema "Create users table" 157 | $ mgrt add -c perms "Grant permissions on users table" 158 | 159 | then, to perform the above revisions you would, 160 | 161 | $ mgrt run -c schema -db prod 162 | $ mgrt run -c perms -db prod 163 | 164 | ## Revision log 165 | 166 | Each time a revision is performed, a log will be made of that revision. This log 167 | is stored in the database, in the `mgrt_revisions` table. This will contain the 168 | ID, the author, the comment (if any), and the SQL code itself, along with the 169 | time of execution. 170 | 171 | The revisions performed against a database can be viewed with `mgrt log`, 172 | 173 | $ mgrt log -db local-dev 174 | revision 20060102150405 175 | Author: Andrew Pillar 176 | Performed: Mon Jan 6 15:04:05 2006 177 | 178 | My first revision 179 | 180 | ## Viewing revisions 181 | 182 | Local revisions can be viewed with `mgrt cat`. This simply takes a list of 183 | revision IDs to view. 184 | 185 | $ mgrt cat 20060102150405 186 | /* 187 | Revision: 20060102150405 188 | Author: Andrew Pillar 189 | 190 | My first revision 191 | */ 192 | 193 | CREATE TABLE users ( 194 | id INT NOT NULL UNIQUE 195 | ); 196 | 197 | The `-sql` flag can be passed to the command too to only display the SQL portion 198 | of the revision, 199 | 200 | $ mgrt cat -sql 20060102150405 201 | CREATE TABLE users ( 202 | id INT NOT NULL UNIQUE 203 | ); 204 | 205 | performed revisions can also be seen with `mgrt show`. You can pass a revision 206 | ID to `mgrt show` to view an individual revision. If no revision ID is given, 207 | then the latest revision is shown. 208 | 209 | $ mgrt show -db local-dev 20060102150405 210 | revision 20060102150405 211 | Author: Andrew Pillar 212 | Performed: Mon Jan 6 15:04:05 2006 213 | 214 | My first revision 215 | 216 | CREATE TABLE users ( 217 | id INT NOT NULL UNIQUE 218 | ); 219 | 220 | ## Library usage 221 | 222 | As well as a CLI application, mgrt can be used as a library should you want to 223 | be able to have revisions performed directly in your application. To start using 224 | it just import the repository into your code, 225 | 226 | import "github.com/andrewpillar/mgrt" 227 | 228 | from here you will be able to start creating revisions and performing them 229 | against any pre-existing database connection you may have, 230 | 231 | // mgrt.Open will wrap sql.Open from the stdlib, and initialize the database 232 | // for performing revisions. 233 | db, err := mgrt.Open("sqlite3", "acme.db") 234 | 235 | if err != nil { 236 | panic(err) // maybe acceptable here 237 | } 238 | 239 | rev := mgrt.NewRevision("Andrew", "This is being done from Go.") 240 | 241 | if err := rev.Perform(db); err != nil { 242 | if !errors.Is(err, mgrt.ErrPerformed) { 243 | panic(err) // not best practice 244 | } 245 | } 246 | 247 | all pre-existing revisions can be retrieved via GetRevisions, 248 | 249 | revs, err := mgrt.GetRevisions(db) 250 | 251 | if err != nil { 252 | panic(err) // don't actually do this 253 | } 254 | 255 | more information about using mgrt as a library can be found in the 256 | [Go doc](https://pkg.go.dev/github.com/andrewpillar/mgrt) itself for mgrt. 257 | -------------------------------------------------------------------------------- /revision.go: -------------------------------------------------------------------------------- 1 | // package mgrt provides a collection of functions for performing revisions 2 | // against any given database connection. 3 | package mgrt 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "database/sql" 9 | "errors" 10 | "io" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // node is a node in the binary tree of a Collection. This stores the val used 18 | // for sorting revisions in a Collection. The val will be the Unix time of the 19 | // Revision ID, since Revision IDs are a time in the layout of 20060102150405. 20 | type node struct { 21 | val int64 22 | rev *Revision 23 | left *node 24 | right *node 25 | } 26 | 27 | // Errors is a collection of errors that occurred. 28 | type Errors []error 29 | 30 | // Revision is the type that represents what SQL code has been executed against 31 | // a database as a revision. Typically, this would be changes made to the 32 | // database schema itself. 33 | type Revision struct { 34 | ID string // ID is the unique ID of the Revision. 35 | Category string // Category of the revision. 36 | Author string // Author is who authored the original Revision. 37 | Comment string // Comment provides a short description for the Revision. 38 | SQL string // SQL is the code that will be executed when the Revision is performed. 39 | PerformedAt time.Time // PerformedAt is when the Revision was executed. 40 | } 41 | 42 | // RevisionError represents an error that occurred with a revision. 43 | type RevisionError struct { 44 | ID string // ID is the ID of the revisions that errored. 45 | Err error // Err is the underlying error itself. 46 | } 47 | 48 | // Collection stores revisions in a binary tree. This ensures that when they are 49 | // retrieved, they will be retrieved in ascending order from when they were 50 | // initially added. 51 | type Collection struct { 52 | len int 53 | root *node 54 | } 55 | 56 | var ( 57 | revisionIdFormat = "20060102150405" 58 | 59 | // ErrInvalid is returned whenever an invalid Revision ID is encountered. A 60 | // Revision ID is considered invalid when the time layout 20060102150405 61 | // cannot be used for parse the ID. 62 | ErrInvalid = errors.New("revision id invalid") 63 | 64 | // ErrPerformed is returned whenever a Revision has already been performed. 65 | // This can be treated as a benign error. 66 | ErrPerformed = errors.New("revision already performed") 67 | 68 | ErrNotFound = errors.New("revision not found") 69 | ) 70 | 71 | func insertNode(n **node, val int64, r *Revision) { 72 | if (*n) == nil { 73 | (*n) = &node{ 74 | val: val, 75 | rev: r, 76 | } 77 | return 78 | } 79 | 80 | if val < (*n).val { 81 | insertNode(&(*n).left, val, r) 82 | return 83 | } 84 | insertNode(&(*n).right, val, r) 85 | } 86 | 87 | // NewRevision creates a new Revision with the given author, and comment. 88 | func NewRevision(author, comment string) *Revision { 89 | return &Revision{ 90 | ID: time.Now().Format(revisionIdFormat), 91 | Author: author, 92 | Comment: comment, 93 | } 94 | } 95 | 96 | // NewRevisionCategory creates a new Revision in the given category with the 97 | // given author and comment. 98 | func NewRevisionCategory(category, author, comment string) *Revision { 99 | rev := NewRevision(author, comment) 100 | rev.Category = category 101 | return rev 102 | } 103 | 104 | // RevisionPerformed checks to see if the given Revision has been performed 105 | // against the given database. 106 | func RevisionPerformed(db *DB, rev *Revision) error { 107 | var count int64 108 | 109 | if _, err := time.Parse(revisionIdFormat, rev.ID); err != nil { 110 | return ErrInvalid 111 | } 112 | 113 | q := db.Parameterize("SELECT COUNT(id) FROM mgrt_revisions WHERE (id = ?)") 114 | 115 | if err := db.QueryRow(q, rev.Slug()).Scan(&count); err != nil { 116 | return &RevisionError{ 117 | ID: rev.Slug(), 118 | Err: err, 119 | } 120 | } 121 | 122 | if count > 0 { 123 | return &RevisionError{ 124 | ID: rev.Slug(), 125 | Err: ErrPerformed, 126 | } 127 | } 128 | return nil 129 | } 130 | 131 | // GetRevision get's the Revision with the given ID. 132 | func GetRevision(db *DB, id string) (*Revision, error) { 133 | var ( 134 | rev Revision 135 | sec int64 136 | ) 137 | 138 | q := "SELECT id, author, comment, sql, performed_at FROM mgrt_revisions WHERE (id = ?)" 139 | 140 | row := db.QueryRow(db.Parameterize(q), id) 141 | 142 | var categoryid string 143 | 144 | if err := row.Scan(&categoryid, &rev.Author, &rev.Comment, &rev.SQL, &sec); err != nil { 145 | if errors.Is(err, sql.ErrNoRows) { 146 | return nil, &RevisionError{ 147 | ID: categoryid, 148 | Err: ErrNotFound, 149 | } 150 | } 151 | return nil, err 152 | } 153 | 154 | parts := strings.Split(categoryid, "/") 155 | 156 | end := len(parts) - 1 157 | 158 | rev.ID = parts[end] 159 | rev.Category = strings.Join(parts[:end], "/") 160 | 161 | rev.PerformedAt = time.Unix(sec, 0) 162 | return &rev, nil 163 | } 164 | 165 | // GetRevisions returns a list of all the revisions that have been performed 166 | // against the given database. If n is <= 0 then all of the revisions will be 167 | // retrieved, otherwise, only the given amount will be retrieved. The returned 168 | // revisions will be ordered by their performance date descending. 169 | func GetRevisions(db *DB, n int) ([]*Revision, error) { 170 | count := int64(n) 171 | 172 | if n <= 0 { 173 | q0 := "SELECT COUNT(id) FROM mgrt_revisions" 174 | 175 | if err := db.QueryRow(q0).Scan(&count); err != nil { 176 | return nil, err 177 | } 178 | } 179 | 180 | revs := make([]*Revision, 0, int(count)) 181 | 182 | q := "SELECT id, author, comment, sql, performed_at FROM mgrt_revisions ORDER BY performed_at DESC LIMIT ?" 183 | 184 | rows, err := db.Query(db.Parameterize(q), count) 185 | 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | defer rows.Close() 191 | 192 | for rows.Next() { 193 | var ( 194 | rev Revision 195 | sec int64 196 | categoryid string 197 | ) 198 | 199 | err = rows.Scan(&categoryid, &rev.Author, &rev.Comment, &rev.SQL, &sec) 200 | 201 | if err != nil { 202 | return nil, err 203 | } 204 | 205 | parts := strings.Split(categoryid, "/") 206 | 207 | end := len(parts) - 1 208 | 209 | rev.ID = parts[end] 210 | rev.Category = strings.Join(parts[:end], "/") 211 | 212 | rev.PerformedAt = time.Unix(sec, 0) 213 | revs = append(revs, &rev) 214 | } 215 | 216 | if err := rows.Err(); err != nil { 217 | return nil, err 218 | } 219 | return revs, nil 220 | } 221 | 222 | // PerformRevisions will perform the given revisions against the given database. 223 | // The given revisions will be sorted into ascending order first before they 224 | // are performed. If any of the given revisions have already been performed then 225 | // the Errors type will be returned containing *RevisionError for each revision 226 | // that was already performed. 227 | func PerformRevisions(db *DB, revs0 ...*Revision) error { 228 | var c Collection 229 | 230 | for _, rev := range revs0 { 231 | c.Put(rev) 232 | } 233 | 234 | errs := Errors(make([]error, 0, len(revs0))) 235 | revs := c.Slice() 236 | 237 | for _, rev := range revs { 238 | if err := rev.Perform(db); err != nil { 239 | if errors.Is(err, ErrPerformed) { 240 | errs = append(errs, err) 241 | continue 242 | } 243 | return err 244 | } 245 | } 246 | return errs.err() 247 | } 248 | 249 | // LoadRevisions loads all of the revisions from the given directory. This will 250 | // only load from a file with the .sql suffix in the name. 251 | func LoadRevisions(dir string) ([]*Revision, error) { 252 | revs := make([]*Revision, 0) 253 | 254 | visit := func(path string, info os.FileInfo, err error) error { 255 | if err != nil { 256 | return err 257 | } 258 | 259 | if info.IsDir() { 260 | return nil 261 | } 262 | 263 | if !strings.HasSuffix(path, ".sql") { 264 | return nil 265 | } 266 | 267 | rev, err := OpenRevision(path) 268 | 269 | if err != nil { 270 | return err 271 | } 272 | 273 | revs = append(revs, rev) 274 | return nil 275 | } 276 | 277 | if err := filepath.Walk(dir, visit); err != nil { 278 | return nil, err 279 | } 280 | return revs, nil 281 | } 282 | 283 | // OpenRevision opens the revision at the given path. 284 | func OpenRevision(path string) (*Revision, error) { 285 | f, err := os.Open(path) 286 | 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | defer f.Close() 292 | 293 | return UnmarshalRevision(f) 294 | } 295 | 296 | // UnmarshalRevision will unmarshal a Revision from the given io.Reader. This 297 | // will expect to see a comment block header that contains the metadata about 298 | // the Revision itself. This will check to see if the given Revision ID is 299 | // valid. A Revision id is considered valid when it can be parsed into a 300 | // valid time via time.Parse using the layout of 20060102150405. 301 | func UnmarshalRevision(r io.Reader) (*Revision, error) { 302 | br := bufio.NewReader(r) 303 | 304 | rev := &Revision{} 305 | 306 | var ( 307 | buf []rune = make([]rune, 0) 308 | r0 rune 309 | inBlock bool 310 | ) 311 | 312 | for { 313 | r, _, err := br.ReadRune() 314 | 315 | if err != nil { 316 | if err != io.EOF { 317 | return nil, err 318 | } 319 | rev.SQL = strings.TrimSpace(string(buf)) 320 | break 321 | } 322 | 323 | if r == '*' { 324 | if r0 == '/' { 325 | inBlock = true 326 | continue 327 | } 328 | } 329 | 330 | if r == '/' { 331 | if r0 == '*' { 332 | rev.Comment = strings.TrimSpace(string(buf)) 333 | buf = buf[0:0] 334 | inBlock = false 335 | continue 336 | } 337 | } 338 | 339 | if inBlock { 340 | if r == '\n' { 341 | if r0 == '\n' { 342 | if rev.ID == "" && rev.Author == "" { 343 | buf = buf[0:0] 344 | } 345 | goto cont 346 | } 347 | 348 | pos := -1 349 | 350 | for i, r := range buf { 351 | if r == ':' { 352 | pos = i 353 | break 354 | } 355 | } 356 | 357 | if pos < 0 { 358 | goto cont 359 | } 360 | 361 | if string(buf[pos-6:pos]) == "Author" { 362 | rev.Author = strings.TrimSpace(string(buf[pos+1:])) 363 | buf = buf[0:0] 364 | continue 365 | } 366 | 367 | if string(buf[pos-8:pos]) == "Revision" { 368 | rev.ID = strings.TrimSpace(string(buf[pos+1:])) 369 | buf = buf[0:0] 370 | continue 371 | } 372 | } 373 | } 374 | 375 | if r == '*' { 376 | peek, _, err := br.ReadRune() 377 | 378 | if err != nil { 379 | if err != io.EOF { 380 | return nil, err 381 | } 382 | continue 383 | } 384 | 385 | if peek == '/' { 386 | br.UnreadRune() 387 | r0 = r 388 | continue 389 | } 390 | } 391 | 392 | cont: 393 | buf = append(buf, r) 394 | r0 = r 395 | } 396 | 397 | parts := strings.Split(rev.ID, "/") 398 | end := len(parts) - 1 399 | 400 | rev.ID = parts[len(parts)-1] 401 | rev.Category = strings.Join(parts[:end], "/") 402 | 403 | if _, err := time.Parse(revisionIdFormat, rev.ID); err != nil { 404 | return nil, ErrInvalid 405 | } 406 | return rev, nil 407 | } 408 | 409 | func (n *node) walk(visit func(*Revision)) { 410 | if n.left != nil { 411 | n.left.walk(visit) 412 | } 413 | 414 | visit(n.rev) 415 | 416 | if n.right != nil { 417 | n.right.walk(visit) 418 | } 419 | } 420 | 421 | func (e Errors) err() error { 422 | if len(e) == 0 { 423 | return nil 424 | } 425 | return e 426 | } 427 | 428 | // Error returns the string representation of all the errors in the underlying 429 | // slice. Each error will be on a separate line in the returned string. 430 | func (e Errors) Error() string { 431 | var buf bytes.Buffer 432 | 433 | for _, err := range e { 434 | buf.WriteString(err.Error() + "\n") 435 | } 436 | return buf.String() 437 | } 438 | 439 | // Put puts the given Revision in the current Collection. 440 | func (c *Collection) Put(r *Revision) error { 441 | if r.ID == "" { 442 | return ErrInvalid 443 | } 444 | 445 | t, err := time.Parse(revisionIdFormat, r.ID) 446 | 447 | if err != nil { 448 | return ErrInvalid 449 | } 450 | 451 | insertNode(&c.root, t.Unix(), r) 452 | c.len++ 453 | return nil 454 | } 455 | 456 | // Len returns the number of items in the collection. 457 | func (c *Collection) Len() int { return c.len } 458 | 459 | // Slice returns a sorted slice of all the revisions in the collection. 460 | func (c *Collection) Slice() []*Revision { 461 | if c.len == 0 { 462 | return nil 463 | } 464 | 465 | revs := make([]*Revision, 0, c.len) 466 | 467 | c.root.walk(func(r *Revision) { 468 | revs = append(revs, r) 469 | }) 470 | return revs 471 | } 472 | 473 | func (e *RevisionError) Error() string { 474 | return "revision error " + e.ID + ": " + e.Err.Error() 475 | } 476 | 477 | // Unwrap returns the underlying error that caused the original RevisionError. 478 | func (e *RevisionError) Unwrap() error { return e.Err } 479 | 480 | // Slug returns the slug of the revision ID, this will be in the format of 481 | // category/id if the revision belongs to a category. 482 | func (r *Revision) Slug() string { 483 | if r.Category != "" { 484 | return r.Category + "/" + r.ID 485 | } 486 | return r.ID 487 | } 488 | 489 | // Perform will perform the current Revision against the given database. If 490 | // the Revision is emtpy, then nothing happens. If the Revision has already 491 | // been performed, then ErrPerformed is returned. 492 | func (r *Revision) Perform(db *DB) error { 493 | if r.SQL == "" { 494 | return nil 495 | } 496 | 497 | if err := RevisionPerformed(db, r); err != nil { 498 | return err 499 | } 500 | 501 | if _, err := db.Exec(r.SQL); err != nil { 502 | return &RevisionError{ 503 | ID: r.Slug(), 504 | Err: err, 505 | } 506 | } 507 | 508 | q := db.Parameterize("INSERT INTO mgrt_revisions (id, author, comment, sql, performed_at) VALUES (?, ?, ?, ?, ?)") 509 | 510 | if _, err := db.Exec(q, r.Slug(), r.Author, r.Comment, r.SQL, time.Now().Unix()); err != nil { 511 | return &RevisionError{ 512 | ID: r.Slug(), 513 | Err: err, 514 | } 515 | } 516 | return nil 517 | } 518 | 519 | // Title will extract the title from the comment of the current Revision. First, 520 | // this will truncate the title to being 72 characters. If the comment was longer 521 | // than 72 characters, then the title will be suffixed with "...". If a LF 522 | // character can be found in the title, then the title will be truncated again 523 | // up to where that LF character occurs. 524 | func (r *Revision) Title() string { 525 | title := r.Comment 526 | 527 | if l := len(title); l >= 72 { 528 | title = title[:72] 529 | 530 | if l > 72 { 531 | title += "..." 532 | } 533 | } 534 | 535 | if i := bytes.IndexByte([]byte(title), '\n'); i > 0 { 536 | title = title[:i] 537 | } 538 | return title 539 | } 540 | 541 | // String returns the string representation of the Revision. This will be the 542 | // comment block header followed by the Revision SQL itself. 543 | func (r *Revision) String() string { 544 | var buf bytes.Buffer 545 | 546 | buf.WriteString("/*\n") 547 | buf.WriteString("Revision: " + r.Slug() + "\n") 548 | buf.WriteString("Author: " + r.Author + "\n") 549 | 550 | if r.Comment != "" { 551 | buf.WriteString("\n" + r.Comment + "\n") 552 | } 553 | buf.WriteString("*/\n\n") 554 | buf.WriteString(r.SQL) 555 | return buf.String() 556 | } 557 | -------------------------------------------------------------------------------- /revision_test.go: -------------------------------------------------------------------------------- 1 | package mgrt 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | _ "github.com/mattn/go-sqlite3" 11 | ) 12 | 13 | func Test_UnmarshalRevision(t *testing.T) { 14 | r := strings.NewReader(`/* 15 | Revision: 20060102150405 16 | Author: Author 17 | 18 | Title 19 | 20 | Comment line 1 21 | Comment line 2 22 | */ 23 | DROP TABLE users;`) 24 | 25 | rev, err := UnmarshalRevision(r) 26 | 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | if rev.ID != "20060102150405" { 32 | t.Errorf("unexpected revision id, expected=%q, got=%q\n", "20060102150405", rev.ID) 33 | } 34 | 35 | if rev.Author != "Author " { 36 | t.Errorf("unexpected revision author, expected=%q, got=%q\n", "Author ", rev.Author) 37 | } 38 | 39 | if rev.Comment != "Title\n\nComment line 1\nComment line 2" { 40 | t.Errorf("unexpected revision comment, expected=%q, got=%q\n", "Title\n\nComment line 1\nComment line 2", rev.Comment) 41 | } 42 | 43 | if title := rev.Title(); title != "Title" { 44 | t.Errorf("unexpected revision comment title, expected=%q, got=%q\n", "Title", title) 45 | } 46 | 47 | if rev.SQL != "DROP TABLE users;" { 48 | t.Errorf("unexpected revision sql, expected=%q, got=%q\n", "DROP TABLE users;", rev.SQL) 49 | } 50 | } 51 | 52 | func Test_RevisionTitle(t *testing.T) { 53 | singleLineComment := "A title that is longer than 72 characters in length this should be trimmed with an ellipsis." 54 | multiLineComment := `A comment that will have multiple lines and a long title line 55 | 56 | This is the body of the comment.` 57 | shortComment := "A simple comment that is shorter thant 72 characters in length" 58 | 59 | tests := []struct { 60 | comment string 61 | expected string 62 | }{ 63 | { 64 | comment: singleLineComment, 65 | expected: "A title that is longer than 72 characters in length this should be trimm...", 66 | }, 67 | { 68 | comment: multiLineComment, 69 | expected: "A comment that will have multiple lines and a long title line", 70 | }, 71 | { 72 | comment: shortComment, 73 | expected: shortComment, 74 | }, 75 | } 76 | 77 | for i, test := range tests { 78 | rev := Revision{Comment: test.comment} 79 | 80 | if title := rev.Title(); title != test.expected { 81 | t.Errorf("tests[%d] - expected=%q, got=%q\n", i, test.expected, title) 82 | } 83 | } 84 | } 85 | 86 | func Test_RevisionPerformMultiple(t *testing.T) { 87 | tmp, err := ioutil.TempFile("", "mgrt-db-*") 88 | 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | defer os.Remove(tmp.Name()) 94 | 95 | db, err := Open("sqlite3", tmp.Name()) 96 | 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | defer db.Close() 102 | 103 | tests := []struct { 104 | id string 105 | author string 106 | comment string 107 | sql string 108 | }{ 109 | { 110 | "20060102150407", 111 | "Andrew", 112 | "Add password to users table", 113 | "ALTER TABLE users ADD COLUMN password VARCHAR NOT NULL;", 114 | }, 115 | { 116 | "20060102150405", 117 | "Andrew", 118 | "Add users table", 119 | "CREATE TABLE users ( id INT NOT NULL UNIQUE );", 120 | }, 121 | { 122 | "20060102150406", 123 | "Andrew", 124 | "Add username to users table", 125 | "ALTER TABLE users ADD COLUMN username VARCHAR NOT NULL;", 126 | }, 127 | } 128 | 129 | revs := make([]*Revision, 0, len(tests)) 130 | 131 | for _, test := range tests { 132 | rev := NewRevision(test.author, test.comment) 133 | rev.ID = test.id 134 | rev.SQL = test.sql 135 | 136 | revs = append(revs, rev) 137 | } 138 | 139 | if err := PerformRevisions(db, revs...); err != nil { 140 | t.Fatal(err) 141 | } 142 | 143 | _, err = GetRevision(db, "foo") 144 | 145 | if !errors.Is(err, ErrNotFound) { 146 | t.Fatalf("unexpected error, expected=%T, got=%T\n", ErrNotFound, err) 147 | } 148 | 149 | if _, err = GetRevision(db, "20060102150406"); err != nil { 150 | t.Fatal(err) 151 | } 152 | } 153 | 154 | func Test_RevisionPerform(t *testing.T) { 155 | tmp, err := ioutil.TempFile("", "mgrt-db-*") 156 | 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | defer os.Remove(tmp.Name()) 162 | 163 | db, err := Open("sqlite3", tmp.Name()) 164 | 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | 169 | defer db.Close() 170 | 171 | tests := []struct { 172 | id string 173 | repeat bool 174 | author string 175 | comment string 176 | sql string 177 | }{ 178 | { 179 | "20060102150405", 180 | true, 181 | "Andrew", 182 | "Add users table", 183 | "CREATE TABLE users ( id INT NOT NULL UNIQUE );", 184 | }, 185 | { 186 | "20060102150406", 187 | false, 188 | "Andrew", 189 | "Add username to users table", 190 | "ALTER TABLE users ADD COLUMN username VARCHAR NOT NULL;", 191 | }, 192 | { 193 | "20060102150407", 194 | false, 195 | "Andrew", 196 | "Add password to users table", 197 | "ALTER TABLE users ADD COLUMN password VARCHAR NOT NULL;", 198 | }, 199 | } 200 | 201 | for i, test := range tests { 202 | rev := NewRevision(test.author, test.comment) 203 | rev.ID = test.id 204 | rev.SQL = test.sql 205 | 206 | if err := rev.Perform(db); err != nil { 207 | t.Fatalf("tests[%d] - unexpected error %T %q\n", i, err, err) 208 | } 209 | 210 | if test.repeat { 211 | if err := rev.Perform(db); err != nil { 212 | if !errors.Is(err, ErrPerformed) { 213 | t.Fatalf("tests[%d] - unexpected error, expected=%T, got=%t\n", i, ErrPerformed, errors.Unwrap(err)) 214 | } 215 | } 216 | } 217 | } 218 | 219 | revs, err := GetRevisions(db, -1) 220 | 221 | if err != nil { 222 | t.Fatal(err) 223 | } 224 | 225 | if len(revs) != len(tests) { 226 | t.Fatalf("unexpected revision count, expected=%d, got=%d\n", len(tests), len(revs)) 227 | } 228 | } 229 | 230 | func Test_LoadRevisions(t *testing.T) { 231 | revs, err := LoadRevisions("revisions") 232 | 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | if l := len(revs); l != 2 { 238 | t.Fatalf("unexpected revision count, expected=%d, got=%d\n", l, 2) 239 | } 240 | } 241 | --------------------------------------------------------------------------------