github.com/olitvin/migrate/v4@v4.14.3-0.20210330111251-992b37ee04c8/database/pgx/pgx_test.go (about) 1 package pgx 2 3 // error codes https://github.com/jackc/pgerrcode/blob/master/errcode.go 4 5 import ( 6 "context" 7 "database/sql" 8 sqldriver "database/sql/driver" 9 "errors" 10 "fmt" 11 "log" 12 13 "io" 14 "strconv" 15 "strings" 16 "sync" 17 "testing" 18 19 "github.com/olitvin/migrate/v4" 20 21 "github.com/dhui/dktest" 22 23 "github.com/olitvin/migrate/v4/database" 24 dt "github.com/olitvin/migrate/v4/database/testing" 25 "github.com/olitvin/migrate/v4/dktesting" 26 _ "github.com/olitvin/migrate/v4/source/file" 27 ) 28 29 const ( 30 pgPassword = "postgres" 31 ) 32 33 var ( 34 opts = dktest.Options{ 35 Env: map[string]string{"POSTGRES_PASSWORD": pgPassword}, 36 PortRequired: true, ReadyFunc: isReady} 37 // Supported versions: https://www.postgresql.org/support/versioning/ 38 specs = []dktesting.ContainerSpec{ 39 {ImageName: "postgres:9.5", Options: opts}, 40 {ImageName: "postgres:9.6", Options: opts}, 41 {ImageName: "postgres:10", Options: opts}, 42 {ImageName: "postgres:11", Options: opts}, 43 {ImageName: "postgres:12", Options: opts}, 44 } 45 ) 46 47 func pgConnectionString(host, port string, options ...string) string { 48 options = append(options, "sslmode=disable") 49 return fmt.Sprintf("postgres://postgres:%s@%s:%s/postgres?%s", pgPassword, host, port, strings.Join(options, "&")) 50 } 51 52 func isReady(ctx context.Context, c dktest.ContainerInfo) bool { 53 ip, port, err := c.FirstPort() 54 if err != nil { 55 return false 56 } 57 58 db, err := sql.Open("pgx", pgConnectionString(ip, port)) 59 if err != nil { 60 return false 61 } 62 defer func() { 63 if err := db.Close(); err != nil { 64 log.Println("close error:", err) 65 } 66 }() 67 if err = db.PingContext(ctx); err != nil { 68 switch err { 69 case sqldriver.ErrBadConn, io.EOF: 70 return false 71 default: 72 log.Println(err) 73 } 74 return false 75 } 76 77 return true 78 } 79 80 func mustRun(t *testing.T, d database.Driver, statements []string) { 81 for _, statement := range statements { 82 if err := d.Run(strings.NewReader(statement)); err != nil { 83 t.Fatal(err) 84 } 85 } 86 } 87 88 func Test(t *testing.T) { 89 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 90 ip, port, err := c.FirstPort() 91 if err != nil { 92 t.Fatal(err) 93 } 94 95 addr := pgConnectionString(ip, port) 96 p := &Postgres{} 97 d, err := p.Open(addr) 98 if err != nil { 99 t.Fatal(err) 100 } 101 defer func() { 102 if err := d.Close(); err != nil { 103 t.Error(err) 104 } 105 }() 106 dt.Test(t, d, []byte("SELECT 1")) 107 }) 108 } 109 110 func TestMigrate(t *testing.T) { 111 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 112 ip, port, err := c.FirstPort() 113 if err != nil { 114 t.Fatal(err) 115 } 116 117 addr := pgConnectionString(ip, port) 118 p := &Postgres{} 119 d, err := p.Open(addr) 120 if err != nil { 121 t.Fatal(err) 122 } 123 defer func() { 124 if err := d.Close(); err != nil { 125 t.Error(err) 126 } 127 }() 128 m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "pgx", d) 129 if err != nil { 130 t.Fatal(err) 131 } 132 dt.TestMigrate(t, m) 133 }) 134 } 135 136 func TestMultipleStatements(t *testing.T) { 137 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 138 ip, port, err := c.FirstPort() 139 if err != nil { 140 t.Fatal(err) 141 } 142 143 addr := pgConnectionString(ip, port) 144 p := &Postgres{} 145 d, err := p.Open(addr) 146 if err != nil { 147 t.Fatal(err) 148 } 149 defer func() { 150 if err := d.Close(); err != nil { 151 t.Error(err) 152 } 153 }() 154 if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);")); err != nil { 155 t.Fatalf("expected err to be nil, got %v", err) 156 } 157 158 // make sure second table exists 159 var exists bool 160 if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil { 161 t.Fatal(err) 162 } 163 if !exists { 164 t.Fatalf("expected table bar to exist") 165 } 166 }) 167 } 168 169 func TestMultipleStatementsInMultiStatementMode(t *testing.T) { 170 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 171 ip, port, err := c.FirstPort() 172 if err != nil { 173 t.Fatal(err) 174 } 175 176 addr := pgConnectionString(ip, port, "x-multi-statement=true") 177 p := &Postgres{} 178 d, err := p.Open(addr) 179 if err != nil { 180 t.Fatal(err) 181 } 182 defer func() { 183 if err := d.Close(); err != nil { 184 t.Error(err) 185 } 186 }() 187 if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE INDEX CONCURRENTLY idx_foo ON foo (foo);")); err != nil { 188 t.Fatalf("expected err to be nil, got %v", err) 189 } 190 191 // make sure created index exists 192 var exists bool 193 if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = (SELECT current_schema()) AND indexname = 'idx_foo')").Scan(&exists); err != nil { 194 t.Fatal(err) 195 } 196 if !exists { 197 t.Fatalf("expected table bar to exist") 198 } 199 }) 200 } 201 202 func TestErrorParsing(t *testing.T) { 203 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 204 ip, port, err := c.FirstPort() 205 if err != nil { 206 t.Fatal(err) 207 } 208 209 addr := pgConnectionString(ip, port) 210 p := &Postgres{} 211 d, err := p.Open(addr) 212 if err != nil { 213 t.Fatal(err) 214 } 215 defer func() { 216 if err := d.Close(); err != nil { 217 t.Error(err) 218 } 219 }() 220 221 wantErr := `migration failed: syntax error at or near "TABLEE" (column 37) in line 1: CREATE TABLE foo ` + 222 `(foo text); CREATE TABLEE bar (bar text); (details: ERROR: syntax error at or near "TABLEE" (SQLSTATE 42601))` 223 if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text);")); err == nil { 224 t.Fatal("expected err but got nil") 225 } else if err.Error() != wantErr { 226 t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error()) 227 } 228 }) 229 } 230 231 func TestFilterCustomQuery(t *testing.T) { 232 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 233 ip, port, err := c.FirstPort() 234 if err != nil { 235 t.Fatal(err) 236 } 237 238 addr := pgConnectionString(ip, port, "x-custom=foobar") 239 p := &Postgres{} 240 d, err := p.Open(addr) 241 if err != nil { 242 t.Fatal(err) 243 } 244 defer func() { 245 if err := d.Close(); err != nil { 246 t.Error(err) 247 } 248 }() 249 }) 250 } 251 252 func TestWithSchema(t *testing.T) { 253 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 254 ip, port, err := c.FirstPort() 255 if err != nil { 256 t.Fatal(err) 257 } 258 259 addr := pgConnectionString(ip, port) 260 p := &Postgres{} 261 d, err := p.Open(addr) 262 if err != nil { 263 t.Fatal(err) 264 } 265 defer func() { 266 if err := d.Close(); err != nil { 267 t.Fatal(err) 268 } 269 }() 270 271 // create foobar schema 272 if err := d.Run(strings.NewReader("CREATE SCHEMA foobar AUTHORIZATION postgres")); err != nil { 273 t.Fatal(err) 274 } 275 if err := d.SetVersion(1, false); err != nil { 276 t.Fatal(err) 277 } 278 279 // re-connect using that schema 280 d2, err := p.Open(pgConnectionString(ip, port, "search_path=foobar")) 281 if err != nil { 282 t.Fatal(err) 283 } 284 defer func() { 285 if err := d2.Close(); err != nil { 286 t.Fatal(err) 287 } 288 }() 289 290 version, _, err := d2.Version() 291 if err != nil { 292 t.Fatal(err) 293 } 294 if version != database.NilVersion { 295 t.Fatal("expected NilVersion") 296 } 297 298 // now update version and compare 299 if err := d2.SetVersion(2, false); err != nil { 300 t.Fatal(err) 301 } 302 version, _, err = d2.Version() 303 if err != nil { 304 t.Fatal(err) 305 } 306 if version != 2 { 307 t.Fatal("expected version 2") 308 } 309 310 // meanwhile, the public schema still has the other version 311 version, _, err = d.Version() 312 if err != nil { 313 t.Fatal(err) 314 } 315 if version != 1 { 316 t.Fatal("expected version 2") 317 } 318 }) 319 } 320 321 func TestFailToCreateTableWithoutPermissions(t *testing.T) { 322 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 323 ip, port, err := c.FirstPort() 324 if err != nil { 325 t.Fatal(err) 326 } 327 328 addr := pgConnectionString(ip, port) 329 330 // Check that opening the postgres connection returns NilVersion 331 p := &Postgres{} 332 333 d, err := p.Open(addr) 334 335 if err != nil { 336 t.Fatal(err) 337 } 338 339 defer func() { 340 if err := d.Close(); err != nil { 341 t.Error(err) 342 } 343 }() 344 345 // create user who is not the owner. Although we're concatenating strings in an sql statement it should be fine 346 // since this is a test environment and we're not expecting to the pgPassword to be malicious 347 mustRun(t, d, []string{ 348 "CREATE USER not_owner WITH ENCRYPTED PASSWORD '" + pgPassword + "'", 349 "CREATE SCHEMA barfoo AUTHORIZATION postgres", 350 "GRANT USAGE ON SCHEMA barfoo TO not_owner", 351 "REVOKE CREATE ON SCHEMA barfoo FROM PUBLIC", 352 "REVOKE CREATE ON SCHEMA barfoo FROM not_owner", 353 }) 354 355 // re-connect using that schema 356 d2, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo", 357 pgPassword, ip, port)) 358 359 defer func() { 360 if d2 == nil { 361 return 362 } 363 if err := d2.Close(); err != nil { 364 t.Fatal(err) 365 } 366 }() 367 368 var e *database.Error 369 if !errors.As(err, &e) || err == nil { 370 t.Fatal("Unexpected error, want permission denied error. Got: ", err) 371 } 372 373 if !strings.Contains(e.OrigErr.Error(), "permission denied for schema barfoo") { 374 t.Fatal(e) 375 } 376 }) 377 } 378 379 func TestCheckBeforeCreateTable(t *testing.T) { 380 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 381 ip, port, err := c.FirstPort() 382 if err != nil { 383 t.Fatal(err) 384 } 385 386 addr := pgConnectionString(ip, port) 387 388 // Check that opening the postgres connection returns NilVersion 389 p := &Postgres{} 390 391 d, err := p.Open(addr) 392 393 if err != nil { 394 t.Fatal(err) 395 } 396 397 defer func() { 398 if err := d.Close(); err != nil { 399 t.Error(err) 400 } 401 }() 402 403 // create user who is not the owner. Although we're concatenating strings in an sql statement it should be fine 404 // since this is a test environment and we're not expecting to the pgPassword to be malicious 405 mustRun(t, d, []string{ 406 "CREATE USER not_owner WITH ENCRYPTED PASSWORD '" + pgPassword + "'", 407 "CREATE SCHEMA barfoo AUTHORIZATION postgres", 408 "GRANT USAGE ON SCHEMA barfoo TO not_owner", 409 "GRANT CREATE ON SCHEMA barfoo TO not_owner", 410 }) 411 412 // re-connect using that schema 413 d2, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo", 414 pgPassword, ip, port)) 415 416 if err != nil { 417 t.Fatal(err) 418 } 419 420 if err := d2.Close(); err != nil { 421 t.Fatal(err) 422 } 423 424 // revoke privileges 425 mustRun(t, d, []string{ 426 "REVOKE CREATE ON SCHEMA barfoo FROM PUBLIC", 427 "REVOKE CREATE ON SCHEMA barfoo FROM not_owner", 428 }) 429 430 // re-connect using that schema 431 d3, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo", 432 pgPassword, ip, port)) 433 434 if err != nil { 435 t.Fatal(err) 436 } 437 438 version, _, err := d3.Version() 439 440 if err != nil { 441 t.Fatal(err) 442 } 443 444 if version != database.NilVersion { 445 t.Fatal("Unexpected version, want database.NilVersion. Got: ", version) 446 } 447 448 defer func() { 449 if err := d3.Close(); err != nil { 450 t.Fatal(err) 451 } 452 }() 453 }) 454 } 455 456 func TestParallelSchema(t *testing.T) { 457 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 458 ip, port, err := c.FirstPort() 459 if err != nil { 460 t.Fatal(err) 461 } 462 463 addr := pgConnectionString(ip, port) 464 p := &Postgres{} 465 d, err := p.Open(addr) 466 if err != nil { 467 t.Fatal(err) 468 } 469 defer func() { 470 if err := d.Close(); err != nil { 471 t.Error(err) 472 } 473 }() 474 475 // create foo and bar schemas 476 if err := d.Run(strings.NewReader("CREATE SCHEMA foo AUTHORIZATION postgres")); err != nil { 477 t.Fatal(err) 478 } 479 if err := d.Run(strings.NewReader("CREATE SCHEMA bar AUTHORIZATION postgres")); err != nil { 480 t.Fatal(err) 481 } 482 483 // re-connect using that schemas 484 dfoo, err := p.Open(pgConnectionString(ip, port, "search_path=foo")) 485 if err != nil { 486 t.Fatal(err) 487 } 488 defer func() { 489 if err := dfoo.Close(); err != nil { 490 t.Error(err) 491 } 492 }() 493 494 dbar, err := p.Open(pgConnectionString(ip, port, "search_path=bar")) 495 if err != nil { 496 t.Fatal(err) 497 } 498 defer func() { 499 if err := dbar.Close(); err != nil { 500 t.Error(err) 501 } 502 }() 503 504 if err := dfoo.Lock(); err != nil { 505 t.Fatal(err) 506 } 507 508 if err := dbar.Lock(); err != nil { 509 t.Fatal(err) 510 } 511 512 if err := dbar.Unlock(); err != nil { 513 t.Fatal(err) 514 } 515 516 if err := dfoo.Unlock(); err != nil { 517 t.Fatal(err) 518 } 519 }) 520 } 521 522 func TestPostgres_Lock(t *testing.T) { 523 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 524 ip, port, err := c.FirstPort() 525 if err != nil { 526 t.Fatal(err) 527 } 528 529 addr := pgConnectionString(ip, port) 530 p := &Postgres{} 531 d, err := p.Open(addr) 532 if err != nil { 533 t.Fatal(err) 534 } 535 536 dt.Test(t, d, []byte("SELECT 1")) 537 538 ps := d.(*Postgres) 539 540 err = ps.Lock() 541 if err != nil { 542 t.Fatal(err) 543 } 544 545 err = ps.Unlock() 546 if err != nil { 547 t.Fatal(err) 548 } 549 550 err = ps.Lock() 551 if err != nil { 552 t.Fatal(err) 553 } 554 555 err = ps.Unlock() 556 if err != nil { 557 t.Fatal(err) 558 } 559 }) 560 } 561 562 func TestWithInstance_Concurrent(t *testing.T) { 563 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 564 ip, port, err := c.FirstPort() 565 if err != nil { 566 t.Fatal(err) 567 } 568 569 // The number of concurrent processes running WithInstance 570 const concurrency = 30 571 572 // We can instantiate a single database handle because it is 573 // actually a connection pool, and so, each of the below go 574 // routines will have a high probability of using a separate 575 // connection, which is something we want to exercise. 576 db, err := sql.Open("pgx", pgConnectionString(ip, port)) 577 if err != nil { 578 t.Fatal(err) 579 } 580 defer func() { 581 if err := db.Close(); err != nil { 582 t.Error(err) 583 } 584 }() 585 586 db.SetMaxIdleConns(concurrency) 587 db.SetMaxOpenConns(concurrency) 588 589 var wg sync.WaitGroup 590 defer wg.Wait() 591 592 wg.Add(concurrency) 593 for i := 0; i < concurrency; i++ { 594 go func(i int) { 595 defer wg.Done() 596 _, err := WithInstance(db, &Config{}) 597 if err != nil { 598 t.Errorf("process %d error: %s", i, err) 599 } 600 }(i) 601 } 602 }) 603 } 604 func Test_computeLineFromPos(t *testing.T) { 605 testcases := []struct { 606 pos int 607 wantLine uint 608 wantCol uint 609 input string 610 wantOk bool 611 }{ 612 { 613 15, 2, 6, "SELECT *\nFROM foo", true, // foo table does not exists 614 }, 615 { 616 16, 3, 6, "SELECT *\n\nFROM foo", true, // foo table does not exists, empty line 617 }, 618 { 619 25, 3, 7, "SELECT *\nFROM foo\nWHERE x", true, // x column error 620 }, 621 { 622 27, 5, 7, "SELECT *\n\nFROM foo\n\nWHERE x", true, // x column error, empty lines 623 }, 624 { 625 10, 2, 1, "SELECT *\nFROMM foo", true, // FROMM typo 626 }, 627 { 628 11, 3, 1, "SELECT *\n\nFROMM foo", true, // FROMM typo, empty line 629 }, 630 { 631 17, 2, 8, "SELECT *\nFROM foo", true, // last character 632 }, 633 { 634 18, 0, 0, "SELECT *\nFROM foo", false, // invalid position 635 }, 636 } 637 for i, tc := range testcases { 638 t.Run("tc"+strconv.Itoa(i), func(t *testing.T) { 639 run := func(crlf bool, nonASCII bool) { 640 var name string 641 if crlf { 642 name = "crlf" 643 } else { 644 name = "lf" 645 } 646 if nonASCII { 647 name += "-nonascii" 648 } else { 649 name += "-ascii" 650 } 651 t.Run(name, func(t *testing.T) { 652 input := tc.input 653 if crlf { 654 input = strings.Replace(input, "\n", "\r\n", -1) 655 } 656 if nonASCII { 657 input = strings.Replace(input, "FROM", "FRÖM", -1) 658 } 659 gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos) 660 661 if tc.wantOk { 662 t.Logf("pos %d, want %d:%d, %#v", tc.pos, tc.wantLine, tc.wantCol, input) 663 } 664 665 if gotOK != tc.wantOk { 666 t.Fatalf("expected ok %v but got %v", tc.wantOk, gotOK) 667 } 668 if gotLine != tc.wantLine { 669 t.Fatalf("expected line %d but got %d", tc.wantLine, gotLine) 670 } 671 if gotCol != tc.wantCol { 672 t.Fatalf("expected col %d but got %d", tc.wantCol, gotCol) 673 } 674 }) 675 } 676 run(false, false) 677 run(true, false) 678 run(false, true) 679 run(true, true) 680 }) 681 } 682 683 }