diff --git a/cmd/db_schema_declarative.go b/cmd/db_schema_declarative.go index 8cb98b991..896b7bd58 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 8ba8fb0fe..dd6af8492 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 c6881c84c..aeb1ebfbc 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 2c57eb506..f9009a320 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 9a1c420fc..7ba482d96 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -86,6 +86,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 8e5a4b943..e2697ad82 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 abd581d11..d7bca3948 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 000000000..297a4c34b --- /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 2bb6db6f4..4c004d4ee 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"),