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