github.com/amacneil/dbmate@v1.16.3-0.20230225174651-ca89b10d75d7/pkg/driver/postgres/postgres_test.go (about) 1 package postgres 2 3 import ( 4 "database/sql" 5 "net/url" 6 "os" 7 "runtime" 8 "testing" 9 10 "github.com/amacneil/dbmate/pkg/dbmate" 11 "github.com/amacneil/dbmate/pkg/dbutil" 12 13 "github.com/stretchr/testify/require" 14 ) 15 16 func testPostgresDriver(t *testing.T) *Driver { 17 u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL")) 18 drv, err := dbmate.New(u).Driver() 19 require.NoError(t, err) 20 21 return drv.(*Driver) 22 } 23 24 func prepTestPostgresDB(t *testing.T) *sql.DB { 25 drv := testPostgresDriver(t) 26 27 // drop any existing database 28 err := drv.DropDatabase() 29 require.NoError(t, err) 30 31 // create database 32 err = drv.CreateDatabase() 33 require.NoError(t, err) 34 35 // connect database 36 db, err := sql.Open("postgres", drv.databaseURL.String()) 37 require.NoError(t, err) 38 39 return db 40 } 41 42 func TestGetDriver(t *testing.T) { 43 db := dbmate.New(dbutil.MustParseURL("postgres://")) 44 drvInterface, err := db.Driver() 45 require.NoError(t, err) 46 47 // driver should have URL and default migrations table set 48 drv, ok := drvInterface.(*Driver) 49 require.True(t, ok) 50 require.Equal(t, db.DatabaseURL.String(), drv.databaseURL.String()) 51 require.Equal(t, "schema_migrations", drv.migrationsTableName) 52 } 53 54 func defaultConnString() string { 55 switch runtime.GOOS { 56 case "linux": 57 return "postgres://:5432/foo?host=%2Fvar%2Frun%2Fpostgresql" 58 case "darwin", "freebsd", "dragonfly", "openbsd", "netbsd": 59 return "postgres://:5432/foo?host=%2Ftmp" 60 default: 61 return "postgres://localhost:5432/foo" 62 } 63 } 64 65 func TestConnectionString(t *testing.T) { 66 cases := []struct { 67 input string 68 expected string 69 }{ 70 // defaults 71 {"postgres:///foo", defaultConnString()}, 72 // support custom url params 73 {"postgres://bob:secret@myhost:1234/foo?bar=baz", "postgres://bob:secret@myhost:1234/foo?bar=baz"}, 74 // support `host` and `port` via url params 75 {"postgres://bob:secret@myhost:1234/foo?host=new&port=9999", "postgres://bob:secret@:9999/foo?host=new"}, 76 {"postgres://bob:secret@myhost:1234/foo?port=9999&bar=baz", "postgres://bob:secret@myhost:9999/foo?bar=baz"}, 77 // support unix sockets via `host` or `socket` param 78 {"postgres://bob:secret@myhost:1234/foo?host=/var/run/postgresql", "postgres://bob:secret@:1234/foo?host=%2Fvar%2Frun%2Fpostgresql"}, 79 {"postgres://bob:secret@localhost/foo?socket=/var/run/postgresql", "postgres://bob:secret@:5432/foo?host=%2Fvar%2Frun%2Fpostgresql"}, 80 {"postgres:///foo?socket=/var/run/postgresql", "postgres://:5432/foo?host=%2Fvar%2Frun%2Fpostgresql"}, 81 {"postgres://bob:secret@/foo?socket=/var/run/postgresql", "postgres://bob:secret@:5432/foo?host=%2Fvar%2Frun%2Fpostgresql"}, 82 {"postgres://bob:secret@/foo?host=/var/run/postgresql", "postgres://bob:secret@:5432/foo?host=%2Fvar%2Frun%2Fpostgresql"}, 83 } 84 85 for _, c := range cases { 86 t.Run(c.input, func(t *testing.T) { 87 u, err := url.Parse(c.input) 88 require.NoError(t, err) 89 90 actual := connectionString(u) 91 require.Equal(t, c.expected, actual) 92 }) 93 } 94 } 95 96 func TestConnectionArgsForDump(t *testing.T) { 97 cases := []struct { 98 input string 99 expected []string 100 }{ 101 // defaults 102 {"postgres:///foo", []string{defaultConnString()}}, 103 // support single schema 104 {"postgres:///foo?search_path=foo", []string{"--schema", "foo", defaultConnString()}}, 105 // support multiple schemas 106 {"postgres:///foo?search_path=foo,public", []string{"--schema", "foo", "--schema", "public", defaultConnString()}}, 107 } 108 109 for _, c := range cases { 110 t.Run(c.input, func(t *testing.T) { 111 u, err := url.Parse(c.input) 112 require.NoError(t, err) 113 114 actual := connectionArgsForDump(u) 115 require.Equal(t, c.expected, actual) 116 }) 117 } 118 } 119 120 func TestPostgresCreateDropDatabase(t *testing.T) { 121 drv := testPostgresDriver(t) 122 123 // drop any existing database 124 err := drv.DropDatabase() 125 require.NoError(t, err) 126 127 // create database 128 err = drv.CreateDatabase() 129 require.NoError(t, err) 130 131 // check that database exists and we can connect to it 132 func() { 133 db, err := sql.Open("postgres", drv.databaseURL.String()) 134 require.NoError(t, err) 135 defer dbutil.MustClose(db) 136 137 err = db.Ping() 138 require.NoError(t, err) 139 }() 140 141 // drop the database 142 err = drv.DropDatabase() 143 require.NoError(t, err) 144 145 // check that database no longer exists 146 func() { 147 db, err := sql.Open("postgres", drv.databaseURL.String()) 148 require.NoError(t, err) 149 defer dbutil.MustClose(db) 150 151 err = db.Ping() 152 require.Error(t, err) 153 require.Equal(t, "pq: database \"dbmate_test\" does not exist", err.Error()) 154 }() 155 } 156 157 func TestPostgresDumpSchema(t *testing.T) { 158 t.Run("default migrations table", func(t *testing.T) { 159 drv := testPostgresDriver(t) 160 161 // prepare database 162 db := prepTestPostgresDB(t) 163 defer dbutil.MustClose(db) 164 err := drv.CreateMigrationsTable(db) 165 require.NoError(t, err) 166 167 // insert migration 168 err = drv.InsertMigration(db, "abc1") 169 require.NoError(t, err) 170 err = drv.InsertMigration(db, "abc2") 171 require.NoError(t, err) 172 173 // DumpSchema should return schema 174 schema, err := drv.DumpSchema(db) 175 require.NoError(t, err) 176 require.Contains(t, string(schema), "CREATE TABLE public.schema_migrations") 177 require.Contains(t, string(schema), "\n--\n"+ 178 "-- PostgreSQL database dump complete\n"+ 179 "--\n\n\n"+ 180 "--\n"+ 181 "-- Dbmate schema migrations\n"+ 182 "--\n\n"+ 183 "INSERT INTO public.schema_migrations (version) VALUES\n"+ 184 " ('abc1'),\n"+ 185 " ('abc2');\n") 186 187 // DumpSchema should return error if command fails 188 drv.databaseURL.Path = "/fakedb" 189 schema, err = drv.DumpSchema(db) 190 require.Nil(t, schema) 191 require.Error(t, err) 192 require.Contains(t, err.Error(), "database \"fakedb\" does not exist") 193 }) 194 195 t.Run("custom migrations table with schema", func(t *testing.T) { 196 drv := testPostgresDriver(t) 197 drv.migrationsTableName = "camelSchema.testMigrations" 198 199 // prepare database 200 db := prepTestPostgresDB(t) 201 defer dbutil.MustClose(db) 202 err := drv.CreateMigrationsTable(db) 203 require.NoError(t, err) 204 205 // insert migration 206 err = drv.InsertMigration(db, "abc1") 207 require.NoError(t, err) 208 err = drv.InsertMigration(db, "abc2") 209 require.NoError(t, err) 210 211 // DumpSchema should return schema 212 schema, err := drv.DumpSchema(db) 213 require.NoError(t, err) 214 require.Contains(t, string(schema), "CREATE TABLE \"camelSchema\".\"testMigrations\"") 215 require.Contains(t, string(schema), "\n--\n"+ 216 "-- PostgreSQL database dump complete\n"+ 217 "--\n\n\n"+ 218 "--\n"+ 219 "-- Dbmate schema migrations\n"+ 220 "--\n\n"+ 221 "INSERT INTO \"camelSchema\".\"testMigrations\" (version) VALUES\n"+ 222 " ('abc1'),\n"+ 223 " ('abc2');\n") 224 }) 225 } 226 227 func TestPostgresDatabaseExists(t *testing.T) { 228 drv := testPostgresDriver(t) 229 230 // drop any existing database 231 err := drv.DropDatabase() 232 require.NoError(t, err) 233 234 // DatabaseExists should return false 235 exists, err := drv.DatabaseExists() 236 require.NoError(t, err) 237 require.Equal(t, false, exists) 238 239 // create database 240 err = drv.CreateDatabase() 241 require.NoError(t, err) 242 243 // DatabaseExists should return true 244 exists, err = drv.DatabaseExists() 245 require.NoError(t, err) 246 require.Equal(t, true, exists) 247 } 248 249 func TestPostgresDatabaseExists_Error(t *testing.T) { 250 drv := testPostgresDriver(t) 251 drv.databaseURL.User = url.User("invalid") 252 253 exists, err := drv.DatabaseExists() 254 require.Error(t, err) 255 require.Equal(t, "pq: password authentication failed for user \"invalid\"", err.Error()) 256 require.Equal(t, false, exists) 257 } 258 259 func TestPostgresCreateMigrationsTable(t *testing.T) { 260 t.Run("default schema", func(t *testing.T) { 261 drv := testPostgresDriver(t) 262 db := prepTestPostgresDB(t) 263 defer dbutil.MustClose(db) 264 265 // migrations table should not exist 266 count := 0 267 err := db.QueryRow("select count(*) from public.schema_migrations").Scan(&count) 268 require.Error(t, err) 269 require.Equal(t, "pq: relation \"public.schema_migrations\" does not exist", err.Error()) 270 271 // create table 272 err = drv.CreateMigrationsTable(db) 273 require.NoError(t, err) 274 275 // migrations table should exist 276 err = db.QueryRow("select count(*) from public.schema_migrations").Scan(&count) 277 require.NoError(t, err) 278 279 // create table should be idempotent 280 err = drv.CreateMigrationsTable(db) 281 require.NoError(t, err) 282 }) 283 284 t.Run("custom search path", func(t *testing.T) { 285 drv := testPostgresDriver(t) 286 drv.migrationsTableName = "testMigrations" 287 288 u, err := url.Parse(drv.databaseURL.String() + "&search_path=camelFoo") 289 require.NoError(t, err) 290 drv.databaseURL = u 291 292 db := prepTestPostgresDB(t) 293 defer dbutil.MustClose(db) 294 295 // delete schema 296 _, err = db.Exec("drop schema if exists \"camelFoo\"") 297 require.NoError(t, err) 298 299 // drop any testMigrations table in public schema 300 _, err = db.Exec("drop table if exists public.\"testMigrations\"") 301 require.NoError(t, err) 302 303 // migrations table should not exist in either schema 304 count := 0 305 err = db.QueryRow("select count(*) from \"camelFoo\".\"testMigrations\"").Scan(&count) 306 require.Error(t, err) 307 require.Equal(t, "pq: relation \"camelFoo.testMigrations\" does not exist", err.Error()) 308 err = db.QueryRow("select count(*) from public.\"testMigrations\"").Scan(&count) 309 require.Error(t, err) 310 require.Equal(t, "pq: relation \"public.testMigrations\" does not exist", err.Error()) 311 312 // create table 313 err = drv.CreateMigrationsTable(db) 314 require.NoError(t, err) 315 316 // camelFoo schema should be created, and migrations table should exist only in camelFoo schema 317 err = db.QueryRow("select count(*) from \"camelFoo\".\"testMigrations\"").Scan(&count) 318 require.NoError(t, err) 319 err = db.QueryRow("select count(*) from public.\"testMigrations\"").Scan(&count) 320 require.Error(t, err) 321 require.Equal(t, "pq: relation \"public.testMigrations\" does not exist", err.Error()) 322 323 // create table should be idempotent 324 err = drv.CreateMigrationsTable(db) 325 require.NoError(t, err) 326 }) 327 328 t.Run("custom schema", func(t *testing.T) { 329 drv := testPostgresDriver(t) 330 drv.migrationsTableName = "camelSchema.testMigrations" 331 332 u, err := url.Parse(drv.databaseURL.String() + "&search_path=foo") 333 require.NoError(t, err) 334 drv.databaseURL = u 335 336 db := prepTestPostgresDB(t) 337 defer dbutil.MustClose(db) 338 339 // delete schemas 340 _, err = db.Exec("drop schema if exists foo") 341 require.NoError(t, err) 342 _, err = db.Exec("drop schema if exists \"camelSchema\"") 343 require.NoError(t, err) 344 345 // migrations table should not exist 346 count := 0 347 err = db.QueryRow("select count(*) from \"camelSchema\".\"testMigrations\"").Scan(&count) 348 require.Error(t, err) 349 require.Equal(t, "pq: relation \"camelSchema.testMigrations\" does not exist", err.Error()) 350 351 // create table 352 err = drv.CreateMigrationsTable(db) 353 require.NoError(t, err) 354 355 // camelSchema should be created, and testMigrations table should exist 356 err = db.QueryRow("select count(*) from \"camelSchema\".\"testMigrations\"").Scan(&count) 357 require.NoError(t, err) 358 // testMigrations table should not exist in foo schema because 359 // schema specified with migrations table name takes priority over search path 360 err = db.QueryRow("select count(*) from foo.\"testMigrations\"").Scan(&count) 361 require.Error(t, err) 362 require.Equal(t, "pq: relation \"foo.testMigrations\" does not exist", err.Error()) 363 364 // create table should be idempotent 365 err = drv.CreateMigrationsTable(db) 366 require.NoError(t, err) 367 }) 368 } 369 370 func TestPostgresSelectMigrations(t *testing.T) { 371 drv := testPostgresDriver(t) 372 drv.migrationsTableName = "test_migrations" 373 374 db := prepTestPostgresDB(t) 375 defer dbutil.MustClose(db) 376 377 err := drv.CreateMigrationsTable(db) 378 require.NoError(t, err) 379 380 _, err = db.Exec(`insert into public.test_migrations (version) 381 values ('abc2'), ('abc1'), ('abc3')`) 382 require.NoError(t, err) 383 384 migrations, err := drv.SelectMigrations(db, -1) 385 require.NoError(t, err) 386 require.Equal(t, true, migrations["abc1"]) 387 require.Equal(t, true, migrations["abc2"]) 388 require.Equal(t, true, migrations["abc2"]) 389 390 // test limit param 391 migrations, err = drv.SelectMigrations(db, 1) 392 require.NoError(t, err) 393 require.Equal(t, true, migrations["abc3"]) 394 require.Equal(t, false, migrations["abc1"]) 395 require.Equal(t, false, migrations["abc2"]) 396 } 397 398 func TestPostgresInsertMigration(t *testing.T) { 399 drv := testPostgresDriver(t) 400 drv.migrationsTableName = "test_migrations" 401 402 db := prepTestPostgresDB(t) 403 defer dbutil.MustClose(db) 404 405 err := drv.CreateMigrationsTable(db) 406 require.NoError(t, err) 407 408 count := 0 409 err = db.QueryRow("select count(*) from public.test_migrations").Scan(&count) 410 require.NoError(t, err) 411 require.Equal(t, 0, count) 412 413 // insert migration 414 err = drv.InsertMigration(db, "abc1") 415 require.NoError(t, err) 416 417 err = db.QueryRow("select count(*) from public.test_migrations where version = 'abc1'"). 418 Scan(&count) 419 require.NoError(t, err) 420 require.Equal(t, 1, count) 421 } 422 423 func TestPostgresDeleteMigration(t *testing.T) { 424 drv := testPostgresDriver(t) 425 drv.migrationsTableName = "test_migrations" 426 427 db := prepTestPostgresDB(t) 428 defer dbutil.MustClose(db) 429 430 err := drv.CreateMigrationsTable(db) 431 require.NoError(t, err) 432 433 _, err = db.Exec(`insert into public.test_migrations (version) 434 values ('abc1'), ('abc2')`) 435 require.NoError(t, err) 436 437 err = drv.DeleteMigration(db, "abc2") 438 require.NoError(t, err) 439 440 count := 0 441 err = db.QueryRow("select count(*) from public.test_migrations").Scan(&count) 442 require.NoError(t, err) 443 require.Equal(t, 1, count) 444 } 445 446 func TestPostgresPing(t *testing.T) { 447 drv := testPostgresDriver(t) 448 449 // drop any existing database 450 err := drv.DropDatabase() 451 require.NoError(t, err) 452 453 // ping database 454 err = drv.Ping() 455 require.NoError(t, err) 456 457 // ping invalid host should return error 458 drv.databaseURL.Host = "postgres:404" 459 err = drv.Ping() 460 require.Error(t, err) 461 require.Contains(t, err.Error(), "connect: connection refused") 462 } 463 464 func TestPostgresQuotedMigrationsTableName(t *testing.T) { 465 t.Run("default schema", func(t *testing.T) { 466 drv := testPostgresDriver(t) 467 db := prepTestPostgresDB(t) 468 defer dbutil.MustClose(db) 469 470 name, err := drv.quotedMigrationsTableName(db) 471 require.NoError(t, err) 472 require.Equal(t, "public.schema_migrations", name) 473 }) 474 475 t.Run("custom schema", func(t *testing.T) { 476 drv := testPostgresDriver(t) 477 u, err := url.Parse(drv.databaseURL.String() + "&search_path=foo,bar,public") 478 require.NoError(t, err) 479 drv.databaseURL = u 480 481 db := prepTestPostgresDB(t) 482 defer dbutil.MustClose(db) 483 484 _, err = db.Exec("drop schema if exists foo") 485 require.NoError(t, err) 486 _, err = db.Exec("drop schema if exists bar") 487 require.NoError(t, err) 488 489 // should use first schema from search path 490 name, err := drv.quotedMigrationsTableName(db) 491 require.NoError(t, err) 492 require.Equal(t, "foo.schema_migrations", name) 493 }) 494 495 t.Run("no schema", func(t *testing.T) { 496 drv := testPostgresDriver(t) 497 db := prepTestPostgresDB(t) 498 defer dbutil.MustClose(db) 499 500 // this is an unlikely edge case, but if for some reason there is 501 // no current schema then we should default to "public" 502 _, err := db.Exec("select pg_catalog.set_config('search_path', '', false)") 503 require.NoError(t, err) 504 505 name, err := drv.quotedMigrationsTableName(db) 506 require.NoError(t, err) 507 require.Equal(t, "public.schema_migrations", name) 508 }) 509 510 t.Run("custom table name", func(t *testing.T) { 511 drv := testPostgresDriver(t) 512 db := prepTestPostgresDB(t) 513 defer dbutil.MustClose(db) 514 515 drv.migrationsTableName = "simple_name" 516 name, err := drv.quotedMigrationsTableName(db) 517 require.NoError(t, err) 518 require.Equal(t, "public.simple_name", name) 519 }) 520 521 t.Run("custom table name quoted", func(t *testing.T) { 522 drv := testPostgresDriver(t) 523 db := prepTestPostgresDB(t) 524 defer dbutil.MustClose(db) 525 526 // this table name will need quoting 527 drv.migrationsTableName = "camelCase" 528 name, err := drv.quotedMigrationsTableName(db) 529 require.NoError(t, err) 530 require.Equal(t, "public.\"camelCase\"", name) 531 }) 532 533 t.Run("custom table name with custom schema", func(t *testing.T) { 534 drv := testPostgresDriver(t) 535 u, err := url.Parse(drv.databaseURL.String() + "&search_path=foo") 536 require.NoError(t, err) 537 drv.databaseURL = u 538 539 db := prepTestPostgresDB(t) 540 defer dbutil.MustClose(db) 541 542 _, err = db.Exec("create schema if not exists foo") 543 require.NoError(t, err) 544 545 drv.migrationsTableName = "simple_name" 546 name, err := drv.quotedMigrationsTableName(db) 547 require.NoError(t, err) 548 require.Equal(t, "foo.simple_name", name) 549 }) 550 551 t.Run("custom table name overrides schema", func(t *testing.T) { 552 drv := testPostgresDriver(t) 553 u, err := url.Parse(drv.databaseURL.String() + "&search_path=foo") 554 require.NoError(t, err) 555 drv.databaseURL = u 556 557 db := prepTestPostgresDB(t) 558 defer dbutil.MustClose(db) 559 560 _, err = db.Exec("create schema if not exists foo") 561 require.NoError(t, err) 562 _, err = db.Exec("create schema if not exists bar") 563 require.NoError(t, err) 564 565 // if schema is specified as part of table name, it should override search_path 566 drv.migrationsTableName = "bar.simple_name" 567 name, err := drv.quotedMigrationsTableName(db) 568 require.NoError(t, err) 569 require.Equal(t, "bar.simple_name", name) 570 571 // schema and table name should be quoted if necessary 572 drv.migrationsTableName = "barName.camelTable" 573 name, err = drv.quotedMigrationsTableName(db) 574 require.NoError(t, err) 575 require.Equal(t, "\"barName\".\"camelTable\"", name) 576 577 // more than 2 components is unexpected but we will quote and pass it along anyway 578 drv.migrationsTableName = "whyWould.i.doThis" 579 name, err = drv.quotedMigrationsTableName(db) 580 require.NoError(t, err) 581 require.Equal(t, "\"whyWould\".i.\"doThis\"", name) 582 }) 583 }