Skip to content

Commit 0e06255

Browse files
avalleteclaude
andauthored
feat(cli): set up platform baseline before declarative apply (#5515)
Refs CLI-1601 — https://linear.app/supabase/issue/CLI-1601/support-auth-dependencies-in-shadow-db-migrations Provision the Supabase platform schema (auth, storage, realtime, etc.) on shadow databases before applying declarative schemas, ensuring Supabase-managed dependencies resolve correctly during both declarative apply and cache warmup. ## Changes - **New `SetupShadowDatabase` function** in `diff.go`: Provisions the platform baseline on a freshly created shadow database without applying user migrations. This allows declarative apply to share the same starting point as migration-based workflows. - **Refactored shadow setup logic**: Extracted common platform baseline setup into `setupShadowConn` helper, used by both `SetupShadowDatabase` and `MigrateShadowDatabase` to avoid duplication. - **Updated declarative apply flow**: - `Generate` now calls `setupShadowDatabase` when reusing the baseline shadow for cache warmup - `getDeclarativeCatalogRef` calls `setupShadowDatabase` before applying declarative schemas - This ensures platform objects (auth.sessions, auth.jwt(), etc.) are available during schema application and cancel out of diffs - **Added tests**: `TestSetupShadowDatabase` validates that the function sets up the platform baseline without applying migrations, and the `Generate` reuse test asserts the baseline is set up before declarative apply. ## Note on baseline caching A persistent "replayable SQL" baseline cache (so services don't boot on cold runs) was considered and intentionally not pursued — the `supabase/postgres` image pre-bakes part of the `auth` schema (overlap on replay), a full `pg_dumpall` replay is fragile (extensions/`shared_preload_libraries`/pgsodium/roles), and a pg-delta-derived baseline currently drops grants. See CLI-1601 for the full rationale. https://claude.ai/code/session_01ACrX8NXcsYLENnCRXAub3S Co-authored-by: Claude <noreply@anthropic.com>
1 parent 27b6af1 commit 0e06255

4 files changed

Lines changed: 105 additions & 5 deletions

File tree

apps/cli-go/internal/db/declarative/declarative.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ var (
5858
exportCatalog = diff.ExportCatalogPgDelta
5959
applyDeclarative = pgdelta.ApplyDeclarative
6060
declarativeExportRef = diff.DeclarativeExportPgDeltaRef
61+
// setupShadowDatabase provisions the Supabase platform baseline (auth/storage/
62+
// realtime) on a shadow database before declarative schemas are applied, so
63+
// Supabase-managed dependencies (auth.sessions, auth.jwt(), ...) resolve. It is
64+
// a package var so tests can inject a no-op without a real shadow database.
65+
setupShadowDatabase = diff.SetupShadowDatabase
6166
// generateBaselineCatalogRefResolver allows Generate to reuse a freshly
6267
// provisioned baseline shadow for declarative cache warmup.
6368
generateBaselineCatalogRefResolver = getGenerateBaselineCatalogRef
@@ -118,6 +123,13 @@ func Generate(ctx context.Context, schema []string, config pgconn.Config, overwr
118123
// can reuse it without provisioning another shadow database.
119124
if !noCache {
120125
if baseline.shadow != nil {
126+
// The baseline catalog was already exported from the empty image
127+
// baseline above. Set up the platform baseline on the reused shadow
128+
// before applying declarative schemas so Supabase-managed dependencies
129+
// (auth.sessions, auth.jwt(), ...) resolve during cache warmup.
130+
if err := setupShadowDatabase(ctx, baseline.shadow.container, fsys, options...); err != nil {
131+
return err
132+
}
121133
hash, err := hashDeclarativeSchemas(fsys)
122134
if err != nil {
123135
return err
@@ -392,6 +404,14 @@ func getDeclarativeCatalogRef(ctx context.Context, noCache bool, fsys afero.Fs,
392404
return "", err
393405
}
394406
defer utils.DockerRemove(shadow)
407+
// Apply the Supabase platform baseline (auth/storage/realtime) before applying
408+
// declarative schemas so dependencies on Supabase-managed objects (auth.sessions,
409+
// auth.jwt(), ...) resolve. This keeps the declarative shadow in parity with the
410+
// migrations shadow (diff.MigrateShadowDatabase), so platform objects cancel out
411+
// of the diff instead of surfacing as spurious changes or "stuck" applies.
412+
if err := setupShadowDatabase(ctx, shadow, fsys, options...); err != nil {
413+
return "", err
414+
}
395415
return writeDeclarativeCatalogFromConfig(ctx, config, hash, prefix, noCache, fsys, options...)
396416
}
397417

apps/cli-go/internal/db/declarative/declarative_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,16 +436,19 @@ func TestGenerateReusesBaselineShadowForDeclarativeWarmup(t *testing.T) {
436436
originalResolver := declarativeCatalogRefResolver
437437
originalApplyDeclarative := applyDeclarative
438438
originalExportCatalog := exportCatalog
439+
originalSetupShadow := setupShadowDatabase
439440
t.Cleanup(func() {
440441
utils.Config.Experimental.PgDelta = originalPgDelta
441442
declarativeExportRef = originalExportRef
442443
generateBaselineCatalogRefResolver = originalBaselineResolver
443444
declarativeCatalogRefResolver = originalResolver
444445
applyDeclarative = originalApplyDeclarative
445446
exportCatalog = originalExportCatalog
447+
setupShadowDatabase = originalSetupShadow
446448
})
447449

448450
const baselinePath = ".temp/pgdelta/catalog-baseline-test.json"
451+
const shadowContainer = "test-shadow-container"
449452
shadowConfig := pgconn.Config{
450453
Host: "127.0.0.1",
451454
Port: 5432,
@@ -457,10 +460,17 @@ func TestGenerateReusesBaselineShadowForDeclarativeWarmup(t *testing.T) {
457460
return generateBaselineCatalogRef{
458461
ref: baselinePath,
459462
shadow: &shadowSession{
460-
config: shadowConfig,
463+
container: shadowContainer,
464+
config: shadowConfig,
461465
},
462466
}, nil
463467
}
468+
setupCalled := false
469+
setupShadowDatabase = func(_ context.Context, container string, _ afero.Fs, _ ...func(*pgx.ConnConfig)) error {
470+
setupCalled = true
471+
assert.Equal(t, shadowContainer, container)
472+
return nil
473+
}
464474
declarativeExportRef = func(_ context.Context, sourceRef, _ string, _ []string, _ string, _ ...func(*pgx.ConnConfig)) (diff.DeclarativeOutput, error) {
465475
assert.Equal(t, baselinePath, sourceRef)
466476
return diff.DeclarativeOutput{
@@ -488,6 +498,7 @@ func TestGenerateReusesBaselineShadowForDeclarativeWarmup(t *testing.T) {
488498

489499
err := Generate(t.Context(), nil, pgconn.Config{Host: "127.0.0.1", Port: 5432, User: "postgres", Password: "postgres", Database: "postgres"}, true, false, fsys)
490500
require.NoError(t, err)
501+
assert.True(t, setupCalled, "generate should set up the platform baseline on the reused shadow before applying declarative schema")
491502
assert.True(t, applyCalled, "generate should apply declarative schema using reused baseline shadow")
492503
assert.False(t, fallbackCalled, "fallback declarative resolver should not run when baseline shadow is reusable")
493504

apps/cli-go/internal/db/diff/diff.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,35 @@ func ConnectShadowDatabase(ctx context.Context, timeout time.Duration, options .
141141
// Required to bypass pg_cron check: https://github.com/citusdata/pg_cron/blob/main/pg_cron.sql#L3
142142
const CREATE_TEMPLATE = "CREATE DATABASE contrib_regression TEMPLATE postgres"
143143

144+
// setupShadowConn applies the Supabase platform schema (auth, storage, realtime,
145+
// etc.) to an already-connected shadow database and creates the pg_cron template
146+
// database. It deliberately stops short of applying user migrations so that
147+
// callers which only need the platform baseline (declarative apply) share the
148+
// exact same starting point as callers that also replay migrations.
149+
func setupShadowConn(ctx context.Context, conn *pgx.Conn, container string, fsys afero.Fs) error {
150+
if err := start.SetupDatabase(ctx, conn, container[:12], os.Stderr, fsys); err != nil {
151+
return err
152+
}
153+
if _, err := conn.Exec(ctx, CREATE_TEMPLATE); err != nil {
154+
return errors.Errorf("failed to create template database: %w", err)
155+
}
156+
return nil
157+
}
158+
159+
// SetupShadowDatabase provisions the Supabase platform baseline (service schemas
160+
// such as auth/storage/realtime) on a freshly created shadow database, without
161+
// applying user migrations. Declarative apply uses this so the shadow matches the
162+
// real database closely enough for Supabase-managed dependencies (auth.sessions,
163+
// auth.jwt(), ...) to resolve.
164+
func SetupShadowDatabase(ctx context.Context, container string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
165+
conn, err := ConnectShadowDatabase(ctx, 10*time.Second, options...)
166+
if err != nil {
167+
return err
168+
}
169+
defer conn.Close(context.Background())
170+
return setupShadowConn(ctx, conn, container, fsys)
171+
}
172+
144173
func MigrateShadowDatabase(ctx context.Context, container string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
145174
migrations, err := migration.ListLocalMigrations(utils.MigrationsDir, afero.NewIOFS(fsys))
146175
if err != nil {
@@ -151,12 +180,9 @@ func MigrateShadowDatabase(ctx context.Context, container string, fsys afero.Fs,
151180
return err
152181
}
153182
defer conn.Close(context.Background())
154-
if err := start.SetupDatabase(ctx, conn, container[:12], os.Stderr, fsys); err != nil {
183+
if err := setupShadowConn(ctx, conn, container, fsys); err != nil {
155184
return err
156185
}
157-
if _, err := conn.Exec(ctx, CREATE_TEMPLATE); err != nil {
158-
return errors.Errorf("failed to create template database: %w", err)
159-
}
160186
return migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys))
161187
}
162188

apps/cli-go/internal/db/diff/diff_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,49 @@ func TestMigrateShadow(t *testing.T) {
186186
})
187187
}
188188

189+
func TestSetupShadowDatabase(t *testing.T) {
190+
utils.Config.Db.MajorVersion = 14
191+
192+
t.Run("sets up platform baseline without applying migrations", func(t *testing.T) {
193+
utils.Config.Db.ShadowPort = 54320
194+
utils.GlobalsSql = "create schema public"
195+
utils.InitialSchemaPg14Sql = "create schema private"
196+
// A migration exists on disk, but SetupShadowDatabase must not apply it:
197+
// the mock below only scripts the platform setup + template, so any
198+
// migration-history query would surface as an unmatched request.
199+
fsys := afero.NewMemMapFs()
200+
path := filepath.Join(utils.MigrationsDir, "0_test.sql")
201+
require.NoError(t, afero.WriteFile(fsys, path, []byte("create schema test"), 0644))
202+
// Setup mock postgres
203+
conn := pgtest.NewConn()
204+
defer conn.Close(t)
205+
conn.Query(utils.GlobalsSql).
206+
Reply("CREATE SCHEMA").
207+
Query(utils.InitialSchemaPg14Sql).
208+
Reply("CREATE SCHEMA").
209+
Query(CREATE_TEMPLATE).
210+
Reply("CREATE DATABASE")
211+
// Run test
212+
err := SetupShadowDatabase(context.Background(), "test-shadow-db", fsys, conn.Intercept)
213+
// Check error
214+
assert.NoError(t, err)
215+
})
216+
217+
t.Run("throws error on globals schema", func(t *testing.T) {
218+
utils.Config.Db.ShadowPort = 54320
219+
utils.GlobalsSql = "create schema public"
220+
// Setup mock postgres
221+
conn := pgtest.NewConn()
222+
defer conn.Close(t)
223+
conn.Query(utils.GlobalsSql).
224+
ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`)
225+
// Run test
226+
err := SetupShadowDatabase(context.Background(), "test-shadow-db", afero.NewMemMapFs(), conn.Intercept)
227+
// Check error
228+
assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`)
229+
})
230+
}
231+
189232
func TestDiffDatabase(t *testing.T) {
190233
utils.Config.Db.MajorVersion = 14
191234
utils.Config.Db.ShadowPort = 54320

0 commit comments

Comments
 (0)