diff --git a/apps/cli-go/internal/utils/config.go b/apps/cli-go/internal/utils/config.go index d171d06753..108c3391a1 100644 --- a/apps/cli-go/internal/utils/config.go +++ b/apps/cli-go/internal/utils/config.go @@ -216,7 +216,6 @@ func ToRealtimeEnv(addr config.AddressFamily) string { type InitParams struct { ProjectId string UseOrioleDB bool - UsePgDelta bool Overwrite bool } @@ -226,10 +225,6 @@ func InitConfig(params InitParams, fsys afero.Fs) error { if params.UseOrioleDB { c.Experimental.OrioleDBVersion = "15.1.0.150" } - // The supabase init command opts new projects into pg-delta. Existing configs are - // unaffected because mergeDefaultValues ejects with this flag false (default stays - // migra), and other InitConfig callers leave it disabled. - c.Experimental.PgDeltaInitEnabled = params.UsePgDelta // Create config file if err := MkdirIfNotExistFS(fsys, SupabaseDirPath); err != nil { return err diff --git a/apps/cli-go/internal/utils/config_test.go b/apps/cli-go/internal/utils/config_test.go index 6d829304f1..cd2a69d2a8 100644 --- a/apps/cli-go/internal/utils/config_test.go +++ b/apps/cli-go/internal/utils/config_test.go @@ -72,22 +72,7 @@ func TestInitConfig(t *testing.T) { assert.True(t, exists) }) - t.Run("generated config enables pgdelta when requested", func(t *testing.T) { - fsys := afero.NewMemMapFs() - params := InitParams{ - ProjectId: "test-project", - UsePgDelta: true, - } - - err := InitConfig(params, fsys) - - require.NoError(t, err) - content, err := afero.ReadFile(fsys, ConfigPath) - require.NoError(t, err) - assert.Contains(t, string(content), "[experimental.pgdelta]\nenabled = true") - }) - - t.Run("generated config leaves pgdelta disabled by default", func(t *testing.T) { + t.Run("generated config enables pgdelta by default", func(t *testing.T) { fsys := afero.NewMemMapFs() params := InitParams{ ProjectId: "test-project", @@ -98,7 +83,7 @@ func TestInitConfig(t *testing.T) { require.NoError(t, err) content, err := afero.ReadFile(fsys, ConfigPath) require.NoError(t, err) - assert.Contains(t, string(content), "[experimental.pgdelta]\nenabled = false") + assert.Contains(t, string(content), "[experimental.pgdelta]\nenabled = true") }) t.Run("creates config with orioledb", func(t *testing.T) { diff --git a/apps/cli-go/internal/utils/misc.go b/apps/cli-go/internal/utils/misc.go index 186573146b..eeb4081005 100644 --- a/apps/cli-go/internal/utils/misc.go +++ b/apps/cli-go/internal/utils/misc.go @@ -123,7 +123,12 @@ func GetDeclarativeDir() string { } func IsPgDeltaEnabled() bool { - return Config.Experimental.PgDelta != nil && Config.Experimental.PgDelta.Enabled + // pg-delta is the default diff engine: an absent [experimental.pgdelta] + // section (nil before config load) resolves to enabled. The config template + // ejects `enabled = true` as the viper default, so a section that omits the + // key also resolves to enabled; only an explicit `enabled = false` opts back + // into migra. + return Config.Experimental.PgDelta == nil || Config.Experimental.PgDelta.Enabled } func GetCurrentTimestamp() string { diff --git a/apps/cli-go/pkg/config/config.go b/apps/cli-go/pkg/config/config.go index 04eae99289..0ea170a069 100644 --- a/apps/cli-go/pkg/config/config.go +++ b/apps/cli-go/pkg/config/config.go @@ -345,11 +345,6 @@ type ( Webhooks *webhooks `toml:"webhooks" json:"webhooks"` PgDelta *PgDeltaConfig `toml:"pgdelta" json:"pgdelta"` Inspect inspect `toml:"inspect" json:"inspect"` - // PgDeltaInitEnabled drives the [experimental.pgdelta] enabled value rendered - // by Eject. It is true only for the supabase init scaffold so freshly generated - // projects opt into pg-delta, and false when Eject feeds mergeDefaultValues so - // existing configs without the section keep resolving to migra (non-breaking). - PgDeltaInitEnabled bool `toml:"-" json:"-"` } ) diff --git a/apps/cli-go/pkg/config/config_test.go b/apps/cli-go/pkg/config/config_test.go index 4190d71371..cbc1d196a4 100644 --- a/apps/cli-go/pkg/config/config_test.go +++ b/apps/cli-go/pkg/config/config_test.go @@ -245,8 +245,6 @@ format_options = "not-json" t.Run("init scaffold opts into pgdelta", func(t *testing.T) { config := NewConfig() - // supabase init renders the scaffold with the pg-delta opt-in flag set - config.Experimental.PgDeltaInitEnabled = true var buf bytes.Buffer require.NoError(t, config.Eject(&buf)) fsys := fs.MapFS{"supabase/config.toml": &fs.MapFile{Data: buf.Bytes()}} @@ -256,7 +254,7 @@ format_options = "not-json" assert.True(t, config.Experimental.PgDelta.Enabled) }) - t.Run("absent pgdelta section falls back to migra", func(t *testing.T) { + t.Run("absent pgdelta section defaults to pg-delta", func(t *testing.T) { config := NewConfig() fsys := fs.MapFS{ "supabase/config.toml": &fs.MapFile{Data: []byte(` @@ -265,11 +263,28 @@ orioledb_version = "" `)}, } - // The default ejected by mergeDefaultValues keeps pg-delta disabled, so a config - // without the section resolves to migra (PgDelta is non-nil only for version pinning). + // The default ejected by mergeDefaultValues enables pg-delta, so a config + // without the section resolves to pg-delta. require.NoError(t, config.Load("", fsys)) require.NotNil(t, config.Experimental.PgDelta) - assert.False(t, config.Experimental.PgDelta.Enabled) + assert.True(t, config.Experimental.PgDelta.Enabled) + }) + + t.Run("pgdelta section without enabled key defaults to pg-delta", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[experimental.pgdelta] +declarative_schema_path = "./db/decl" +`)}, + } + + // viper merges the user file over the ejected defaults key-by-key, so a + // section that omits enabled keeps the default true rather than the Go + // zero value false. + require.NoError(t, config.Load("", fsys)) + require.NotNil(t, config.Experimental.PgDelta) + assert.True(t, config.Experimental.PgDelta.Enabled) }) t.Run("explicit enabled false restores migra", func(t *testing.T) { diff --git a/apps/cli-go/pkg/config/templates/config.toml b/apps/cli-go/pkg/config/templates/config.toml index 07bacc0ade..2967caac8d 100644 --- a/apps/cli-go/pkg/config/templates/config.toml +++ b/apps/cli-go/pkg/config/templates/config.toml @@ -404,10 +404,10 @@ s3_access_key = "env(S3_ACCESS_KEY)" # Configures AWS_SECRET_ACCESS_KEY for S3 bucket s3_secret_key = "env(S3_SECRET_KEY)" -# pg-delta is the schema diff engine for db diff / db pull / db remote commit. +# pg-delta is the default schema diff engine for db diff / db pull / db remote commit. # Set enabled = false to fall back to the legacy migra engine. [experimental.pgdelta] -enabled = {{ .Experimental.PgDeltaInitEnabled }} +enabled = true # Directory under `supabase/` where declarative files are written. # declarative_schema_path = "./schemas" # JSON string passed through to pg-delta SQL formatting. When omitted, SQL is diff --git a/apps/cli/docs/go-cli-divergences.md b/apps/cli/docs/go-cli-divergences.md index 1abb22e6bb..a730471392 100644 --- a/apps/cli/docs/go-cli-divergences.md +++ b/apps/cli/docs/go-cli-divergences.md @@ -20,15 +20,14 @@ These commands exist in the TS CLI today but have no direct top-level equivalent - `db diff`, `db pull`, and `db schema declarative generate`/`sync` have a TS-only `--strict-coverage` flag (no Go equivalent). It applies whenever the bundled - pg-delta engine runs (always for the declarative commands; for `db diff` and - migration-style `db pull` when pg-delta is selected via - `[experimental.pgdelta] enabled = true`, `--use-pg-delta`, or - `--diff-engine pg-delta`): coverage gaps that the engine reports — statements - it skipped or objects it could not represent — normally surface as warnings, - and `--strict-coverage` promotes them to hard failures. Selecting migra (the - `db diff` / migration-style `db pull` default, or explicitly via `--use-migra` - / `--diff-engine migra`) accepts the flag but has no effect, since migra does - not emit coverage diagnostics. Default behavior (omitted flag) matches Go. + pg-delta engine runs — always for the declarative commands, and by default for + `db diff` and migration-style `db pull`: coverage gaps that the engine reports — + statements it skipped or objects it could not represent — normally surface as + warnings, and `--strict-coverage` promotes them to hard failures. Selecting migra + instead (`--use-migra`, `--diff-engine migra`, or + `[experimental.pgdelta] enabled = false`) accepts the flag but has no effect, + since migra does not emit coverage diagnostics. Default behavior (omitted flag) + matches Go. - `db push` has a TS-only `--skip-vault` flag. It applies migrations without resolving or updating `[db.vault]` secrets; default behavior still matches Go. - Every command that resolves a linked project ref for its own database diff --git a/apps/cli/docs/go-cli-reference.md b/apps/cli/docs/go-cli-reference.md index e557eefadf..c75eb9bfac 100644 --- a/apps/cli/docs/go-cli-reference.md +++ b/apps/cli/docs/go-cli-reference.md @@ -248,7 +248,7 @@ Flags: --linked Diffs local migration files against the linked project. --local Diffs local migration files against the local database. (default true) -s, --schema strings Comma separated list of schema to include. - --use-migra Use migra to generate schema diff. (default true) + --use-migra Use migra to generate schema diff. --use-pg-delta Use pg-delta to generate schema diff. --use-pg-schema Use pg-schema-diff to generate schema diff. --use-pgadmin Use pgAdmin to generate schema diff. diff --git a/apps/cli/docs/supabase/db/diff.md b/apps/cli/docs/supabase/db/diff.md index 6ad22f7e1a..1bb15f522b 100644 --- a/apps/cli/docs/supabase/db/diff.md +++ b/apps/cli/docs/supabase/db/diff.md @@ -4,13 +4,13 @@ Diffs schema changes made to the local or remote database. Requires the local development stack to be running when diffing against the local database. To diff against a remote or self-hosted database, specify the `--linked` or `--db-url` flag respectively. -Runs [djrobstep/migra](https://github.com/djrobstep/migra) in a container to compare schema differences between the target database and a shadow database. The shadow database is created by applying migrations in local `supabase/migrations` directory in a separate container. Output is written to stdout by default. For convenience, you can also save the schema diff as a new migration file by passing in `-f` flag. +Compares schema differences between the target database and a shadow database, using the bundled pg-delta engine by default. The legacy [djrobstep/migra](https://github.com/djrobstep/migra) engine, which runs in a container, remains available as a fallback (see below). The shadow database is created by applying migrations in local `supabase/migrations` directory in a separate container. Output is written to stdout by default. For convenience, you can also save the schema diff as a new migration file by passing in `-f` flag. Explicit `--from`/`--to` mode always uses pg-delta. In this mode, `-f` is ignored and stdout (or `--output`) is a flattened representation for review, not a portable apply script. Do not apply it directly with plain `psql -f`: transactional units can contain `SET LOCAL` preambles that only take effect inside a transaction, while plans that mix transactional and non-transactional units cannot safely be wrapped in one transaction. To create an applicable migration, use normal target mode with `supabase db diff -f `, then apply it through `supabase db reset` locally or `supabase db push` against the linked project. These paths preserve the plan's per-unit transaction semantics. By default, all schemas in the target database are diffed. Use the `--schema public,extensions` flag to restrict diffing to a subset of schemas. -Projects created by a recent `supabase init` default to the pg-delta diff engine (`[experimental.pgdelta] enabled = true` in `config.toml`). Existing projects are unaffected and keep using migra unless they opt in. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]`, or pass `--use-migra` for a single run. +pg-delta is the default diff engine for all projects. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]` in `config.toml`, or pass `--use-migra` for a single run. With the bundled pg-delta engine, diff SQL defaults to uppercase keywords, indent 2, a maximum width of 180, trailing commas, and column/key alignment, matching its declarative export. When `-f` writes migrations, execution-aware transaction semantics are preserved as ordered per-unit files; non-transactional units carry a directive that the CLI apply path honors. Flattened review output retains the rendered SQL and preambles, but not the unit boundaries supplied to a migration runner. Configure overrides with `[experimental.pgdelta] format_options`, or set `format_options = "null"` to emit raw, unformatted statements. diff --git a/apps/cli/docs/supabase/db/pull.md b/apps/cli/docs/supabase/db/pull.md index c0b917a4c5..b692c49756 100644 --- a/apps/cli/docs/supabase/db/pull.md +++ b/apps/cli/docs/supabase/db/pull.md @@ -8,15 +8,15 @@ Requires your local project to be linked to a remote database by running `supaba Optionally, a new row can be inserted into the migration history table to reflect the current state of the remote database. -If no entries exist in the migration history table, the default diff engine uses `pg_dump` to capture all contents of the remote schemas you have created. Otherwise, this command will only diff schema changes against the remote database, similar to running `db diff --linked`. +If no entries exist in the migration history table, pg-delta (the default diff engine) produces the full migration from the shadow diff alone; with `--diff-engine migra`, the initial pull instead uses `pg_dump` to capture all contents of the remote schemas you have created. Otherwise, this command will only diff schema changes against the remote database, similar to running `db diff --linked`. -Pass `--diff-engine pg-delta` to keep the migration-file `db pull` workflow while using pg-delta for the shadow diff step. On initial pull, pg-delta replaces `pg_dump` and produces the full migration from the shadow diff alone. Pass `--declarative` to switch to the declarative pg-delta export workflow instead. +Pass `--declarative` to switch to the declarative pg-delta export workflow instead of writing a migration file. pg-delta plans are execution-aware: when a plan crosses a transaction boundary — for example `ALTER TYPE ... ADD VALUE` followed by a statement that uses the new enum value, which cannot run in the same transaction — `db pull` writes one ordered migration file per plan unit instead of a single file (for example `_remote_schema_schema_changes.sql` and `_remote_schema_after_enum_values.sql`), each recorded in the migration history. The common case (a single unit) still produces exactly one `_remote_schema.sql` file. By default the emitted SQL is formatted with the same settings the declarative export uses (uppercase keywords, wrapped at a max width of 180, indented and column-aligned). Configure overrides with `[experimental.pgdelta] format_options` in `config.toml`, or set `format_options = "null"` to opt out and emit raw, unformatted statements. -When `[experimental.pgdelta] enabled = true` (the default for projects created by a recent `supabase init`), the migration-file `db pull` workflow uses pg-delta for the shadow diff step by default; it does not switch to declarative output. Existing projects without the section are unaffected and keep using migra. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]`, or pass `--diff-engine migra` for a single run. +pg-delta is the default diff engine: the migration-file `db pull` workflow uses pg-delta for the shadow diff step unless configured otherwise; it does not switch to declarative output. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]` in `config.toml`, or pass `--diff-engine migra` for a single run. When pulling from a remote database with `--db-url`, prefer a direct connection (`db..supabase.co:5432`) over the connection pooler so pg-delta can introspect the full catalog reliably. diff --git a/apps/cli/docs/supabase/db/schema-declarative-generate.md b/apps/cli/docs/supabase/db/schema-declarative-generate.md index 1cd416e747..9e97bdb7c4 100644 --- a/apps/cli/docs/supabase/db/schema-declarative-generate.md +++ b/apps/cli/docs/supabase/db/schema-declarative-generate.md @@ -8,4 +8,4 @@ The bundled pg-delta engine writes one directory per schema at the root of that Emitted SQL uses the same default format as `db pull` (uppercase keywords, indent 2, width 180, column-aligned). Override with `[experimental.pgdelta] format_options`, or set `format_options = "null"` for raw statements. -Requires `--experimental` flag or `[experimental.pgdelta] enabled = true` in config. +pg-delta is on by default. The command is closed only when `[experimental.pgdelta] enabled = false` and `--experimental` is omitted. diff --git a/apps/cli/docs/supabase/db/schema-declarative-sync.md b/apps/cli/docs/supabase/db/schema-declarative-sync.md index 1932b16f11..58a0dbc163 100644 --- a/apps/cli/docs/supabase/db/schema-declarative-sync.md +++ b/apps/cli/docs/supabase/db/schema-declarative-sync.md @@ -4,4 +4,4 @@ Generate a new migration by diffing your declarative schema files against the cu When no declarative schema exists yet, the command offers to run `generate` first. After computing the diff, you can optionally name the migration and apply it to the local database. -Requires `--experimental` flag or `[experimental.pgdelta] enabled = true` in config. +pg-delta is on by default. The command is closed only when `[experimental.pgdelta] enabled = false` and `--experimental` is omitted. diff --git a/apps/cli/src/command-internal/db-config.toml-read.ts b/apps/cli/src/command-internal/db-config.toml-read.ts index 9cb638dc8a..0ea96adc04 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.ts @@ -40,14 +40,6 @@ type EnvLookup = (name: string) => string | undefined; */ export interface DbTomlValues { readonly projectEnv: Readonly>; - /** - * Resolves a `SUPABASE_*` env var with Go's precedence: shell env (non-empty) - * wins, then the loaded project `.env*` files (non-empty), else undefined. - * Go writes project `.env` into the process env before viper's `AutomaticEnv` - * reads these, so handlers must consult both - * rather than `process.env` alone (e.g. `SUPABASE_EXPERIMENTAL_PG_DELTA`). - */ - readonly envLookup: (name: string) => string | undefined; readonly apiSchemas: ReadonlyArray; /** `[db] port`, default 54322 (`packages/config/src/db.ts`). */ readonly port: number; @@ -184,7 +176,7 @@ interface BaselineTomlConfig { /** The `[experimental.pgdelta]` subtree. */ export interface PgDeltaTomlConfig { - /** `[experimental.pgdelta] enabled`, default false. `IsPgDeltaEnabled`. */ + /** `[experimental.pgdelta] enabled`, default true. `IsPgDeltaEnabled`. */ readonly enabled: boolean; /** * `[experimental.pgdelta] declarative_schema_path`, resolved to a @@ -1733,7 +1725,8 @@ const readDbTomlCore = Effect.fnUntraced(function* ( // Go decodes this bool via `strconv.ParseBool` (mapstructure weakly typed), so `"1"` // counts as true and a malformed value (`SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED=maybe`) // aborts the load. The env override wins (viper AutomaticEnv), then the TOML bool, then - // an `env(VAR)` string, defaulting to false when absent. + // an `env(VAR)` string, defaulting to true when absent: pg-delta is the default + // diff engine, and only an explicit `enabled = false` opts back into migra. let enabled: boolean; if (enabledEnv !== undefined) { // The AutomaticEnv override is decoded through `LoadEnvHook`, so an `env(VAR)` @@ -1765,7 +1758,7 @@ const readDbTomlCore = Effect.fnUntraced(function* ( } enabled = parsed; } else { - enabled = false; + enabled = true; } const declarativeSchemaPathRaw = pgDeltaRaw?.["declarative_schema_path"]; @@ -2636,7 +2629,6 @@ const readDbTomlCore = Effect.fnUntraced(function* ( const values: DbTomlValues = { projectEnv, - envLookup: envOverride, apiSchemas, port, shadowPort, diff --git a/apps/cli/src/command-internal/db-config.toml-read.unit.test.ts b/apps/cli/src/command-internal/db-config.toml-read.unit.test.ts index 0e34035a18..469ed6b4d6 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.unit.test.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.unit.test.ts @@ -1029,15 +1029,16 @@ describe("readDbToml", () => { it.effect("SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED still wins when the block omits pgdelta", () => { // Control: the env override is suppressed only for keys the matched block explicitly set; - // a block that omits experimental.pgdelta.enabled leaves the env override in force. + // a block that omits experimental.pgdelta.enabled leaves the env override in force + // (an explicit false beats the enabled-by-default resolution). const ref = "abcdefghijklmnopqrst"; const previous = process.env["SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED"]; - process.env["SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED"] = "true"; + process.env["SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED"] = "false"; const dir = withConfig(["[remotes.prod]", `project_id = "${ref}"`, ""].join("\n")); return readRef(dir, ref).pipe( Effect.tap((v) => Effect.sync(() => { - expect(v.pgDelta.enabled).toBe(true); + expect(v.pgDelta.enabled).toBe(false); }), ), Effect.ensuring( @@ -2703,12 +2704,12 @@ describe("readDbToml", () => { }); describe("readDbToml [experimental.pgdelta]", () => { - it.effect("defaults pg-delta to disabled with no config", () => { + it.effect("defaults pg-delta to enabled with no config", () => { const dir = withConfig(undefined); return read(dir).pipe( Effect.tap((v) => Effect.sync(() => { - expect(v.pgDelta.enabled).toBe(false); + expect(v.pgDelta.enabled).toBe(true); expect(Option.isNone(v.pgDelta.declarativeSchemaPath)).toBe(true); expect(Option.isNone(v.pgDelta.formatOptions)).toBe(true); rmSync(dir, { recursive: true, force: true }); diff --git a/apps/cli/src/command-internal/diff-engine.ts b/apps/cli/src/command-internal/diff-engine.ts index c6504d452b..c3af2a1007 100644 --- a/apps/cli/src/command-internal/diff-engine.ts +++ b/apps/cli/src/command-internal/diff-engine.ts @@ -5,25 +5,25 @@ export const schemaPathsTransitionWarning = "WARNING: [db.migrations].schema_paths no longer changes the migrations baseline used by db diff or migration-style db pull. These commands always compare local migrations with the selected database. Use `supabase db schema declarative sync` to compare declarative schema files.\n"; /** - * Whether pg-delta is the active default engine. Mirrors `shouldUsePgDelta`: - * `utils.IsPgDeltaEnabled() || usePgDelta || viper.GetBool("EXPERIMENTAL_PG_DELTA")`. - * The three inputs are the resolved config flag (`[experimental.pgdelta].enabled`), - * the command's `--use-pg-delta` flag, and the `SUPABASE_EXPERIMENTAL_PG_DELTA` - * env var. + * Whether pg-delta is the active default engine. pg-delta is on unless the project + * explicitly rolls back with `[experimental.pgdelta] enabled = false`; the command's + * `--use-pg-delta` flag is a per-run opt-in that overrides that rollback. The historical + * `SUPABASE_EXPERIMENTAL_PG_DELTA` opt-in env var is intentionally not an input: now + * that pg-delta is the default it adds nothing when the config is on, and honoring a + * stale opt-in would silently defeat the documented config rollback. */ export function shouldUsePgDelta(inputs: { readonly configEnabled: boolean; readonly usePgDeltaFlag: boolean; - readonly envEnabled: boolean; }): boolean { - return inputs.configEnabled || inputs.usePgDeltaFlag || inputs.envEnabled; + return inputs.configEnabled || inputs.usePgDeltaFlag; } /** * Reports whether `db diff` should run in pg-delta mode. Mirrors Go's * `resolveDiffEngine`: an explicit `--use-migra`, * `--use-pgadmin`, or `--use-pg-schema` is an authoritative rollback that clears - * pg-delta mode; `--use-migra` defaults to true so only an explicit pass + * pg-delta mode. `--use-migra` is off unless passed, so only an explicit pass * (`useMigraChanged`) counts as opting out. */ export function resolveDiffEngine(inputs: { diff --git a/apps/cli/src/command-internal/diff-engine.unit.test.ts b/apps/cli/src/command-internal/diff-engine.unit.test.ts index 7f1a7998b9..e8eeb825f6 100644 --- a/apps/cli/src/command-internal/diff-engine.unit.test.ts +++ b/apps/cli/src/command-internal/diff-engine.unit.test.ts @@ -9,19 +9,10 @@ import { } from "./diff-engine.ts"; describe("shouldUsePgDelta", () => { - it("is the OR of config, flag, and env", () => { - expect( - shouldUsePgDelta({ configEnabled: false, usePgDeltaFlag: false, envEnabled: false }), - ).toBe(false); - expect( - shouldUsePgDelta({ configEnabled: true, usePgDeltaFlag: false, envEnabled: false }), - ).toBe(true); - expect( - shouldUsePgDelta({ configEnabled: false, usePgDeltaFlag: true, envEnabled: false }), - ).toBe(true); - expect( - shouldUsePgDelta({ configEnabled: false, usePgDeltaFlag: false, envEnabled: true }), - ).toBe(true); + it("follows the config default and lets --use-pg-delta override an explicit rollback", () => { + expect(shouldUsePgDelta({ configEnabled: false, usePgDeltaFlag: false })).toBe(false); + expect(shouldUsePgDelta({ configEnabled: true, usePgDeltaFlag: false })).toBe(true); + expect(shouldUsePgDelta({ configEnabled: false, usePgDeltaFlag: true })).toBe(true); }); }); diff --git a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md index 569e59e07f..4755677c9b 100644 --- a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md @@ -89,7 +89,6 @@ of this command's own target resolve, ahead of the differ container. | `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow container/network onto an existing Docker network | no | | `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | | `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline PGDATA is snapshotted to a tar and restored into the next run's fresh container (see Notes) | no | -| `SUPABASE_EXPERIMENTAL_PG_DELTA` | force pg-delta engine | no | | `PGDELTA_DEBUG` | pg-delta debug capture | no | | `SUPABASE_SSL_DEBUG` | migra SSL debug logging | no | | `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the differ's / shadow's image registry (shell **or** project `.env`, applied for the run via `applyProjectEnv`, matching `db push`/`db pull`/`db dump`) | no | @@ -98,10 +97,9 @@ of this command's own target resolve, ahead of the differ container. `SUPABASE_DB_HEALTH_TIMEOUT` all apply to `--use-pgadmin` too — its shadow is provisioned through the same primitives. -`SUPABASE_EXPERIMENTAL_PG_DELTA` is **read, no effect** on the pgadmin path: the pg-delta -engine-selection lookup (`shouldUsePgDelta`) runs unconditionally, before the -`--use-pgadmin` branch, but the pgadmin branch is chosen first and never consults the -resulting `useDelta` value. +The historical `SUPABASE_EXPERIMENTAL_PG_DELTA` opt-in env var is **no longer read**: +pg-delta is the default engine, and the explicit `[experimental.pgdelta] enabled = false` +config rollback is authoritative. `SUPABASE_INTERNAL_IMAGE_REGISTRY` applies to the differ's own image resolution too. The docker-run layer's resolver (`docker-run.layer.ts`) is built once, statically, with @@ -180,9 +178,10 @@ transaction metadata. ## Notes / Delegation -- `--use-migra` (default), `--use-pgadmin`, `--use-pg-schema`, `--use-pg-delta` are a - mutually-exclusive engine group; `--db-url` / `--linked` / `--local` are a - mutually-exclusive target group (default `--local`). +- `--use-migra`, `--use-pgadmin`, `--use-pg-schema`, `--use-pg-delta` are a + mutually-exclusive engine group (pg-delta is the default when none is passed); + `--db-url` / `--linked` / `--local` are a mutually-exclusive target group + (default `--local`). - **`--project-ref`** (TS-only, no Go equivalent on any user-facing `db` command) overrides ONLY the linked-ref resolution `ProjectRefResolver` performs (flag > `SUPABASE_PROJECT_ID` > `.temp/project-ref`) — unlike @@ -303,7 +302,7 @@ Given that, the flag is now deprecated rather than ported: - A TS-only stderr deprecation warning is printed immediately before delegating (both text and machine `--output-format` modes — diagnostics stay stderr-only, - the CLI-1546 rule): `"--use-pg-schema" is deprecated. Use the pg-delta engine ([experimental.pgdelta] enabled = true / --use-pg-delta) or the default migra engine instead.` + the CLI-1546 rule): `"--use-pg-schema" is deprecated. Use the default pg-delta engine or the migra engine (--use-migra) instead.` The warning text intentionally does not promise a removal timeline. - This is **additive** to (printed before) Go's own pre-existing "experimental" warning (`cmd/db.go:121`, unchanged): `--use-pg-schema flag is experimental and may not include all entities, such as views and grants.` The delegated child diff --git a/apps/cli/src/commands/db/diff/diff.command.ts b/apps/cli/src/commands/db/diff/diff.command.ts index 43bfa182ad..aa3be4911c 100644 --- a/apps/cli/src/commands/db/diff/diff.command.ts +++ b/apps/cli/src/commands/db/diff/diff.command.ts @@ -8,10 +8,9 @@ import { dbDiff } from "./diff.handler.ts"; import { dbDiffRuntimeLayer } from "./diff.layers.ts"; const config = { - // The four engine flags are a mutually-exclusive group, and `--use-migra` - // defaults to true, so they are modelled as `Option` to track whether the flag - // was passed: the mutex check and `resolveDiffEngine`'s `useMigraChanged` key - // off whether the flag was passed, not its value. + // The four engine flags are a mutually-exclusive group, modelled as `Option` + // so the mutex check and `resolveDiffEngine`'s `useMigraChanged` key off + // whether the flag was passed, not its value. useMigra: Flag.boolean("use-migra").pipe( Flag.withDescription("Use migra to generate schema diff."), Flag.optional, @@ -21,13 +20,13 @@ const config = { Flag.optional, ), usePgSchema: Flag.boolean("use-pg-schema").pipe( - // Deprecated in favor of the pg-delta engine (or the default migra engine) — + // Deprecated in favor of the default pg-delta engine (or the migra engine) — // a keep-in-Go exception (in-process stripe/pg-schema-diff library, no // TS/container equivalent — see SIDE_EFFECTS.md). This description-only // notice is not enforced by the flag framework — see diff.handler.ts's // runtime warning for the enforced half of the deprecation. Flag.withDescription( - "Use pg-schema-diff to generate schema diff. Deprecated: use the pg-delta engine ([experimental.pgdelta] enabled = true / --use-pg-delta) or the default migra engine instead.", + "Use pg-schema-diff to generate schema diff. Deprecated: use the default pg-delta engine or the migra engine (--use-migra) instead.", ), Flag.optional, ), diff --git a/apps/cli/src/commands/db/diff/diff.handler.ts b/apps/cli/src/commands/db/diff/diff.handler.ts index 5c8c35de28..3a3a5d2259 100644 --- a/apps/cli/src/commands/db/diff/diff.handler.ts +++ b/apps/cli/src/commands/db/diff/diff.handler.ts @@ -40,7 +40,6 @@ import { import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; import { - parseBoolEnv, resolveDiffEngine, schemaPathsTransitionWarning, shouldUsePgDelta, @@ -85,7 +84,7 @@ Run ${aqua("supabase db reset")} to verify that the new migration does not gener // SIDE_EFFECTS.md). The flag is deprecated in favor of the pg-delta engine. // This warning is additive to (and prints before) the delegated child's own // "experimental" warning, which it still prints unchanged. -const warnPgSchemaDeprecated = `${yellow("WARNING:")} "--use-pg-schema" is deprecated. Use the pg-delta engine ([experimental.pgdelta] enabled = true / --use-pg-delta) or the default migra engine instead.`; +const warnPgSchemaDeprecated = `${yellow("WARNING:")} "--use-pg-schema" is deprecated. Use the default pg-delta engine or the migra engine (--use-migra) instead.`; const declarativeBaselineAdvisory = (declarativePath: string | null) => ({ code: "DeclarativeSchemaNotUsedAsDiffBaseline", @@ -550,12 +549,11 @@ export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) { }; const formatOptions = Option.getOrElse(cfg.pgDelta.formatOptions, () => ""); - // Engine resolution: the pg-delta env/config/flag gate, read from the + // Engine resolution: the pg-delta config/flag gate, read from the // (possibly remote-merged) config. const pgDeltaDefault = shouldUsePgDelta({ configEnabled: cfg.pgDelta.enabled, usePgDeltaFlag: Option.getOrElse(flags.usePgDelta, () => false), - envEnabled: parseBoolEnv(cfg.envLookup("SUPABASE_EXPERIMENTAL_PG_DELTA")), }); const useDelta = resolveDiffEngine({ useMigraChanged: Option.isSome(flags.useMigra), diff --git a/apps/cli/src/commands/db/diff/diff.integration.test.ts b/apps/cli/src/commands/db/diff/diff.integration.test.ts index d3739af49b..4cd6f54b68 100644 --- a/apps/cli/src/commands/db/diff/diff.integration.test.ts +++ b/apps/cli/src/commands/db/diff/diff.integration.test.ts @@ -541,46 +541,54 @@ const PGADMIN_SOURCE_URL = const PGADMIN_TARGET_URL = "postgresql://postgres:postgres@127.0.0.1:54320/postgres"; describe("db diff", () => { - it.effect("diffs local with the default migra engine and prints SQL to stdout", () => { - const s = setup(tmp.current, { diffSql: "create table players ();\n" }); - return Effect.gen(function* () { - yield* dbDiff(flags()); - // The native shadow was created once (one `docker create`) and removed once - // (one `docker rm -f -v`) — see `mockShadowContainerCliSpawner`. - expect(s.shadowSpawned.filter((c) => c.args[0] === "create")).toHaveLength(1); - expect(s.shadowSpawned.filter((c) => c.args[0] === "rm")).toHaveLength(1); - expect(stdout(s.out)).toBe("create table players ();\n\n"); - expect(stderr(s.out)).toContain("Creating shadow database..."); - expect(stderr(s.out)).toContain("Diffing schemas..."); - expect(stderr(s.out)).toContain("Finished supabase db diff on branch"); - expect(s.telemetry.flushed).toBe(true); - // The shadow's PG15+ one-shot platform-baseline job(s) connect to the shadow over - // Docker's embedded DNS using the shadow container's OWN 12-char short id as - // `DB_HOST` — NOT the real `db` container's name, and not some other slice length - // (a mutation from `.slice(0, 12)` to `.slice(0, 8)` must fail this). This is the - // one shadow-specific parameterization that matters - // (`buildShadowSetupDatabaseInput`'s `dbHost`). The default config enables - // realtime (and PG >= 15 by default), so this always exercises at least one - // one-shot job — Realtime's own env sets `DB_HOST` directly; Storage/Auth embed - // the same host inside a `DATABASE_URL`-style connection string instead. - const expectedHost = FAKE_SHADOW_CONTAINER_ID.slice(0, 12); - expect(s.shadowSetupJobCalls.length).toBeGreaterThan(0); - let sawHost = false; - for (const call of s.shadowSetupJobCalls) { - if (call.env["DB_HOST"] !== undefined) { - expect(call.env["DB_HOST"]).toBe(expectedHost); - sawHost = true; - } - for (const value of Object.values(call.env)) { - if (value.includes("@") && value.includes(":")) { - expect(value).toContain(`@${expectedHost}:`); + it.effect( + "diffs local with the default pg-delta engine (no pgdelta config section) and prints SQL to stdout", + () => { + const s = setup(tmp.current, { diffSql: "create table players ();\n" }); + return Effect.gen(function* () { + yield* dbDiff(flags()); + // With NO `[experimental.pgdelta]` section, `enabled` defaults to TRUE + // (CLI-1588), so the diff routes through the pg-delta engine — not migra's + // edge-runtime script. + expect(s.databaseDiffCalls).toHaveLength(1); + expect(s.edgeCalls).toEqual([]); + // The native shadow was created once (one `docker create`) and removed once + // (one `docker rm -f -v`) — see `mockShadowContainerCliSpawner`. + expect(s.shadowSpawned.filter((c) => c.args[0] === "create")).toHaveLength(1); + expect(s.shadowSpawned.filter((c) => c.args[0] === "rm")).toHaveLength(1); + expect(stdout(s.out)).toBe("create table players ();\n\n"); + expect(stderr(s.out)).toContain("Creating shadow database..."); + expect(stderr(s.out)).toContain("Diffing schemas..."); + expect(stderr(s.out)).toContain("Finished supabase db diff on branch"); + expect(s.telemetry.flushed).toBe(true); + // The shadow's PG15+ one-shot platform-baseline job(s) connect to the shadow over + // Docker's embedded DNS using the shadow container's OWN 12-char short id as + // `DB_HOST` — NOT the real `db` container's name, and not some other slice length + // (a mutation from `.slice(0, 12)` to `.slice(0, 8)` must fail this). This is the + // one shadow-specific parameterization that matters + // (`buildShadowSetupDatabaseInput`'s `dbHost`). The default config enables + // realtime (and PG >= 15 by default), so this always exercises at least one + // one-shot job — Realtime's own env sets `DB_HOST` directly; Storage/Auth embed + // the same host inside a `DATABASE_URL`-style connection string instead. + const expectedHost = FAKE_SHADOW_CONTAINER_ID.slice(0, 12); + expect(s.shadowSetupJobCalls.length).toBeGreaterThan(0); + let sawHost = false; + for (const call of s.shadowSetupJobCalls) { + if (call.env["DB_HOST"] !== undefined) { + expect(call.env["DB_HOST"]).toBe(expectedHost); sawHost = true; } + for (const value of Object.values(call.env)) { + if (value.includes("@") && value.includes(":")) { + expect(value).toContain(`@${expectedHost}:`); + sawHost = true; + } + } } - } - expect(sawHost).toBe(true); - }).pipe(Effect.provide(s.layer)); - }); + expect(sawHost).toBe(true); + }).pipe(Effect.provide(s.layer)); + }, + ); it.effect("diffs local with pgdelta when --use-pg-delta is set", () => { const s = setup(tmp.current, { diffSql: "create table p ();\n" }); @@ -616,6 +624,38 @@ describe("db diff", () => { }).pipe(Effect.provide(s.layer)); }); + it.effect("an explicit [experimental.pgdelta] enabled = false selects the migra engine", () => { + mkdirSync(join(tmp.current, "supabase"), { recursive: true }); + writeFileSync( + join(tmp.current, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); + const s = setup(tmp.current, { diffSql: "create table players ();\n" }); + return Effect.gen(function* () { + yield* dbDiff(flags()); + // Explicitly disabling pg-delta opts back into migra: the diff runs through + // migra's edge-runtime script (not pg-delta's `renderPlanFiles` script, and + // not the pg-delta engine service). + expect(s.databaseDiffCalls).toEqual([]); + expect(s.edgeCalls).toHaveLength(1); + expect(s.edgeCalls[0]?.script).not.toContain("renderPlanFiles"); + expect(stdout(s.out)).toBe("create table players ();\n\n"); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("--use-migra overrides the pg-delta default when no pgdelta config exists", () => { + const s = setup(tmp.current, { diffSql: "create table players ();\n" }); + return Effect.gen(function* () { + yield* dbDiff(flags({ useMigra: Option.some(true) })); + // No `[experimental.pgdelta]` section, so pg-delta is the default — the + // `--use-migra` flag must still win and route through migra. + expect(s.databaseDiffCalls).toEqual([]); + expect(s.edgeCalls).toHaveLength(1); + expect(s.edgeCalls[0]?.script).not.toContain("renderPlanFiles"); + expect(stdout(s.out)).toBe("create table players ();\n\n"); + }).pipe(Effect.provide(s.layer)); + }); + it.effect("pg-delta local diff ignores schema_paths and declarative files", () => { mkdirSync(join(tmp.current, "supabase", "schemas"), { recursive: true }); writeFileSync( @@ -1031,7 +1071,9 @@ describe("db diff", () => { writeFileSync(join(tmp.current, "supabase", "schemas", "public.sql"), "select 1;\n"); const s = setup(tmp.current, { diffSql: "create table o ();\n" }); return Effect.gen(function* () { - yield* dbDiff(flags()); + // Migra is opt-in now that pg-delta is the default engine (CLI-1588); + // pg-delta ignores the declarative contrib_regression override entirely. + yield* dbDiff(flags({ useMigra: Option.some(true) })); expect(stdout(s.out)).toBe("create table o ();\n\n"); // The declarative-schema file was migrated into the contrib_regression override. expect(s.shadowConnectedDatabases).toContain("contrib_regression"); @@ -1284,7 +1326,7 @@ describe("db diff", () => { return Effect.gen(function* () { yield* dbDiff(flags({ usePgSchema: Option.some(true) })); // The TS wrapper prints its own deprecation notice pointing at pg-delta / - // the default migra engine, additive to (not a replacement for) the + // the migra rollback, additive to (not a replacement for) the // delegated Go child's own "experimental" warning (unchanged, printed by // the real Go binary rather than this mocked proxy). Assert on a stable // substring so future wording tweaks don't require touching every test site. @@ -1900,7 +1942,9 @@ describe("db diff", () => { it.effect("emits a json envelope with --output-format json (payload-only stdout)", () => { const s = setup(tmp.current, { format: "json", diffSql: "create table j ();\n" }); return Effect.gen(function* () { - yield* dbDiff(flags()); + // Migra is opt-in now that pg-delta is the default engine (CLI-1588); this + // test pins the migra envelope's `engine` value specifically. + yield* dbDiff(flags({ useMigra: Option.some(true) })); // No raw SQL on stdout in machine mode; the envelope carries it instead. expect(stdout(s.out)).toBe(""); const success = s.out.messages.find((m) => m.type === "success"); @@ -1927,7 +1971,9 @@ describe("db diff", () => { "error diffing schema: error running script:\nTypeError: Cannot read properties of undefined (reading 'constraints')\nPGDELTA_SCRIPT_ERROR\n", }); return Effect.gen(function* () { - const exit = yield* dbDiff(flags()).pipe(Effect.exit); + // Migra is opt-in now that pg-delta is the default engine (CLI-1588); the + // crash is injected into migra's edge-runtime script run. + const exit = yield* dbDiff(flags({ useMigra: Option.some(true) })).pipe(Effect.exit); expect(Exit.isFailure(exit)).toBe(true); expect(stderr(s.out)).not.toContain("No schema changes found"); }).pipe(Effect.provide(s.layer)); @@ -1936,8 +1982,9 @@ describe("db diff", () => { it.effect("falls back to the migra Docker image when edge-runtime OOMs", () => { const s = setup(tmp.current, { oom: true, diffSql: "create table fb ();\n", isLocal: true }); return Effect.gen(function* () { + // Migra is opt-in now that pg-delta is the default engine (CLI-1588). // Pass --schema so the fallback does not need a live DB to list schemas. - yield* dbDiff(flags({ schema: ["public"] })); + yield* dbDiff(flags({ useMigra: Option.some(true), schema: ["public"] })); expect(s.dockerCalls).toHaveLength(1); expect(stdout(s.out)).toBe("create table fb ();\n\n"); }).pipe(Effect.provide(s.layer)); @@ -1953,7 +2000,8 @@ describe("db diff", () => { networkId: "my-net", }); return Effect.gen(function* () { - yield* dbDiff(flags({ schema: ["public"] })); + // Migra is opt-in now that pg-delta is the default engine (CLI-1588). + yield* dbDiff(flags({ useMigra: Option.some(true), schema: ["public"] })); expect(s.dockerCalls).toHaveLength(1); expect((s.dockerCalls[0] as { network: unknown }).network).toEqual({ _tag: "named", diff --git a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md index df9f592b0a..58f0f53d7a 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -108,7 +108,6 @@ at all, so nothing is cached for it. | `SUPABASE_USE_SLIM_IMAGES` | resolves the current-pin shadow Postgres, `pg_dump`, PG15+ realtime/storage/auth migrate-job images (migration-style cold shadow), and (for migra) the edge-runtime image from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB, flag-off `15.8.1.085`, and `deno_version = 1` stay on docker.io | no | | `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | | `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline PGDATA is snapshotted to a tar and restored into the next run's fresh container (see Notes) | no | -| `SUPABASE_EXPERIMENTAL_PG_DELTA` | force pg-delta diff engine | no | | `SUPABASE_EXPERIMENTAL` | selects the deprecated in-process structured-dump export (same as `--declarative`) when `--declarative` is not set | no | ## Exit Codes diff --git a/apps/cli/src/commands/db/pull/pull.handler.ts b/apps/cli/src/commands/db/pull/pull.handler.ts index 275a219037..1b1e085431 100644 --- a/apps/cli/src/commands/db/pull/pull.handler.ts +++ b/apps/cli/src/commands/db/pull/pull.handler.ts @@ -45,7 +45,6 @@ import { writeDeclarativeSchemas, } from "../shared/pgdelta.write.ts"; import { - parseBoolEnv, resolveDeclarativeFromArgs, resolvePullDiffEngine, schemaPathsTransitionWarning, @@ -90,8 +89,7 @@ import { import { updateMigrationHistory } from "./pull.sync.ts"; // Established output contract; ends with a `.`. -const DEPRECATION_LINE = - "Flag --use-pg-delta has been deprecated, use --declarative with [experimental.pgdelta] enabled = true in your config.toml instead."; +const DEPRECATION_LINE = "Flag --use-pg-delta has been deprecated, use --declarative instead."; /** * Explains the in-sync non-zero exit. Go prints its generic @@ -349,7 +347,6 @@ export const dbPull = Effect.fn("db.pull")(function* (flags: DbPullFlags, invoke pgDeltaDefault: shouldUsePgDelta({ configEnabled: toml.pgDelta.enabled, usePgDeltaFlag: false, - envEnabled: parseBoolEnv(toml.envLookup("SUPABASE_EXPERIMENTAL_PG_DELTA")), }), }); diff --git a/apps/cli/src/commands/db/pull/pull.integration.test.ts b/apps/cli/src/commands/db/pull/pull.integration.test.ts index 50adf37e5b..43c7c2d472 100644 --- a/apps/cli/src/commands/db/pull/pull.integration.test.ts +++ b/apps/cli/src/commands/db/pull/pull.integration.test.ts @@ -785,9 +785,18 @@ describe("db pull", () => { it.effect("pulls with migra and does not warn about schema_paths", () => { seedMigration(tmp.current, "20240101000000"); + // pg-delta is the default engine now, so migra requires the explicit + // config opt-out. writeFileSync( join(tmp.current, "supabase", "config.toml"), - ["[db.migrations]", 'schema_paths = ["database/*.sql"]', ""].join("\n"), + [ + "[db.migrations]", + 'schema_paths = ["database/*.sql"]', + "", + "[experimental.pgdelta]", + "enabled = false", + "", + ].join("\n"), ); const s = setup(tmp.current, { remoteVersions: ["20240101000000"], @@ -901,7 +910,10 @@ describe("db pull", () => { // config (db pull does not force-enable it), so later db reset/db diff read // the pulled files. mkdirSync(join(tmp.current, "supabase"), { recursive: true }); - writeFileSync(join(tmp.current, "supabase", "config.toml"), "[db]\n"); + writeFileSync( + join(tmp.current, "supabase", "config.toml"), + "[db]\n\n[experimental.pgdelta]\nenabled = false\n", + ); const s = setup(tmp.current, { edgeStdout: EXPORT_JSON }); return Effect.gen(function* () { yield* dbPull(flags({ declarative: Option.some(true) })); @@ -932,7 +944,7 @@ describe("db pull", () => { mkdirSync(join(tmp.current, "supabase"), { recursive: true }); writeFileSync( join(tmp.current, "supabase", "config.toml"), - '[db.migrations]\nschema_paths = [\n "schemas/*.sql",\n]\n', + '[db.migrations]\nschema_paths = [\n "schemas/*.sql",\n]\n\n[experimental.pgdelta]\nenabled = false\n', ); const s = setup(tmp.current, { edgeStdout: EXPORT_JSON }); return Effect.gen(function* () { @@ -1009,6 +1021,13 @@ describe("db pull", () => { // invocation ends false => migration mode + history repair, NOT declarative // export. OR-ing the two parsed flags would wrongly take the declarative path. seedMigration(tmp.current, "20240101000000"); + // The raw-SQL `edgeStdout` below is migra output; opt out of the pg-delta + // default via config (the diff-engine flag is mutually exclusive with the + // declarative alias this test exercises). + writeFileSync( + join(tmp.current, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); const s = setup(tmp.current, { remoteVersions: ["20240101000000"], edgeStdout: "create table remote ();\n", @@ -1026,6 +1045,12 @@ describe("db pull", () => { "--use-pg-delta --declarative=false stays in migration mode (Go last-occurrence-wins)", () => { seedMigration(tmp.current, "20240101000000"); + // Same config opt-out as above: raw-SQL `edgeStdout` is migra output and + // the diff-engine flag would trip the declarative-alias mutual exclusion. + writeFileSync( + join(tmp.current, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); const s = setup(tmp.current, { remoteVersions: ["20240101000000"], edgeStdout: "create table remote ();\n", @@ -1075,7 +1100,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.proxyCalls).toHaveLength(0); expect(s.proxyCaptureCalls).toHaveLength(0); // pg_dump ran with the schema-dump env (internal-schema exclude + comment strip). @@ -1115,7 +1140,7 @@ describe("db pull", () => { edgeStdout: "create table diffed ();\n", }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.proxyCalls).toHaveLength(0); expect(s.proxyCaptureCalls).toHaveLength(0); const success = s.out.messages.find((m) => m.type === "success"); @@ -1146,7 +1171,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - const exit = yield* dbPull(flags()).pipe(Effect.exit); + const exit = yield* dbPull(flags({ diffEngine: Option.some("migra") })).pipe(Effect.exit); expect(Exit.isSuccess(exit)).toBe(true); const dir = join(tmp.current, "supabase", "migrations"); const file = readdirSync(dir).find((f) => f.endsWith("_remote_schema.sql")); @@ -1208,7 +1233,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - const error = yield* dbPull(flags()).pipe(Effect.flip); + const error = yield* dbPull(flags({ diffEngine: Option.some("migra") })).pipe(Effect.flip); expect(error.message).toBe("No schema changes found"); expect(s.dumpCalls).toHaveLength(2); // direct attempt + pooler retry expect(s.historyUpserts).toHaveLength(0); // no migration-history row written @@ -1223,7 +1248,7 @@ describe("db pull", () => { dumpStderr: "connection refused", }); return Effect.gen(function* () { - const error = yield* dbPull(flags()).pipe(Effect.flip); + const error = yield* dbPull(flags({ diffEngine: Option.some("migra") })).pipe(Effect.flip); expect(error.message).toContain("error running container: exit 1"); // The diff pass never ran — the dump failure aborts before provisioning a shadow. expect(s.shadowSpawned.filter((c) => c.args[0] === "create")).toEqual([]); @@ -1242,7 +1267,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.dumpCalls).toHaveLength(2); // direct attempt + pooler retry expect(s.poolerFallbackCalls).toHaveLength(1); const err = streamText(s.out, "stderr"); @@ -1262,7 +1287,7 @@ describe("db pull", () => { poolerAvailable: false, }); return Effect.gen(function* () { - const error = yield* dbPull(flags()).pipe(Effect.flip); + const error = yield* dbPull(flags({ diffEngine: Option.some("migra") })).pipe(Effect.flip); expect(error.message).toContain("error running container: exit 1"); expect(s.poolerFallbackCalls).toHaveLength(1); // gate checked, no pooler resolved expect(streamText(s.out, "stderr")).not.toContain("Retrying via the IPv4 connection pooler"); @@ -1339,7 +1364,7 @@ describe("db pull", () => { promptConfirmResponses: [true], }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(1); }).pipe(Effect.provide(s.layer)); }); @@ -1353,7 +1378,7 @@ describe("db pull", () => { promptConfirmResponses: [false], }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(0); }).pipe(Effect.provide(s.layer)); }); @@ -1371,7 +1396,7 @@ describe("db pull", () => { stdinIsTty: false, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(1); }).pipe(Effect.provide(s.layer)); }); @@ -1388,7 +1413,7 @@ describe("db pull", () => { pipedAnswers: ["n"], }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(0); // Prints the label then echoes the consumed answer. expect(streamText(s.out, "stderr")).toContain( @@ -1406,7 +1431,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(streamText(s.out, "stdout")).not.toContain("Finished supabase db pull."); // Diagnostics still go to stderr in machine mode (the Connecting line is // written regardless of output format); stdout stays payload-only. @@ -1425,7 +1450,7 @@ describe("db pull", () => { // no --yes: a non-interactive prompt falls back to the default (true). }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(1); }).pipe(Effect.provide(s.layer)); }); @@ -1444,7 +1469,7 @@ describe("db pull", () => { stdinIsTty: true, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(1); expect(streamText(s.out, "stderr")).toContain( "Update remote migration history table? [Y/n] y", @@ -1478,7 +1503,7 @@ describe("db pull", () => { pipedAnswers: ["n"], }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(1); }).pipe( Effect.ensuring( @@ -1512,7 +1537,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.dumpCalls.length).toBeGreaterThanOrEqual(1); // The pg_dump container image is rewritten to the configured mirror. expect(s.dumpCalls[0]?.image).toMatch(/^my-mirror\.example\.com\/supabase\//u); @@ -1545,7 +1570,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.dumpCalls.length).toBeGreaterThanOrEqual(1); expect(s.dumpCalls[0]?.network).toEqual({ _tag: "named", name: "dotenv-net" }); }).pipe( @@ -1576,7 +1601,7 @@ describe("db pull", () => { args: ["db", "pull", "--yes=false"], }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(0); }).pipe( Effect.ensuring( @@ -1608,7 +1633,7 @@ describe("db pull", () => { args: ["db", "pull", "--password", "--yes=false"], }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(1); expect(streamText(s.out, "stderr")).toContain( "Update remote migration history table? [Y/n] y", @@ -1745,7 +1770,7 @@ describe("db pull", () => { args: ["db", "pull", "--experimental=false"], }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(streamText(s.out, "stderr")).toContain("Connecting to remote database...\n"); }).pipe( Effect.ensuring( @@ -1841,20 +1866,93 @@ describe("db pull", () => { }, ); - it.effect("a project supabase/.env enabling pg-delta selects the pg-delta engine", () => { - // A project .env must select pg-delta even when the shell env doesn't set it. - // The handler reads it via toml.envLookup, not process.env. + it.effect( + "config enabled = false selects migra even with a stale SUPABASE_EXPERIMENTAL_PG_DELTA opt-in", + () => { + // The explicit config rollback is authoritative: the historical + // SUPABASE_EXPERIMENTAL_PG_DELTA opt-in (here in the project .env) is no + // longer consulted, so it cannot silently defeat `enabled = false`. + seedMigration(tmp.current, "20240101000000"); + writeFileSync( + join(tmp.current, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); + writeFileSync(join(tmp.current, "supabase", ".env"), "SUPABASE_EXPERIMENTAL_PG_DELTA=true\n"); + const s = setup(tmp.current, { + remoteVersions: ["20240101000000"], + edgeStdout: "create table remote ();\n", + yes: true, + }); + return Effect.gen(function* () { + yield* dbPull(flags()); + expect(s.engineCalls).toHaveLength(0); + expect(s.edgeRunCount).toBe(1); + }).pipe(Effect.provide(s.layer)); + }, + ); + + it.effect( + "defaults to the pg-delta engine when config has no [experimental.pgdelta] section", + () => { + // CLI-1588: pg-delta is the default schema diff engine. With no config + // section and no --diff-engine flag, the migration-style pull must call + // the pg-delta engine's diffDatabase, never migra's edge-runtime script. + seedMigration(tmp.current, "20240101000000"); + const s = setup(tmp.current, { + remoteVersions: ["20240101000000"], + edgeStdout: pgDeltaDiffEnvelope([ + { name: "schema_changes", sql: "create table remote ();" }, + ]), + yes: true, + }); + return Effect.gen(function* () { + yield* dbPull(flags()); + expect(s.engineCalls).toHaveLength(1); + expect(s.engineCalls[0]?.operation).toBe("diff"); + expect(s.edgeRunCount).toBe(0); + }).pipe(Effect.provide(s.layer)); + }, + ); + + it.effect("[experimental.pgdelta] enabled = false in config selects the migra engine", () => { + // Explicit config opt-out from the pg-delta default: the pull must run + // migra's edge-runtime diff and never touch the pg-delta engine. Migra + // selection is also proven by the raw-SQL `edgeStdout` being written as a + // migration (pg-delta would fail to JSON.parse it). seedMigration(tmp.current, "20240101000000"); - mkdirSync(join(tmp.current, "supabase"), { recursive: true }); - writeFileSync(join(tmp.current, "supabase", ".env"), "SUPABASE_EXPERIMENTAL_PG_DELTA=true\n"); + writeFileSync( + join(tmp.current, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); const s = setup(tmp.current, { remoteVersions: ["20240101000000"], - edgeStdout: pgDeltaDiffEnvelope([{ name: "schema_changes", sql: "create table remote ();" }]), + edgeStdout: "create table remote ();\n", yes: true, }); return Effect.gen(function* () { yield* dbPull(flags()); - expect(s.engineCalls[0]?.operation).toBe("diff"); + expect(s.engineCalls).toHaveLength(0); + expect(s.edgeRunCount).toBe(1); + const dir = join(tmp.current, "supabase", "migrations"); + expect(readdirSync(dir).some((f) => f.endsWith("_remote_schema.sql"))).toBe(true); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("--diff-engine migra forces migra even when config leaves the default on", () => { + // No config.toml at all, so the config-level default is pg-delta; the + // explicit flag must still win and select migra. + seedMigration(tmp.current, "20240101000000"); + const s = setup(tmp.current, { + remoteVersions: ["20240101000000"], + edgeStdout: "create table remote ();\n", + yes: true, + }); + return Effect.gen(function* () { + yield* dbPull(flags({ diffEngine: Option.some("migra") })); + expect(s.engineCalls).toHaveLength(0); + expect(s.edgeRunCount).toBe(1); + const dir = join(tmp.current, "supabase", "migrations"); + expect(readdirSync(dir).some((f) => f.endsWith("_remote_schema.sql"))).toBe(true); }).pipe(Effect.provide(s.layer)); }); @@ -1889,7 +1987,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - yield* dbPull(flags({ local: Option.some(true) })); + yield* dbPull(flags({ local: Option.some(true), diffEngine: Option.some("migra") })); expect(s.connectedDatabases).toContain("contrib_regression"); // A local target prints the local wording (established output contract). expect(streamText(s.out, "stderr")).toContain("Connecting to local database...\n"); @@ -1963,7 +2061,7 @@ describe("db pull", () => { // no --yes }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.historyUpserts.length).toBe(1); const success = s.out.messages.find((m) => m.type === "success"); expect(success?.data).toMatchObject({ remoteHistoryUpdated: true }); @@ -2070,7 +2168,7 @@ describe("db pull", () => { resolvedRef: "abcdefghijklmnopqrst", }); return Effect.gen(function* () { - yield* dbPull(flags({ linked: Option.some(true) })); + yield* dbPull(flags({ linked: Option.some(true), diffEngine: Option.some("migra") })); const createArgs = s.shadowSpawned.find((c) => c.args[0] === "create")?.args ?? []; expect(createArgs).toContain("--tmpfs"); }).pipe(Effect.provide(s.layer)); diff --git a/apps/cli/src/commands/db/remote/commit/SIDE_EFFECTS.md b/apps/cli/src/commands/db/remote/commit/SIDE_EFFECTS.md index a51ddc3614..7d3d85d4fa 100644 --- a/apps/cli/src/commands/db/remote/commit/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/remote/commit/SIDE_EFFECTS.md @@ -66,5 +66,9 @@ Same envelope as migration-style `db pull`. ## Notes - Deprecated: use `db pull` instead. +- pg-delta is the default shadow-diff engine, running in-process exactly as for + migration-style `db pull`. Rollback is `[experimental.pgdelta] enabled = false` + in `config.toml` — this command has no per-run engine flag, so it always follows + the config default. - `--schema` / `-s` restricts the commit to specific schemas. - `--db-url` and `--linked` are mutually exclusive. diff --git a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md index 9be8bb75b5..e7ea7b59ba 100644 --- a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md @@ -4,7 +4,8 @@ Reinitialises a database from local migrations (plus seed). Both targets are fully native. The **remote** path (`--linked`, or a remote `--db-url`) drops all user schemas, upserts vault secrets, then either re-applies migrations (the default) or, on a versionless `--experimental`/`SUPABASE_EXPERIMENTAL` reset -with pg-delta not enabled, applies the declarative `[db.migrations].schema_paths` +with pg-delta explicitly disabled (`[experimental.pgdelta] enabled = false`; pg-delta +is enabled by default), applies the declarative `[db.migrations].schema_paths` files instead (the `MigrateAndSeed` EXPERIMENTAL branch, CLI-1958), then seeds. The **local** path (`--local`/default, or a `--db-url` pointing at the local stack) is ALSO fully native (CLI-1955 removed the hidden Go `db __db-bootstrap` seam diff --git a/apps/cli/src/commands/db/reset/reset.integration.test.ts b/apps/cli/src/commands/db/reset/reset.integration.test.ts index c3dd5515db..d54058143b 100644 --- a/apps/cli/src/commands/db/reset/reset.integration.test.ts +++ b/apps/cli/src/commands/db/reset/reset.integration.test.ts @@ -1206,7 +1206,7 @@ describe("db reset", () => { // such file exists), failing the whole reset, instead of `supabase/schema.sql` // (where this test actually places the file). const { layer, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n[db]\nmajor_version = 14\n[db.migrations]\nschema_paths = ["schema.sql"]\n', + toml: 'project_id = "test"\n[db]\nmajor_version = 14\n[db.migrations]\nschema_paths = ["schema.sql"]\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schema.sql": "create table schema_paths_marker ();" }, args: ["db", "reset", "--local"], isLocal: true, @@ -1684,7 +1684,7 @@ describe("db reset", () => { // order ACROSS patterns (no global re-sort) — `zz/*.sql`'s files // must all run before `aa/*.sql`'s, even though "aa" sorts before "zz". const { layer, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["zz/*.sql", "aa/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["zz/*.sql", "aa/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/zz/b.sql": "create table zz_b ();", "supabase/zz/a.sql": "create table zz_a ();", @@ -1711,7 +1711,7 @@ describe("db reset", () => { // (not a plain-files glob), which expands a directory match to its // regular `.sql` files, recursively — unlike a plain glob pattern. const { layer, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["some-dir"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["some-dir"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/some-dir/01_top.sql": "create table dir_top ();", "supabase/some-dir/nested/02_nested.sql": "create table dir_nested ();", @@ -1734,7 +1734,7 @@ describe("db reset", () => { // schema-files apply is a silent no-op — it does NOT fall back to // replaying migrations (a hard if/else-if). const { layer, out, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n', + toml: 'project_id = "test"\n\n[experimental.pgdelta]\nenabled = false\n', files: migrationFile("20240101000000", "create table migrated_table ();"), experimental: true, confirm: [true], @@ -1773,11 +1773,36 @@ describe("db reset", () => { }, ); + it.live( + "replays migrations instead of schema files on an experimental remote reset with no pgdelta config section (pg-delta default, CLI-1588)", + () => { + // With NO `[experimental.pgdelta]` section, `enabled` now defaults to + // TRUE, so an experimental versionless remote reset suppresses the + // schema-files branch and replays migrations — only an explicit + // `enabled = false` re-enables the schema-files path. + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n', + files: { + "supabase/schemas/01_users.sql": "create table schema_users ();", + ...migrationFile("20240101000000", "create table migrated_table ();"), + }, + experimental: true, + confirm: [true], + }); + return Effect.gen(function* () { + yield* dbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(conn.execs.some((s) => s.includes("create table migrated_table"))).toBe(true); + expect(conn.execs.some((s) => s.includes("create table schema_users"))).toBe(false); + expect(out.stderrText).toContain("Applying migration"); + }); + }, + ); + it.live( "replays migrations instead of schema files on an experimental remote reset with a resolved version", () => { const { layer, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schemas/01_users.sql": "create table schema_users ();", ...migrationFile("20240101000000", "create table migrated_table ();"), @@ -1803,7 +1828,7 @@ describe("db reset", () => { "fails an experimental remote reset when no schema_paths pattern matches anything", () => { const { layer, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["nomatch/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["nomatch/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', experimental: true, confirm: [true], }); @@ -1829,7 +1854,7 @@ describe("db reset", () => { // The joined glob error only surfaces when NO pattern matched anything // at all; a partial failure is silently dropped. const { layer, out, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql", "typo/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql", "typo/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schemas/01_users.sql": "create table schema_users ();", // Present so the (unrelated) seed glob's own "no files matched" WARN line @@ -1850,7 +1875,7 @@ describe("db reset", () => { "attaches Go's schema-file suggestion when a schema file fails to apply on an experimental remote reset", () => { const { layer } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schemas/01_users.sql": "not valid sql;" }, experimental: true, confirm: [true], @@ -1885,7 +1910,7 @@ describe("db reset", () => { // must fail WITHOUT the suggestion, unlike the exec-failure case above. const schemaFile = join(tmp.current, "supabase", "schemas", "01_users.sql"); const { layer, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schemas/01_users.sql": "create table schema_users ();" }, experimental: true, confirm: [true], @@ -1917,7 +1942,7 @@ describe("db reset", () => { // having applied nothing. const schemasDir = join(tmp.current, "supabase", "schemas"); const { layer, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schemas/01_users.sql": "create table schema_users ();" }, experimental: true, confirm: [true], @@ -1950,7 +1975,7 @@ describe("db reset", () => { const previous = process.env["SUPABASE_EXPERIMENTAL"]; delete process.env["SUPABASE_EXPERIMENTAL"]; const { layer, out, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/.env": "SUPABASE_EXPERIMENTAL=true\n", "supabase/schemas/01_users.sql": "create table schema_users ();", @@ -1996,7 +2021,7 @@ describe("db reset", () => { "applies configured schema files and skips seeding on an experimental remote --db-url reset", () => { const { layer, conn, resolver } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schemas/01_users.sql": "create table schema_users ();" }, experimental: true, args: ["db", "reset", "--db-url", "postgresql://db.example.com:5432/postgres"], @@ -2213,7 +2238,7 @@ describe("db reset", () => { // branch of the migrate-and-seed step ran — seeding sits outside the // if/else-if, and the seed override is resolved entirely upstream of it. const { layer, out, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n', + toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schemas/01_users.sql": "create table schema_users ();", "supabase/custom-seed.sql": "insert into t values (2);", diff --git a/apps/cli/src/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts b/apps/cli/src/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts index 66311e5435..945a83612c 100644 --- a/apps/cli/src/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts +++ b/apps/cli/src/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts @@ -39,7 +39,6 @@ const ctx = (cwd: string, declarativeDir: string): DeclarativeRunContext => ({ // write a `config.toml`). const toml: DbTomlValues = { projectEnv: {}, - envLookup: () => undefined, apiSchemas: ["public", "graphql_public"], port: 54322, shadowPort: 54320, @@ -50,7 +49,7 @@ const toml: DbTomlValues = { orioledbVersion: Option.none(), denoVersion: 2, pgDelta: { - enabled: false, + enabled: true, declarativeSchemaPath: Option.none(), formatOptions: Option.none(), }, diff --git a/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md b/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md index 78945c5395..520aa3f9df 100644 --- a/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md @@ -49,17 +49,17 @@ formatting without disabling safe compaction. ## Exit Codes -| Code | Condition | -| ---- | --------------------------------------------------------------------- | -| `0` | success (files written, or skipped after a declined prompt) | -| `1` | pg-delta not enabled (no `--experimental` / `[experimental.pgdelta]`) | -| `1` | conflicting `--db-url`/`--linked`/`--local` (mutually exclusive) | -| `1` | non-interactive mode with no explicit target | -| `1` | local-database bring-up / pg-delta engine / export failure | +| Code | Condition | +| ---- | ------------------------------------------------------------------------------------ | +| `0` | success (files written, or skipped after a declined prompt) | +| `1` | pg-delta disabled (`[experimental.pgdelta] enabled = false` and no `--experimental`) | +| `1` | conflicting `--db-url`/`--linked`/`--local` (mutually exclusive) | +| `1` | non-interactive mode with no explicit target | +| `1` | local-database bring-up / pg-delta engine / export failure | The pg-delta gate and the mutex check are both raised before any side effects run, but the gate wins when both conditions apply simultaneously: the gate check runs -first, so a closed gate (missing `--experimental`) surfaces before a +first, so a closed gate (`enabled = false` and no `--experimental`) surfaces before a `--db-url`/`--linked`/`--local` conflict is ever checked. ## Output @@ -76,7 +76,8 @@ always go to stderr, in every `--output-format`. On success: ## Notes -- Requires `--experimental` or `[experimental.pgdelta] enabled = true`. +- pg-delta is on by default. The gate closes only when + `[experimental.pgdelta] enabled = false` and `--experimental` is omitted. - `--db-url` / `--linked` / `--local` are mutually exclusive; absent all three, smart mode prompts (existing-files overwrite → Local/Custom choice + reset offer). - `--output-dir ` selects a destination for this invocation without changing diff --git a/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts b/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts index 8877e5035a..07d2ebb5c2 100644 --- a/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts +++ b/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts @@ -269,10 +269,24 @@ const flags = ( const failError = (exit: Exit.Exit) => Exit.isFailure(exit) ? exit.cause.reasons.find(Cause.isFailReason)?.error : undefined; +// pg-delta is the default schema diff engine (CLI-1588): an absent +// `[experimental.pgdelta]` section resolves to enabled = true, so gate-closed +// scenarios must now disable it explicitly. +const seedPgDeltaDisabledConfig = (workdir: string) => { + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); +}; + describe("db schema declarative generate integration", () => { const tmp = useTempWorkdir(); - it.effect("gate: fails when neither --experimental nor config enables pg-delta", () => { + it.effect("gate: fails when config disables pg-delta and --experimental is not passed", () => { + // pg-delta is the default engine (CLI-1588): the gate only closes when the + // config EXPLICITLY sets `enabled = false` and --experimental is absent. + seedPgDeltaDisabledConfig(tmp.current); const { layer } = setup(tmp.current, { experimental: false }); return Effect.gen(function* () { const exit = yield* Effect.exit( @@ -283,6 +297,19 @@ describe("db schema declarative generate integration", () => { }).pipe(Effect.provide(layer)); }); + it.effect( + "gate: open by default — no [experimental.pgdelta] section and no --experimental", + () => { + // The pg-delta default flip (CLI-1588): an absent section resolves to + // enabled = true, so generate proceeds without --experimental. + const s = setup(tmp.current, { experimental: false }); + return Effect.gen(function* () { + yield* dbSchemaDeclarativeGenerate(flags({ local: Option.some(true) })); + expect(s.engineExportCalls).toHaveLength(1); + }).pipe(Effect.provide(s.layer)); + }, + ); + it.effect("--local --linked with --experimental fails with the mutex error", () => { // Go's declarative PersistentPreRunE gate (db_schema_declarative.go:49-99) runs // BEFORE cobra's ValidateFlagGroups() mutex check (cobra@v1.10.2/command.go:985, @@ -306,7 +333,9 @@ describe("db schema declarative generate integration", () => { () => { // Mirrors storage's experimental-gate-vs-mutex ordering fix (CLI-1855 / CLI-1876): // the pg-delta gate runs before the mutex check, so an unopened gate wins even - // when the flags would also violate mutual exclusivity. + // when the flags would also violate mutual exclusivity. Closing the gate now + // requires an explicit `enabled = false` (pg-delta default flip, CLI-1588). + seedPgDeltaDisabledConfig(tmp.current); const { layer } = setup(tmp.current, { experimental: false }); return Effect.gen(function* () { const exit = yield* Effect.exit( @@ -356,7 +385,9 @@ describe("db schema declarative generate integration", () => { // viper's bound-pflag lookup returns the flag value whenever Changed is true — // BEFORE falling back to AutomaticEnv (viper@v1.21.0/viper.go:1176-1178) — so an // explicit --experimental=false must win over SUPABASE_EXPERIMENTAL=1, closing the - // gate instead of letting the env value override it. + // gate instead of letting the env value override it. The config must disable + // pg-delta explicitly, or the new default (CLI-1588) keeps the gate open anyway. + seedPgDeltaDisabledConfig(tmp.current); const { layer } = setup(tmp.current, { experimental: false, args: ["db", "schema", "declarative", "generate", "--experimental=false"], @@ -741,13 +772,17 @@ describe("db schema declarative generate integration", () => { () => { // Go gates pg-delta on the base LoadConfig (declarative PersistentPreRunE) before the // root ParseDatabaseConfig reloads the remote block, so a remote enabled=true must NOT - // enable a base-disabled command without --experimental. + // enable a base-disabled command without --experimental. The base disables pg-delta + // explicitly (an absent section would be enabled by default since CLI-1588), keeping + // the subject — the gate reads the BASE config — testable. const ref = "abcdefghijklmnopqrst"; mkdirSync(join(tmp.current, "supabase"), { recursive: true }); writeFileSync( join(tmp.current, "supabase", "config.toml"), [ 'project_id = "base"', + "[experimental.pgdelta]", + "enabled = false", "[remotes.prod]", `project_id = "${ref}"`, "[remotes.prod.experimental.pgdelta]", diff --git a/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md b/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md index bebbb3fbb9..d75ad67c8a 100644 --- a/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md @@ -67,7 +67,7 @@ disabling safe compaction. The pg-delta gate and the mutex check are both raised before any side effects run, but the gate wins when both conditions apply simultaneously: the gate check runs -first, so a closed gate (missing `--experimental`) surfaces before an +first, so a closed gate (`enabled = false` and no `--experimental`) surfaces before an `--apply`/`--no-apply` conflict is ever checked. ## Output @@ -100,7 +100,8 @@ existing SQL or creates an export manifest. ## Notes -- Requires `--experimental` or `[experimental.pgdelta] enabled = true`. +- pg-delta is on by default. The gate closes only when + `[experimental.pgdelta] enabled = false` and `--experimental` is omitted. - `--file` sets the migration filename stem (default `declarative_sync`); `--name` overrides it. In a TTY without `--name`/`--yes`, the name is prompted. - When no declarative files exist, a TTY offers to generate them (from local) first. diff --git a/apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts b/apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts index 95b10245c9..570c7c3062 100644 --- a/apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts +++ b/apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts @@ -124,34 +124,49 @@ function setup(workdir: string, opts: SetupOpts = {}) { // expects to stay empty until the REAL local-apply connection // (`applyMigrationToLocal`, `toml.port`) runs. const SHADOW_PORT = 54320; + // `applyFails` fails only the FIRST attempt at the migration's ALTER statement. + // With pg-delta enabled by default (CLI-1588), the recovery reset's in-process + // `resetLocalDatabase` replays timestamped migration files natively + // (`migrateAndSeed`'s migrations branch — it no longer takes the + // experimental schema-files branch, whose empty `schema_paths = []` default + // applied nothing), so the reapply on the freshly reset database must succeed, + // mirroring a failure that a reset actually recovers from. + let applyFailed = false; const dbConn = Layer.succeed(DbConnection, { connect: (cfg: PgConnInput) => Effect.succeed({ exec: (sql: string) => - opts.applyFails === true && sql.startsWith("ALTER") - ? Effect.fail({ _tag: "DbExecError", message: "boom" } as never) - : Effect.sync(() => { - if (cfg.port !== SHADOW_PORT) dbExec.push(sql); - }), - execBatch: (statements: ReadonlyArray) => { - const sql = statements.map((statement) => statement.sql); - const failureIndex = - opts.applyFails === true - ? sql.findIndex((statement) => statement.startsWith("ALTER")) - : -1; - return failureIndex >= 0 - ? Effect.fail({ + Effect.suspend(() => { + if (opts.applyFails === true && !applyFailed && sql.startsWith("ALTER")) { + applyFailed = true; + return Effect.fail({ _tag: "DbExecError", message: "boom" } as never); + } + return Effect.sync(() => { + if (cfg.port !== SHADOW_PORT) dbExec.push(sql); + }); + }), + execBatch: (statements: ReadonlyArray) => + Effect.suspend(() => { + const sql = statements.map((statement) => statement.sql); + const failureIndex = + opts.applyFails === true && !applyFailed + ? sql.findIndex((statement) => statement.startsWith("ALTER")) + : -1; + if (failureIndex >= 0) { + applyFailed = true; + return Effect.fail({ _tag: "DbExecError", message: "boom", statementIndex: failureIndex, - } as never) - : Effect.sync(() => { - if (cfg.port !== SHADOW_PORT) { - dbBatches.push(sql); - dbExec.push(...sql); - } - }); - }, + } as never); + } + return Effect.sync(() => { + if (cfg.port !== SHADOW_PORT) { + dbBatches.push(sql); + dbExec.push(...sql); + } + }); + }), query: (sql: string) => Effect.sync(() => { if (cfg.port !== SHADOW_PORT) dbExec.push(sql); @@ -305,6 +320,17 @@ const seedDeclarative = (workdir: string) => { writeFileSync(join(dir, "public.sql"), "create table a();"); }; +// pg-delta is the default schema diff engine (CLI-1588): an absent +// `[experimental.pgdelta]` section resolves to enabled = true, so gate-closed +// scenarios must now disable it explicitly. +const seedPgDeltaDisabledConfig = (workdir: string) => { + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); +}; + const seedUuidDeclarative = (workdir: string, directory = "schemas") => { const dir = join(workdir, "supabase", directory); mkdirSync(join(dir, "schemas", "app", "tables"), { recursive: true }); @@ -348,8 +374,11 @@ describe("db schema declarative sync integration", () => { const tmp = useTempWorkdir(); useShadowCacheDisabled(); - it.effect("gate: fails when pg-delta is not enabled", () => { + it.effect("gate: fails when config disables pg-delta and --experimental is not passed", () => { + // pg-delta is the default engine (CLI-1588): the gate only closes when the + // config EXPLICITLY sets `enabled = false` and --experimental is absent. seedDeclarative(tmp.current); + seedPgDeltaDisabledConfig(tmp.current); const { layer } = setup(tmp.current, { experimental: false }); return Effect.gen(function* () { const exit = yield* Effect.exit(dbSchemaDeclarativeSync(flags())); @@ -357,6 +386,20 @@ describe("db schema declarative sync integration", () => { }).pipe(Effect.provide(layer)); }); + it.effect( + "gate: open by default — no [experimental.pgdelta] section and no --experimental", + () => { + // The pg-delta default flip (CLI-1588): an absent section resolves to + // enabled = true, so sync proceeds without --experimental. + seedDeclarative(tmp.current); + const s = setup(tmp.current, { experimental: false, diffSql: "" }); + return Effect.gen(function* () { + yield* dbSchemaDeclarativeSync(flags({ noApply: Option.some(true) })); + expect(s.out.rawChunks.some((c) => c.text.includes("No schema changes found"))).toBe(true); + }).pipe(Effect.provide(s.layer)); + }, + ); + it.effect("--apply and --no-apply together with --experimental fail with the mutex error", () => { // Go's declarative PersistentPreRunE gate (db_schema_declarative.go:49-99) runs // BEFORE cobra's ValidateFlagGroups() mutex check (cobra@v1.10.2/command.go:985, @@ -380,7 +423,9 @@ describe("db schema declarative sync integration", () => { () => { // Mirrors storage's experimental-gate-vs-mutex ordering fix (CLI-1855 / CLI-1876): // the pg-delta gate runs before the mutex check, so an unopened gate wins even - // when the flags would also violate mutual exclusivity. + // when the flags would also violate mutual exclusivity. Closing the gate now + // requires an explicit `enabled = false` (pg-delta default flip, CLI-1588). + seedPgDeltaDisabledConfig(tmp.current); const { layer } = setup(tmp.current, { experimental: false }); return Effect.gen(function* () { const exit = yield* Effect.exit( @@ -426,7 +471,9 @@ describe("db schema declarative sync integration", () => { // viper's bound-pflag lookup returns the flag value whenever Changed is true — // BEFORE falling back to AutomaticEnv (viper@v1.21.0/viper.go:1176-1178) — so an // explicit --experimental=false must win over SUPABASE_EXPERIMENTAL=1, closing the - // gate instead of letting the env value override it. + // gate instead of letting the env value override it. The config must disable + // pg-delta explicitly, or the new default (CLI-1588) keeps the gate open anyway. + seedPgDeltaDisabledConfig(tmp.current); const { layer } = setup(tmp.current, { experimental: false, args: ["db", "schema", "declarative", "sync", "--experimental=false"], @@ -1252,6 +1299,10 @@ describe("db schema declarative sync integration", () => { // effect, not just a tracked call. expect(localResetRemovedContainers(s.child.spawned)).toContain("supabase_db_test"); expect(localResetCreateArgs(s.child.spawned)).not.toBeUndefined(); + // With pg-delta enabled by default (CLI-1588), the reset replays the + // just-written migration file natively — the reapply succeeds on the + // freshly reset database. + expect(s.dbExec.some((sql) => sql.includes("ALTER TABLE a ADD COLUMN b int"))).toBe(true); expect(s.out.rawChunks.some((c) => c.text.includes("Resetting local database"))).toBe(true); expect( s.out.rawChunks.some((c) => diff --git a/apps/cli/src/commands/db/shared/pgdelta-engine.next.layer.integration.test.ts b/apps/cli/src/commands/db/shared/pgdelta-engine.next.layer.integration.test.ts index daa26fe648..47cb2bfea7 100644 --- a/apps/cli/src/commands/db/shared/pgdelta-engine.next.layer.integration.test.ts +++ b/apps/cli/src/commands/db/shared/pgdelta-engine.next.layer.integration.test.ts @@ -26,7 +26,6 @@ const common = { const toml: DbTomlValues = { projectEnv: {}, - envLookup: () => undefined, apiSchemas: ["public", "graphql_public"], port: 54322, shadowPort: 54320, diff --git a/apps/cli/src/docs/docs-spec.tables.ts b/apps/cli/src/docs/docs-spec.tables.ts index 44a41a173a..bcd81e5010 100644 --- a/apps/cli/src/docs/docs-spec.tables.ts +++ b/apps/cli/src/docs/docs-spec.tables.ts @@ -144,12 +144,11 @@ export const DOCS_DEFAULT_OVERRIDES: Readonly> = { "supabase-db-advisors local": "true", "supabase-db-advisors type": "all", "supabase-db-diff local": "true", - "supabase-db-diff use-migra": "true", "supabase-db-dump linked": "true", "supabase-db-lint fail-on": "none", "supabase-db-lint level": "warning", "supabase-db-lint local": "true", - "supabase-db-pull diff-engine": "migra", + "supabase-db-pull diff-engine": "pg-delta", "supabase-db-pull linked": "true", "supabase-db-push linked": "true", "supabase-db-query local": "true", diff --git a/apps/cli/src/shared/init/project-init.templates.ts b/apps/cli/src/shared/init/project-init.templates.ts index b114e50c39..2bd93a1e87 100644 --- a/apps/cli/src/shared/init/project-init.templates.ts +++ b/apps/cli/src/shared/init/project-init.templates.ts @@ -403,7 +403,7 @@ s3_access_key = "env(S3_ACCESS_KEY)" # Configures AWS_SECRET_ACCESS_KEY for S3 bucket s3_secret_key = "env(S3_SECRET_KEY)" -# pg-delta is the schema diff engine for db diff / db pull / db remote commit. +# pg-delta is the default schema diff engine for db diff / db pull / db remote commit. # Set enabled = false to fall back to the legacy migra engine. [experimental.pgdelta] enabled = true diff --git a/apps/cli/src/shared/init/project-init.templates.unit.test.ts b/apps/cli/src/shared/init/project-init.templates.unit.test.ts index 611379fc2f..2a1ac0748f 100644 --- a/apps/cli/src/shared/init/project-init.templates.unit.test.ts +++ b/apps/cli/src/shared/init/project-init.templates.unit.test.ts @@ -42,14 +42,9 @@ function resolveGoTemplateEscapes(template: string): string { // Emulates what Go's config.Eject writes to disk for a fresh `supabase init` project. function renderExpectedGoEject(): string { - return ( - resolveGoTemplateEscapes(readGoTemplate("pkg", "config", "templates", "config.toml")) - .replace("{{ .ProjectId }}", "demo-project") - .replace("{{ .Experimental.OrioleDBVersion }}", "15.1.0.150") - // supabase init always opts new projects into pg-delta; the Go template renders - // this from a flag set only on the init path (false when deriving defaults). - .replace("{{ .Experimental.PgDeltaInitEnabled }}", "true") - ); + return resolveGoTemplateEscapes(readGoTemplate("pkg", "config", "templates", "config.toml")) + .replace("{{ .ProjectId }}", "demo-project") + .replace("{{ .Experimental.OrioleDBVersion }}", "15.1.0.150"); } // The residual Go scaffold still describes `auto_expose_new_tables` as unset-means-revoked and diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 41bfb54b32..b9e3497cce 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -2385,8 +2385,8 @@ "properties": { "enabled": { "type": "boolean", - "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", - "default": false + "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit (the default). Set false to fall back to the legacy migra engine.", + "default": true }, "declarative_schema_path": { "type": "string", @@ -4829,8 +4829,8 @@ "properties": { "enabled": { "type": "boolean", - "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", - "default": false + "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit (the default). Set false to fall back to the legacy migra engine.", + "default": true }, "declarative_schema_path": { "type": "string", diff --git a/apps/docs/public/cli/project-config.schema.json b/apps/docs/public/cli/project-config.schema.json index 73b6747a72..9f13826477 100644 --- a/apps/docs/public/cli/project-config.schema.json +++ b/apps/docs/public/cli/project-config.schema.json @@ -1918,8 +1918,8 @@ "properties": { "enabled": { "type": "boolean", - "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", - "default": false + "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit (the default). Set false to fall back to the legacy migra engine.", + "default": true }, "declarative_schema_path": { "type": "string", diff --git a/packages/config/src/experimental.ts b/packages/config/src/experimental.ts index 7a04fec9ab..1ed1613cdb 100644 --- a/packages/config/src/experimental.ts +++ b/packages/config/src/experimental.ts @@ -78,11 +78,11 @@ export const experimental = Schema.Struct({ pgdelta: Schema.optionalKey( Schema.Struct({ enabled: Schema.Boolean.annotate({ - default: false, + default: true, description: - "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", + "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit (the default). Set false to fall back to the legacy migra engine.", tags, - }).pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))), + }).pipe(Schema.withDecodingDefaultKey(Effect.succeed(true))), declarative_schema_path: Schema.optionalKey( Schema.String.annotate({ description: "Directory under supabase/ where declarative schema files are written.",