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