Skip to content

Commit 43d9c3f

Browse files
author
Chris Hundt
committed
Add library for working with schemas in postgres
1 parent 21bcde6 commit 43d9c3f

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

pgutils/pgutils.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package pgutils
2+
3+
import (
4+
"crypto/sha1"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
"testing"
9+
10+
"github.com/jmoiron/sqlx"
11+
"github.com/lib/pq"
12+
)
13+
14+
// Given a postgres connection string, generate a new one with the search path
15+
// (https://www.postgresql.org/docs/17/ddl-schemas.html#DDL-SCHEMAS-PATH)
16+
// set to just the given schema name.
17+
func GetConnectionStringWithSearchPath(connStr, schemaName string) (string, error) {
18+
kvconn := connStr
19+
if strings.HasPrefix(connStr, "postgres://") || strings.HasPrefix(connStr, "postgresql://") {
20+
var err error
21+
kvconn, err = pq.ParseURL(connStr)
22+
if err != nil {
23+
return "", fmt.Errorf("Error parsing DB connection string: %w", err)
24+
}
25+
}
26+
kvconn = getCanonicalFormat(kvconn)
27+
filteredKVs := []string{}
28+
kvs := strings.Split(kvconn, " ")
29+
for _, kv := range kvs {
30+
pieces := strings.SplitN(kv, "=", 2)
31+
if pieces[0] == "search_path" {
32+
return "", fmt.Errorf("search_path already set to %q", pieces[1])
33+
}
34+
filteredKVs = append(filteredKVs, kv)
35+
}
36+
filteredKVs = append(filteredKVs, fmt.Sprintf("search_path=%s", schemaName))
37+
return strings.Join(filteredKVs, " "), nil
38+
}
39+
40+
// Connect to the database specified by the connection string, with the search path
41+
// (https://www.postgresql.org/docs/17/ddl-schemas.html#DDL-SCHEMAS-PATH)
42+
// set to just the given schema name.
43+
func ConnectWithSchema(dbConnectionString, schemaName string) (*sqlx.DB, error) {
44+
connStringWithSearchPath, err := GetConnectionStringWithSearchPath(dbConnectionString, schemaName)
45+
if err != nil {
46+
return nil, fmt.Errorf("Error getting updated connection string: %w", err)
47+
}
48+
49+
// Connect to database and set search_path
50+
db, err := sqlx.Connect("postgres", connStringWithSearchPath)
51+
if err != nil {
52+
return nil, fmt.Errorf("Error connecting to db: %w", err)
53+
}
54+
q := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schemaName)
55+
_, err = db.Exec(q)
56+
if err != nil {
57+
return nil, fmt.Errorf("Error ensuring schema %q: %w", schemaName, err)
58+
}
59+
60+
return db, nil
61+
}
62+
63+
func getSchemaName(testName string) string {
64+
name := strings.ToLower(strings.ReplaceAll(testName, " ", ""))
65+
if strings.HasPrefix(name, "_") {
66+
name = strings.Replace(name, "_", "", 1)
67+
}
68+
69+
// [20]byte
70+
hashedName := sha1.Sum([]byte(name))
71+
return fmt.Sprintf("a%x", hashedName)[:31]
72+
}
73+
74+
// This function connects to the database as specified in the connection string
75+
// and creates a new schema with a name based on the test name. It returns a new
76+
// database connection with that schema set as the search path.
77+
// (https://www.postgresql.org/docs/17/ddl-schemas.html#DDL-SCHEMAS-PATH)
78+
// It also registers a cleanup function (https://pkg.go.dev/testing#T.Cleanup)
79+
// that will drop the schema after the test completes.
80+
func ReconnectWithSchemaForTest(dbConnectionString string, t *testing.T) *sqlx.DB {
81+
// Create test schema and set search path
82+
schemaName := getSchemaName(t.Name())
83+
84+
connStringWithSearchPath, err := GetConnectionStringWithSearchPath(dbConnectionString, schemaName)
85+
if err != nil {
86+
t.Fatalf("Error getting connection string: %s", err)
87+
}
88+
89+
// Connect to database and set search_path
90+
db := sqlx.MustConnect("postgres", connStringWithSearchPath)
91+
q := fmt.Sprintf(`
92+
DO $$
93+
BEGIN
94+
IF EXISTS (
95+
SELECT 1
96+
FROM information_schema.schemata
97+
WHERE schema_name = '%s'
98+
) THEN
99+
RAISE EXCEPTION 'Failed to create schema "%s". It already exists.';
100+
END IF;
101+
END
102+
$$;
103+
`, schemaName, schemaName)
104+
_, err = db.Exec(q)
105+
if err != nil {
106+
t.Fatalf("Schema %s may not have been deleted after a previous test run. If it exists, delete it.", schemaName)
107+
}
108+
q = fmt.Sprintf("CREATE SCHEMA %s", schemaName)
109+
db.MustExec(q)
110+
111+
t.Cleanup(func() {
112+
q := fmt.Sprintf("DROP SCHEMA %s CASCADE", schemaName)
113+
db.MustExec(q)
114+
115+
err := db.Close()
116+
if err != nil {
117+
t.Fatalf("failed to close connection to postgres database: %s", err)
118+
}
119+
})
120+
121+
return db
122+
}
123+
124+
func getCanonicalFormat(s string) string {
125+
re := regexp.MustCompile(`\s+`)
126+
str := re.ReplaceAllString(s, " ")
127+
re = regexp.MustCompile(`\s*=\s*`)
128+
str = re.ReplaceAllString(str, "=")
129+
return str
130+
}

0 commit comments

Comments
 (0)