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  }