Repository: sqlc-dev/sqlc
Pull request #3309 (2024-04-03 04:54:02)

fix(docs): correct go install instructions
Go 1.21+ is required for recent versions of sqlc. This is because it uses components that require the 1.21 toolchain. Coincidentally 1.21 is also when forward compatibility was introduced.

You can test this
```
$ docker run -it --rm golang:1.19
% go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
...
pkg/mod/github.com/sqlc-dev/sqlc@latest/internal/bundler/upload.go:7:2: package log/slog is not in GOROOT (/usr/local/go/src/log/slog)
pkg/mod/github.com/sqlc-dev/sqlc@latest/internal/codegen/golang/opts/options.go:6:2: package maps is not in GOROOT (/usr/local/go/src/maps)
```


Patch:
diff --git a/docs/overview/install.md b/docs/overview/install.md
index 4e1f46f53..db25fac41 100644
--- a/docs/overview/install.md
+++ b/docs/overview/install.md
@@ -14,20 +14,14 @@ brew install sqlc
 sudo snap install sqlc
 ```
 
-## go install 
+## go install
 
-### Go >= 1.17:
+Installing recent versions of sqlc requires Go 1.21+.
 
 ```
 go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
 ```
 
-### Go < 1.17:
-
-```
-go get github.com/sqlc-dev/sqlc/cmd/sqlc
-```
-
 ## Docker
 
 ```
diff --git a/go.mod b/go.mod
index 7ae554b5c..e7eaea69d 100644
--- a/go.mod
+++ b/go.mod
@@ -58,10 +58,10 @@ require (
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.26.0 // indirect
-	golang.org/x/crypto v0.20.0 // indirect
+	golang.org/x/crypto v0.21.0 // indirect
 	golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
-	golang.org/x/net v0.21.0 // indirect
-	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/net v0.23.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
diff --git a/go.sum b/go.sum
index 3adb56ab0..c05feef80 100644
--- a/go.sum
+++ b/go.sum
@@ -223,8 +223,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
-golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -237,8 +237,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@@ -255,8 +255,8 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/internal/config/v_two.go b/internal/config/v_two.go
index 1d3b224dd..0fe22ffa2 100644
--- a/internal/config/v_two.go
+++ b/internal/config/v_two.go
@@ -76,7 +76,7 @@ func v2ParseConfig(rd io.Reader) (Config, error) {
 			if cg.Out == "" {
 				return conf, ErrNoOutPath
 			}
-			// TOOD: Allow the use of built-in codegen from here
+			// TODO: Allow the use of built-in codegen from here
 			if _, ok := plugins[cg.Plugin]; !ok {
 				return conf, ErrPluginNotFound
 			}
diff --git a/internal/pgx/poolcache/poolcache.go b/internal/pgx/poolcache/poolcache.go
new file mode 100644
index 000000000..93401ec93
--- /dev/null
+++ b/internal/pgx/poolcache/poolcache.go
@@ -0,0 +1,32 @@
+package poolcache
+
+import (
+	"context"
+	"sync"
+
+	"github.com/jackc/pgx/v5/pgxpool"
+)
+
+var lock sync.RWMutex
+var pools = map[string]*pgxpool.Pool{}
+
+func New(ctx context.Context, uri string) (*pgxpool.Pool, error) {
+	lock.RLock()
+	existing, found := pools[uri]
+	lock.RUnlock()
+
+	if found {
+		return existing, nil
+	}
+
+	pool, err := pgxpool.New(ctx, uri)
+	if err != nil {
+		return nil, err
+	}
+
+	lock.Lock()
+	pools[uri] = pool
+	lock.Unlock()
+
+	return pool, nil
+}


Test patch:
diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go
index 8420340d8..c2792ee1d 100644
--- a/internal/endtoend/endtoend_test.go
+++ b/internal/endtoend/endtoend_test.go
@@ -120,7 +120,6 @@ func TestReplay(t *testing.T) {
 		"managed-db": {
 			Mutate: func(t *testing.T, path string) func(*config.Config) {
 				return func(c *config.Config) {
-					c.Cloud.Project = "01HAQMMECEYQYKFJN8MP16QC41" // TODO: Read from environment
 					for i := range c.SQL {
 						files := []string{}
 						for _, s := range c.SQL[i].Schema {
@@ -128,7 +127,7 @@ func TestReplay(t *testing.T) {
 						}
 						switch c.SQL[i].Engine {
 						case config.EnginePostgreSQL:
-							uri := local.PostgreSQL(t, files)
+							uri := local.ReadOnlyPostgreSQL(t, files)
 							c.SQL[i].Database = &config.Database{
 								URI: uri,
 							}
@@ -138,18 +137,12 @@ func TestReplay(t *testing.T) {
 						// 		URI: uri,
 						// 	}
 						default:
-							c.SQL[i].Database = &config.Database{
-								Managed: true,
-							}
+							// pass
 						}
 					}
 				}
 			},
 			Enabled: func() bool {
-				// Return false if no auth token exists
-				if len(os.Getenv("SQLC_AUTH_TOKEN")) == 0 {
-					return false
-				}
 				if len(os.Getenv("POSTGRESQL_SERVER_URI")) == 0 {
 					return false
 				}
diff --git a/internal/endtoend/testdata/golang_initialisms_url/schema.sql b/internal/endtoend/testdata/golang_initialisms_url/schema.sql
index 83ab16012..d0b250690 100644
--- a/internal/endtoend/testdata/golang_initialisms_url/schema.sql
+++ b/internal/endtoend/testdata/golang_initialisms_url/schema.sql
@@ -1,4 +1,4 @@
-CREATE TABLE foo(
+CREATE TABLE foo (
         bar_id text,
         site_url text
 );
diff --git a/internal/endtoend/testdata/golang_invalid_sql_package/schema.sql b/internal/endtoend/testdata/golang_invalid_sql_package/schema.sql
index 1bd72529f..e29621be5 100644
--- a/internal/endtoend/testdata/golang_invalid_sql_package/schema.sql
+++ b/internal/endtoend/testdata/golang_invalid_sql_package/schema.sql
@@ -1,3 +1,3 @@
-CREATE TABLE foo(
+CREATE TABLE foo (
         bar text
 );
diff --git a/internal/endtoend/testdata/schema_scoped_filter/mysql/schema.sql b/internal/endtoend/testdata/schema_scoped_filter/mysql/schema.sql
index a22ab0907..e5cb43f4b 100644
--- a/internal/endtoend/testdata/schema_scoped_filter/mysql/schema.sql
+++ b/internal/endtoend/testdata/schema_scoped_filter/mysql/schema.sql
@@ -1,3 +1,2 @@
 CREATE SCHEMA foo;
 CREATE TABLE foo.bar (id serial not null);
-
diff --git a/internal/sqltest/local/postgres.go b/internal/sqltest/local/postgres.go
index a283273ac..3520d42b8 100644
--- a/internal/sqltest/local/postgres.go
+++ b/internal/sqltest/local/postgres.go
@@ -3,23 +3,31 @@ package local
 import (
 	"context"
 	"fmt"
+	"hash/fnv"
 	"net/url"
 	"os"
 	"strings"
-	"sync"
 	"testing"
 
 	"github.com/jackc/pgx/v5"
-	"github.com/jackc/pgx/v5/pgxpool"
+	"golang.org/x/sync/singleflight"
 
 	migrate "github.com/sqlc-dev/sqlc/internal/migrations"
+	"github.com/sqlc-dev/sqlc/internal/pgx/poolcache"
 	"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
 )
 
-var postgresPool *pgxpool.Pool
-var postgresSync sync.Once
+var flight singleflight.Group
 
 func PostgreSQL(t *testing.T, migrations []string) string {
+	return postgreSQL(t, migrations, true)
+}
+
+func ReadOnlyPostgreSQL(t *testing.T, migrations []string) string {
+	return postgreSQL(t, migrations, false)
+}
+
+func postgreSQL(t *testing.T, migrations []string, rw bool) string {
 	ctx := context.Background()
 	t.Helper()
 
@@ -28,16 +36,9 @@ func PostgreSQL(t *testing.T, migrations []string) string {
 		t.Skip("POSTGRESQL_SERVER_URI is empty")
 	}
 
-	postgresSync.Do(func() {
-		pool, err := pgxpool.New(ctx, dburi)
-		if err != nil {
-			t.Fatal(err)
-		}
-		postgresPool = pool
-	})
-
-	if postgresPool == nil {
-		t.Fatalf("PostgreSQL pool creation failed")
+	postgresPool, err := poolcache.New(ctx, dburi)
+	if err != nil {
+		t.Fatalf("PostgreSQL pool creation failed: %s", err)
 	}
 
 	var seed []string
@@ -45,48 +46,73 @@ func PostgreSQL(t *testing.T, migrations []string) string {
 	if err != nil {
 		t.Fatal(err)
 	}
+
+	h := fnv.New64()
 	for _, f := range files {
 		blob, err := os.ReadFile(f)
 		if err != nil {
 			t.Fatal(err)
 		}
+		h.Write(blob)
 		seed = append(seed, migrate.RemoveRollbackStatements(string(blob)))
 	}
 
-	uri, err := url.Parse(dburi)
-	if err != nil {
-		t.Fatal(err)
+	var name string
+	if rw {
+		name = fmt.Sprintf("sqlc_test_%s", id())
+	} else {
+		name = fmt.Sprintf("sqlc_test_%x", h.Sum(nil))
 	}
 
-	name := fmt.Sprintf("sqlc_test_%s", id())
-
-	if _, err := postgresPool.Exec(ctx, fmt.Sprintf(`CREATE DATABASE "%s"`, name)); err != nil {
+	uri, err := url.Parse(dburi)
+	if err != nil {
 		t.Fatal(err)
 	}
-
 	uri.Path = name
 	dropQuery := fmt.Sprintf(`DROP DATABASE IF EXISTS "%s" WITH (FORCE)`, name)
 
-	t.Cleanup(func() {
-		if _, err := postgresPool.Exec(ctx, dropQuery); err != nil {
-			t.Fatal(err)
+	key := uri.String()
+
+	_, err, _ = flight.Do(key, func() (interface{}, error) {
+		row := postgresPool.QueryRow(ctx,
+			fmt.Sprintf(`SELECT datname FROM pg_database WHERE datname = '%s'`, name))
+
+		var datname string
+		if err := row.Scan(&datname); err == nil {
+			t.Logf("database exists: %s", name)
+			return nil, nil
 		}
-	})
 
-	conn, err := pgx.Connect(ctx, uri.String())
-	if err != nil {
-		t.Fatalf("connect %s: %s", name, err)
-	}
-	defer conn.Close(ctx)
+		t.Logf("creating database: %s", name)
+		if _, err := postgresPool.Exec(ctx, fmt.Sprintf(`CREATE DATABASE "%s"`, name)); err != nil {
+			return nil, err
+		}
 
-	for _, q := range seed {
-		if len(strings.TrimSpace(q)) == 0 {
-			continue
+		conn, err := pgx.Connect(ctx, uri.String())
+		if err != nil {
+			return nil, fmt.Errorf("connect %s: %s", name, err)
 		}
-		if _, err := conn.Exec(ctx, q); err != nil {
-			t.Fatalf("%s: %s", q, err)
+		defer conn.Close(ctx)
+
+		for _, q := range seed {
+			if len(strings.TrimSpace(q)) == 0 {
+				continue
+			}
+			if _, err := conn.Exec(ctx, q); err != nil {
+				return nil, fmt.Errorf("%s: %s", q, err)
+			}
 		}
+		return nil, nil
+	})
+	if rw || err != nil {
+		t.Cleanup(func() {
+			if _, err := postgresPool.Exec(ctx, dropQuery); err != nil {
+				t.Fatalf("failed cleaning up: %s", err)
+			}
+		})
 	}
-
-	return uri.String()
+	if err != nil {
+		t.Fatalf("create db: %s", err)
+	}
+	return key
 }
diff --git a/scripts/cleanup-test-dbs/main.go b/scripts/cleanup-test-dbs/main.go
new file mode 100644
index 000000000..51093db30
--- /dev/null
+++ b/scripts/cleanup-test-dbs/main.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/pgxpool"
+)
+
+func main() {
+	if err := run(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+const query = `
+SELECT datname
+FROM pg_database
+WHERE datname LIKE 'sqlc_test_%'
+`
+
+func run() error {
+	ctx := context.Background()
+	dburi := os.Getenv("POSTGRESQL_SERVER_URI")
+	if dburi == "" {
+		return fmt.Errorf("POSTGRESQL_SERVER_URI is empty")
+	}
+	pool, err := pgxpool.New(ctx, dburi)
+	if err != nil {
+		return err
+	}
+
+	rows, err := pool.Query(ctx, query)
+	if err != nil {
+		return err
+	}
+
+	names, err := pgx.CollectRows(rows, pgx.RowTo[string])
+	if err != nil {
+		return err
+	}
+
+	for _, name := range names {
+		drop := fmt.Sprintf(`DROP DATABASE IF EXISTS "%s" WITH (FORCE)`, name)
+		if _, err := pool.Exec(ctx, drop); err != nil {
+			return err
+		}
+		log.Println("dropping database", name)
+	}
+
+	return nil
+}