From 2a4b558fa6461822e9efdf00bc8b2b1f73700a5c Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 30 Apr 2026 16:25:32 +0200 Subject: [PATCH] feat(pg-delta): enhance pg-delta version handling and script interpolation - Updated the configuration to read the pg-delta npm version from the `.temp/pgdelta-version` file, defaulting to "1.0.0-alpha.22" if the file is missing or empty. - Implemented the `InterpolatePgDeltaScript` function to replace version placeholders in embedded TypeScript scripts with the effective pg-delta npm version. - Modified various functions to utilize the new script interpolation method for executing pg-delta related scripts, ensuring consistent version usage across the application. - Added tests to verify the correct behavior of the new version handling and interpolation logic. This change improves the management of pg-delta versions and enhances the flexibility of script execution. --- cmd/db_schema_declarative.go | 7 ++++- internal/db/diff/pgdelta.go | 10 ++++--- internal/db/pgcache/cache.go | 4 ++- internal/pgdelta/apply.go | 4 ++- internal/utils/misc.go | 1 + pkg/config/config.go | 12 +++++++++ pkg/config/config_test.go | 51 +++++++++++++++++++++++++++++++++++ pkg/config/pgdelta_version.go | 28 +++++++++++++++++++ pkg/config/utils.go | 2 ++ 9 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 pkg/config/pgdelta_version.go diff --git a/cmd/db_schema_declarative.go b/cmd/db_schema_declarative.go index 8cb98b9912..896b7bd582 100644 --- a/cmd/db_schema_declarative.go +++ b/cmd/db_schema_declarative.go @@ -52,7 +52,12 @@ var ( // If the user has passed the --experimental flag and pg-delta is not enabled, enable it // so in the rest of the code we can know that we're running pg-delta logic. if viper.GetBool("EXPERIMENTAL") && !utils.IsPgDeltaEnabled() { - utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{Enabled: true} + if utils.Config.Experimental.PgDelta == nil { + utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{Enabled: true} + } else { + // We preserve the version set into `.temp/pgdelta-version` by just enabling pg-delta. + utils.Config.Experimental.PgDelta.Enabled = true + } } if !utils.IsPgDeltaEnabled() { utils.CmdSuggestion = fmt.Sprintf("Either pass %s or add %s with %s to %s", diff --git a/internal/db/diff/pgdelta.go b/internal/db/diff/pgdelta.go index 8ba8fb0fe9..dd6af84924 100644 --- a/internal/db/diff/pgdelta.go +++ b/internal/db/diff/pgdelta.go @@ -14,6 +14,7 @@ import ( "github.com/jackc/pgx/v4" "github.com/supabase/cli/internal/gen/types" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/pkg/config" ) //go:embed templates/pgdelta.ts @@ -104,7 +105,8 @@ func DiffPgDeltaRef(ctx context.Context, sourceRef, targetRef string, schema []s binds = append(binds, cwd+":/workspace") } var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaScript, binds, "error diffing schema", &stdout, &stderr); err != nil { + script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaScript) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error diffing schema", &stdout, &stderr); err != nil { return "", err } return stdout.String(), nil @@ -143,7 +145,8 @@ func DeclarativeExportPgDeltaRef(ctx context.Context, sourceRef, targetRef strin binds = append(binds, cwd+":/workspace") } var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaDeclarativeExportScript, binds, "error exporting declarative schema", &stdout, &stderr); err != nil { + script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaDeclarativeExportScript) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error exporting declarative schema", &stdout, &stderr); err != nil { return DeclarativeOutput{}, err } if stdout.Len() == 0 { @@ -179,7 +182,8 @@ func ExportCatalogPgDelta(ctx context.Context, targetRef, role string, options . binds = append(binds, cwd+":/workspace") } var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaCatalogExportScript, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { + script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaCatalogExportScript) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { return "", err } snapshot := strings.TrimSpace(stdout.String()) diff --git a/internal/db/pgcache/cache.go b/internal/db/pgcache/cache.go index c6881c84cc..aeb1ebfbce 100644 --- a/internal/db/pgcache/cache.go +++ b/internal/db/pgcache/cache.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/viper" "github.com/supabase/cli/internal/gen/types" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/pkg/config" "github.com/supabase/cli/pkg/migration" ) @@ -253,7 +254,8 @@ func exportCatalog(ctx context.Context, targetRef string, options ...func(*pgx.C } binds := []string{utils.EdgeRuntimeId + ":/root/.cache/deno:rw"} var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaCatalogExportTS, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { + script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaCatalogExportTS) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { return "", err } snapshot := strings.TrimSpace(stdout.String()) diff --git a/internal/pgdelta/apply.go b/internal/pgdelta/apply.go index 2c57eb506b..f9009a3202 100644 --- a/internal/pgdelta/apply.go +++ b/internal/pgdelta/apply.go @@ -15,6 +15,7 @@ import ( "github.com/spf13/afero" "github.com/spf13/viper" "github.com/supabase/cli/internal/utils" + pkgconfig "github.com/supabase/cli/pkg/config" ) //go:embed templates/pgdelta_declarative_apply.ts @@ -321,7 +322,8 @@ func ApplyDeclarative(ctx context.Context, config pgconn.Config, fsys afero.Fs) fmt.Fprintln(os.Stderr, "Applying declarative schemas via pg-delta...") var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaDeclarativeApplyScript, binds, "error running pg-delta script", &stdout, &stderr); err != nil { + script := pkgconfig.InterpolatePgDeltaScript(pkgconfig.Config(&utils.Config), pgDeltaDeclarativeApplyScript) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error running pg-delta script", &stdout, &stderr); err != nil { return err } diff --git a/internal/utils/misc.go b/internal/utils/misc.go index 149dfee320..2c877079df 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -76,6 +76,7 @@ var ( PgmetaVersionPath = filepath.Join(TempDir, "pgmeta-version") PoolerVersionPath = filepath.Join(TempDir, "pooler-version") RealtimeVersionPath = filepath.Join(TempDir, "realtime-version") + PgDeltaVersionPath = filepath.Join(TempDir, "pgdelta-version") CliVersionPath = filepath.Join(TempDir, "cli-latest") CurrBranchPath = filepath.Join(SupabaseDirPath, ".branches", "_current_branch") // DeclarativeDir is the canonical location for pg-delta declarative schema diff --git a/pkg/config/config.go b/pkg/config/config.go index 8e5a4b943f..e2697ad825 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -229,6 +229,8 @@ type ( Enabled bool `toml:"enabled" json:"enabled"` DeclarativeSchemaPath string `toml:"declarative_schema_path" json:"declarative_schema_path"` FormatOptions string `toml:"format_options" json:"format_options"` + // NpmVersion is set from .temp/pgdelta-version during Load (not from TOML). + NpmVersion string `toml:"-" json:"-"` } inspect struct { @@ -690,6 +692,16 @@ func (c *config) Load(path string, fsys fs.FS, overrides ...ConfigEditor) error if version, err := fs.ReadFile(fsys, builder.LogflareVersionPath); err == nil && len(version) > 0 { c.Analytics.Image = replaceImageTag(Images.Logflare, string(version)) } + v := DefaultPgDeltaNpmVersion + if version, err := fs.ReadFile(fsys, builder.PgDeltaVersionPath); err == nil { + if trimmed := strings.TrimSpace(string(version)); len(trimmed) > 0 { + v = trimmed + } + } + if c.Experimental.PgDelta == nil { + c.Experimental.PgDelta = &PgDeltaConfig{} + } + c.Experimental.PgDelta.NpmVersion = v // TODO: replace derived config resolution with viper decode hooks if err := c.resolve(builder, fsys); err != nil { return err diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index abd581d11e..d7bca3948d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -243,6 +243,57 @@ format_options = "not-json" }) } +func TestPgDeltaNpmVersionPinning(t *testing.T) { + t.Run("defaults when pgdelta-version file missing", func(t *testing.T) { + c := NewConfig() + require.NoError(t, c.Load("", fs.MapFS{})) + require.NotNil(t, c.Experimental.PgDelta) + assert.Equal(t, DefaultPgDeltaNpmVersion, c.Experimental.PgDelta.NpmVersion) + assert.Equal(t, DefaultPgDeltaNpmVersion, EffectivePgDeltaNpmVersion(Config(&c))) + }) + + t.Run("EffectivePgDeltaNpmVersion nil config uses default", func(t *testing.T) { + assert.Equal(t, DefaultPgDeltaNpmVersion, EffectivePgDeltaNpmVersion(nil)) + }) + + t.Run("reads trimmed version from supabase/.temp/pgdelta-version", func(t *testing.T) { + c := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[experimental.pgdelta] +enabled = true +`)}, + "supabase/.temp/pgdelta-version": &fs.MapFile{Data: []byte(" 9.9.9-test \n")}, + } + require.NoError(t, c.Load("", fsys)) + require.NotNil(t, c.Experimental.PgDelta) + assert.Equal(t, "9.9.9-test", c.Experimental.PgDelta.NpmVersion) + assert.Equal(t, "9.9.9-test", EffectivePgDeltaNpmVersion(Config(&c))) + }) + + t.Run("whitespace-only pgdelta-version keeps default", func(t *testing.T) { + c := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[experimental.pgdelta] +enabled = true +`)}, + "supabase/.temp/pgdelta-version": &fs.MapFile{Data: []byte(" \n")}, + } + require.NoError(t, c.Load("", fsys)) + require.NotNil(t, c.Experimental.PgDelta) + assert.Equal(t, DefaultPgDeltaNpmVersion, c.Experimental.PgDelta.NpmVersion) + }) + + t.Run("InterpolatePgDeltaScript substitutes placeholder", func(t *testing.T) { + c := NewConfig() + require.NoError(t, c.Load("", fs.MapFS{})) + // Embedded TS pins use this semver literal before InterpolatePgDeltaScript runs. + got := InterpolatePgDeltaScript(Config(&c), `from "npm:@supabase/pg-delta@1.0.0-alpha.20";`) + assert.Equal(t, `from "npm:@supabase/pg-delta@`+DefaultPgDeltaNpmVersion+`";`, got) + }) +} + func TestRemoteOverride(t *testing.T) { t.Run("load staging override", func(t *testing.T) { config := NewConfig() diff --git a/pkg/config/pgdelta_version.go b/pkg/config/pgdelta_version.go new file mode 100644 index 0000000000..297a4c34b1 --- /dev/null +++ b/pkg/config/pgdelta_version.go @@ -0,0 +1,28 @@ +package config + +import "strings" + +// DefaultPgDeltaNpmVersion is the npm dist-tag/version used for @supabase/pg-delta +// when supabase/.temp/pgdelta-version is absent or empty. +const DefaultPgDeltaNpmVersion = "1.0.0-alpha.22" + +const pgDeltaNpmVersionPlaceholder = "1.0.0-alpha.20" + +// EffectivePgDeltaNpmVersion returns the pg-delta npm version from loaded config, +// or DefaultPgDeltaNpmVersion when unset (e.g. before Load or empty field). +func EffectivePgDeltaNpmVersion(c Config) string { + if c == nil { + return DefaultPgDeltaNpmVersion + } + if c.Experimental.PgDelta != nil { + if v := strings.TrimSpace(c.Experimental.PgDelta.NpmVersion); v != "" { + return v + } + } + return DefaultPgDeltaNpmVersion +} + +// InterpolatePgDeltaScript substitutes pg delta npm version placeholders in embedded TS. +func InterpolatePgDeltaScript(c Config, script string) string { + return strings.ReplaceAll(script, pgDeltaNpmVersionPlaceholder, EffectivePgDeltaNpmVersion(c)) +} diff --git a/pkg/config/utils.go b/pkg/config/utils.go index 2bb6db6f4b..4c004d4eeb 100644 --- a/pkg/config/utils.go +++ b/pkg/config/utils.go @@ -28,6 +28,7 @@ type pathBuilder struct { RealtimeVersionPath string EdgeRuntimeVersionPath string LogflareVersionPath string + PgDeltaVersionPath string CliVersionPath string CurrBranchPath string SchemasDir string @@ -64,6 +65,7 @@ func NewPathBuilder(configPath string) pathBuilder { PoolerVersionPath: filepath.Join(base, ".temp", "pooler-version"), RealtimeVersionPath: filepath.Join(base, ".temp", "realtime-version"), LogflareVersionPath: filepath.Join(base, ".temp", "logflare-version"), + PgDeltaVersionPath: filepath.Join(base, ".temp", "pgdelta-version"), CliVersionPath: filepath.Join(base, ".temp", "cli-latest"), CurrBranchPath: filepath.Join(base, ".branches", "_current_branch"), SchemasDir: filepath.Join(base, "schemas"),