@@ -10,6 +10,7 @@ import (
10
10
"os/exec"
11
11
"path/filepath"
12
12
"regexp"
13
+ "strconv"
13
14
"strings"
14
15
"testing"
15
16
"time"
@@ -184,20 +185,21 @@ func DumpOnFailure(t testing.TB, connectionURL string) {
184
185
now := time .Now ()
185
186
timeSuffix := fmt .Sprintf ("%d%d%d%d%d%d" , now .Year (), now .Month (), now .Day (), now .Hour (), now .Minute (), now .Second ())
186
187
outPath := filepath .Join (cwd , snakeCaseName + "." + timeSuffix + ".test.sql" )
187
- dump , err := pgDump (connectionURL )
188
+ dump , err := PGDump (connectionURL )
188
189
if err != nil {
189
190
t .Errorf ("dump on failure: failed to run pg_dump" )
190
191
return
191
192
}
192
- if err := os .WriteFile (outPath , filterDump (dump ), 0o600 ); err != nil {
193
+ if err := os .WriteFile (outPath , normalizeDump (dump ), 0o600 ); err != nil {
193
194
t .Errorf ("dump on failure: failed to write: %s" , err .Error ())
194
195
return
195
196
}
196
197
t .Logf ("Dumped database to %q due to failed test. I hope you find what you're looking for!" , outPath )
197
198
}
198
199
199
- // pgDump runs pg_dump against dbURL and returns the output.
200
- func pgDump (dbURL string ) ([]byte , error ) {
200
+ // PGDump runs pg_dump against dbURL and returns the output.
201
+ // It is used by DumpOnFailure().
202
+ func PGDump (dbURL string ) ([]byte , error ) {
201
203
if _ , err := exec .LookPath ("pg_dump" ); err != nil {
202
204
return nil , xerrors .Errorf ("could not find pg_dump in path: %w" , err )
203
205
}
@@ -230,16 +232,79 @@ func pgDump(dbURL string) ([]byte, error) {
230
232
return stdout .Bytes (), nil
231
233
}
232
234
233
- // Unfortunately, some insert expressions span multiple lines.
234
- // The below may be over-permissive but better that than truncating data.
235
- var insertExpr = regexp .MustCompile (`(?s)\bINSERT[^;]+;` )
235
+ const minimumPostgreSQLVersion = 13
236
236
237
- func filterDump (dump []byte ) []byte {
238
- var buf bytes.Buffer
239
- matches := insertExpr .FindAll (dump , - 1 )
240
- for _ , m := range matches {
241
- _ , _ = buf .Write (m )
242
- _ , _ = buf .WriteRune ('\n' )
237
+ // PGDumpSchemaOnly is for use by gen/dump only.
238
+ // It runs pg_dump against dbURL and sets a consistent timezone and encoding.
239
+ func PGDumpSchemaOnly (dbURL string ) ([]byte , error ) {
240
+ hasPGDump := false
241
+ if _ , err := exec .LookPath ("pg_dump" ); err == nil {
242
+ out , err := exec .Command ("pg_dump" , "--version" ).Output ()
243
+ if err == nil {
244
+ // Parse output:
245
+ // pg_dump (PostgreSQL) 14.5 (Ubuntu 14.5-0ubuntu0.22.04.1)
246
+ parts := strings .Split (string (out ), " " )
247
+ if len (parts ) > 2 {
248
+ version , err := strconv .Atoi (strings .Split (parts [2 ], "." )[0 ])
249
+ if err == nil && version >= minimumPostgreSQLVersion {
250
+ hasPGDump = true
251
+ }
252
+ }
253
+ }
243
254
}
244
- return buf .Bytes ()
255
+
256
+ cmdArgs := []string {
257
+ "pg_dump" ,
258
+ "--schema-only" ,
259
+ dbURL ,
260
+ "--no-privileges" ,
261
+ "--no-owner" ,
262
+ "--no-privileges" ,
263
+ "--no-publication" ,
264
+ "--no-security-labels" ,
265
+ "--no-subscriptions" ,
266
+ "--no-tablespaces" ,
267
+
268
+ // We never want to manually generate
269
+ // queries executing against this table.
270
+ "--exclude-table=schema_migrations" ,
271
+ }
272
+
273
+ if ! hasPGDump {
274
+ cmdArgs = append ([]string {
275
+ "docker" ,
276
+ "run" ,
277
+ "--rm" ,
278
+ "--network=host" ,
279
+ fmt .Sprintf ("gcr.io/coder-dev-1/postgres:%d" , minimumPostgreSQLVersion ),
280
+ }, cmdArgs ... )
281
+ }
282
+ cmd := exec .Command (cmdArgs [0 ], cmdArgs [1 :]... ) //#nosec
283
+ cmd .Env = append (os .Environ (), []string {
284
+ "PGTZ=UTC" ,
285
+ "PGCLIENTENCODING=UTF8" ,
286
+ }... )
287
+ var output bytes.Buffer
288
+ cmd .Stdout = & output
289
+ cmd .Stderr = os .Stderr
290
+ err := cmd .Run ()
291
+ if err != nil {
292
+ return nil , err
293
+ }
294
+ return normalizeDump (output .Bytes ()), nil
295
+ }
296
+
297
+ func normalizeDump (schema []byte ) []byte {
298
+ // Remove all comments.
299
+ schema = regexp .MustCompile (`(?im)^(--.*)$` ).ReplaceAll (schema , []byte {})
300
+ // Public is implicit in the schema.
301
+ schema = regexp .MustCompile (`(?im)( |::|'|\()public\.` ).ReplaceAll (schema , []byte (`$1` ))
302
+ // Remove database settings.
303
+ schema = regexp .MustCompile (`(?im)^(SET.*;)` ).ReplaceAll (schema , []byte (`` ))
304
+ // Remove select statements
305
+ schema = regexp .MustCompile (`(?im)^(SELECT.*;)` ).ReplaceAll (schema , []byte (`` ))
306
+ // Removes multiple newlines.
307
+ schema = regexp .MustCompile (`(?im)\n{3,}` ).ReplaceAll (schema , []byte ("\n \n " ))
308
+
309
+ return schema
245
310
}
0 commit comments