Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ e2e package with `pnpm run test:e2e --shard=1/3`.

## E2E Compatibility Test Suite

`apps/cli-e2e` implements the replay-and-record compatibility harness for the TypeScript Legacy CLI (`ts-legacy`, the only shipped CLI shell). Live tests are owned by `apps/cli` and run from the command they cover. The CLI still shells out to the bundled Go binary for the handful of commands the TS port proxies (`db diff`, `db pull`, `db branch *`, `db remote *`, `gen keys`, `functions download`), so `apps/cli-go/` is built alongside the TS CLI for these suites, but there is no Go-vs-TypeScript parity runner.
`apps/cli-e2e` implements the replay-and-record compatibility harness for the TypeScript Legacy CLI (`ts-legacy`, the only shipped CLI shell). Live tests are owned by `apps/cli` and run from the command they cover. The CLI still shells out to the bundled Go binary for the handful of commands the TS port proxies (`db diff --use-pg-schema`, `db branch *`, `db remote changes`, `gen keys`, `functions download`), so `apps/cli-go/` is built alongside the TS CLI for these suites, but there is no Go-vs-TypeScript parity runner.

### Architecture

Expand Down
2 changes: 1 addition & 1 deletion apps/cli-e2e/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ CLI_HARNESS_TARGET=ts-legacy
SUPABASE_ACCESS_TOKEN=sbp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# ts-legacy shells out to the bundled Go binary for the proxied commands
# (db diff --use-pg-schema, db pull --experimental, db branch/remote, gen keys,
# (db diff --use-pg-schema, db branch *, db remote changes, gen keys,
# functions download --legacy-bundle). Point at a freshly built binary:
# cd apps/cli-go && go build -o /tmp/supabase-test-binary .
SUPABASE_GO_BINARY=/tmp/supabase-test-binary
Expand Down
3 changes: 1 addition & 2 deletions apps/cli-e2e/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,7 @@ SUPABASE_GO_BINARY=/tmp/supabase-test-binary \
Commands currently requiring this — the full proxied surface, nothing else needs a Go binary at all:

- `db diff` (for `--use-pg-schema`)
- `db pull` (for `--experimental`)
- `db branch create`, `db branch delete`, `db branch list`, `db branch switch`
- `db remote changes`, `db remote commit`
- `db remote changes`
- `gen keys`
- `functions download` (for the hidden `--legacy-bundle` flag)
49 changes: 6 additions & 43 deletions apps/cli-e2e/src/tests/go-binary-surface.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,17 @@ describe.skipIf(GO_BINARY === undefined)("go binary spawn surface (CLI-1970)", (

// The complete spawn surface, mirrored from `LegacyGoProxy` call sites:
// - db diff (diff.handler.ts, `--use-pg-schema` delegate path)
// - db pull (pull.handler.ts, `--experimental` delegate path)
// - db branch create|delete|list|switch (thin proxies)
// - db remote changes|commit (thin proxies)
// - db remote changes (thin proxy)
// - gen keys (keys.handler.ts)
// - functions download (shared/functions/download.ts, `--legacy-bundle`)
const RETAINED_COMMAND_PATHS: ReadonlyArray<ReadonlyArray<string>> = [
["db", "diff"],
["db", "pull"],
["db", "branch", "create"],
["db", "branch", "delete"],
["db", "branch", "list"],
["db", "branch", "switch"],
["db", "remote", "changes"],
["db", "remote", "commit"],
["gen", "keys"],
["functions", "download"],
];
Expand Down Expand Up @@ -145,12 +142,11 @@ describe.skipIf(GO_BINARY === undefined)("go binary spawn surface (CLI-1970)", (
expect(stderr).not.toMatch(/unknown flag|invalid argument/i);
}, 5_000);

// `db pull --experimental` (pull.handler.ts's `rebuildDelegateArgs`), with
// the complete global-flag set root.ts can prepend (globalArgs) — the
// only invocation in this suite exercising all ten at once. `db pull`
// connects directly to --db-url before touching Docker, so this fails at
// connect regardless of the (unused here) --network-id/--profile values.
test("db pull --experimental (full global flag set)", () => {
// `db remote changes` (changes.handler.ts), with the complete global-flag
// set root.ts can prepend (globalArgs) — the only invocation in this
// suite exercising all ten at once. Also provisions a Docker shadow first
// (same as `db diff`), so the bogus DOCKER_HOST is what trips this one.
test("db remote changes (full global flag set)", () => {
const { exitCode, stderr } = runGo([
"--output",
"json",
Expand All @@ -168,23 +164,6 @@ describe.skipIf(GO_BINARY === undefined)("go binary spawn surface (CLI-1970)", (
"--create-ticket",
"--agent",
"no",
"db",
"pull",
"--experimental",
"--db-url",
"postgresql://u:p@127.0.0.1:1/x",
"--schema",
"public",
]);
expect(exitCode).toBe(1);
expect(stderr).not.toMatch(/unknown flag|invalid argument/i);
}, 5_000);

// `db remote changes` (changes.handler.ts). Also provisions a Docker
// shadow first (same as `db diff`), so the bogus DOCKER_HOST is what
// trips this one too.
test("db remote changes", () => {
const { exitCode, stderr } = runGo([
"db",
"remote",
"changes",
Expand All @@ -197,22 +176,6 @@ describe.skipIf(GO_BINARY === undefined)("go binary spawn surface (CLI-1970)", (
expect(stderr).not.toMatch(/unknown flag|invalid argument/i);
}, 5_000);

// `db remote commit` (commit.handler.ts). Connects directly to --db-url
// before touching Docker (same as `db pull`).
test("db remote commit", () => {
const { exitCode, stderr } = runGo([
"db",
"remote",
"commit",
"--db-url",
"postgresql://u:p@127.0.0.1:1/x",
"--schema",
"public",
]);
expect(exitCode).toBe(1);
expect(stderr).not.toMatch(/unknown flag|invalid argument/i);
}, 5_000);

// `gen keys` (keys.handler.ts). Gated behind Go's Management-API login
// check before any network call, so an isolated SUPABASE_HOME (no stored
// credentials) plus the bogus --profile fails fast without ever reaching
Expand Down
50 changes: 4 additions & 46 deletions apps/cli-go/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,49 +44,7 @@ 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.

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

```sh
cd pg-toolbelt
bun run verdaccio:start
```

Verdaccio listens on `http://localhost:4873`. `@supabase/*` packages you publish locally are served from local storage; other `@supabase/*` dependencies (for example `@supabase/pg-topo`) are proxied to npmjs.

### 2. Publish a local pg-delta build

After changing `packages/pg-delta`:

```sh
bun run pg-delta:publish-local \
--write-version-to=/path/to/test-project/supabase/.temp/pgdelta-version
```

This publishes a fresh `0.0.0-local.<timestamp>` version and restores `package.json` afterward. The version file tells the CLI which npm version to request (`EffectivePgDeltaNpmVersion`).

Re-run whenever you change pg-delta source.

### 3. Run the CLI against the local registry

Set `PGDELTA_NPM_REGISTRY` to a URL reachable **from inside the edge-runtime Docker container**:

```sh
# Docker Desktop (macOS / Windows)
export PGDELTA_NPM_REGISTRY=http://host.docker.internal:4873

# Linux (Docker 20.10+)
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:

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

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`).

Unset `PGDELTA_NPM_REGISTRY` to return to the npmjs version pinned in config / `supabase/.temp/pgdelta-version`.
The Go binary no longer runs pg-delta. The TypeScript CLI bundles
`@supabase/pg-delta` in-process. To test a local pg-delta build, update the
`@supabase/pg-delta` dependency pin in `apps/cli/package.json` /
`pnpm-workspace.yaml`.
146 changes: 11 additions & 135 deletions apps/cli-go/cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/supabase/cli/internal/db/diff"
"github.com/supabase/cli/internal/db/pull"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/legacy/branch/create"
Expand Down Expand Up @@ -70,74 +69,21 @@ var (
},
}

useMigra bool
usePgAdmin bool
usePgSchema bool
usePgDelta bool
useDeclarative bool
pullDiffEngine = utils.EnumFlag{
Allowed: []string{"migra", "pg-delta"},
Value: "migra",
}
diffFrom string
diffTo string
outputPath string
schema []string
file string
dbPassword string
// Bound so the TS `--use-pg-schema` proxy can forward these without unknown-flag errors.
usePgSchema bool
outputPath string
schema []string
file string
dbPassword string

dbDiffCmd = &cobra.Command{
Use: "diff",
Short: "Diffs the local database for schema changes",
RunE: func(cmd *cobra.Command, args []string) error {
if len(diffFrom) > 0 || len(diffTo) > 0 {
switch {
case len(diffFrom) == 0 || len(diffTo) == 0:
return fmt.Errorf("must set both --from and --to when using explicit diff mode")
default:
return diff.RunExplicit(cmd.Context(), diffFrom, diffTo, schema, outputPath, afero.NewOsFs())
}
}
useDelta := resolveDiffEngine(cmd.Flags().Changed("use-migra"), usePgAdmin, usePgSchema, shouldUsePgDelta())
if usePgAdmin {
return diff.RunPgAdmin(cmd.Context(), schema, file, flags.DbConfig, afero.NewOsFs())
}
differ := diff.DiffSchemaMigra
if usePgSchema {
differ = diff.DiffPgSchema
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as views and grants.")
} else if useDelta {
differ = diff.DiffPgDelta
}
return diff.Run(cmd.Context(), schema, file, flags.DbConfig, differ, useDelta, afero.NewOsFs())
},
}

dbPullCmd = &cobra.Command{
Use: "pull [migration name]",
Short: "Pull schema from the remote database",
RunE: func(cmd *cobra.Command, args []string) error {
name := "remote_schema"
if len(args) > 0 {
name = args[0]
}
// Declarative export is opt-in via --declarative. Enabling pg-delta in config
// does not switch db pull to declarative output; it keeps the migration-file
// workflow and only defaults the shadow diff engine below.
useDeclarativePgDelta := useDeclarative
usePgDeltaDiff := resolvePullDiffEngine(
cmd.Flags().Changed("diff-engine"),
pullDiffEngine.Value,
shouldUsePgDelta(),
)
pullDiffer := diff.DiffSchemaMigra
if usePgDeltaDiff {
pullDiffer = diff.DiffPgDelta
}
return pull.Run(cmd.Context(), schema, flags.DbConfig, name, useDeclarativePgDelta, usePgDeltaDiff, pullDiffer, afero.NewOsFs())
},
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("Finished " + utils.Aqua("supabase db pull") + ".")
// TypeScript only proxies `--use-pg-schema` (stripe/pg-schema-diff).
// Other engines run in-process in the TS CLI.
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as views and grants.")
return diff.Run(cmd.Context(), schema, file, flags.DbConfig, diff.DiffPgSchema, afero.NewOsFs())
},
}

Expand All @@ -153,56 +99,11 @@ var (
Short: "Show changes on the remote database",
Long: "Show changes on the remote database since last migration.",
RunE: func(cmd *cobra.Command, args []string) error {
return diff.Run(cmd.Context(), schema, file, flags.DbConfig, diff.DiffSchemaMigra, false, afero.NewOsFs())
},
}

dbRemoteCommitCmd = &cobra.Command{
Deprecated: "use \"db pull\" instead.\n",
Use: "commit",
Short: "Commit remote changes as a new migration",
RunE: func(cmd *cobra.Command, args []string) error {
// remote commit always writes a timestamped migration file. When pg-delta is
// enabled it only swaps the shadow diff engine; it never switches to the
// declarative export path.
usePgDeltaDiff := shouldUsePgDelta()
pullDiffer := diff.DiffSchemaMigra
if usePgDeltaDiff {
pullDiffer = diff.DiffPgDelta
}
return pull.Run(cmd.Context(), schema, flags.DbConfig, "remote_commit", false, usePgDeltaDiff, pullDiffer, afero.NewOsFs())
return diff.Run(cmd.Context(), schema, file, flags.DbConfig, diff.DiffSchemaMigra, afero.NewOsFs())
},
}
)

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

// 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
// (useMigraChanged) counts as opting out.
func resolveDiffEngine(useMigraChanged, usePgAdmin, usePgSchema, pgDeltaDefault bool) bool {
if useMigraChanged || usePgAdmin || usePgSchema {
return false
}
return 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).
func resolvePullDiffEngine(engineFlagChanged bool, engine string, pgDeltaDefault bool) bool {
if engineFlagChanged {
return engine == "pg-delta"
}
return pgDeltaDefault
}

func init() {
// Build branch command
dbBranchCmd.AddCommand(dbBranchCreateCmd)
Expand All @@ -212,13 +113,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(&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.")
dbDiffCmd.MarkFlagsMutuallyExclusive("use-migra", "use-pgadmin", "use-pg-schema", "use-pg-delta")
diffFlags.StringVar(&diffFrom, "from", "", "Diff from local, linked, migrations, or a Postgres URL.")
diffFlags.StringVar(&diffTo, "to", "", "Diff to local, linked, migrations, or a Postgres URL.")
diffFlags.StringVarP(&outputPath, "output", "o", "", "Write explicit diff output to a file path.")
diffFlags.String("db-url", "", "Diffs against the database specified by the connection string (must be percent-encoded).")
diffFlags.Bool("linked", false, "Diffs local migration files against the linked project.")
Expand All @@ -227,24 +122,6 @@ func init() {
diffFlags.StringVarP(&file, "file", "f", "", "Saves schema diff to a new migration file.")
diffFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.")
dbCmd.AddCommand(dbDiffCmd)
// Build pull command
pullFlags := dbPullCmd.Flags()
// --declarative switches pull output from a timestamped migration to declarative
// 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."))
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).")
pullFlags.Bool("linked", true, "Pulls from the linked project.")
pullFlags.Bool("local", false, "Pulls from the local database.")
dbPullCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local")
dbPullCmd.MarkFlagsMutuallyExclusive("declarative", "diff-engine")
dbPullCmd.MarkFlagsMutuallyExclusive("use-pg-delta", "diff-engine")
pullFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.")
cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", pullFlags.Lookup("password")))
dbCmd.AddCommand(dbPullCmd)
// Build remote command
remoteFlags := dbRemoteCmd.PersistentFlags()
remoteFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.")
Expand All @@ -254,7 +131,6 @@ func init() {
remoteFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.")
cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", remoteFlags.Lookup("password")))
dbRemoteCmd.AddCommand(dbRemoteChangesCmd)
dbRemoteCmd.AddCommand(dbRemoteCommitCmd)
dbCmd.AddCommand(dbRemoteCmd)
rootCmd.AddCommand(dbCmd)
}
Loading
Loading