Skip to content

Commit 78bc7f7

Browse files
feat(config): allow local postgres configuration (#2796)
* feat(cli): allow local postgres.conf configuration Closes: #2611 * chore: test ToPostgresConfig * chore: add start runtime test * chore: use less max_connections * fix: db start tests * chore: add prefix to postgres config * fix: test * chore: serialise postgres conf as toml --------- Co-authored-by: Qiao Han <[email protected]>
1 parent 56c2cc4 commit 78bc7f7

File tree

4 files changed

+116
-6
lines changed

4 files changed

+116
-6
lines changed

internal/db/start/start.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ func NewContainerConfig() container.Config {
5656
env := []string{
5757
"POSTGRES_PASSWORD=" + utils.Config.Db.Password,
5858
"POSTGRES_HOST=/var/run/postgresql",
59-
"POSTGRES_INITDB_ARGS=--lc-ctype=C.UTF-8",
6059
"JWT_SECRET=" + utils.Config.Auth.JwtSecret,
6160
fmt.Sprintf("JWT_EXP=%d", utils.Config.Auth.JwtExpiry),
6261
}
@@ -81,13 +80,18 @@ func NewContainerConfig() container.Config {
8180
Timeout: 2 * time.Second,
8281
Retries: 3,
8382
},
84-
Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/postgresql.schema.sql && cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && docker-entrypoint.sh postgres -D /etc/postgresql
83+
Entrypoint: []string{"sh", "-c", `
84+
cat <<'EOF' > /etc/postgresql.schema.sql && \
85+
cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \
86+
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
87+
docker-entrypoint.sh postgres -D /etc/postgresql
8588
` + initialSchema + `
8689
` + _supabaseSchema + `
8790
EOF
8891
` + utils.Config.Db.RootKey + `
8992
EOF
90-
`},
93+
` + utils.Config.Db.Settings.ToPostgresConfig() + `
94+
EOF`},
9195
}
9296
if utils.Config.Db.MajorVersion >= 14 {
9397
config.Cmd = []string{"postgres",
@@ -124,11 +128,13 @@ func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
124128
}
125129
if utils.Config.Db.MajorVersion <= 14 {
126130
config.Entrypoint = []string{"sh", "-c", `
127-
cat <<'EOF' > /docker-entrypoint-initdb.d/supabase_schema.sql
131+
cat <<'EOF' > /docker-entrypoint-initdb.d/supabase_schema.sql && \
132+
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
133+
docker-entrypoint.sh postgres -D /etc/postgresql
128134
` + _supabaseSchema + `
129135
EOF
130-
docker-entrypoint.sh postgres -D /etc/postgresql
131-
`}
136+
` + utils.Config.Db.Settings.ToPostgresConfig() + `
137+
EOF`}
132138
hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""}
133139
}
134140
// Creating volume will not override existing volume, so we must inspect explicitly

internal/db/start/start_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/supabase/cli/internal/testing/apitest"
1818
"github.com/supabase/cli/internal/testing/fstest"
1919
"github.com/supabase/cli/internal/utils"
20+
"github.com/supabase/cli/pkg/cast"
2021
"github.com/supabase/cli/pkg/pgtest"
2122
)
2223

@@ -308,3 +309,55 @@ func TestSetupDatabase(t *testing.T) {
308309
assert.Empty(t, apitest.ListUnmatchedRequests())
309310
})
310311
}
312+
func TestStartDatabaseWithCustomSettings(t *testing.T) {
313+
t.Run("starts database with custom MaxConnections", func(t *testing.T) {
314+
// Setup
315+
utils.Config.Db.MajorVersion = 15
316+
utils.DbId = "supabase_db_test"
317+
utils.ConfigId = "supabase_config_test"
318+
utils.Config.Db.Port = 5432
319+
utils.Config.Db.Settings.MaxConnections = cast.Ptr(uint(50))
320+
321+
// Setup in-memory fs
322+
fsys := afero.NewMemMapFs()
323+
324+
// Setup mock docker
325+
require.NoError(t, apitest.MockDocker(utils.Docker))
326+
defer gock.OffAll()
327+
gock.New(utils.Docker.DaemonHost()).
328+
Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
329+
Reply(http.StatusNotFound).
330+
JSON(volume.Volume{})
331+
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), utils.DbId)
332+
gock.New(utils.Docker.DaemonHost()).
333+
Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
334+
Reply(http.StatusOK).
335+
JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
336+
State: &types.ContainerState{
337+
Running: true,
338+
Health: &types.Health{Status: types.Healthy},
339+
},
340+
}})
341+
342+
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime")
343+
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", ""))
344+
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage")
345+
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", ""))
346+
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth")
347+
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", ""))
348+
// Setup mock postgres
349+
conn := pgtest.NewConn()
350+
defer conn.Close(t)
351+
352+
// Run test
353+
err := StartDatabase(context.Background(), fsys, io.Discard, conn.Intercept)
354+
355+
// Check error
356+
assert.NoError(t, err)
357+
assert.Empty(t, apitest.ListUnmatchedRequests())
358+
359+
// Check if the custom MaxConnections setting was applied
360+
config := NewContainerConfig()
361+
assert.Contains(t, config.Entrypoint[2], "max_connections = 50")
362+
})
363+
}

pkg/config/db.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package config
22

33
import (
4+
"bytes"
5+
46
"github.com/google/go-cmp/cmp"
57
v1API "github.com/supabase/cli/pkg/api"
68
"github.com/supabase/cli/pkg/cast"
@@ -146,6 +148,17 @@ func (a *settings) fromRemoteConfig(remoteConfig v1API.PostgresConfigResponse) s
146148
return result
147149
}
148150

151+
const pgConfHeader = "\n# supabase [db.settings] configuration\n"
152+
153+
// create a valid string to append to /etc/postgresql/postgresql.conf
154+
func (a *settings) ToPostgresConfig() string {
155+
// Assuming postgres settings is always a flat struct, we can serialise
156+
// using toml, then replace double quotes with single.
157+
data, _ := ToTomlBytes(*a)
158+
body := bytes.ReplaceAll(data, []byte{'"'}, []byte{'\''})
159+
return pgConfHeader + string(body)
160+
}
161+
149162
func (a *settings) DiffWithRemote(remoteConfig v1API.PostgresConfigResponse) ([]byte, error) {
150163
// Convert the config values into easily comparable remoteConfig values
151164
currentValue, err := ToTomlBytes(a)

pkg/config/db_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,41 @@ func TestDbSettingsDiffWithRemote(t *testing.T) {
153153
assert.Contains(t, string(diff), "-shared_buffers = \"1GB\"")
154154
})
155155
}
156+
157+
func TestSettingsToPostgresConfig(t *testing.T) {
158+
t.Run("Only set values should appear", func(t *testing.T) {
159+
settings := settings{
160+
MaxConnections: cast.Ptr(uint(100)),
161+
MaxLocksPerTransaction: cast.Ptr(uint(64)),
162+
SharedBuffers: cast.Ptr("128MB"),
163+
WorkMem: cast.Ptr("4MB"),
164+
}
165+
got := settings.ToPostgresConfig()
166+
167+
assert.Contains(t, got, "max_connections = 100")
168+
assert.Contains(t, got, "max_locks_per_transaction = 64")
169+
assert.Contains(t, got, "shared_buffers = '128MB'")
170+
assert.Contains(t, got, "work_mem = '4MB'")
171+
172+
assert.NotContains(t, got, "effective_cache_size")
173+
assert.NotContains(t, got, "maintenance_work_mem")
174+
assert.NotContains(t, got, "max_parallel_workers")
175+
})
176+
177+
t.Run("SessionReplicationRole should be handled correctly", func(t *testing.T) {
178+
settings := settings{
179+
SessionReplicationRole: cast.Ptr(SessionReplicationRoleOrigin),
180+
}
181+
got := settings.ToPostgresConfig()
182+
183+
assert.Contains(t, got, "session_replication_role = 'origin'")
184+
})
185+
186+
t.Run("Empty settings should result in empty string", func(t *testing.T) {
187+
settings := settings{}
188+
got := settings.ToPostgresConfig()
189+
190+
assert.Equal(t, got, "\n# supabase [db.settings] configuration\n")
191+
assert.NotContains(t, got, "=")
192+
})
193+
}

0 commit comments

Comments
 (0)