├── .golangci.yml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── collection.go ├── db_test.go ├── default.go ├── embed_test.go ├── example ├── 1_initial.go ├── 2_add_id.go ├── 3_seed_data.go ├── 4_delete_value.tx.down.sql ├── 4_insert_value.tx.up.sql └── main.go ├── go.mod ├── go.sum ├── migrations.go ├── testdata └── sqlmigrations │ ├── 1_initial.down.sql │ └── 1_initial.up.sql └── vfs_test.go /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | addons: 4 | postgresql: "9.6" 5 | 6 | go: 7 | - 1.16.x 8 | - tip 9 | 10 | matrix: 11 | allow_failures: 12 | - go: tip 13 | 14 | go_import_path: github.com/go-pg/migrations 15 | 16 | before_install: 17 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.21.0 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v6.5 4 | 5 | - `go run *.go init` must be used to create the `gopg_migrations` table. 6 | - `gopg_migrations` table is locked during migration. 7 | - Versions are updated within a PostgreSQL migration. 8 | 9 | ## v6.3 10 | 11 | - Added support for pure SQL migrations located files `123_comment.up.sql` and `123_comment.down.sql`. 12 | - Added `RegisterTx` and `MustRegisterTx` for migrations that must be run in transactions. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 github.com/go-pg/migrations Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go test ./... 3 | go test ./... -short -race 4 | go test ./... -run=NONE -bench=. 5 | env GOOS=linux GOARCH=386 go test ./... 6 | golangci-lint run 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQL migrations for Golang and PostgreSQL 2 | 3 | [![Build Status](https://travis-ci.org/go-pg/migrations.svg)](https://travis-ci.org/go-pg/migrations) 4 | [![GoDoc](https://godoc.org/github.com/go-pg/migrations?status.svg)](https://godoc.org/github.com/go-pg/migrations) 5 | 6 | This package allows you to run migrations on your PostgreSQL database using [Golang Postgres client](https://github.com/go-pg/pg). See [example](example) for details. 7 | 8 | You may also want to check [go-pg-migrations](https://github.com/robinjoseph08/go-pg-migrations) before making a decision. 9 | 10 | # Installation 11 | 12 | go-pg/migrations requires a Go version with [Modules](https://github.com/golang/go/wiki/Modules) support and uses import path versioning. So please make sure to initialize a Go module: 13 | 14 | ```shell 15 | go mod init github.com/my/repo 16 | go get github.com/go-pg/migrations/v8 17 | ``` 18 | 19 | # Usage 20 | 21 | To run migrations on your project you should fulfill the following steps: 22 | 23 | 1. define the migration list; 24 | 1. implement an executable app that calls migration tool; 25 | 1. run migrations. 26 | 27 | ## Define Migrations 28 | 29 | ### Migration Files 30 | 31 | You can save SQL migration files at the same directory as your `main.go` file, they should have proper file extensions ([more about migration files](#sql-migrations)). 32 | 33 | ### Registered Migrations 34 | 35 | Migrations can be registered in the code using `migrations.RegisterTx` and `migrations.MustRegisterTx` functions. [More details](#registering-migrations) about migration registering. 36 | 37 | ## Implement app to run the tool 38 | 39 | You can run migrations from any place of your app or ecosystem. It can be a standalone application of a part of a big program, or maybe an HTTP handler, etc. Check [example](#example) for some helpful information about practical usage. 40 | 41 | ## Run Migrations 42 | 43 | Run migration tool by providing CLI arguments to the `migrations.Run` function. 44 | 45 | Currently, the following arguments are supported: 46 | 47 | - `up` - runs all available migrations; 48 | - `up [target]` - runs available migrations up to the target one; 49 | - `down` - reverts last migration; 50 | - `reset` - reverts all migrations; 51 | - `version` - prints current db version; 52 | - `set_version [version]` - sets db version without running migrations. 53 | 54 | # Example 55 | 56 | You need to create database `pg_migrations_example` before running the [example](example). 57 | 58 | ```bash 59 | > cd example 60 | 61 | > psql -c "CREATE DATABASE pg_migrations_example" 62 | CREATE DATABASE 63 | 64 | > go run *.go init 65 | version is 0 66 | 67 | > go run *.go version 68 | version is 0 69 | 70 | > go run *.go 71 | creating table my_table... 72 | adding id column... 73 | seeding my_table... 74 | migrated from version 0 to 4 75 | 76 | > go run *.go version 77 | version is 4 78 | 79 | > go run *.go reset 80 | truncating my_table... 81 | dropping id column... 82 | dropping table my_table... 83 | migrated from version 4 to 0 84 | 85 | > go run *.go up 2 86 | creating table my_table... 87 | adding id column... 88 | migrated from version 0 to 2 89 | 90 | > go run *.go 91 | seeding my_table... 92 | migrated from version 2 to 4 93 | 94 | > go run *.go down 95 | truncating my_table... 96 | migrated from version 4 to 3 97 | 98 | > go run *.go version 99 | version is 3 100 | 101 | > go run *.go set_version 1 102 | migrated from version 3 to 1 103 | 104 | > go run *.go create add email to users 105 | created migration 5_add_email_to_users.go 106 | ``` 107 | 108 | ## Registering Migrations 109 | 110 | ### `migrations.RegisterTx` and `migrations.MustRegisterTx` 111 | 112 | Registers migrations to be executed inside transactions. 113 | 114 | ### `migrations.Register` and `migrations.MustRegister` 115 | 116 | Registers migrations to be executed without any transaction. 117 | 118 | ## SQL migrations 119 | 120 | SQL migrations are automatically picked up if placed in the same folder with `main.go` or Go migrations. 121 | SQL migrations may be manually registered using `DiscoverSQLMigrations` (from OS directory) or `DiscoverSQLMigrationsFromFilesystem`. 122 | It may be used with new go 1.16 embedding feature. Example: 123 | ```go 124 | //go:embed migrations/*.sql 125 | var migrations embed.FS 126 | 127 | collection := migrations.NewCollection() 128 | collection.DiscoverSQLMigrationsFromFilesystem(http.FS(migrations), "migrations") 129 | ``` 130 | SQL migrations must have one of the following extensions: 131 | 132 | - .up.sql - up migration; 133 | - .down.sql - down migration; 134 | - .tx.up.sql - transactional up migration; 135 | - .tx.down.sql - transactional down migration. 136 | 137 | By default SQL migrations are executed as single PostgreSQL statement. `--gopg:split` directive can be used to split migration into several statements: 138 | 139 | ```sql 140 | SET statement_timeout = 60000; 141 | SET lock_timeout = 60000; 142 | 143 | --gopg:split 144 | 145 | CREATE INDEX CONCURRENTLY ...; 146 | ``` 147 | 148 | ## Transactions 149 | 150 | By default, the migrations are executed outside without any transactions. Individual migrations can however be marked to be executed inside transactions by using the `RegisterTx` function instead of `Register`. 151 | 152 | ### Global Transactions 153 | 154 | ```go 155 | var oldVersion, newVersion int64 156 | 157 | err := db.RunInTransaction(func(tx *pg.Tx) (err error) { 158 | oldVersion, newVersion, err = migrations.Run(tx, flag.Args()...) 159 | return 160 | }) 161 | if err != nil { 162 | exitf(err.Error()) 163 | } 164 | ``` 165 | -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "math" 10 | "net/http" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "regexp" 15 | "runtime" 16 | "sort" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | 21 | "github.com/go-pg/pg/v10" 22 | ) 23 | 24 | type Migration struct { 25 | Version int64 26 | 27 | UpTx bool 28 | Up func(DB) error 29 | 30 | DownTx bool 31 | Down func(DB) error 32 | } 33 | 34 | func (m *Migration) String() string { 35 | return strconv.FormatInt(m.Version, 10) 36 | } 37 | 38 | type Collection struct { 39 | tableName string 40 | sqlAutodiscoverDisabled bool 41 | 42 | mu sync.Mutex 43 | visitedDirs map[string]struct{} 44 | migrations []*Migration // sorted 45 | } 46 | 47 | func NewCollection(migrations ...*Migration) *Collection { 48 | c := &Collection{ 49 | tableName: "gopg_migrations", 50 | } 51 | for _, m := range migrations { 52 | c.addMigration(m) 53 | } 54 | return c 55 | } 56 | 57 | func (c *Collection) SetTableName(tableName string) *Collection { 58 | c.tableName = tableName 59 | return c 60 | } 61 | 62 | func (c *Collection) schemaTableName() (string, string) { 63 | if ind := strings.IndexByte(c.tableName, '.'); ind >= 0 { 64 | return c.tableName[:ind], c.tableName[ind+1:] 65 | } 66 | return "public", c.tableName 67 | } 68 | 69 | func (c *Collection) DisableSQLAutodiscover(flag bool) *Collection { 70 | c.sqlAutodiscoverDisabled = flag 71 | return c 72 | } 73 | 74 | // Register registers new database migration. Must be called 75 | // from a file with name like "1_initialize_db.go". 76 | func (c *Collection) Register(fns ...func(DB) error) error { 77 | return c.register(false, fns...) 78 | } 79 | 80 | // RegisterTx is like Register, but migration will be run in a transaction. 81 | func (c *Collection) RegisterTx(fns ...func(DB) error) error { 82 | return c.register(true, fns...) 83 | } 84 | 85 | func (c *Collection) register(tx bool, fns ...func(DB) error) error { 86 | var up, down func(DB) error 87 | switch len(fns) { 88 | case 0: 89 | return errors.New("Register expects at least 1 arg") 90 | case 1: 91 | up = fns[0] 92 | case 2: 93 | up = fns[0] 94 | down = fns[1] 95 | default: 96 | return fmt.Errorf("Register expects at most 2 args, got %d", len(fns)) 97 | } 98 | 99 | file := migrationFile() 100 | version, err := extractVersionGo(file) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if !c.sqlAutodiscoverDisabled { 106 | err = c.DiscoverSQLMigrations(filepath.Dir(file)) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | 112 | c.addMigration(&Migration{ 113 | Version: version, 114 | 115 | UpTx: tx, 116 | Up: up, 117 | 118 | DownTx: tx, 119 | Down: down, 120 | }) 121 | 122 | return nil 123 | } 124 | 125 | func migrationFile() string { 126 | const depth = 32 127 | var pcs [depth]uintptr 128 | n := runtime.Callers(1, pcs[:]) 129 | frames := runtime.CallersFrames(pcs[:n]) 130 | 131 | for { 132 | f, ok := frames.Next() 133 | if !ok { 134 | break 135 | } 136 | if !strings.Contains(f.Function, "/go-pg/migrations") { 137 | return f.File 138 | } 139 | } 140 | 141 | return "" 142 | } 143 | 144 | // DiscoverSQLMigrations scan the dir for files with .sql extension 145 | // and adds discovered SQL migrations to the collection. 146 | func (c *Collection) DiscoverSQLMigrations(dir string) error { 147 | dir, err := filepath.Abs(dir) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return c.DiscoverSQLMigrationsFromFilesystem(osfilesystem{}, dir) 153 | } 154 | 155 | // DiscoverSQLMigrations scan the dir from the given filesystem for files with .sql extension 156 | // and adds discovered SQL migrations to the collection. 157 | func (c *Collection) DiscoverSQLMigrationsFromFilesystem(fs http.FileSystem, dir string) error { 158 | if c.isVisitedDir(dir) { 159 | return nil 160 | } 161 | 162 | f, err := fs.Open(dir) 163 | if os.IsNotExist(err) { 164 | return nil 165 | } 166 | if err != nil { 167 | return err 168 | } 169 | defer f.Close() 170 | 171 | if _, err := f.Stat(); os.IsNotExist(err) { 172 | return nil 173 | } 174 | 175 | var ms []*Migration 176 | newMigration := func(version int64) *Migration { 177 | for i := range ms { 178 | m := ms[i] 179 | if m.Version == version { 180 | return m 181 | } 182 | } 183 | 184 | ms = append(ms, &Migration{ 185 | Version: version, 186 | }) 187 | return ms[len(ms)-1] 188 | } 189 | 190 | files, err := f.Readdir(-1) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | // Sort files to have consistent errors. 196 | sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() }) 197 | 198 | for _, f := range files { 199 | if f.IsDir() { 200 | continue 201 | } 202 | 203 | fileName := f.Name() 204 | if !strings.HasSuffix(fileName, ".sql") { 205 | continue 206 | } 207 | 208 | idx := strings.IndexByte(fileName, '_') 209 | if idx == -1 { 210 | err := fmt.Errorf( 211 | "file=%q must have name in format version_comment, e.g. 1_initial", 212 | fileName) 213 | return err 214 | } 215 | 216 | version, err := strconv.ParseInt(fileName[:idx], 10, 64) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | m := newMigration(version) 222 | filePath := filepath.Join(dir, fileName) 223 | 224 | if strings.HasSuffix(fileName, ".up.sql") { 225 | if m.Up != nil { 226 | return fmt.Errorf("migration=%d already has Up func", version) 227 | } 228 | m.UpTx = strings.HasSuffix(fileName, ".tx.up.sql") 229 | m.Up = newSQLMigration(fs, filePath) 230 | continue 231 | } 232 | 233 | if strings.HasSuffix(fileName, ".down.sql") { 234 | if m.Down != nil { 235 | return fmt.Errorf("migration=%d already has Down func", version) 236 | } 237 | m.DownTx = strings.HasSuffix(fileName, ".tx.down.sql") 238 | m.Down = newSQLMigration(fs, filePath) 239 | continue 240 | } 241 | 242 | return fmt.Errorf( 243 | "file=%q must have extension .up.sql or .down.sql", fileName) 244 | } 245 | 246 | for _, m := range ms { 247 | c.addMigration(m) 248 | } 249 | 250 | return nil 251 | } 252 | 253 | func (c *Collection) isVisitedDir(dir string) bool { 254 | c.mu.Lock() 255 | defer c.mu.Unlock() 256 | 257 | if _, ok := c.visitedDirs[dir]; ok { 258 | return true 259 | } 260 | 261 | if c.visitedDirs == nil { 262 | c.visitedDirs = make(map[string]struct{}) 263 | } 264 | c.visitedDirs[dir] = struct{}{} 265 | 266 | return false 267 | } 268 | 269 | func newSQLMigration(fs http.FileSystem, filePath string) func(DB) error { 270 | return func(db DB) error { 271 | f, err := fs.Open(filePath) 272 | if err != nil { 273 | return err 274 | } 275 | defer f.Close() 276 | 277 | scanner := bufio.NewScanner(f) 278 | 279 | var query []byte 280 | var queries []string 281 | for scanner.Scan() { 282 | b := scanner.Bytes() 283 | 284 | const prefix = "--gopg:" 285 | if bytes.HasPrefix(b, []byte(prefix)) { 286 | b = b[len(prefix):] 287 | if bytes.Equal(b, []byte("split")) { 288 | queries = append(queries, string(query)) 289 | query = query[:0] 290 | continue 291 | } 292 | return fmt.Errorf("unknown gopg directive: %q", b) 293 | } 294 | 295 | query = append(query, b...) 296 | query = append(query, '\n') 297 | } 298 | if len(query) > 0 { 299 | queries = append(queries, string(query)) 300 | } 301 | 302 | if err := scanner.Err(); err != nil { 303 | return err 304 | } 305 | 306 | if len(queries) > 1 { 307 | switch v := db.(type) { 308 | case *pg.DB: 309 | conn := v.Conn() 310 | defer conn.Close() 311 | db = conn 312 | } 313 | } 314 | 315 | for _, q := range queries { 316 | _, err = db.Exec(q) 317 | if err != nil { 318 | return err 319 | } 320 | } 321 | 322 | return nil 323 | } 324 | } 325 | 326 | func (c *Collection) addMigration(migration *Migration) { 327 | c.mu.Lock() 328 | defer c.mu.Unlock() 329 | 330 | for i, m := range c.migrations { 331 | if m.Version > migration.Version { 332 | c.migrations = insert(c.migrations, i, migration) 333 | return 334 | } 335 | } 336 | 337 | c.migrations = append(c.migrations, migration) 338 | } 339 | 340 | func insert(s []*Migration, i int, x *Migration) []*Migration { 341 | s = append(s, nil) 342 | copy(s[i+1:], s[i:]) 343 | s[i] = x 344 | return s 345 | } 346 | 347 | func (c *Collection) MustRegister(fns ...func(DB) error) { 348 | err := c.Register(fns...) 349 | if err != nil { 350 | panic(err) 351 | } 352 | } 353 | 354 | func (c *Collection) MustRegisterTx(fns ...func(DB) error) { 355 | err := c.RegisterTx(fns...) 356 | if err != nil { 357 | panic(err) 358 | } 359 | } 360 | 361 | func (c *Collection) Migrations() []*Migration { 362 | if !c.sqlAutodiscoverDisabled { 363 | _ = c.DiscoverSQLMigrations(filepath.Dir(migrationFile())) 364 | 365 | dir, err := os.Getwd() 366 | if err == nil { 367 | _ = c.DiscoverSQLMigrations(dir) 368 | } 369 | } 370 | 371 | c.mu.Lock() 372 | defer c.mu.Unlock() 373 | 374 | // Make a copy to avoid side effects. 375 | migrations := make([]*Migration, len(c.migrations)) 376 | copy(migrations, c.migrations) 377 | 378 | return migrations 379 | } 380 | 381 | func (c *Collection) Run(db DB, a ...string) (oldVersion, newVersion int64, err error) { 382 | migrations := c.Migrations() 383 | err = validateMigrations(migrations) 384 | if err != nil { 385 | return 386 | } 387 | 388 | cmd := "up" 389 | if len(a) > 0 { 390 | cmd = a[0] 391 | } 392 | 393 | switch cmd { 394 | case "init": 395 | err = c.createTable(db) 396 | if err != nil { 397 | return 398 | } 399 | return 400 | case "create": 401 | if len(a) < 2 { 402 | fmt.Println("please provide migration description") 403 | return 404 | } 405 | 406 | var version int64 407 | if len(migrations) > 0 { 408 | version = migrations[len(migrations)-1].Version 409 | } 410 | 411 | filename := fmtMigrationFilename(version+1, strings.Join(a[1:], "_")) 412 | err = createMigrationFile(filename) 413 | if err != nil { 414 | return 415 | } 416 | 417 | fmt.Println("created new migration", filename) 418 | return 419 | } 420 | 421 | exists, err := c.tableExists(db) 422 | if err != nil { 423 | return 424 | } 425 | if !exists { 426 | err = fmt.Errorf("table %q does not exist; did you run init?", c.tableName) 427 | return 428 | } 429 | 430 | tx, version, err := c.begin(db) 431 | if err != nil { 432 | return 433 | } 434 | defer tx.Close() //nolint 435 | 436 | oldVersion = version 437 | newVersion = version 438 | 439 | switch cmd { 440 | case "version": 441 | case "up": 442 | target := int64(math.MaxInt64) 443 | if len(a) > 1 { 444 | target, err = strconv.ParseInt(a[1], 10, 64) 445 | if err != nil { 446 | return 447 | } 448 | if version > target { 449 | break 450 | } 451 | } 452 | 453 | for _, m := range migrations { 454 | if m.Version > target { 455 | break 456 | } 457 | 458 | if tx == nil { 459 | tx, version, err = c.begin(db) 460 | if err != nil { 461 | return 462 | } 463 | } 464 | 465 | if m.Version <= version { 466 | continue 467 | } 468 | 469 | newVersion, err = c.runUp(db, tx, m) 470 | if err != nil { 471 | return 472 | } 473 | 474 | err = tx.Commit() 475 | if err != nil { 476 | return 477 | } 478 | tx = nil 479 | } 480 | case "down": 481 | newVersion, err = c.down(db, tx, migrations, version) 482 | if err != nil { 483 | return 484 | } 485 | case "reset": 486 | for { 487 | if tx == nil { 488 | tx, version, err = c.begin(db) 489 | if err != nil { 490 | return 491 | } 492 | } 493 | 494 | newVersion, err = c.down(db, tx, migrations, version) 495 | if err != nil { 496 | return 497 | } 498 | 499 | err = tx.Commit() 500 | if err != nil { 501 | return 502 | } 503 | tx = nil 504 | 505 | if newVersion == version { 506 | break 507 | } 508 | version = newVersion 509 | } 510 | case "set_version": 511 | if len(a) < 2 { 512 | err = fmt.Errorf("set_version requires version as 2nd arg, e.g. set_version 42") 513 | return 514 | } 515 | 516 | newVersion, err = strconv.ParseInt(a[1], 10, 64) 517 | if err != nil { 518 | return 519 | } 520 | err = c.SetVersion(tx, newVersion) 521 | if err != nil { 522 | return 523 | } 524 | default: 525 | err = fmt.Errorf("unsupported command: %q", cmd) 526 | if err != nil { 527 | return 528 | } 529 | } 530 | 531 | if tx != nil { 532 | err = tx.Commit() 533 | } 534 | return 535 | } 536 | 537 | func validateMigrations(migrations []*Migration) error { 538 | versions := make(map[int64]struct{}, len(migrations)) 539 | for _, m := range migrations { 540 | if _, ok := versions[m.Version]; ok { 541 | return fmt.Errorf( 542 | "there are multiple migrations with version=%d", m.Version) 543 | } 544 | versions[m.Version] = struct{}{} 545 | } 546 | return nil 547 | } 548 | 549 | func (c *Collection) runUp(db DB, tx *pg.Tx, m *Migration) (int64, error) { 550 | if m.UpTx { 551 | db = tx 552 | } 553 | return c.run(tx, func() (int64, error) { 554 | err := m.Up(db) 555 | if err != nil { 556 | return 0, err 557 | } 558 | return m.Version, nil 559 | }) 560 | } 561 | 562 | func (c *Collection) runDown(db DB, tx *pg.Tx, m *Migration) (int64, error) { 563 | if m.DownTx { 564 | db = tx 565 | } 566 | return c.run(tx, func() (int64, error) { 567 | if m.Down != nil { 568 | err := m.Down(db) 569 | if err != nil { 570 | return 0, err 571 | } 572 | } 573 | return m.Version - 1, nil 574 | }) 575 | } 576 | 577 | func (c *Collection) run( 578 | tx *pg.Tx, fn func() (int64, error), 579 | ) (newVersion int64, err error) { 580 | newVersion, err = fn() 581 | if err != nil { 582 | return 583 | } 584 | err = c.SetVersion(tx, newVersion) 585 | return 586 | } 587 | 588 | func (c *Collection) down(db DB, tx *pg.Tx, migrations []*Migration, oldVersion int64) (int64, error) { 589 | if oldVersion == 0 { 590 | return 0, nil 591 | } 592 | 593 | var m *Migration 594 | for i := len(migrations) - 1; i >= 0; i-- { 595 | mm := migrations[i] 596 | if mm.Version <= oldVersion { 597 | m = mm 598 | break 599 | } 600 | } 601 | 602 | if m == nil { 603 | return oldVersion, nil 604 | } 605 | return c.runDown(db, tx, m) 606 | } 607 | 608 | func (c *Collection) schemaExists(db DB) (bool, error) { 609 | schema, _ := c.schemaTableName() 610 | return db.Model(). 611 | Table("information_schema.schemata"). 612 | Where("schema_name = '?'", pg.SafeQuery(schema)). 613 | Exists() 614 | } 615 | 616 | func (c *Collection) tableExists(db DB) (bool, error) { 617 | schema, table := c.schemaTableName() 618 | return db.Model(). 619 | Table("pg_tables"). 620 | Where("schemaname = '?'", pg.SafeQuery(schema)). 621 | Where("tablename = '?'", pg.SafeQuery(table)). 622 | Exists() 623 | } 624 | 625 | func (c *Collection) Version(db DB) (int64, error) { 626 | var version int64 627 | _, err := db.QueryOne(pg.Scan(&version), ` 628 | SELECT version FROM ? ORDER BY id DESC LIMIT 1 629 | `, pg.SafeQuery(c.tableName)) 630 | if err != nil { 631 | if err == pg.ErrNoRows { 632 | return 0, nil 633 | } 634 | return 0, err 635 | } 636 | return version, nil 637 | } 638 | 639 | func (c *Collection) SetVersion(db DB, version int64) error { 640 | _, err := db.Exec(` 641 | INSERT INTO ? (version, created_at) VALUES (?, now()) 642 | `, pg.SafeQuery(c.tableName), version) 643 | return err 644 | } 645 | 646 | func (c *Collection) createTable(db DB) error { 647 | exists, err := c.schemaExists(db) 648 | if err != nil { 649 | return err 650 | } 651 | if !exists { 652 | schema, _ := c.schemaTableName() 653 | _, err := db.Exec(`CREATE SCHEMA IF NOT EXISTS ?`, pg.SafeQuery(schema)) 654 | if err != nil { 655 | return err 656 | } 657 | } 658 | 659 | _, err = db.Exec(` 660 | CREATE TABLE IF NOT EXISTS ? ( 661 | id serial, 662 | version bigint, 663 | created_at timestamptz 664 | ) 665 | `, pg.SafeQuery(c.tableName)) 666 | return err 667 | } 668 | 669 | const ( 670 | cockroachdbErrorMatch = `at or near "lock"` 671 | yugabytedbErrorMatch = `lock mode not supported yet` 672 | ) 673 | 674 | func (c *Collection) begin(db DB) (*pg.Tx, int64, error) { 675 | tx, err := db.Begin() 676 | if err != nil { 677 | return nil, 0, err 678 | } 679 | 680 | // If there is an error setting this, rollback the transaction and don't bother doing it 681 | // because Postgres < 9.6 doesn't support this 682 | _, err = tx.Exec("SET idle_in_transaction_session_timeout = 0") 683 | if err != nil { 684 | _ = tx.Rollback() 685 | 686 | tx, err = db.Begin() 687 | if err != nil { 688 | return nil, 0, err 689 | } 690 | } 691 | // If there is an error setting this, rollback the transaction and don't bother doing it 692 | // because neither CockroachDB nor Yugabyte support it 693 | _, err = tx.Exec("LOCK TABLE ? IN EXCLUSIVE MODE", pg.SafeQuery(c.tableName)) 694 | if err != nil { 695 | _ = tx.Rollback() 696 | 697 | if !strings.Contains(err.Error(), cockroachdbErrorMatch) && !strings.Contains(err.Error(), yugabytedbErrorMatch) { 698 | return nil, 0, err 699 | } 700 | tx, err = db.Begin() 701 | if err != nil { 702 | return nil, 0, err 703 | } 704 | } 705 | 706 | version, err := c.Version(tx) 707 | if err != nil { 708 | _ = tx.Rollback() 709 | return nil, 0, err 710 | } 711 | 712 | return tx, version, nil 713 | } 714 | 715 | func extractVersionGo(name string) (int64, error) { 716 | base := filepath.Base(name) 717 | if !strings.HasSuffix(name, ".go") { 718 | return 0, fmt.Errorf("file=%q must have extension .go", base) 719 | } 720 | 721 | idx := strings.IndexByte(base, '_') 722 | if idx == -1 { 723 | err := fmt.Errorf( 724 | "file=%q must have name in format version_comment, e.g. 1_initial", 725 | base) 726 | return 0, err 727 | } 728 | 729 | n, err := strconv.ParseInt(base[:idx], 10, 64) 730 | if err != nil { 731 | return 0, err 732 | } 733 | 734 | return n, nil 735 | } 736 | 737 | var migrationNameRE = regexp.MustCompile(`[^a-z0-9]+`) 738 | 739 | func fmtMigrationFilename(version int64, descr string) string { 740 | descr = strings.ToLower(descr) 741 | descr = migrationNameRE.ReplaceAllString(descr, "_") 742 | return fmt.Sprintf("%d_%s.go", version, descr) 743 | } 744 | 745 | func createMigrationFile(filename string) error { 746 | basepath, err := os.Getwd() 747 | if err != nil { 748 | return err 749 | } 750 | filename = path.Join(basepath, filename) 751 | 752 | _, err = os.Stat(filename) 753 | if !os.IsNotExist(err) { 754 | return fmt.Errorf("file=%q already exists (%s)", filename, err) 755 | } 756 | 757 | return ioutil.WriteFile(filename, migrationTemplate, 0o644) 758 | } 759 | 760 | var migrationTemplate = []byte(`package main 761 | 762 | import ( 763 | "github.com/go-pg/migrations" 764 | ) 765 | 766 | func init() { 767 | migrations.MustRegisterTx(func(db migrations.DB) error { 768 | _, err := db.Exec("") 769 | return err 770 | }, func(db migrations.DB) error { 771 | _, err := db.Exec("") 772 | return err 773 | }) 774 | } 775 | `) 776 | 777 | type osfilesystem struct{} 778 | 779 | func (osfilesystem) Open(name string) (http.File, error) { 780 | return os.Open(name) 781 | } 782 | -------------------------------------------------------------------------------- /db_test.go: -------------------------------------------------------------------------------- 1 | package migrations_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/go-pg/migrations/v8" 8 | 9 | "github.com/go-pg/pg/v10" 10 | ) 11 | 12 | func connectDB() *pg.DB { 13 | db := pg.Connect(&pg.Options{ 14 | User: "postgres", 15 | }) 16 | 17 | _, err := db.Exec("DROP TABLE IF EXISTS gopg_migrations") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | _, _, err = migrations.Run(db, "init") 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | return db 28 | } 29 | 30 | func TestVersion(t *testing.T) { 31 | db := connectDB() 32 | 33 | version, err := migrations.Version(db) 34 | if err != nil { 35 | t.Fatalf("Version failed: %s", err) 36 | } 37 | if version != 0 { 38 | t.Fatalf("got version %d, wanted 0", version) 39 | } 40 | 41 | if err := migrations.SetVersion(db, 999); err != nil { 42 | t.Fatalf("SetVersion failed: %s", err) 43 | } 44 | 45 | version, err = migrations.Version(db) 46 | if err != nil { 47 | t.Fatalf("Version failed: %s", err) 48 | } 49 | if version != 999 { 50 | t.Fatalf("got version %d, wanted 999", version) 51 | } 52 | } 53 | 54 | func TestUpDown(t *testing.T) { 55 | db := connectDB() 56 | 57 | coll := migrations.NewCollection([]*migrations.Migration{ 58 | {Version: 2, Up: doNothing, Down: doNothing}, 59 | {Version: 1, Up: doNothing, Down: doNothing}, 60 | {Version: 3, Up: doNothing, Down: doNothing}, 61 | }...) 62 | oldVersion, newVersion, err := coll.Run(db, "up") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | if oldVersion != 0 { 67 | t.Fatalf("got %d, wanted 0", oldVersion) 68 | } 69 | if newVersion != 3 { 70 | t.Fatalf("got %d, wanted 3", newVersion) 71 | } 72 | 73 | version, err := coll.Version(db) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | if version != 3 { 78 | t.Fatalf("got version %d, wanted 3", version) 79 | } 80 | 81 | for i := 2; i >= -5; i-- { 82 | wantOldVersion := int64(i + 1) 83 | wantNewVersion := int64(i) 84 | if wantNewVersion < 0 { 85 | wantOldVersion = 0 86 | wantNewVersion = 0 87 | } 88 | 89 | oldVersion, newVersion, err = coll.Run(db, "down") 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | if oldVersion != wantOldVersion { 94 | t.Fatalf("got %d, wanted %d", oldVersion, wantOldVersion) 95 | } 96 | if newVersion != wantNewVersion { 97 | t.Fatalf("got %d, wanted %d", newVersion, wantNewVersion) 98 | } 99 | 100 | version, err = coll.Version(db) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | if version != wantNewVersion { 105 | t.Fatalf("got version %d, wanted %d", version, wantNewVersion) 106 | } 107 | } 108 | } 109 | 110 | func TestSetVersion(t *testing.T) { 111 | db := connectDB() 112 | 113 | coll := migrations.NewCollection([]*migrations.Migration{ 114 | {Version: 3, Up: doPanic, Down: doPanic}, 115 | {Version: 1, Up: doPanic, Down: doPanic}, 116 | {Version: 2, Up: doPanic, Down: doPanic}, 117 | }...) 118 | 119 | for i := 0; i < 5; i++ { 120 | wantOldVersion := int64(i) 121 | wantNewVersion := int64(i + 1) 122 | 123 | oldVersion, newVersion, err := coll.Run( 124 | db, "set_version", fmt.Sprint(wantNewVersion)) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | if oldVersion != wantOldVersion { 129 | t.Fatalf("got %d, wanted %d", oldVersion, wantOldVersion) 130 | } 131 | if newVersion != wantNewVersion { 132 | t.Fatalf("got %d, wanted %d", newVersion, wantNewVersion) 133 | } 134 | 135 | version, err := coll.Version(db) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | if version != wantNewVersion { 140 | t.Fatalf("got version %d, wanted %d", version, wantNewVersion) 141 | } 142 | } 143 | } 144 | 145 | func doNothing(db migrations.DB) error { 146 | return nil 147 | } 148 | 149 | func doPanic(db migrations.DB) error { 150 | panic("this migration should not be run") 151 | } 152 | -------------------------------------------------------------------------------- /default.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | var DefaultCollection = NewCollection() 4 | 5 | func SetTableName(name string) { 6 | DefaultCollection.SetTableName(name) 7 | } 8 | 9 | func Version(db DB) (int64, error) { 10 | return DefaultCollection.Version(db) 11 | } 12 | 13 | func SetVersion(db DB, version int64) error { 14 | return DefaultCollection.SetVersion(db, version) 15 | } 16 | 17 | // Register registers new database migration. Must be called 18 | // from file with name like "1_initialize_db.go", where: 19 | // - 1 - migration version; 20 | // - initialize_db - comment. 21 | func Register(fns ...func(DB) error) error { 22 | return DefaultCollection.Register(fns...) 23 | } 24 | 25 | // RegisterTx is just like Register but marks the migration to be executed inside a transaction. 26 | func RegisterTx(fns ...func(DB) error) error { 27 | return DefaultCollection.RegisterTx(fns...) 28 | } 29 | 30 | func MustRegister(fns ...func(DB) error) { 31 | DefaultCollection.MustRegister(fns...) 32 | } 33 | 34 | func MustRegisterTx(fns ...func(DB) error) { 35 | DefaultCollection.MustRegisterTx(fns...) 36 | } 37 | 38 | // RegisteredMigrations returns currently registered Migrations. 39 | func RegisteredMigrations() []*Migration { 40 | return DefaultCollection.Migrations() 41 | } 42 | 43 | // Run runs command on the db. Supported commands are: 44 | // - up [target] - runs all available migrations by default or up to target one if argument is provided. 45 | // - down - reverts last migration. 46 | // - reset - reverts all migrations. 47 | // - version - prints current db version. 48 | // - set_version - sets db version without running migrations. 49 | func Run(db DB, a ...string) (oldVersion, newVersion int64, err error) { 50 | return DefaultCollection.Run(db, a...) 51 | } 52 | -------------------------------------------------------------------------------- /embed_test.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "embed" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | //go:embed testdata/sqlmigrations/*.sql 10 | var embedSQLMigrations embed.FS 11 | 12 | func TestEmbedFS(t *testing.T) { 13 | coll := NewCollection() 14 | coll.DisableSQLAutodiscover(true) 15 | coll.DiscoverSQLMigrationsFromFilesystem(http.FS(embedSQLMigrations), "not-existing-dir") 16 | coll.DiscoverSQLMigrationsFromFilesystem(http.FS(embedSQLMigrations), "testdata/sqlmigrations") 17 | m := coll.Migrations() 18 | if len(m) != 1 { 19 | t.Fatal("could not init migrations from filesystem") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/1_initial.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-pg/migrations/v8" 7 | ) 8 | 9 | func init() { 10 | migrations.MustRegisterTx(func(db migrations.DB) error { 11 | fmt.Println("creating table my_table...") 12 | _, err := db.Exec(`CREATE TABLE my_table()`) 13 | return err 14 | }, func(db migrations.DB) error { 15 | fmt.Println("dropping table my_table...") 16 | _, err := db.Exec(`DROP TABLE my_table`) 17 | return err 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /example/2_add_id.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-pg/migrations/v8" 7 | ) 8 | 9 | func init() { 10 | migrations.MustRegisterTx(func(db migrations.DB) error { 11 | fmt.Println("adding id column...") 12 | _, err := db.Exec(`ALTER TABLE my_table ADD id serial`) 13 | return err 14 | }, func(db migrations.DB) error { 15 | fmt.Println("dropping id column...") 16 | _, err := db.Exec(`ALTER TABLE my_table DROP id`) 17 | return err 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /example/3_seed_data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-pg/migrations/v8" 7 | ) 8 | 9 | func init() { 10 | migrations.MustRegisterTx(func(db migrations.DB) error { 11 | fmt.Println("seeding my_table...") 12 | _, err := db.Exec(`INSERT INTO my_table VALUES (1)`) 13 | return err 14 | }, func(db migrations.DB) error { 15 | fmt.Println("truncating my_table...") 16 | _, err := db.Exec(`TRUNCATE my_table`) 17 | return err 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /example/4_delete_value.tx.down.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM my_table WHERE id = 2; 2 | -------------------------------------------------------------------------------- /example/4_insert_value.tx.up.sql: -------------------------------------------------------------------------------- 1 | SET statement_timeout = 60000; -- 60 seconds 2 | SET lock_timeout = 60000; -- 60 seconds 3 | 4 | --gopg:split 5 | 6 | INSERT INTO my_table VALUES (2); 7 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/go-pg/migrations/v8" 9 | "github.com/go-pg/pg/v10" 10 | ) 11 | 12 | const usageText = `This program runs command on the db. Supported commands are: 13 | - init - creates version info table in the database 14 | - up - runs all available migrations. 15 | - up [target] - runs available migrations up to the target one. 16 | - down - reverts last migration. 17 | - reset - reverts all migrations. 18 | - version - prints current db version. 19 | - set_version [version] - sets db version without running migrations. 20 | 21 | Usage: 22 | go run *.go [args] 23 | ` 24 | 25 | func main() { 26 | flag.Usage = usage 27 | flag.Parse() 28 | 29 | db := pg.Connect(&pg.Options{ 30 | User: "postgres", 31 | Database: "pg_migrations_example", 32 | }) 33 | 34 | oldVersion, newVersion, err := migrations.Run(db, flag.Args()...) 35 | if err != nil { 36 | exitf(err.Error()) 37 | } 38 | if newVersion != oldVersion { 39 | fmt.Printf("migrated from version %d to %d\n", oldVersion, newVersion) 40 | } else { 41 | fmt.Printf("version is %d\n", oldVersion) 42 | } 43 | } 44 | 45 | func usage() { 46 | fmt.Print(usageText) 47 | flag.PrintDefaults() 48 | os.Exit(2) 49 | } 50 | 51 | func errorf(s string, args ...interface{}) { 52 | fmt.Fprintf(os.Stderr, s+"\n", args...) 53 | } 54 | 55 | func exitf(s string, args ...interface{}) { 56 | errorf(s, args...) 57 | os.Exit(1) 58 | } 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-pg/migrations/v8 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-pg/pg/v10 v10.4.0 7 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect 8 | golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 // indirect 9 | golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 9 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 10 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 11 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 12 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 13 | github.com/go-pg/pg/v10 v10.4.0 h1:CKNyax2tGE435X2DsUY9ShE+Q2w8BKLZVO3XgLN+w0Q= 14 | github.com/go-pg/pg/v10 v10.4.0/go.mod h1:BfgPoQnD2wXNd986RYEHzikqv9iE875PrFaZ9vXvtNM= 15 | github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= 16 | github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= 17 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 18 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 19 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 22 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 23 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 24 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 25 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 26 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 27 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 28 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 29 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 30 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 31 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 32 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 33 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 34 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 36 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 39 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 40 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 41 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 42 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 43 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 45 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 46 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 47 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 48 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 49 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 50 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 51 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 52 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 53 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 54 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 55 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 56 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 57 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 58 | github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= 59 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 60 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 61 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 62 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 65 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 66 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 67 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= 68 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= 69 | github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= 70 | github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= 71 | github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 72 | github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 h1:d71/KA0LhvkrJ/Ok+Wx9qK7bU8meKA1Hk0jpVI5kJjk= 73 | github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI= 74 | github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= 75 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 76 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 77 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 78 | go.opentelemetry.io/otel v0.13.0 h1:2isEnyzjjJZq6r2EKMsFj4TxiQiexsM04AVhwbR/oBA= 79 | go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY= 80 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 81 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 82 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 83 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= 84 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 85 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= 86 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 87 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 88 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 89 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 90 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 91 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 92 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 93 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 94 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 95 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 96 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 97 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 98 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 100 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 101 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY= 102 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 103 | golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg= 104 | golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 105 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 106 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 111 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88= 120 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 h1:XtNJkfEjb4zR3q20BBBcYUykVOEMgZeIUOpBPfNYgxg= 122 | golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 124 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 125 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 126 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 127 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 128 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 129 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 130 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 131 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 132 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 133 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 134 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 136 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 137 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 138 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 139 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 140 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 141 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 142 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 143 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 144 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 145 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 146 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 147 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 148 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 149 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 150 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 151 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 152 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 153 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 154 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 155 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 156 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 157 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 158 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 159 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 160 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 161 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 162 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 163 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 164 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 165 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 166 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 167 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 168 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 169 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 170 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 171 | mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w= 172 | mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= 173 | -------------------------------------------------------------------------------- /migrations.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/go-pg/pg/v10" 8 | "github.com/go-pg/pg/v10/orm" 9 | ) 10 | 11 | // DB is a common interface for pg.DB and pg.Tx types. 12 | type DB interface { 13 | Model(model ...interface{}) *orm.Query 14 | 15 | Exec(query interface{}, params ...interface{}) (orm.Result, error) 16 | ExecOne(query interface{}, params ...interface{}) (orm.Result, error) 17 | Query(coll, query interface{}, params ...interface{}) (orm.Result, error) 18 | QueryOne(model, query interface{}, params ...interface{}) (orm.Result, error) 19 | 20 | Begin() (*pg.Tx, error) 21 | 22 | CopyFrom(r io.Reader, query interface{}, params ...interface{}) (orm.Result, error) 23 | CopyTo(w io.Writer, query interface{}, params ...interface{}) (orm.Result, error) 24 | 25 | Context() context.Context 26 | } 27 | -------------------------------------------------------------------------------- /testdata/sqlmigrations/1_initial.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE my_table 2 | -------------------------------------------------------------------------------- /testdata/sqlmigrations/1_initial.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE my_table() 2 | -------------------------------------------------------------------------------- /vfs_test.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | pathpkg "path" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestVfs(t *testing.T) { 17 | coll := NewCollection() 18 | coll.DisableSQLAutodiscover(true) 19 | coll.DiscoverSQLMigrationsFromFilesystem(Assets, "/not-existing-dir") 20 | coll.DiscoverSQLMigrationsFromFilesystem(Assets, "/") 21 | m := coll.Migrations() 22 | if len(m) != 1 { 23 | t.Fatal("could not init migrations from filesystem") 24 | } 25 | } 26 | 27 | // Assets statically implements the virtual filesystem provided to vfsgen. 28 | var Assets = func() http.FileSystem { 29 | fs := vfsgen۰FS{ 30 | "/": &vfsgen۰DirInfo{ 31 | name: "/", 32 | modTime: time.Date(2019, 6, 19, 7, 42, 22, 486882626, time.UTC), 33 | }, 34 | "/01_initial.down.sql": &vfsgen۰FileInfo{ 35 | name: "01_initial.down.sql", 36 | modTime: time.Date(2019, 6, 19, 7, 42, 22, 482685195, time.UTC), 37 | content: []byte("\x44\x52\x4f\x50\x20\x54\x41\x42\x4c\x45\x20\x63\x6f\x6e\x74\x65\x6e\x74\x74\x68\x65\x6d\x65\x73\x2e\x73\x74\x61\x74\x75\x73\x0a"), 38 | }, 39 | "/01_initial.up.sql": &vfsgen۰CompressedFileInfo{ 40 | name: "01_initial.up.sql", 41 | modTime: time.Date(2019, 6, 19, 7, 42, 22, 486395665, time.UTC), 42 | uncompressedSize: 223, 43 | 44 | compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x64\x8d\xc1\x4a\xc3\x40\x14\x45\xf7\xf3\x15\x77\x37\x09\x14\x7f\xc0\xd5\x38\x7d\x9a\xa1\xc9\xb4\xbc\x3c\x85\xae\x42\x6c\x9f\x18\xd0\x04\x9c\x17\xf0\xf3\x45\x03\x82\x78\x57\x77\x71\x38\x27\x32\x05\x21\x48\xb8\x6b\x09\x97\x65\x36\x9d\xcd\x5e\xf5\x5d\xcb\x4d\xb1\xd1\xd6\xe2\x2a\x07\x00\xd3\x15\x7f\x97\xb2\xd0\x03\x31\x4e\x9c\xba\xc0\x67\x1c\xe8\xbc\xfb\x21\xf5\x53\x2f\xab\x4d\xcb\x3c\x6c\x02\x3c\x05\x8e\x4d\x60\x00\xf9\x28\xc8\x8f\x6d\x8b\xd8\x50\x3c\xa0\xfa\x87\xa6\x8c\xca\xf7\x12\x58\x68\xef\x77\xf0\xc4\x7c\xe4\xef\x73\x9f\x72\xea\x1b\xda\xfb\xba\xde\x2a\x6f\x63\xb1\xe1\x3a\x9a\x0e\xcf\xfa\xb2\x7c\x28\x24\x75\xd4\x4b\xe8\x4e\xbf\x15\x57\xdf\xba\xaf\x00\x00\x00\xff\xff\xfe\xb6\x6e\x0d\xdf\x00\x00\x00"), 45 | }, 46 | } 47 | fs["/"].(*vfsgen۰DirInfo).entries = []os.FileInfo{ 48 | fs["/01_initial.down.sql"].(os.FileInfo), 49 | fs["/01_initial.up.sql"].(os.FileInfo), 50 | } 51 | 52 | return fs 53 | }() 54 | 55 | type vfsgen۰FS map[string]interface{} 56 | 57 | func (fs vfsgen۰FS) Open(path string) (http.File, error) { 58 | path = pathpkg.Clean("/" + path) 59 | f, ok := fs[path] 60 | if !ok { 61 | return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist} 62 | } 63 | 64 | switch f := f.(type) { 65 | case *vfsgen۰CompressedFileInfo: 66 | gr, err := gzip.NewReader(bytes.NewReader(f.compressedContent)) 67 | if err != nil { 68 | // This should never happen because we generate the gzip bytes such that they are always valid. 69 | panic("unexpected error reading own gzip compressed bytes: " + err.Error()) 70 | } 71 | return &vfsgen۰CompressedFile{ 72 | vfsgen۰CompressedFileInfo: f, 73 | gr: gr, 74 | }, nil 75 | case *vfsgen۰FileInfo: 76 | return &vfsgen۰File{ 77 | vfsgen۰FileInfo: f, 78 | Reader: bytes.NewReader(f.content), 79 | }, nil 80 | case *vfsgen۰DirInfo: 81 | return &vfsgen۰Dir{ 82 | vfsgen۰DirInfo: f, 83 | }, nil 84 | default: 85 | // This should never happen because we generate only the above types. 86 | panic(fmt.Sprintf("unexpected type %T", f)) 87 | } 88 | } 89 | 90 | // vfsgen۰CompressedFileInfo is a static definition of a gzip compressed file. 91 | type vfsgen۰CompressedFileInfo struct { 92 | name string 93 | modTime time.Time 94 | compressedContent []byte 95 | uncompressedSize int64 96 | } 97 | 98 | func (f *vfsgen۰CompressedFileInfo) Readdir(count int) ([]os.FileInfo, error) { 99 | return nil, fmt.Errorf("cannot Readdir from file %s", f.name) 100 | } 101 | func (f *vfsgen۰CompressedFileInfo) Stat() (os.FileInfo, error) { return f, nil } 102 | 103 | func (f *vfsgen۰CompressedFileInfo) GzipBytes() []byte { 104 | return f.compressedContent 105 | } 106 | 107 | func (f *vfsgen۰CompressedFileInfo) Name() string { return f.name } 108 | func (f *vfsgen۰CompressedFileInfo) Size() int64 { return f.uncompressedSize } 109 | func (f *vfsgen۰CompressedFileInfo) Mode() os.FileMode { return 0o444 } 110 | func (f *vfsgen۰CompressedFileInfo) ModTime() time.Time { return f.modTime } 111 | func (f *vfsgen۰CompressedFileInfo) IsDir() bool { return false } 112 | func (f *vfsgen۰CompressedFileInfo) Sys() interface{} { return nil } 113 | 114 | // vfsgen۰CompressedFile is an opened compressedFile instance. 115 | type vfsgen۰CompressedFile struct { 116 | *vfsgen۰CompressedFileInfo 117 | gr *gzip.Reader 118 | grPos int64 // Actual gr uncompressed position. 119 | seekPos int64 // Seek uncompressed position. 120 | } 121 | 122 | func (f *vfsgen۰CompressedFile) Read(p []byte) (n int, err error) { 123 | if f.grPos > f.seekPos { 124 | // Rewind to beginning. 125 | err = f.gr.Reset(bytes.NewReader(f.compressedContent)) 126 | if err != nil { 127 | return 0, err 128 | } 129 | f.grPos = 0 130 | } 131 | if f.grPos < f.seekPos { 132 | // Fast-forward. 133 | _, err = io.CopyN(ioutil.Discard, f.gr, f.seekPos-f.grPos) 134 | if err != nil { 135 | return 0, err 136 | } 137 | f.grPos = f.seekPos 138 | } 139 | n, err = f.gr.Read(p) 140 | f.grPos += int64(n) 141 | f.seekPos = f.grPos 142 | return n, err 143 | } 144 | 145 | func (f *vfsgen۰CompressedFile) Seek(offset int64, whence int) (int64, error) { 146 | switch whence { 147 | case io.SeekStart: 148 | f.seekPos = 0 + offset 149 | case io.SeekCurrent: 150 | f.seekPos += offset 151 | case io.SeekEnd: 152 | f.seekPos = f.uncompressedSize + offset 153 | default: 154 | panic(fmt.Errorf("invalid whence value: %v", whence)) 155 | } 156 | return f.seekPos, nil 157 | } 158 | 159 | func (f *vfsgen۰CompressedFile) Close() error { 160 | return f.gr.Close() 161 | } 162 | 163 | // vfsgen۰FileInfo is a static definition of an uncompressed file (because it's not worth gzip compressing). 164 | type vfsgen۰FileInfo struct { 165 | name string 166 | modTime time.Time 167 | content []byte 168 | } 169 | 170 | func (f *vfsgen۰FileInfo) Readdir(count int) ([]os.FileInfo, error) { 171 | return nil, fmt.Errorf("cannot Readdir from file %s", f.name) 172 | } 173 | func (f *vfsgen۰FileInfo) Stat() (os.FileInfo, error) { return f, nil } 174 | 175 | func (f *vfsgen۰FileInfo) NotWorthGzipCompressing() {} 176 | 177 | func (f *vfsgen۰FileInfo) Name() string { return f.name } 178 | func (f *vfsgen۰FileInfo) Size() int64 { return int64(len(f.content)) } 179 | func (f *vfsgen۰FileInfo) Mode() os.FileMode { return 0o444 } 180 | func (f *vfsgen۰FileInfo) ModTime() time.Time { return f.modTime } 181 | func (f *vfsgen۰FileInfo) IsDir() bool { return false } 182 | func (f *vfsgen۰FileInfo) Sys() interface{} { return nil } 183 | 184 | // vfsgen۰File is an opened file instance. 185 | type vfsgen۰File struct { 186 | *vfsgen۰FileInfo 187 | *bytes.Reader 188 | } 189 | 190 | func (f *vfsgen۰File) Close() error { 191 | return nil 192 | } 193 | 194 | // vfsgen۰DirInfo is a static definition of a directory. 195 | type vfsgen۰DirInfo struct { 196 | name string 197 | modTime time.Time 198 | entries []os.FileInfo 199 | } 200 | 201 | func (d *vfsgen۰DirInfo) Read([]byte) (int, error) { 202 | return 0, fmt.Errorf("cannot Read from directory %s", d.name) 203 | } 204 | func (d *vfsgen۰DirInfo) Close() error { return nil } 205 | func (d *vfsgen۰DirInfo) Stat() (os.FileInfo, error) { return d, nil } 206 | 207 | func (d *vfsgen۰DirInfo) Name() string { return d.name } 208 | func (d *vfsgen۰DirInfo) Size() int64 { return 0 } 209 | func (d *vfsgen۰DirInfo) Mode() os.FileMode { return 0o755 | os.ModeDir } 210 | func (d *vfsgen۰DirInfo) ModTime() time.Time { return d.modTime } 211 | func (d *vfsgen۰DirInfo) IsDir() bool { return true } 212 | func (d *vfsgen۰DirInfo) Sys() interface{} { return nil } 213 | 214 | // vfsgen۰Dir is an opened dir instance. 215 | type vfsgen۰Dir struct { 216 | *vfsgen۰DirInfo 217 | pos int // Position within entries for Seek and Readdir. 218 | } 219 | 220 | func (d *vfsgen۰Dir) Seek(offset int64, whence int) (int64, error) { 221 | if offset == 0 && whence == io.SeekStart { 222 | d.pos = 0 223 | return 0, nil 224 | } 225 | return 0, fmt.Errorf("unsupported Seek in directory %s", d.name) 226 | } 227 | 228 | func (d *vfsgen۰Dir) Readdir(count int) ([]os.FileInfo, error) { 229 | if d.pos >= len(d.entries) && count > 0 { 230 | return nil, io.EOF 231 | } 232 | if count <= 0 || count > len(d.entries)-d.pos { 233 | count = len(d.entries) - d.pos 234 | } 235 | e := d.entries[d.pos : d.pos+count] 236 | d.pos += count 237 | return e, nil 238 | } 239 | --------------------------------------------------------------------------------