Skip to content

Commit 3af1181

Browse files
Support --restrict-key (#727)
Fixes #678 Use `--restrict-key` for `pgdump` > `17.6` so that we can generate deterministic dumps. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 43294f7 commit 3af1181

File tree

3 files changed

+102
-3
lines changed

3 files changed

+102
-3
lines changed

pkg/dbmate/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package dbmate
22

33
// Version of dbmate
4-
const Version = "2.29.2"
4+
const Version = "2.29.3"

pkg/driver/postgres/postgres.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"io"
88
"net/url"
9+
"os/exec"
10+
"regexp"
911
"runtime"
1012
"strconv"
1113
"strings"
@@ -16,6 +18,50 @@ import (
1618
"github.com/lib/pq"
1719
)
1820

21+
// pgDumpVersion represents a parsed pg_dump version
22+
type pgDumpVersion struct {
23+
major int
24+
minor int
25+
}
26+
27+
// pgDumpVersionRegexp matches pg_dump version output like "pg_dump (PostgreSQL) 17.6 (Debian 17.6-1.pgdg120+1)"
28+
var pgDumpVersionRegexp = regexp.MustCompile(`\(PostgreSQL\) (\d+)\.(\d+)`)
29+
30+
// getPgDumpVersion returns the version of pg_dump, or nil if it cannot be determined
31+
func getPgDumpVersion() *pgDumpVersion {
32+
cmd := exec.Command("pg_dump", "--version")
33+
output, err := cmd.Output()
34+
if err != nil {
35+
return nil
36+
}
37+
38+
matches := pgDumpVersionRegexp.FindStringSubmatch(string(output))
39+
if len(matches) < 3 {
40+
return nil
41+
}
42+
43+
major, err := strconv.Atoi(matches[1])
44+
if err != nil {
45+
return nil
46+
}
47+
48+
minor, err := strconv.Atoi(matches[2])
49+
if err != nil {
50+
return nil
51+
}
52+
53+
return &pgDumpVersion{major: major, minor: minor}
54+
}
55+
56+
// supportsRestrictKey returns true if pg_dump supports --restrict-key (added in PostgreSQL 17.6)
57+
func (v *pgDumpVersion) supportsRestrictKey() bool {
58+
if v == nil {
59+
return false
60+
}
61+
// --restrict-key was added in PostgreSQL 17.6
62+
return v.major > 17 || (v.major == 17 && v.minor >= 6)
63+
}
64+
1965
func init() {
2066
dbmate.RegisterDriver(NewDriver, "postgres")
2167
dbmate.RegisterDriver(NewDriver, "postgresql")
@@ -199,8 +245,17 @@ func (drv *Driver) schemaMigrationsDump(db *sql.DB) ([]byte, error) {
199245
// DumpSchema returns the current database schema
200246
func (drv *Driver) DumpSchema(db *sql.DB) ([]byte, error) {
201247
// load schema
202-
args := append([]string{"--format=plain", "--encoding=UTF8", "--schema-only",
203-
"--no-privileges", "--no-owner"}, connectionArgsForDump(drv.databaseURL)...)
248+
args := []string{"--format=plain", "--encoding=UTF8", "--schema-only",
249+
"--no-privileges", "--no-owner"}
250+
251+
// PostgreSQL 17.6+ adds \restrict/\unrestrict commands to pg_dump output with a random key
252+
// by default, making the output non-deterministic. Use a fixed key for reproducible output.
253+
// See: https://github.com/amacneil/dbmate/issues/678
254+
if version := getPgDumpVersion(); version.supportsRestrictKey() {
255+
args = append(args, "--restrict-key=dbmate")
256+
}
257+
258+
args = append(args, connectionArgsForDump(drv.databaseURL)...)
204259
schema, err := dbutil.RunCommand("pg_dump", args...)
205260
if err != nil {
206261
return nil, err

pkg/driver/postgres/postgres_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,50 @@ func TestGetDriver(t *testing.T) {
100100
require.Equal(t, "schema_migrations", drv.migrationsTableName)
101101
}
102102

103+
func TestPgDumpVersionRegexp(t *testing.T) {
104+
cases := []struct {
105+
input string
106+
expectedMajor int
107+
expectedMinor int
108+
}{
109+
{"pg_dump (PostgreSQL) 17.6 (Debian 17.6-1.pgdg120+1)", 17, 6},
110+
{"pg_dump (PostgreSQL) 16.0", 16, 0},
111+
{"pg_dump (PostgreSQL) 15.12", 15, 12},
112+
}
113+
114+
for _, c := range cases {
115+
t.Run(c.input, func(t *testing.T) {
116+
matches := pgDumpVersionRegexp.FindStringSubmatch(c.input)
117+
require.Len(t, matches, 3)
118+
require.Equal(t, fmt.Sprintf("%d", c.expectedMajor), matches[1])
119+
require.Equal(t, fmt.Sprintf("%d", c.expectedMinor), matches[2])
120+
})
121+
}
122+
}
123+
124+
func TestPgDumpVersionSupportsRestrictKey(t *testing.T) {
125+
cases := []struct {
126+
name string
127+
version *pgDumpVersion
128+
expected bool
129+
}{
130+
{"nil version", nil, false},
131+
{"PostgreSQL 16.0", &pgDumpVersion{major: 16, minor: 0}, false},
132+
{"PostgreSQL 17.0", &pgDumpVersion{major: 17, minor: 0}, false},
133+
{"PostgreSQL 17.5", &pgDumpVersion{major: 17, minor: 5}, false},
134+
{"PostgreSQL 17.6", &pgDumpVersion{major: 17, minor: 6}, true},
135+
{"PostgreSQL 17.7", &pgDumpVersion{major: 17, minor: 7}, true},
136+
{"PostgreSQL 18.0", &pgDumpVersion{major: 18, minor: 0}, true},
137+
}
138+
139+
for _, c := range cases {
140+
t.Run(c.name, func(t *testing.T) {
141+
result := c.version.supportsRestrictKey()
142+
require.Equal(t, c.expected, result)
143+
})
144+
}
145+
}
146+
103147
func defaultConnString() string {
104148
switch runtime.GOOS {
105149
case "linux":

0 commit comments

Comments
 (0)