Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b0a3cbc
feat(cli): remove the legacy edge-runtime pg-delta engine and its esc…
claude Aug 28, 2026
cd004ef
feat(cli): default the schema diff engine to pg-delta for all project…
claude Aug 28, 2026
9bac2a7
test(cli): cover the pg-delta default in the declarative gate tests (…
claude Aug 28, 2026
656fa8a
test(cli): cover the pg-delta default in the db pull tests (CLI-1588)
claude Aug 28, 2026
09f187d
test(cli): cover the pg-delta default in the db diff and db reset tes…
claude Aug 28, 2026
6b1d545
chore(cli): gofmt two stale files in apps/cli-go
claude Aug 28, 2026
2684446
test(cli): make the two chmod-based permission tests root-safe
claude Aug 28, 2026
2f97b70
chore(cli): oxfmt the db pull integration test
claude Aug 28, 2026
4234d36
fix(cli): drop the Go migrations-catalog warmup now that pg-delta def…
claude Aug 29, 2026
64831cd
refactor(cli): address review feedback on legacy-migration-list
claude Aug 29, 2026
1b603b7
fix(cli): make the pgdelta config rollback authoritative and fix engi…
claude Aug 29, 2026
5553191
chore(cli): drop leftover catalog-warmup wiring after the pg-delta de…
avallete Aug 31, 2026
d71f5dd
chore(cli): drop leftover --use-migra docs default and warmup comments
avallete Aug 31, 2026
7797c01
chore(cli): fix formatting after warmup-wiring cleanup
claude Aug 31, 2026
3e97502
Merge branch 'claude/pg-delta-default-engine-rfpryl' of https://githu…
claude Aug 31, 2026
a10eda1
chore(cli): stop advertising migra and opt-in gates as the published …
avallete Aug 31, 2026
09ebc69
chore(cli): fix formatting in pull handler
claude Aug 31, 2026
1e30d99
chore: sync API types from infrastructure
claude Aug 31, 2026
c66ff25
fix(cli): bump @supabase/pg-delta to 1.0.0-alpha.48
claude Aug 31, 2026
f7d2498
Merge remote-tracking branch 'origin/develop' into claude/pg-delta-de…
claude Aug 31, 2026
e80e88a
fix(cli): bump @supabase/pg-topo to 1.0.0-alpha.6
claude Aug 31, 2026
89db526
refactor(cli): drop as-casts from the migration lister
claude Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions apps/cli-go/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@ The Supabase API client is generated from OpenAPI spec. See [our guide](api/READ

## Testing local pg-delta builds

To exercise unpublished `@supabase/pg-delta` changes inside CLI edge-runtime scripts (`db pull`, `db diff`, `db push`, etc.), publish a local build via Verdaccio in [pg-toolbelt](https://github.com/supabase/pg-toolbelt) and point the CLI at that registry.
> **Scope:** this workflow only applies to the Go binary's own edge-runtime pg-delta
> path, which the TypeScript CLI still reaches through the delegated
> `db remote commit` command. The main TypeScript CLI
> bundles `@supabase/pg-delta` in-process and reads neither `PGDELTA_NPM_REGISTRY`
Comment thread
avallete marked this conversation as resolved.
> nor `supabase/.temp/pgdelta-version` — to test a local pg-delta build there,
> update the `@supabase/pg-delta` dependency pin in `apps/cli/package.json` /
> `pnpm-workspace.yaml` instead.

To exercise unpublished `@supabase/pg-delta` changes inside the Go binary's edge-runtime scripts, publish a local build via Verdaccio in [pg-toolbelt](https://github.com/supabase/pg-toolbelt) and point the Go binary at that registry.

### 1. Start Verdaccio (pg-toolbelt)

Expand Down Expand Up @@ -81,10 +89,12 @@ export PGDELTA_NPM_REGISTRY=http://host.docker.internal:4873
# or: export PGDELTA_NPM_REGISTRY=http://172.17.0.1:4873
```

Then run any pg-delta-backed command, for example:
Then run one of the delegated commands that still reach the Go binary's edge-runtime
pg-delta path (ordinary `db diff` / `db pull` run the TypeScript in-process engine and
ignore this registry), for example:

```sh
supabase db pull --db-url "$DATABASE_URL" --diff-engine pg-delta
supabase db remote commit
```

When set, the CLI injects a scoped `.npmrc` and forwards `NPM_CONFIG_REGISTRY` into the edge-runtime container (`PgDeltaNpmRegistryOption` in `internal/utils/pgdelta_local.go`).
Expand Down
16 changes: 10 additions & 6 deletions apps/cli-go/cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ var (
useDeclarative bool
pullDiffEngine = utils.EnumFlag{
Allowed: []string{"migra", "pg-delta"},
Value: "migra",
Value: "pg-delta",
}
diffFrom string
diffTo string
Expand Down Expand Up @@ -175,15 +175,19 @@ var (
}
)

// pg-delta is the default engine; an explicit `[experimental.pgdelta] enabled = false`
// is the rollback, overridable per run by --use-pg-delta. The historical
// SUPABASE_EXPERIMENTAL_PG_DELTA opt-in env var is no longer consulted so the
// config rollback stays authoritative.
func shouldUsePgDelta() bool {
return utils.IsPgDeltaEnabled() || usePgDelta || viper.GetBool("EXPERIMENTAL_PG_DELTA")
return utils.IsPgDeltaEnabled() || usePgDelta
}

// resolveDiffEngine reports whether `db diff` should run in pg-delta mode. The config /
// env default (pgDeltaDefault) applies unless an explicit non-pg-delta engine is selected:
// --use-migra, --use-pgadmin, or --use-pg-schema is an authoritative rollback that clears
// pg-delta mode so diff.Run skips pg-delta-specific declarative shadow setup and the
// PGDELTA_DEBUG capture path. --use-migra defaults to true, so only an explicit pass
// PGDELTA_DEBUG capture path. --use-migra is off unless passed, so only an explicit pass
// (useMigraChanged) counts as opting out.
func resolveDiffEngine(useMigraChanged, usePgAdmin, usePgSchema, pgDeltaDefault bool) bool {
if useMigraChanged || usePgAdmin || usePgSchema {
Expand All @@ -195,7 +199,7 @@ func resolveDiffEngine(useMigraChanged, usePgAdmin, usePgSchema, pgDeltaDefault
// resolvePullDiffEngine selects whether migration-style db pull uses pg-delta for the
// shadow diff step. An explicit --diff-engine flag always wins, so --diff-engine migra is
// an authoritative rollback even when pg-delta is enabled in config; otherwise the default
// follows whether pg-delta is the active engine (config / env).
// follows whether pg-delta is the active engine.
func resolvePullDiffEngine(engineFlagChanged bool, engine string, pgDeltaDefault bool) bool {
if engineFlagChanged {
return engine == "pg-delta"
Expand All @@ -212,7 +216,7 @@ func init() {
dbCmd.AddCommand(dbBranchCmd)
// Build diff command
diffFlags := dbDiffCmd.Flags()
diffFlags.BoolVar(&useMigra, "use-migra", true, "Use migra to generate schema diff.")
diffFlags.BoolVar(&useMigra, "use-migra", false, "Use migra to generate schema diff.")
diffFlags.BoolVar(&usePgAdmin, "use-pgadmin", false, "Use pgAdmin to generate schema diff.")
diffFlags.BoolVar(&usePgSchema, "use-pg-schema", false, "Use pg-schema-diff to generate schema diff.")
diffFlags.BoolVar(&usePgDelta, "use-pg-delta", false, "Use pg-delta to generate schema diff.")
Expand All @@ -233,7 +237,7 @@ func init() {
// schema files exported through pg-delta. --use-pg-delta is the deprecated alias.
pullFlags.BoolVar(&useDeclarative, "declarative", false, "Pull schema as declarative files using pg-delta instead of creating a migration.")
pullFlags.BoolVar(&useDeclarative, "use-pg-delta", false, "Use pg-delta to pull declarative schema.")
cobra.CheckErr(pullFlags.MarkDeprecated("use-pg-delta", "use --declarative with [experimental.pgdelta] enabled = true in your config.toml instead."))
cobra.CheckErr(pullFlags.MarkDeprecated("use-pg-delta", "use --declarative instead."))
pullFlags.Var(&pullDiffEngine, "diff-engine", "Diff engine to use for migration-style db pull.")
pullFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.")
pullFlags.String("db-url", "", "Pulls from the database specified by the connection string (must be percent-encoded).")
Expand Down
30 changes: 0 additions & 30 deletions apps/cli-go/internal/db/declarative/declarative.go
Original file line number Diff line number Diff line change
Expand Up @@ -789,36 +789,6 @@ func pgDeltaFormatOptions() string {
return strings.TrimSpace(utils.Config.Experimental.PgDelta.FormatOptions)
}

func TryCacheMigrationsCatalog(ctx context.Context, config pgconn.Config, prefix string, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
if !shouldCacheMigrationsCatalog() || len(version) > 0 {
return nil
}
if len(strings.TrimSpace(prefix)) == 0 {
prefix = catalogPrefixFromConfig(config)
}
hash, err := hashMigrations(fsys)
if err != nil {
return err
}
snapshot, err := exportCatalog(ctx, utils.ToPostgresURL(config), "postgres", options...)
if err != nil {
return err
}
if err := ensureTempDir(fsys); err != nil {
return err
}
_, err = pgcache.WriteMigrationCatalogSnapshot(fsys, prefix, hash, snapshot)
return err
}

func shouldCacheMigrationsCatalog() bool {
return pgcache.ShouldCacheMigrationsCatalog()
}

func catalogPrefixFromConfig(config pgconn.Config) string {
return pgcache.CatalogPrefixFromConfig(config)
}

// findDropStatements extracts DROP statements for safety warnings shown when
// generating migration output from declarative diffs.
func findDropStatements(out string) []string {
Expand Down
81 changes: 9 additions & 72 deletions apps/cli-go/internal/db/declarative/declarative_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package declarative

import (
"context"
"crypto/sha256"
"encoding/hex"
"path/filepath"
"strings"
"testing"
Expand All @@ -20,10 +18,17 @@ import (
)

func TestWriteDeclarativeSchemas(t *testing.T) {
// This verifies the main happy path for declarative export materialization:
// files are written to expected locations and config is updated accordingly.
// This verifies the main happy path for declarative export materialization
// with pg-delta explicitly disabled: files are written to expected locations
// and [db.migrations] schema_paths is updated accordingly. (With pg-delta
// enabled — the default — the config update is skipped; see the tests below.)
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("[db]\n"), 0644))
original := utils.Config.Experimental.PgDelta
utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{Enabled: false}
t.Cleanup(func() {
utils.Config.Experimental.PgDelta = original
})

output := diff.DeclarativeOutput{
Files: []diff.DeclarativeFile{
Expand Down Expand Up @@ -76,74 +81,6 @@ func TestWriteDeclarativeSchemasSkipsConfigUpdateWhenPgDeltaEnabled(t *testing.T
assert.Equal(t, originalConfig, string(cfg))
}

func TestTryCacheMigrationsCatalogWritesPrefixedCache(t *testing.T) {
fsys := afero.NewMemMapFs()
original := utils.Config.Experimental.PgDelta
utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{Enabled: true}
t.Cleanup(func() {
utils.Config.Experimental.PgDelta = original
exportCatalog = diff.ExportCatalogPgDelta
})
p := filepath.Join(utils.MigrationsDir, "20240101000000_first.sql")
require.NoError(t, afero.WriteFile(fsys, p, []byte("create table a();"), 0644))
exportCatalog = func(_ context.Context, targetRef, role string, _ ...func(*pgx.ConnConfig)) (string, error) {
assert.Equal(t, "postgres", role)
assert.Contains(t, targetRef, "db.test.supabase.co")
return `{"version":1}`, nil
}

err := TryCacheMigrationsCatalog(t.Context(), pgconn.Config{
Host: "db.test.supabase.co",
Port: 5432,
User: "postgres",
Password: "postgres",
Database: "postgres",
}, "remote-ref", "", fsys)
require.NoError(t, err)

hash, err := hashMigrations(fsys)
require.NoError(t, err)
cachePath, ok, err := pgcache.ResolveMigrationCatalogPath(fsys, hash, "remote-ref")
require.NoError(t, err)
require.True(t, ok)
cached, err := afero.ReadFile(fsys, cachePath)
require.NoError(t, err)
assert.JSONEq(t, `{"version":1}`, string(cached))
}

func TestTryCacheMigrationsCatalogSkipsPartialApply(t *testing.T) {
fsys := afero.NewMemMapFs()
original := utils.Config.Experimental.PgDelta
utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{Enabled: true}
called := false
t.Cleanup(func() {
utils.Config.Experimental.PgDelta = original
exportCatalog = diff.ExportCatalogPgDelta
})
exportCatalog = func(_ context.Context, _ string, _ string, _ ...func(*pgx.ConnConfig)) (string, error) {
called = true
return `{"version":1}`, nil
}

err := TryCacheMigrationsCatalog(t.Context(), pgconn.Config{
Host: "127.0.0.1", Port: 5432, User: "postgres", Password: "postgres", Database: "postgres",
}, "", "20240101000000", fsys)
require.NoError(t, err)
assert.False(t, called)
}

func TestCatalogPrefixFromConfig(t *testing.T) {
local := catalogPrefixFromConfig(pgconn.Config{Host: utils.Config.Hostname, Port: utils.Config.Db.Port})
assert.Equal(t, "local", local)

linked := catalogPrefixFromConfig(pgconn.Config{Host: "db.abcdefghijklmnopqrst.supabase.co", Port: 5432})
assert.Equal(t, "abcdefghijklmnopqrst", linked)

custom := catalogPrefixFromConfig(pgconn.Config{Host: "db.example.com", Port: 5432, Database: "postgres", User: "postgres"})
sum := sha256.Sum256([]byte("postgres@db.example.com:5432/postgres"))
assert.Equal(t, "url-"+hex.EncodeToString(sum[:])[:12], custom)
}

func TestWriteDeclarativeSchemasUsesConfiguredDir(t *testing.T) {
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("[db]\n"), 0644))
Expand Down
103 changes: 0 additions & 103 deletions apps/cli-go/internal/db/pgcache/cache.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
package pgcache

import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"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"
)

Expand All @@ -30,82 +21,10 @@ const (
migrationsCatalogName = "catalog-%s-migrations-%s-%d.json"
legacyMigrationsCatalogName = "catalog-%s-migrations-%s.json"
catalogRetentionCount = 2
pgDeltaCatalogExportTS = `// This script serializes a database catalog for caching/reuse in declarative
// pg-delta workflows. Uses the same API as pgdelta_catalog_export.ts (main package only, no /catalog subpath).
import {
createManagedPool,
extractCatalog,
serializeCatalog,
stringifyCatalogSnapshot,
} from "npm:@supabase/pg-delta@1.0.0-alpha.20";
const target = Deno.env.get("TARGET");
const role = Deno.env.get("ROLE") ?? undefined;
if (!target) {
console.error("TARGET is required");
throw new Error("");
}
const { pool, close } = await createManagedPool(target, { role });
try {
const catalog = await extractCatalog(pool);
console.log(stringifyCatalogSnapshot(serializeCatalog(catalog)));
} catch (e) {
console.error(e);
// Force close event loop
throw new Error("");
} finally {
await close();
}
// Force close the event loop on the success path too. The connection pool can
// leave keepalive handles registered even after close() resolves, which keeps
// the Edge Runtime worker (and therefore the container) alive after the catalog
// has already been written to stdout. The CLI streams this container's logs with
// Follow:true, so a worker that never exits hangs the migrations-catalog cache
// path (db start / db push with pg-delta caching) indefinitely at 0% CPU
// (supabase/pg-toolbelt#312).
throw new Error("");
`
)

var catalogPrefixRegexp = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)

func TryCacheMigrationsCatalog(ctx context.Context, config pgconn.Config, prefix string, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
if !ShouldCacheMigrationsCatalog() || len(version) > 0 {
return nil
}
if len(strings.TrimSpace(prefix)) == 0 {
prefix = CatalogPrefixFromConfig(config)
}
hash, err := HashMigrations(fsys)
if err != nil {
return err
}
snapshot, err := exportCatalog(ctx, utils.ToPostgresURL(config), options...)
if err != nil {
return err
}
if err := ensureTempDir(fsys); err != nil {
return err
}
_, err = WriteMigrationCatalogSnapshot(fsys, prefix, hash, snapshot)
return err
}

func ShouldCacheMigrationsCatalog() bool {
return utils.IsPgDeltaEnabled() || viper.GetBool("EXPERIMENTAL_PG_DELTA")
}

func CatalogPrefixFromConfig(config pgconn.Config) string {
if utils.IsLocalDatabase(config) {
return "local"
}
if matches := utils.ProjectHostPattern.FindStringSubmatch(config.Host); len(matches) > 2 {
return matches[2]
}
key := fmt.Sprintf("%s@%s:%d/%s", config.User, config.Host, config.Port, config.Database)
sum := sha256.Sum256([]byte(key))
return "url-" + hex.EncodeToString(sum[:])[:12]
}

func MigrationCatalogPath(hash, prefix string, createdAt time.Time) string {
return filepath.Join(pgDeltaTempPath(), fmt.Sprintf(migrationsCatalogName, SanitizedCatalogPrefix(prefix), hash, createdAt.UnixMilli()))
}
Expand Down Expand Up @@ -254,25 +173,3 @@ func ensureTempDir(fsys afero.Fs) error {
func pgDeltaTempPath() string {
return filepath.Join(utils.TempDir, pgDeltaTempDir)
}

func exportCatalog(ctx context.Context, targetRef string, options ...func(*pgx.ConnConfig)) (string, error) {
preparedRef, sslEnv, err := types.PreparePgDeltaPostgresRef(ctx, targetRef, types.PgDeltaTargetSSLRootCert, options...)
if err != nil {
return "", err
}
env := append([]string{"TARGET=" + preparedRef, "ROLE=postgres"}, sslEnv...)
binds := []string{utils.EdgeRuntimeId + ":/root/.cache/deno:rw"}
if cwd, err := os.Getwd(); err == nil {
binds = append(binds, cwd+":/workspace")
}
var stdout, stderr bytes.Buffer
script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaCatalogExportTS)
if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error exporting pg-delta catalog", &stdout, &stderr, utils.PgDeltaNpmRegistryOption()); err != nil {
return "", err
}
snapshot := strings.TrimSpace(stdout.String())
if len(snapshot) == 0 {
return "", errors.Errorf("error exporting pg-delta catalog: edge-runtime script produced no output:\n%s", stderr.String())
}
return snapshot, nil
}
Loading
Loading