From caeac55dcb6943f18bd8498b9ce9aa0790ea7f1d Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Apr 2026 18:25:53 +0200 Subject: [PATCH 1/2] feat(declarative): add tests for skipping config updates when PgDelta is enabled - These tests verify that the configuration remains unchanged when PgDelta is enabled, ensuring the declarative directory is the source of truth. - Updated the WriteDeclarativeSchemas function to reflect the new behavior regarding PgDelta configuration. --- internal/db/declarative/declarative.go | 7 ++- internal/db/declarative/declarative_test.go | 60 +++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/internal/db/declarative/declarative.go b/internal/db/declarative/declarative.go index 2a0454d01d..f7c8ca0021 100644 --- a/internal/db/declarative/declarative.go +++ b/internal/db/declarative/declarative.go @@ -235,10 +235,9 @@ func WriteDeclarativeSchemas(output diff.DeclarativeOutput, fsys afero.Fs) error return err } } - // When pg-delta has its own config section, the declarative path is the single - // source of truth there; do not overwrite [db.migrations] schema_paths. - if utils.IsPgDeltaEnabled() && utils.Config.Experimental.PgDelta != nil && - len(utils.Config.Experimental.PgDelta.DeclarativeSchemaPath) > 0 { + // When pg-delta is enabled, the declarative directory (default or configured) + // is the source of truth; do not overwrite [db.migrations] schema_paths. + if utils.IsPgDeltaEnabled() { return nil } utils.Config.Db.Migrations.SchemaPaths = []string{ diff --git a/internal/db/declarative/declarative_test.go b/internal/db/declarative/declarative_test.go index 73b6f473aa..229e6ffe79 100644 --- a/internal/db/declarative/declarative_test.go +++ b/internal/db/declarative/declarative_test.go @@ -48,6 +48,34 @@ func TestWriteDeclarativeSchemas(t *testing.T) { assert.Contains(t, string(cfg), `"database"`) } +func TestWriteDeclarativeSchemasSkipsConfigUpdateWhenPgDeltaEnabled(t *testing.T) { + fsys := afero.NewMemMapFs() + originalConfig := "[db]\n" + require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte(originalConfig), 0644)) + original := utils.Config.Experimental.PgDelta + utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{Enabled: true} + t.Cleanup(func() { + utils.Config.Experimental.PgDelta = original + }) + + output := diff.DeclarativeOutput{ + Files: []diff.DeclarativeFile{ + {Path: "schemas/public/tables/users.sql", SQL: "create table users(id bigint);"}, + }, + } + + err := WriteDeclarativeSchemas(output, fsys) + require.NoError(t, err) + + users, err := afero.ReadFile(fsys, filepath.Join(utils.DeclarativeDir, "schemas", "public", "tables", "users.sql")) + require.NoError(t, err) + assert.Equal(t, "create table users(id bigint);", string(users)) + + cfg, err := afero.ReadFile(fsys, utils.ConfigPath) + require.NoError(t, err) + assert.Equal(t, originalConfig, string(cfg)) +} + func TestTryCacheMigrationsCatalogWritesPrefixedCache(t *testing.T) { fsys := afero.NewMemMapFs() original := utils.Config.Experimental.PgDelta @@ -146,6 +174,38 @@ func TestWriteDeclarativeSchemasUsesConfiguredDir(t *testing.T) { assert.Contains(t, string(cfg), `db/decl`) } +func TestWriteDeclarativeSchemasSkipsConfigUpdateForPgDeltaCustomDir(t *testing.T) { + fsys := afero.NewMemMapFs() + originalConfig := "[db]\n" + require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte(originalConfig), 0644)) + original := utils.Config.Experimental.PgDelta + utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{ + Enabled: true, + DeclarativeSchemaPath: filepath.Join(utils.SupabaseDirPath, "db", "decl"), + } + t.Cleanup(func() { + utils.Config.Experimental.PgDelta = original + }) + + output := diff.DeclarativeOutput{ + Files: []diff.DeclarativeFile{ + {Path: "cluster/roles.sql", SQL: "create role app;"}, + }, + } + + err := WriteDeclarativeSchemas(output, fsys) + require.NoError(t, err) + + rolesPath := filepath.Join(utils.SupabaseDirPath, "db", "decl", "cluster", "roles.sql") + roles, err := afero.ReadFile(fsys, rolesPath) + require.NoError(t, err) + assert.Equal(t, "create role app;", string(roles)) + + cfg, err := afero.ReadFile(fsys, utils.ConfigPath) + require.NoError(t, err) + assert.Equal(t, originalConfig, string(cfg)) +} + func TestWriteDeclarativeSchemasRejectsUnsafePath(t *testing.T) { // Export paths must stay within supabase/declarative to prevent traversal. fsys := afero.NewMemMapFs() From 40238cd208654e4f1ce55f6a9ac0c4e3f9389f33 Mon Sep 17 00:00:00 2001 From: avallete Date: Mon, 13 Apr 2026 20:12:16 +0200 Subject: [PATCH 2/2] fix(declarative): DSL change due to upgrade --- internal/db/diff/templates/pgdelta.ts | 9 ++++++++- .../templates/pgdelta_declarative_export.ts | 17 ++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/internal/db/diff/templates/pgdelta.ts b/internal/db/diff/templates/pgdelta.ts index 37995c491c..234c91ab06 100644 --- a/internal/db/diff/templates/pgdelta.ts +++ b/internal/db/diff/templates/pgdelta.ts @@ -21,7 +21,14 @@ const target = Deno.env.get("TARGET"); const includedSchemas = Deno.env.get("INCLUDED_SCHEMAS"); if (includedSchemas) { - supabase.filter = { schema: includedSchemas.split(",") }; + const schemas = includedSchemas.split(","); + const schemaFilter = { + or: [{ "*/schema": schemas }, { "schema/name": schemas }], + }; + // CompositionPattern `and` is valid FilterDSL; Deno's structural typing is strict on `or` branches. + supabase.filter = { + and: [supabase.filter!, schemaFilter], + } as typeof supabase.filter; } const formatOptionsRaw = Deno.env.get("FORMAT_OPTIONS"); diff --git a/internal/db/diff/templates/pgdelta_declarative_export.ts b/internal/db/diff/templates/pgdelta_declarative_export.ts index cdb59924f2..dead372a70 100644 --- a/internal/db/diff/templates/pgdelta_declarative_export.ts +++ b/internal/db/diff/templates/pgdelta_declarative_export.ts @@ -22,20 +22,23 @@ async function resolveInput(ref: string | undefined) { const source = Deno.env.get("SOURCE"); const target = Deno.env.get("TARGET"); supabase.filter = { - // Also allow dropped extensions from migrations to be capted in the declarative schema export + // Also allow dropped extensions from migrations to be captured in the declarative schema export // TODO: fix upstream bug into pgdelta supabase integration or: [ - ...supabase.filter.or, - { type: "extension", operation: "drop", scope: "object" }, + ...supabase.filter!.or!, + { objectType: "extension", operation: "drop", scope: "object" }, ], }; const includedSchemas = Deno.env.get("INCLUDED_SCHEMAS"); if (includedSchemas) { - const schemaFilter = { schema: includedSchemas.split(",") }; - supabase.filter = supabase.filter - ? { and: [supabase.filter, schemaFilter] } - : schemaFilter; + const schemas = includedSchemas.split(","); + const schemaFilter = { + or: [{ "*/schema": schemas }, { "schema/name": schemas }], + }; + supabase.filter = { + and: [supabase.filter!, schemaFilter], + } as unknown as typeof supabase.filter; } const formatOptionsRaw = Deno.env.get("FORMAT_OPTIONS");