diff --git a/apps/cli/docs/stack-commands.md b/apps/cli/docs/stack-commands.md index 5cb8d0284b..43695ae3e9 100644 --- a/apps/cli/docs/stack-commands.md +++ b/apps/cli/docs/stack-commands.md @@ -187,7 +187,7 @@ A `--db-url` that matches `config.toml` host and port is still rewritten like a stack target for dump's tool container. Compose names (`supabase_db_*`, `supabase_network_*`, `db:5432`) are not used. The stack backend requires the in-process pg-delta engine; `--use-migra`, `--use-pgadmin`, `--use-pg-schema`, and `db pull --diff-engine migra` are -rejected. The flag also routes local `functions serve` through the stack; other `functions` +rejected (`--use-migra=false` and `--use-pgadmin=false` keep pg-delta and are accepted). The flag also routes local `functions serve` through the stack; other `functions` commands retain their existing behavior. `functions serve` requires a running project stack. It attaches to the existing Functions member, diff --git a/apps/cli/docs/supabase/db/diff.md b/apps/cli/docs/supabase/db/diff.md index 6ad22f7e1a..c46ab5499f 100644 --- a/apps/cli/docs/supabase/db/diff.md +++ b/apps/cli/docs/supabase/db/diff.md @@ -4,13 +4,17 @@ 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. + +When migra diffs the local database and declarative schema files exist, it diffs against those files instead of the live database, choosing them in this order: `[db.migrations] schema_paths` when set; otherwise the `declarative_schema_path` directory while `[experimental.pgdelta] enabled` is true (so with `--use-migra` and the default config); otherwise `supabase/schemas`. Setting `enabled = false` therefore makes migra skip a custom `declarative_schema_path` and fall back to `supabase/schemas`. + +With pg-delta, `[db.migrations] schema_paths` and declarative schema files (such as `supabase/schemas`) are not inputs: `db diff` always compares local migrations with the selected database. `db diff -f` prints a note, with next steps, when it finds declarative files it did not read. To generate a migration from declarative schema files, use `supabase db schema declarative sync`. 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..bc0f2ccae6 100644 --- a/apps/cli/docs/supabase/db/pull.md +++ b/apps/cli/docs/supabase/db/pull.md @@ -8,24 +8,24 @@ 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. With pg-delta, `[db.migrations] schema_paths` does not affect this workflow (migra still reads it, for local targets only); use `supabase db schema declarative sync` for declarative schema files. 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. ## Debugging empty pg-delta pulls -If `db pull --diff-engine pg-delta` reports `No schema changes found` but you expect schema output, set `PGDELTA_DEBUG=1` before running the command. Unlike `--debug`, this keeps SSL enabled for remote Supabase connections. +If `db pull` reports `No schema changes found` but you expect schema output, set `PGDELTA_DEBUG=1` before running the command. Unlike `--debug`, this keeps SSL enabled for remote Supabase connections. ```sh -PGDELTA_DEBUG=1 supabase db pull --db-url "$DATABASE_URL" --diff-engine pg-delta +PGDELTA_DEBUG=1 supabase db pull --db-url "$DATABASE_URL" ``` When pg-delta returns zero statements, the CLI writes a debug bundle under `supabase/.temp/pgdelta/v2/debug/-diff/` containing the source/desired catalog snapshots, the plan, and coverage diagnostics. Catalog files are not written during normal `db pull` runs. diff --git a/apps/cli/docs/supabase/db/schema-declarative-generate.md b/apps/cli/docs/supabase/db/schema-declarative-generate.md index bed6a0dd8b..92313503b3 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. +Enabled by default. When `[experimental.pgdelta] enabled = false` is set in config, pass the `--experimental` flag to run it. diff --git a/apps/cli/docs/supabase/db/schema-declarative-sync.md b/apps/cli/docs/supabase/db/schema-declarative-sync.md index 1932b16f11..ea7e7faa23 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. +Enabled by default. When `[experimental.pgdelta] enabled = false` is set in config, pass the `--experimental` flag to run it. 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 d1532c6adb..db572cd54e 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.ts @@ -39,12 +39,6 @@ type EnvLookup = (name: string) => string | undefined; */ export interface DbTomlValues { readonly projectEnv: Readonly>; - /** - * Resolves a `SUPABASE_*` env var: shell env (non-empty) wins, then the - * loaded project `.env*` files (non-empty), else `undefined`. Handlers must - * call this rather than reading `process.env` directly. - */ - readonly envLookup: (name: string) => string | undefined; readonly apiSchemas: ReadonlyArray; /** `[db] port`, default 54322 (`packages/config/src/db.ts`). */ readonly port: number; @@ -159,7 +153,7 @@ interface BaselineTomlConfig { /** The `[experimental.pgdelta]` subtree. */ export interface PgDeltaTomlConfig { - /** `[experimental.pgdelta] enabled`, default false. */ + /** `[experimental.pgdelta] enabled`, default true. */ readonly enabled: boolean; /** * `[experimental.pgdelta] declarative_schema_path`, resolved to a @@ -1383,7 +1377,7 @@ const readDbTomlCore = Effect.fnUntraced(function* ( ? undefined : envOverride("SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED"); // `"1"` counts as true and a malformed value aborts the load. The env override wins, - // then the TOML bool, then an `env(VAR)` string, defaulting to false when absent. + // then the TOML bool, then an `env(VAR)` string, defaulting to true when absent. let enabled: boolean; if (enabledEnv !== undefined) { // An `env(VAR)` indirection in the override is expanded before the bool parse. @@ -1413,7 +1407,7 @@ const readDbTomlCore = Effect.fnUntraced(function* ( } enabled = parsed; } else { - enabled = false; + enabled = true; } const declarativeSchemaPathRaw = pgDeltaRaw?.["declarative_schema_path"]; @@ -2139,7 +2133,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 a4a58f2147..2a547f3c7d 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 @@ -933,12 +933,12 @@ describe("readDbToml", () => { it.effect("SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED still wins when the block omits pgdelta", () => { 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( @@ -2620,12 +2620,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 }); @@ -2638,7 +2638,7 @@ describe("readDbToml [experimental.pgdelta]", () => { const dir = withConfig( [ "[experimental.pgdelta]", - "enabled = true", + "enabled = false", 'declarative_schema_path = "./db/decl"', 'format_options = "{\\"keywordCase\\":\\"upper\\",\\"indent\\":2}"', "", @@ -2647,7 +2647,7 @@ describe("readDbToml [experimental.pgdelta]", () => { return read(dir).pipe( Effect.tap((v) => Effect.sync(() => { - expect(v.pgDelta.enabled).toBe(true); + expect(v.pgDelta.enabled).toBe(false); expect(Option.getOrNull(v.pgDelta.declarativeSchemaPath)).toBe( join("supabase", "db", "decl"), ); @@ -2660,19 +2660,23 @@ describe("readDbToml [experimental.pgdelta]", () => { ); }); - it.effect("keeps an absolute declarative_schema_path unchanged", () => { - const dir = withConfig( - ["[experimental.pgdelta]", 'declarative_schema_path = "/abs/decl"', ""].join("\n"), - ); - return read(dir).pipe( - Effect.tap((v) => - Effect.sync(() => { - expect(Option.getOrNull(v.pgDelta.declarativeSchemaPath)).toBe("/abs/decl"); - rmSync(dir, { recursive: true, force: true }); - }), - ), - ); - }); + it.effect( + "keeps an absolute declarative_schema_path and enables pg-delta when enabled is omitted", + () => { + const dir = withConfig( + ["[experimental.pgdelta]", 'declarative_schema_path = "/abs/decl"', ""].join("\n"), + ); + return read(dir).pipe( + Effect.tap((v) => + Effect.sync(() => { + expect(v.pgDelta.enabled).toBe(true); + expect(Option.getOrNull(v.pgDelta.declarativeSchemaPath)).toBe("/abs/decl"); + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }, + ); }); describe("resolveDeclarativeDir", () => { diff --git a/apps/cli/src/command-internal/db-pull-run.ts b/apps/cli/src/command-internal/db-pull-run.ts index a583aa810e..eaaaae1fb5 100644 --- a/apps/cli/src/command-internal/db-pull-run.ts +++ b/apps/cli/src/command-internal/db-pull-run.ts @@ -37,7 +37,6 @@ import { writeDeclarativeSchemas, } from "../commands/db/shared/pgdelta.write.ts"; import { - parseBoolEnv, resolveDeclarativeFromArgs, resolvePullDiffEngine, schemaPathsTransitionWarning, @@ -89,8 +88,7 @@ import { updateMigrationHistory } from "../commands/db/pull/pull.sync.ts"; export type { DbPullFlags }; // 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 instead of the generic "Try rerunning the command with @@ -372,7 +370,6 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( shouldUsePgDelta({ configEnabled: toml.pgDelta.enabled, usePgDeltaFlag: false, - envEnabled: parseBoolEnv(toml.envLookup("SUPABASE_EXPERIMENTAL_PG_DELTA")), }), }); if (Option.getOrElse(flags.diffEngine, () => "pg-delta") === "migra") { diff --git a/apps/cli/src/command-internal/diff-engine.ts b/apps/cli/src/command-internal/diff-engine.ts index b88a455853..d6bc7cf89b 100644 --- a/apps/cli/src/command-internal/diff-engine.ts +++ b/apps/cli/src/command-internal/diff-engine.ts @@ -2,32 +2,30 @@ // dependencies — unit-tested directly. 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"; + "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. `supabase db schema declarative sync` reads declarative_schema_path (default supabase/schemas), not schema_paths: move those files there or point declarative_schema_path at them. Only migra (--use-migra, --diff-engine migra, or [experimental.pgdelta] enabled = false) still reads schema_paths, and only for local targets.\n"; /** - * Whether pg-delta is the active default engine: the resolved config flag - * (`[experimental.pgdelta].enabled`), the command's `--use-pg-delta` flag, or the - * `SUPABASE_EXPERIMENTAL_PG_DELTA` env var, whichever is set. + * Whether pg-delta is the active engine: `[experimental.pgdelta].enabled` (default true) or the + * command's `--use-pg-delta` flag. `SUPABASE_EXPERIMENTAL_PG_DELTA` is not an input, so a stale + * opt-in cannot defeat an explicit `enabled = false` 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. An explicit `--use-migra` or - * `--use-pgadmin` is an authoritative rollback that clears pg-delta mode; `--use-migra` defaults - * to true, so only an explicit pass (`useMigraChanged`) counts as opting out. + * Reports whether `db diff` should run in pg-delta mode. A true `--use-migra` or `--use-pgadmin` + * is an authoritative rollback that clears pg-delta mode; `--use-migra=false` keeps the default. */ export function resolveDiffEngine(inputs: { - readonly useMigraChanged: boolean; + readonly useMigra: boolean; readonly usePgAdmin: boolean; readonly pgDeltaDefault: boolean; }): boolean { - if (inputs.useMigraChanged || inputs.usePgAdmin) { + if (inputs.useMigra || inputs.usePgAdmin) { return false; } return inputs.pgDeltaDefault; 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 c1b1978798..8790f24044 100644 --- a/apps/cli/src/command-internal/diff-engine.unit.test.ts +++ b/apps/cli/src/command-internal/diff-engine.unit.test.ts @@ -9,25 +9,16 @@ 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 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); }); }); describe("resolveDiffEngine", () => { const base = { - useMigraChanged: false, + useMigra: false, usePgAdmin: false, pgDeltaDefault: true, }; @@ -38,7 +29,7 @@ describe("resolveDiffEngine", () => { }); it("an explicit --use-migra clears pg-delta mode", () => { - expect(resolveDiffEngine({ ...base, useMigraChanged: true })).toBe(false); + expect(resolveDiffEngine({ ...base, useMigra: true })).toBe(false); }); it("--use-pgadmin clears pg-delta mode", () => { diff --git a/apps/cli/src/command-internal/migrate-and-seed.ts b/apps/cli/src/command-internal/migrate-and-seed.ts index bae901144d..1bf8470264 100644 --- a/apps/cli/src/command-internal/migrate-and-seed.ts +++ b/apps/cli/src/command-internal/migrate-and-seed.ts @@ -45,6 +45,22 @@ const migrationApplyError = ( }); }; +/** Warns a versionless `--experimental` rebuild that `schema_paths` is ignored while pg-delta is on. */ +export const experimentalSchemaPathsIgnoredWarning = + "WARNING: [db.migrations].schema_paths is not applied while pg-delta is enabled (the default). Set [experimental.pgdelta] enabled = false to apply schema_paths files.\n"; + +/** Whether {@link experimentalSchemaPathsIgnoredWarning} applies to this rebuild. */ +export const ignoresExperimentalSchemaPaths = (inputs: { + readonly experimental: boolean; + readonly version: string; + readonly pgDeltaEnabled: boolean; + readonly schemaPaths: ReadonlyArray; +}): boolean => + inputs.experimental && + inputs.version.length === 0 && + inputs.pgDeltaEnabled && + inputs.schemaPaths.length > 0; + /** * Reapplies local migrations up to `version`, then runs seed files. * @@ -64,6 +80,9 @@ export const migrateAndSeed = ( ) => Effect.gen(function* () { const output = yield* Output; + if (ignoresExperimentalSchemaPaths({ ...config, version })) { + yield* output.raw(experimentalSchemaPathsIgnoredWarning, "stderr"); + } if (config.experimental && version.length === 0 && !config.pgDeltaEnabled) { yield* applySchemaFiles( session, diff --git a/apps/cli/src/command-internal/migrate-and-seed.unit.test.ts b/apps/cli/src/command-internal/migrate-and-seed.unit.test.ts index c864bc3e84..9fd71b1d03 100644 --- a/apps/cli/src/command-internal/migrate-and-seed.unit.test.ts +++ b/apps/cli/src/command-internal/migrate-and-seed.unit.test.ts @@ -141,7 +141,9 @@ describe("migrateAndSeed experimental declarative-schema branch", () => { Effect.sync(() => { expect(execs).toContain("create table schema_marker ()"); expect(execs).not.toContain("create table migration_marker ()"); - expect(out.rawChunks.map((c) => c.text).join("")).not.toContain("Applying migration"); + const stderr = out.rawChunks.map((c) => c.text).join(""); + expect(stderr).not.toContain("Applying migration"); + expect(stderr).not.toContain("schema_paths is not applied"); rmSync(workdir, { recursive: true, force: true }); }), ), @@ -150,7 +152,7 @@ describe("migrateAndSeed experimental declarative-schema branch", () => { ); it.effect( - "falls back to migration files when pg-delta is enabled, even with experimental on and an empty version", + "falls back to migration files and warns that schema_paths is ignored when pg-delta is enabled, even with experimental on and an empty version", () => { const workdir = makeWorkdir(); writeFile(workdir, "supabase/schemas/a.sql", "create table schema_marker ();"); @@ -177,6 +179,9 @@ describe("migrateAndSeed experimental declarative-schema branch", () => { Effect.sync(() => { expect(execs).toContain("create table migration_marker ()"); expect(execs).not.toContain("create table schema_marker ()"); + expect(out.rawChunks.map((c) => c.text).join("")).toContain( + "[db.migrations].schema_paths is not applied while pg-delta is enabled", + ); rmSync(workdir, { recursive: true, force: true }); }), ), @@ -251,6 +256,45 @@ describe("migrateAndSeed experimental declarative-schema branch", () => { }, ); + it.effect( + "warns that schema_paths is ignored even when migrations are disabled and pg-delta is enabled", + () => { + const workdir = makeWorkdir(); + writeFile(workdir, "supabase/schemas/a.sql", "create table schema_marker ();"); + writeFile( + workdir, + "supabase/migrations/20240101000000_x.sql", + "create table migration_marker ();", + ); + const { session, execs } = fakeSession(); + const out = mockOutput(); + return run( + workdir, + "", + { + ...baseConfig, + migrationsEnabled: false, + experimental: true, + pgDeltaEnabled: true, + schemaPaths: ["supabase/schemas/a.sql"], + }, + session, + out, + ).pipe( + Effect.tap(() => + Effect.sync(() => { + expect(execs).not.toContain("create table migration_marker ()"); + expect(execs).not.toContain("create table schema_marker ()"); + expect(out.rawChunks.map((c) => c.text).join("")).toContain( + "[db.migrations].schema_paths is not applied while pg-delta is enabled", + ); + rmSync(workdir, { recursive: true, force: true }); + }), + ), + ); + }, + ); + it.effect("still seeds after the declarative-schema branch runs", () => { const workdir = makeWorkdir(); writeFile(workdir, "supabase/schemas/a.sql", "create table schema_marker ();"); diff --git a/apps/cli/src/command-internal/removed-command.unit.test.ts b/apps/cli/src/command-internal/removed-command.unit.test.ts index a5b69eb24a..cd8106beb1 100644 --- a/apps/cli/src/command-internal/removed-command.unit.test.ts +++ b/apps/cli/src/command-internal/removed-command.unit.test.ts @@ -28,7 +28,7 @@ describe("RemovedSurfaceError actionability", () => { const classified = classifyCliErrorActionability( new RemovedSurfaceError({ message: "--use-pg-schema was removed.", - suggestion: "Use the default migra engine or --use-pg-delta.", + suggestion: "Use the default pg-delta engine or --use-migra.", kind: "flag", }), ); @@ -74,7 +74,7 @@ describe("TelemetryState flush", () => { it.effect("flushes on a removedFlag failure", () => { const telemetry = mockTelemetryStateTracked(); - return removedFlag("--use-pg-schema", "Use the default migra engine or --use-pg-delta.").pipe( + return removedFlag("--use-pg-schema", "Use the default pg-delta engine or --use-migra.").pipe( Effect.exit, Effect.tap((exit) => Effect.sync(() => { diff --git a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md index 7fc9c8f54c..c1c6af00c5 100644 --- a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md @@ -112,7 +112,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 state is saved under a managed snapshot key and restored into the next run's fresh stack database (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`, passed to each image operation) | no | @@ -121,10 +120,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 is **not read**: pg-delta is the +default engine, and an explicit `[experimental.pgdelta] enabled = false` rollback is +authoritative. `SUPABASE_INTERNAL_IMAGE_REGISTRY` applies to the differ's own image resolution too. The docker-run layer receives the loaded project environment on each invocation. A registry @@ -169,8 +167,14 @@ the ordered plan units separately for application through `db reset` or `db push Progress strings still go to stderr; stdout carries a single structured envelope `{ diff, file, files, schemas, engine, dropStatements, advisories? }` instead of the raw SQL. Bundled pg-delta reports the best-effort -`DeclarativeSchemaNotUsedAsDiffBaseline` advisory for a non-empty `--file` diff -when declarative files exist. +`DeclarativeSchemaNotUsedAsDiffBaseline` advisory when declarative files exist and +the diff passes a non-empty `--file` or targets the local database. Only `--file` runs +also print a stderr note: the `-f` baseline note for a written migration, or a note that +the files are not read when the diff is empty. Both end with next steps: +`supabase db schema declarative sync` (with `--experimental` when config disables pg-delta) +and, when migra would read that directory (local target, no `schema_paths`, and pg-delta +enabled in config or the directory is `supabase/schemas`; never on the stack backend), +`--use-migra`. In explicit `--from`/`--to` mode, the `diff` field is the same flattened review representation as text stdout; the machine envelope does not restore the per-unit @@ -202,8 +206,8 @@ transaction metadata. ## Notes / Delegation -- `--use-migra` (default), `--use-pgadmin`, `--use-pg-delta` are a mutually-exclusive engine - group; `--db-url` / `--linked` / `--local` are a mutually-exclusive target group (default +- `--use-migra`, `--use-pgadmin`, `--use-pg-delta` are a mutually-exclusive engine group + (pg-delta is the default unless `[experimental.pgdelta] enabled = false`); `--db-url` / `--linked` / `--local` are a mutually-exclusive target group (default `--local`). `--use-pg-schema` is removed and rejects before this group is even checked (see Notes below), so it is never a live member of the group. - **`--project-ref`** overrides ONLY the linked-ref resolution `ProjectRefResolver` @@ -305,8 +309,8 @@ port reports the real diff. ### `--use-pg-schema` is removed The flag is removed: passing it (with any value, including `--use-pg-schema=false`) -fails with a removal error and a suggestion to use the default migra engine or -`--use-pg-delta` instead. It is checked before any other engine-conflict or +fails with a removal error and a suggestion to use the default pg-delta engine or +`--use-migra` instead. It is checked before any other engine-conflict or target resolution, so combining it with another engine flag (e.g. `--use-pgadmin`) still hits the removal error first, not the mutex error. diff --git a/apps/cli/src/commands/db/diff/diff.command.ts b/apps/cli/src/commands/db/diff/diff.command.ts index f8b0538ddd..49c2d2062e 100644 --- a/apps/cli/src/commands/db/diff/diff.command.ts +++ b/apps/cli/src/commands/db/diff/diff.command.ts @@ -8,9 +8,8 @@ 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 it was passed, not its value. + // The engine flags are a mutually-exclusive group, modelled as `Option` so the mutex check keys + // off whether the flag was passed; engine selection uses its value. useMigra: Flag.boolean("use-migra").pipe( Flag.withDescription("Use migra to generate schema diff."), Flag.optional, @@ -22,7 +21,7 @@ const config = { // Kept parsed (and hidden) only so using it produces an actionable removal error instead of // an unknown-flag parse error; see diff.handler.ts. usePgSchema: Flag.boolean("use-pg-schema").pipe( - Flag.withDescription("Removed: use the default migra engine or --use-pg-delta instead."), + Flag.withDescription("Removed: use the default pg-delta engine or --use-migra instead."), Flag.optional, Flag.withHidden, ), diff --git a/apps/cli/src/commands/db/diff/diff.handler.ts b/apps/cli/src/commands/db/diff/diff.handler.ts index e2304b8dba..71dcef04ef 100644 --- a/apps/cli/src/commands/db/diff/diff.handler.ts +++ b/apps/cli/src/commands/db/diff/diff.handler.ts @@ -52,7 +52,6 @@ import { import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; import { - parseBoolEnv, resolveDiffEngine, schemaPathsTransitionWarning, shouldUsePgDelta, @@ -103,12 +102,18 @@ const declarativeBaselineAdvisory = (declarativePath: string | null) => ({ }, }); -const declarativeBaselineNote = (displayPath: string) => - `Note: db diff -f uses supabase/migrations as its baseline. Declarative schema files in ${displayPath} are not part of that baseline. If migrations are empty or outdated, the generated migration may include existing declarative objects. -f names the migration; it does not filter objects.\n`; +const declarativeNextSteps = (suggestMigra: boolean, syncNeedsExperimental: boolean) => + `Run ${aqua(`supabase db schema declarative sync${syncNeedsExperimental ? " --experimental" : ""}`)} to generate a migration from them${suggestMigra ? `, or pass ${aqua("--use-migra")} to diff them with migra` : ""}.`; + +const declarativeBaselineNote = (displayPath: string, nextSteps: string) => + `Note: db diff -f uses supabase/migrations as its baseline. Declarative schema files in ${displayPath} are not part of that baseline. If migrations are empty or outdated, the generated migration may include existing declarative objects. -f names the migration; it does not filter objects. ${nextSteps}\n`; + +const declarativeFilesIgnoredNote = (displayPath: string, nextSteps: string) => + `Note: db diff compares supabase/migrations with the target database; declarative schema files in ${displayPath} are not read. ${nextSteps}\n`; export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) { if (Option.isSome(flags.usePgSchema)) { - return yield* removedFlag("--use-pg-schema", "Use the default migra engine or --use-pg-delta."); + return yield* removedFlag("--use-pg-schema", "Use the default pg-delta engine or --use-migra."); } const output = yield* Output; const resolver = yield* DbConfigResolver; @@ -150,10 +155,11 @@ export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) { message: `if any flags in the group [db-url linked local] are set none of the others can be; [${[...targetSet].sort().join(" ")}] were all set`, }); } - if (Option.isSome(flags.useMigra) || Option.isSome(flags.usePgAdmin)) { - yield* stackRejectNativeDockerDiffEngine( - Option.isSome(flags.useMigra) ? "--use-migra" : "--use-pgadmin", - ); + // `--use-migra=false` / `--use-pgadmin=false` keep the pg-delta default, which the stack + // backend supports, so only a true value is rejected there. + const useMigra = Option.getOrElse(flags.useMigra, () => false); + if (useMigra || Option.getOrElse(flags.usePgAdmin, () => false)) { + yield* stackRejectNativeDockerDiffEngine(useMigra ? "--use-migra" : "--use-pgadmin"); } // Config is read lazily per path, not unconditionally up front: reading the base config @@ -453,17 +459,17 @@ 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 onStackBackend = (yield* currentStackBackend).kind === "stack"; const pgDeltaDefault = - (yield* currentStackBackend).kind === "stack" || + onStackBackend || 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), + useMigra, usePgAdmin, pgDeltaDefault, }); @@ -687,7 +693,11 @@ export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) { "diff.drop_statement_count": drops.length, }); let ignoredDeclarativeAdvisory: ReturnType | undefined; - if (out.length >= 2 && useDelta && Option.isSome(flags.file) && flags.file.value.length > 0) { + const fileRequested = Option.isSome(flags.file) && flags.file.value.length > 0; + const writesMigration = out.length >= 2 && fileRequested; + // `db diff -f` after editing declarative files is the migra-era workflow pg-delta no longer + // serves, so only `-f` runs get a text note; local runs keep the structured advisory. + if (useDelta && (fileRequested || resolved.isLocal)) { // This is an informational, best-effort probe only. Declarative files are // intentionally not inputs to normal db diff, so an unreadable or changing // directory must never turn a previously successful diff into a failure. @@ -703,7 +713,29 @@ export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) { ? "the configured declarative schema directory" : declarativeDir.split("\\").join("/"); ignoredDeclarativeAdvisory = declarativeBaselineAdvisory(isAbsolute ? null : displayPath); - yield* output.raw(declarativeBaselineNote(displayPath), "stderr"); + // Mirrors migra's declarative source precedence (`loadDeclaredSchemas`), which applies + // only to local targets: schema_paths first, then this dir while pg-delta stays enabled + // in config, then supabase/schemas. The stack backend rejects migra outright. + const migraReadsDeclarativeDir = + resolved.isLocal && + cfg.schemaPathPatterns.length === 0 && + (cfg.pgDelta.enabled || + declarativeDirAbsolute === path.resolve(cliSettings.workdir, "supabase", "schemas")); + const suggestMigra = migraReadsDeclarativeDir && !onStackBackend; + if (fileRequested) { + // `declarative sync` gates on the base config, which a `[remotes.]` override may + // contradict, so suggest the always-accepted flag whenever an override was applied. + const nextSteps = declarativeNextSteps( + suggestMigra, + !cfg.pgDelta.enabled || cfg.appliedRemote !== undefined, + ); + yield* output.raw( + writesMigration + ? declarativeBaselineNote(displayPath, nextSteps) + : declarativeFilesIgnoredNote(displayPath, nextSteps), + "stderr", + ); + } } } if (out.length < 2) { 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 0fad0fcc71..4bb9a19bda 100644 --- a/apps/cli/src/commands/db/diff/diff.integration.test.ts +++ b/apps/cli/src/commands/db/diff/diff.integration.test.ts @@ -522,42 +522,47 @@ const PGADMIN_SOURCE_URL = const PGADMIN_TARGET_URL = "postgresql://postgres:postgres@host.docker.internal: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()); - 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..."); - // The temp workdir sits outside any git checkout, so the branch is unknown and - // the "Finished" line omits the clause instead of falsely claiming "main". - expect(stderr(s.out)).toContain("Finished supabase db diff.\n"); - expect(s.telemetry.flushed).toBe(true); - 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 --use-migra over the pg-delta default and prints SQL to stdout", + () => { + const s = setup(tmp.current, { diffSql: "create table players ();\n" }); + return Effect.gen(function* () { + yield* dbDiff(flags({ useMigra: Option.some(true) })); + expect(s.edgeCalls).toHaveLength(1); + expect(s.databaseDiffCalls).toEqual([]); + 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..."); + // The temp workdir sits outside any git checkout, so the branch is unknown and + // the "Finished" line omits the clause instead of falsely claiming "main". + expect(stderr(s.out)).toContain("Finished supabase db diff.\n"); + expect(s.telemetry.flushed).toBe(true); + 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), (body) => withEnvVar("GITHUB_HEAD_REF", undefined, body)); - }); + expect(sawHost).toBe(true); + }).pipe(Effect.provide(s.layer), (body) => withEnvVar("GITHUB_HEAD_REF", undefined, body)); + }, + ); it.effect("forwards SUPABASE_SSL_DEBUG=TRUE to the migra script as true", () => { const s = setup(tmp.current, { diffSql: "create table players ();\n" }); return Effect.gen(function* () { - yield* dbDiff(flags()).pipe( + yield* dbDiff(flags({ useMigra: Option.some(true) })).pipe( Effect.provideService( ConfigProvider.ConfigProvider, ConfigProvider.fromEnvRecord( @@ -574,7 +579,7 @@ describe("db diff", () => { it.effect("omits SUPABASE_SSL_DEBUG from the migra script when it is set empty", () => { const s = setup(tmp.current, { diffSql: "create table players ();\n" }); return Effect.gen(function* () { - yield* dbDiff(flags()).pipe( + yield* dbDiff(flags({ useMigra: Option.some(true) })).pipe( Effect.provideService( ConfigProvider.ConfigProvider, ConfigProvider.fromEnvRecord({ SUPABASE_SSL_DEBUG: "" }, { preserveEmptyStrings: true }), @@ -588,7 +593,7 @@ describe("db diff", () => { it.effect("creates the labeled Deno-cache volume before the migra run mounts it", () => { const s = setup(tmp.current, { diffSql: "create table players ();\n" }); return Effect.gen(function* () { - yield* dbDiff(flags()); + yield* dbDiff(flags({ useMigra: Option.some(true) })); expect(s.edgeCalls[0]?.binds).toEqual(["supabase_edge_runtime_test:/root/.cache/deno:rw"]); expect(s.spawnedBeforeEdgeRun[0]).toContainEqual([ "volume", @@ -608,7 +613,7 @@ describe("db diff", () => { files: { "supabase/.env": "BITBUCKET_CLONE_DIR=/opt/atlassian/pipelines/agent/build\n" }, }); return Effect.gen(function* () { - yield* dbDiff(flags()); + yield* dbDiff(flags({ useMigra: Option.some(true) })); expect(s.edgeCalls).toHaveLength(1); expect( s.shadowSpawned.filter( @@ -651,6 +656,155 @@ describe("db diff", () => { }).pipe(Effect.provide(s.layer)); }); + it.effect("diffs local with pg-delta when no [experimental.pgdelta] config is present", () => { + const s = setup(tmp.current, { diffSql: "create table p ();\n" }); + return Effect.gen(function* () { + yield* dbDiff(flags()); + expect(s.databaseDiffCalls).toHaveLength(1); + expect(s.edgeCalls).toEqual([]); + expect(stdout(s.out)).toBe("create table p ();\n\n"); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("keeps the pg-delta default with --use-migra=false", () => { + const s = setup(tmp.current, { diffSql: "create table p ();\n" }); + return Effect.gen(function* () { + yield* dbDiff(flags({ useMigra: Option.some(false) })); + expect(s.databaseDiffCalls).toHaveLength(1); + expect(s.edgeCalls).toEqual([]); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("diffs local with migra when [experimental.pgdelta] enabled = false", () => { + const s = setup(tmp.current, { + files: { "supabase/config.toml": "[experimental.pgdelta]\nenabled = false\n" }, + diffSql: "create table m ();\n", + }); + return Effect.gen(function* () { + yield* dbDiff(flags()); + expect(s.edgeCalls).toHaveLength(1); + expect(s.databaseDiffCalls).toEqual([]); + expect(stdout(s.out)).toBe("create table m ();\n\n"); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("--use-pg-delta overrides [experimental.pgdelta] enabled = false", () => { + const s = setup(tmp.current, { + files: { "supabase/config.toml": "[experimental.pgdelta]\nenabled = false\n" }, + diffSql: "create table p ();\n", + }); + return Effect.gen(function* () { + yield* dbDiff(flags({ usePgDelta: Option.some(true) })); + expect(s.databaseDiffCalls).toHaveLength(1); + expect(s.edgeCalls).toEqual([]); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("explains ignored declarative files when a default local diff finds no changes", () => { + const s = setup(tmp.current, { + files: { "supabase/schemas/public.sql": "create table declared ();\n" }, + diffSql: "", + }); + return Effect.gen(function* () { + yield* dbDiff(flags({ file: Option.some("declared") })); + expect(s.databaseDiffCalls).toHaveLength(1); + expect(stderr(s.out)).toContain("No schema changes found"); + expect(stderr(s.out)).toContain("declarative schema files in supabase/schemas are not read"); + expect(stderr(s.out)).toContain("Run supabase db schema declarative sync to generate"); + expect(stderr(s.out)).toContain("or pass --use-migra to diff them with migra"); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("keeps a local diff without -f quiet while reporting the declarative advisory", () => { + const s = setup(tmp.current, { + files: { "supabase/schemas/public.sql": "create table declared ();\n" }, + format: "json", + diffSql: "", + }); + return Effect.gen(function* () { + yield* dbDiff(flags()); + expect(stderr(s.out)).not.toContain("are not read"); + const success = s.out.messages.find((message) => message.type === "success"); + expect(success?.data).toHaveProperty("advisories"); + }).pipe(Effect.provide(s.layer)); + }); + + const ignoredDeclarativeNote = (config: string, file: string) => + Effect.gen(function* () { + const s = setup(tmp.current, { + files: { "supabase/config.toml": config, [file]: "create table declared ();\n" }, + diffSql: "", + }); + yield* dbDiff(flags({ usePgDelta: Option.some(true), file: Option.some("declared") })).pipe( + Effect.provide(s.layer), + ); + return stderr(s.out); + }); + + it.effect("suggests --use-migra for supabase/schemas even when config disables pg-delta", () => + Effect.gen(function* () { + const err = yield* ignoredDeclarativeNote( + "[experimental.pgdelta]\nenabled = false\n", + "supabase/schemas/public.sql", + ); + expect(err).toContain("declarative schema files in supabase/schemas are not read"); + expect(err).toContain("Run supabase db schema declarative sync --experimental to generate"); + expect(err).toContain("or pass --use-migra to diff them with migra"); + }), + ); + + it.effect( + "suggests --use-migra for an absolute path to supabase/schemas with pg-delta disabled", + () => + Effect.gen(function* () { + const path = yield* Path.Path; + const absoluteSchemas = path.join(tmp.current, "supabase", "schemas"); + const err = yield* ignoredDeclarativeNote( + `[experimental.pgdelta]\nenabled = false\ndeclarative_schema_path = '${absoluteSchemas}'\n`, + "supabase/schemas/public.sql", + ); + expect(err).toContain("the configured declarative schema directory are not read"); + expect(err).toContain("or pass --use-migra to diff them with migra"); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.effect("omits the migra suggestion for a custom dir migra would not read", () => + Effect.gen(function* () { + const err = yield* ignoredDeclarativeNote( + '[experimental.pgdelta]\nenabled = false\ndeclarative_schema_path = "./decl"\n', + "supabase/decl/public.sql", + ); + expect(err).toContain("declarative schema files in supabase/decl are not read"); + expect(err).toContain("Run supabase db schema declarative sync --experimental to generate"); + expect(err).not.toContain("or pass --use-migra"); + }), + ); + + it.effect("omits the migra suggestion when schema_paths selects other files", () => + Effect.gen(function* () { + const err = yield* ignoredDeclarativeNote( + '[db.migrations]\nschema_paths = ["other.sql"]\n', + "supabase/schemas/public.sql", + ); + expect(err).toContain("declarative schema files in supabase/schemas are not read"); + expect(err).not.toContain("or pass --use-migra"); + }), + ); + + it.effect("does not explain declarative files on a linked diff without -f", () => { + const s = setup(tmp.current, { + files: { "supabase/schemas/public.sql": "create table declared ();\n" }, + isLocal: false, + linkedRef: "abcdefghijklmnopqrst", + diffSql: "alter table x;\n", + }); + return Effect.gen(function* () { + yield* dbDiff(flags({ linked: Option.some(true) })); + expect(s.databaseDiffCalls).toHaveLength(1); + expect(stderr(s.out)).not.toContain("are not read"); + }).pipe(Effect.provide(s.layer)); + }); + it.effect("pg-delta local diff ignores schema_paths and declarative files", () => { const s = setup(tmp.current, { files: { @@ -685,6 +839,9 @@ describe("db diff", () => { }); expect(stderr(s.out)).toContain("schema_paths no longer changes the migrations baseline"); expect(stderr(s.out)).not.toContain("db diff -f uses supabase/migrations"); + expect(stderr(s.out)).toContain("declarative sync` reads declarative_schema_path"); + expect(stderr(s.out)).toContain("still reads schema_paths, and only for local targets"); + expect(stderr(s.out)).not.toContain("are not read"); expect(stdout(s.out)).toBe("create table result ();\n\n"); }).pipe(Effect.provide(s.layer)); }); @@ -806,6 +963,34 @@ describe("db diff", () => { }, ); + it.effect("suggests sync --experimental when a remote block overrides the base gate", () => { + const s = setup(tmp.current, { + files: { + "supabase/config.toml": [ + "[experimental.pgdelta]", + "enabled = false", + "", + "[remotes.staging]", + 'project_id = "abcdefghijklmnopqrst"', + "", + "[remotes.staging.experimental.pgdelta]", + "enabled = true", + "", + ].join("\n"), + "supabase/schemas/public.sql": "create table declared ();\n", + }, + isLocal: false, + linkedRef: "abcdefghijklmnopqrst", + diffSql: "", + }); + return Effect.gen(function* () { + yield* dbDiff(flags({ linked: Option.some(true), file: Option.some("declared") })); + expect(stderr(s.out)).toContain( + "Run supabase db schema declarative sync --experimental to generate", + ); + }).pipe(Effect.provide(s.layer)); + }); + it.effect("the base config (default local target) does not merge a remote block", () => { const s = setup(tmp.current, { files: { @@ -1034,7 +1219,7 @@ describe("db diff", () => { diffSql: "create table o ();\n", }); return Effect.gen(function* () { - yield* dbDiff(flags()); + yield* dbDiff(flags({ useMigra: Option.some(true) })); expect(stdout(s.out)).toBe("create table o ();\n\n"); expect(s.shadowConnectedDatabases).toContain("contrib_regression"); expect(s.shadowSpawned.filter((c) => c.args[0] === "rm")).toHaveLength(1); @@ -1335,6 +1520,7 @@ describe("db diff", () => { expect(stderr(s.out)).toContain("schema_paths no longer changes the migrations baseline"); expect(stderr(s.out)).toContain("db diff -f uses supabase/migrations as its baseline"); expect(stderr(s.out)).toContain("-f names the migration; it does not filter objects"); + expect(stderr(s.out)).toContain("Run supabase db schema declarative sync to generate"); expect(stderr(s.out)).toContain("WARNING: The diff tool is not foolproof"); const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -1775,17 +1961,17 @@ describe("db diff", () => { }).pipe(Effect.provide(Layer.mergeAll(s.layer, stackBackendLayer("stack")))); }); - it.effect("rejects --use-migra=false on the stack backend", () => { + it.effect("accepts --use-migra=false on the stack backend and proceeds to stack services", () => { const s = setup(tmp.current); return Effect.gen(function* () { const exit = yield* dbDiff(flags({ useMigra: Option.some(false) })).pipe(Effect.exit); + expect(stderr(s.out)).toContain("Creating shadow database..."); + // The fixture's stack services are placeholders, so the run stops at its first stack call + // after the engine check; a typed error here would mean the flag was rejected. expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; - const error = Option.getOrUndefined(Cause.findErrorOption(exit.cause)); - expect(error).toBeInstanceOf(StackNativeEngineError); - if (!(error instanceof StackNativeEngineError)) return; - expect(error.message).toContain("The stack backend only supports the pg-delta engine."); - expect(error.message).toContain("--use-migra"); + expect(Cause.pretty(exit.cause)).toContain("Stack services must not run"); + expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))).toBeUndefined(); }).pipe(Effect.provide(Layer.mergeAll(s.layer, stackBackendLayer("stack")))); }); @@ -1835,7 +2021,7 @@ describe("db diff", () => { expect(success?.data).toMatchObject({ diff: "create table j ();\n", file: null, - engine: "migra", + engine: "pg-delta", }); }).pipe(Effect.provide(s.layer)); }); @@ -1855,7 +2041,7 @@ 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); + 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)); @@ -1865,7 +2051,7 @@ describe("db diff", () => { const s = setup(tmp.current, { oom: true, diffSql: "create table fb ();\n", isLocal: true }); return Effect.gen(function* () { // 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)); @@ -1880,7 +2066,7 @@ describe("db diff", () => { networkId: "my-net", }); return Effect.gen(function* () { - yield* dbDiff(flags({ schema: ["public"] })); + 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", @@ -1899,7 +2085,7 @@ describe("db diff", () => { networkId: "my-net", }); return Effect.gen(function* () { - yield* dbDiff(flags({ schema: ["public"] })); + yield* dbDiff(flags({ useMigra: Option.some(true), schema: ["public"] })); const env = (s.dockerCalls[0] as { env: Readonly> }).env; expect(new URL(env["SOURCE"] ?? "").hostname).toBe("host.docker.internal"); expect(new URL(env["TARGET"] ?? "").host).toBe("host.docker.internal:54322"); diff --git a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md index 8ba543b1b6..105dfba299 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -54,7 +54,7 @@ disables formatting without disabling safe compaction. | `/supabase/migrations/_.sql` | SQL | migration-style pull (non-empty diff, or the initial-migra `pg_dump` seed) | | `/supabase/schemas/**` | SQL | `--declarative` or deprecated `--experimental` export | | `/supabase/schemas/.pgdelta-export.json` | JSON | bundled declarative / deprecated-`--experimental` export metadata | -| `/supabase/config.toml` | TOML | declarative export updates `[db.migrations].schema_paths` when `[experimental.pgdelta] enabled` resolves false (section absent, `enabled` omitted, or `enabled = false` — the default); skipped when `enabled = true` | +| `/supabase/config.toml` | TOML | declarative export updates `[db.migrations].schema_paths` only when `[experimental.pgdelta] enabled = false`; skipped when `enabled` resolves true (section absent, `enabled` omitted, or `enabled = true` — the default) | | `/supabase/.temp/pgdelta/v2/debug//*.json` | JSON | bundled engine with `PGDELTA_DEBUG` | | `~/.supabase/cache/shadow-baseline/shadow-baseline-.tar` | tar | cache-enabled COLD shadow provision creates the current key's snapshot, migration-style pull only (never a declarative / deprecated-`--experimental` export, which provisions no shadow); a warm hit `touch`es its mtime (LRU); every cache-eligible acquire may delete other keys under LRU keep-3 + 2-day mtime TTL — ~90MB (`SUPABASE_HOME` overrides the root) | | managed snapshot key `` | backend | stack-mode cache-enabled cold shadow provision saves the database baseline; the last completed save replaces the key | @@ -134,7 +134,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 state is saved under a managed snapshot key and restored into the next run's fresh stack database (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 @@ -176,6 +175,9 @@ Progress strings still go to stderr; stdout carries a single structured envelope - `--declarative` / deprecated `--use-pg-delta` are mutually exclusive with `--diff-engine`; `--db-url` / `--linked` (default) / `--local` are a target group. +- `--diff-engine` defaults to pg-delta. Without the flag, migra is selected only by + `[experimental.pgdelta] enabled = false`; the historical + `SUPABASE_EXPERIMENTAL_PG_DELTA` opt-in is not read. - **`--project-ref`** overrides ONLY the linked-ref resolution `ProjectRefResolver` performs (flag > `SUPABASE_PROJECT_ID` > `.temp/project-ref`) — unlike `SUPABASE_PROJECT_ID`, it does not affect the shadow container's project 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 dcc3c25343..1b0e90b76c 100644 --- a/apps/cli/src/commands/db/pull/pull.integration.test.ts +++ b/apps/cli/src/commands/db/pull/pull.integration.test.ts @@ -796,9 +796,14 @@ describe("db pull", () => { const s = setup(tmp.current, { migrations: ["20240101000000"], files: { - "supabase/config.toml": ["[db.migrations]", 'schema_paths = ["database/*.sql"]', ""].join( - "\n", - ), + "supabase/config.toml": [ + "[db.migrations]", + 'schema_paths = ["database/*.sql"]', + "", + "[experimental.pgdelta]", + "enabled = false", + "", + ].join("\n"), }, remoteVersions: ["20240101000000"], edgeStdout: "create table remote ();\n", @@ -806,7 +811,7 @@ describe("db pull", () => { }); return Effect.gen(function* () { yield* dbPull(flags()); - // Migra engine selection is proven by `edgeStdout` parsing as raw SQL below + // `enabled = false` selecting migra is proven by `edgeStdout` parsing as raw SQL // (a pg-delta selection would instead try — and fail — to `JSON.parse` it). expect(s.shadowSpawned.filter((call) => call.args[0] === "create")).toHaveLength(1); const err = streamText(s.out, "stderr"); @@ -835,7 +840,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.edgeCalls).toHaveLength(1); expect(s.edgeCalls[0]?.binds).toEqual(["supabase_edge_runtime_test:/root/.cache/deno:rw"]); expect(s.spawnedBeforeEdgeRun[0]).toContainEqual([ @@ -859,7 +864,7 @@ describe("db pull", () => { files: { "supabase/.env": "BITBUCKET_CLONE_DIR=/opt/atlassian/pipelines/agent/build\n" }, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); expect(s.edgeCalls).toHaveLength(1); expect( s.shadowSpawned.filter( @@ -961,7 +966,7 @@ describe("db pull", () => { // pulled files (pg-delta stays disabled). const s = setup(tmp.current, { files: { - "supabase/config.toml": "[db]\n", + "supabase/config.toml": "[experimental.pgdelta]\nenabled = false\n", }, edgeStdout: EXPORT_JSON, }); @@ -998,7 +1003,8 @@ describe("db pull", () => { // appending a duplicate. const s = setup(tmp.current, { files: { - "supabase/config.toml": '[db.migrations]\nschema_paths = [\n "schemas/*.sql",\n]\n', + "supabase/config.toml": + '[experimental.pgdelta]\nenabled = false\n\n[db.migrations]\nschema_paths = [\n "schemas/*.sql",\n]\n', }, edgeStdout: EXPORT_JSON, }); @@ -1073,6 +1079,7 @@ describe("db pull", () => { const s = setup(tmp.current, { migrations: ["20240101000000"], remoteVersions: ["20240101000000"], + files: { "supabase/config.toml": "[experimental.pgdelta]\nenabled = false\n" }, edgeStdout: "create table remote ();\n", yes: true, args: ["db", "pull", "--declarative", "--use-pg-delta=false"], @@ -1090,6 +1097,7 @@ describe("db pull", () => { const s = setup(tmp.current, { migrations: ["20240101000000"], remoteVersions: ["20240101000000"], + files: { "supabase/config.toml": "[experimental.pgdelta]\nenabled = false\n" }, edgeStdout: "create table remote ();\n", yes: true, args: ["db", "pull", "--use-pg-delta", "--declarative=false"], @@ -1139,7 +1147,7 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); // pg_dump ran with the schema-dump env (internal-schema exclude + comment strip). expect(s.dumpCalls).toHaveLength(1); expect(s.dumpCalls[0]?.env["EXTRA_SED"]).toBe("/^--/d"); @@ -1181,7 +1189,7 @@ describe("db pull", () => { edgeStdout: "create table diffed ();\n", }); return Effect.gen(function* () { - yield* dbPull(flags()); + yield* dbPull(flags({ diffEngine: Option.some("migra") })); const success = s.out.messages.find((m) => m.type === "success"); // Machine mode never prompts, so history updates by default (true); `schemaWritten` // is the real native path (not null as when delegated). @@ -1208,7 +1216,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 fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -1233,7 +1241,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.isFailure(exit)).toBe(true); const error = Exit.isFailure(exit) ? exit.cause.reasons.find((reason) => reason._tag === "Fail")?.error @@ -1251,7 +1259,7 @@ describe("db pull", () => { // since fetching empty remote history never deletes local files. const s = setup(tmp.current, { remoteVersions: [], dumpStdout: "", edgeStdout: "" }); 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"); const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -1267,7 +1275,7 @@ describe("db pull", () => { return Effect.gen(function* () { const path = yield* Path.Path; const dir = path.join(tmp.current, "supabase", "migrations"); - const first = yield* dbPull(flags()).pipe(Effect.flip); + const first = yield* dbPull(flags({ diffEngine: Option.some("migra") })).pipe(Effect.flip); expect(first.message).toBe("No schema changes found"); const fs = yield* FileSystem.FileSystem; expect((yield* fs.exists(dir)) ? yield* fs.readDirectory(dir) : []).toEqual([]); @@ -1275,7 +1283,7 @@ describe("db pull", () => { // Without the cleanup, the leftover seed file would desync this second run's // reconciliation: an empty remote history plus a local-only version is a // `DbPullMigrationConflictError`, not a repeat "No schema changes found". - const second = yield* dbPull(flags()).pipe(Effect.flip); + const second = yield* dbPull(flags({ diffEngine: Option.some("migra") })).pipe(Effect.flip); expect(second).toMatchObject({ _tag: "DbPullInSyncError", message: "No schema changes found", @@ -1301,7 +1309,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 @@ -1316,7 +1324,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([]); @@ -1335,7 +1343,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"); @@ -1354,7 +1362,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"); @@ -1438,7 +1446,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)); }); @@ -1452,7 +1460,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)); }); @@ -1471,7 +1479,7 @@ describe("db pull", () => { historyUpdateFailWith: "connection reset by peer", }); 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).toMatchObject({ _tag: "DbPullWriteError" }); const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -1500,7 +1508,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)); }); @@ -1516,7 +1524,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); expect(streamText(s.out, "stderr")).toContain( "Update remote migration history table? [Y/n] n", @@ -1533,7 +1541,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; stdout stays payload-only. expect(streamText(s.out, "stderr")).toContain("Connecting to remote database...\n"); @@ -1551,7 +1559,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)); }); @@ -1567,7 +1575,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", @@ -1591,7 +1599,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.provide(s.layer), (body) => // only the project .env value must apply @@ -1614,7 +1622,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); @@ -1643,7 +1651,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(Effect.provide(s.layer), (body) => @@ -1663,7 +1671,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.dumpCalls[0]?.network).toEqual({ _tag: "named", name: "dotenv-net" }); expect(s.dumpCalls[0]?.env["PGHOST"]).toBe("host.docker.internal"); }).pipe(Effect.provide(s.layer), (body) => withEnvVar("SUPABASE_NETWORK_ID", undefined, body)); @@ -1679,7 +1687,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.dumpCalls[0]?.env["PGHOST"]).toBe("127.0.0.1"); }).pipe(Effect.provide(s.layer), (body) => withEnvVar("SUPABASE_NETWORK_ID", undefined, body)); }); @@ -1696,7 +1704,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.provide(s.layer), (body) => withEnvVar("SUPABASE_YES", "1", body)); }); @@ -1716,7 +1724,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", @@ -1866,7 +1874,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.provide(s.layer), (body) => withEnvVar("SUPABASE_EXPERIMENTAL", "true", body)); }, @@ -1924,14 +1932,30 @@ 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( + "a stale SUPABASE_EXPERIMENTAL_PG_DELTA in supabase/.env does not defeat enabled = false", + () => { + const s = setup(tmp.current, { + migrations: ["20240101000000"], + files: { + "supabase/config.toml": "[experimental.pgdelta]\nenabled = false\n", + "supabase/.env": "SUPABASE_EXPERIMENTAL_PG_DELTA=true\n", + }, + remoteVersions: ["20240101000000"], + edgeStdout: "create table remote ();\n", + yes: true, + }); + return Effect.gen(function* () { + yield* dbPull(flags()); + expect(s.engineCalls).toHaveLength(0); + expect(s.edgeCalls).toHaveLength(1); + }).pipe(Effect.provide(s.layer)); + }, + ); + + it.effect("with no pg-delta config and no --diff-engine, the pull uses pg-delta", () => { const s = setup(tmp.current, { migrations: ["20240101000000"], - files: { - "supabase/.env": "SUPABASE_EXPERIMENTAL_PG_DELTA=true\n", - }, remoteVersions: ["20240101000000"], edgeStdout: pgDeltaDiffEnvelope([{ name: "schema_changes", sql: "create table remote ();" }]), yes: true, @@ -1939,6 +1963,22 @@ describe("db pull", () => { return Effect.gen(function* () { yield* dbPull(flags()); expect(s.engineCalls[0]?.operation).toBe("diff"); + expect(s.edgeCalls).toHaveLength(0); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("--diff-engine pg-delta overrides enabled = false", () => { + const s = setup(tmp.current, { + migrations: ["20240101000000"], + files: { "supabase/config.toml": "[experimental.pgdelta]\nenabled = false\n" }, + remoteVersions: ["20240101000000"], + edgeStdout: pgDeltaDiffEnvelope([{ name: "schema_changes", sql: "create table remote ();" }]), + yes: true, + }); + return Effect.gen(function* () { + yield* dbPull(flags({ diffEngine: Option.some("pg-delta") })); + expect(s.engineCalls[0]?.operation).toBe("diff"); + expect(s.edgeCalls).toHaveLength(0); }).pipe(Effect.provide(s.layer)); }); @@ -1973,7 +2013,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"); @@ -2012,7 +2052,9 @@ describe("db pull", () => { yes: true, }); return Effect.gen(function* () { - const exit = yield* dbPull(flags({ name: Option.some("foo/bar") })).pipe(Effect.exit); + const exit = yield* dbPull( + flags({ name: Option.some("foo/bar"), diffEngine: Option.some("migra") }), + ).pipe(Effect.exit); expect(Exit.isFailure(exit)).toBe(true); expect(s.historyUpserts.length).toBe(0); }).pipe(Effect.provide(s.layer)); @@ -2033,7 +2075,10 @@ describe("db pull", () => { }); return Effect.gen(function* () { const exit = yield* dbPull( - flags({ name: Option.some("dir/20250101000000_backfill") }), + flags({ + name: Option.some("dir/20250101000000_backfill"), + diffEngine: Option.some("migra"), + }), ).pipe(Effect.exit); expect(Exit.isFailure(exit)).toBe(true); expect(s.historyUpserts.length).toBe(0); @@ -2054,7 +2099,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 }); @@ -2144,7 +2189,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)); @@ -2319,19 +2364,54 @@ describe("db pull", () => { }); describe("db remote commit", () => { - it.effect("writes a remote_commit migration in-process and skips the pull PostRun line", () => { + it.effect( + "writes a remote_commit migration with pg-delta by default and skips the pull PostRun line", + () => { + const s = setup(tmp.current, { + migrations: ["20240101000000"], + remoteVersions: ["20240101000000"], + edgeStdout: pgDeltaDiffEnvelope([ + { + name: "schema_changes", + sql: "-- Migration unit 1: schema_changes\n\ncreate table remote ();", + }, + ]), + yes: true, + args: ["db", "remote", "commit"], + }); + return Effect.gen(function* () { + yield* dbRemoteCommit(commitFlags()); + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = path.join(tmp.current, "supabase", "migrations"); + const written = (yield* fs.readDirectory(dir)).filter((f) => + f.endsWith("_remote_commit.sql"), + ); + expect(written).toHaveLength(1); + expect(yield* readFileText(path.join(dir, written[0] ?? ""))).toContain( + "create table remote ();", + ); + expect(streamText(s.out, "stderr")).toContain( + `Command "commit" is deprecated, use "db pull" instead.\n`, + ); + expect(streamText(s.out, "stderr")).toContain( + `Schema written to ${path.join("supabase", "migrations", written[0] ?? "")}\n`, + ); + expect(streamText(s.out, "stdout")).not.toContain("Finished supabase db pull."); + expect(s.engineCalls).toHaveLength(1); + expect(s.engineCalls[0]?.operation).toBe("diff"); + }).pipe(Effect.provide(s.layer)); + }, + ); + + it.effect("diffs with migra when config sets enabled = false", () => { const s = setup(tmp.current, { migrations: ["20240101000000"], files: { - "supabase/config.toml": "[experimental.pgdelta]\nenabled = true\n", + "supabase/config.toml": "[experimental.pgdelta]\nenabled = false\n", }, remoteVersions: ["20240101000000"], - edgeStdout: pgDeltaDiffEnvelope([ - { - name: "schema_changes", - sql: "-- Migration unit 1: schema_changes\n\ncreate table remote ();", - }, - ]), + edgeStdout: "create table remote ();\n", yes: true, args: ["db", "remote", "commit"], }); @@ -2347,15 +2427,8 @@ describe("db remote commit", () => { expect(yield* readFileText(path.join(dir, written[0] ?? ""))).toContain( "create table remote ();", ); - expect(streamText(s.out, "stderr")).toContain( - `Command "commit" is deprecated, use "db pull" instead.\n`, - ); - expect(streamText(s.out, "stderr")).toContain( - `Schema written to ${path.join("supabase", "migrations", written[0] ?? "")}\n`, - ); - expect(streamText(s.out, "stdout")).not.toContain("Finished supabase db pull."); - expect(s.engineCalls).toHaveLength(1); - expect(s.engineCalls[0]?.operation).toBe("diff"); + expect(s.engineCalls).toHaveLength(0); + expect(s.edgeCalls).toHaveLength(1); }).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 eb48354736..1c5adf2f17 100644 --- a/apps/cli/src/commands/db/remote/commit/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/remote/commit/SIDE_EFFECTS.md @@ -65,5 +65,7 @@ Same envelope as migration-style `db pull`. ## Notes - Deprecated: use `db pull` instead. +- pg-delta is the default shadow-diff engine, as for migration-style `db pull`. This + command has no engine flag; `[experimental.pgdelta] enabled = false` selects migra. - `--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 8235397545..ab4b7b2c3b 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`; it defaults +to true), 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: the running check, the PG14/PG15 container-recreate @@ -111,7 +112,7 @@ No subprocess delegation occurs on either target — the remote path's | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `dropObjectsSql` `DO` block (drops user schemas/extensions/public objects, truncates auth/migrations) | always, first | | `SELECT vault.update_secret(...)` / `vault.create_secret(...)` | when `[db.vault]` has syncable secrets | -| schema-file statements (no history bookkeeping, no `RESET ALL` between files) | `--experimental` + no resolved version + pg-delta not enabled (see Notes) | +| schema-file statements (no history bookkeeping, no `RESET ALL` between files) | `--experimental` + no resolved version + pg-delta disabled (see Notes) | | migration statements + `schema_migrations` history insert (per file, transactional; pipeline-incompatible statements run standalone — see Notes) | otherwise, when `[db.migrations].enabled`, for migrations `≤ --version` | | seed statements + `seed_files` hash upsert | when `[db.seed].enabled` and not `--no-seed` (runs after either branch above) | | `SET SESSION ROLE postgres` | stepped-down sessions only: after each role-reverting statement, at end of each file, before ledger writes | @@ -284,8 +285,9 @@ to those defaults (the usual outcome for an interactive terminal). - **`--experimental` schema-files apply** (the `MigrateAndSeed` EXPERIMENTAL branch) is taken on EITHER target when `--experimental`/`SUPABASE_EXPERIMENTAL` is set, no - `--version`/`--last` resolved a version, AND `[experimental.pgdelta].enabled` is - NOT set. Taking this branch means timestamped + `--version`/`--last` resolved a version, AND `[experimental.pgdelta].enabled` + resolves false, which requires an explicit `enabled = false` (the default is true). + Taking the schema-files branch means timestamped migrations never run at all, even when `[db.migrations].schema_paths` matches nothing. Two quirks apply: (1) the `schema_paths` default is `[]`, so a stock project running an experimental reset silently applies @@ -304,6 +306,10 @@ to those defaults (the usual outcome for an interactive terminal). decrypts them into `toml.vault`, and `upsertVaultSecrets` upserts the decrypted values unconditionally, before either branch (schema-files or migrations) runs. +- When a versionless `--experimental` reset skips the schema-files branch only because + pg-delta is enabled and `schema_paths` is non-empty, it prints a stderr warning that + `schema_paths` is not applied and that `[experimental.pgdelta] enabled = false` restores it + (before the confirmation prompt on the remote path). - `db schema declarative`/`db schema sync`'s own local-reset paths call `resetLocalDatabase` in-process too (CLI-2062). That in-process call collapses to a single telemetry/linked-project-cache finalizer cycle (the outer `db schema diff --git a/apps/cli/src/commands/db/reset/reset.handler.ts b/apps/cli/src/commands/db/reset/reset.handler.ts index 38232f1853..3ab56d9e68 100644 --- a/apps/cli/src/commands/db/reset/reset.handler.ts +++ b/apps/cli/src/commands/db/reset/reset.handler.ts @@ -17,6 +17,10 @@ import { resetLocalDatabase } from "../../../command-internal/db-bootstrap/reset import { DbConfigResolver } from "../../../command-internal/db-config.service.ts"; import { checkDbToml, loadProjectEnv } from "../../../command-internal/db-config.toml-read.ts"; import { DbConnection } from "../../../command-internal/db-connection.service.ts"; +import { + experimentalSchemaPathsIgnoredWarning, + ignoresExperimentalSchemaPaths, +} from "../../../command-internal/migrate-and-seed.ts"; import { applyMigrations, applySchemaFiles } from "../../../command-internal/migration-apply.ts"; import { parseMigrationVersion } from "../../../command-internal/migration-timestamp.format.ts"; import { listLocalMigrations } from "../../../command-internal/migration-list.ts"; @@ -224,6 +228,16 @@ export const dbReset = Effect.fn("db.reset")(function* (flags: DbResetFlags) { yield* output.raw(`Loading config override: [remotes.${toml.appliedRemote}]\n`, "stderr"); } const vaultSecrets = toml.vault; + if ( + ignoresExperimentalSchemaPaths({ + experimental, + version: resolvedVersion, + pgDeltaEnabled: toml.pgDelta.enabled, + schemaPaths: toml.schemaPaths, + }) + ) { + yield* output.raw(experimentalSchemaPathsIgnoredWarning, "stderr"); + } // Prompt (default false) → cancel, then reset everything. const shouldReset = yield* promptYesNo( 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 77df198d69..7a532f8388 100644 --- a/apps/cli/src/commands/db/reset/reset.integration.test.ts +++ b/apps/cli/src/commands/db/reset/reset.integration.test.ts @@ -2374,7 +2374,7 @@ describe("db reset", () => { // `checkDbToml`) into the final `migrateAndSeed` call — the raw `["schema.sql"]` pattern // would glob-match against the workdir root instead of `supabase/schema.sql`. 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\n[experimental.pgdelta]\nenabled = false\n', files: { "supabase/schema.sql": "create table schema_paths_marker ();" }, args: ["db", "reset", "--local"], isLocal: true, @@ -2802,7 +2802,7 @@ describe("db reset", () => { "applies schema files across multiple schema_paths patterns in declaration order, sorted within each pattern", () => { 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 ();", @@ -2826,7 +2826,7 @@ describe("db reset", () => { "expands a schema_paths directory entry to its nested .sql files on an experimental remote reset", () => { 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 ();", @@ -2846,7 +2846,7 @@ describe("db reset", () => { "silently applies nothing when schema_paths is unset on an experimental remote reset", () => { 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], @@ -2861,10 +2861,10 @@ describe("db reset", () => { ); it.live( - "replays migrations instead of schema files on an experimental remote reset when pg-delta is enabled", + "replays migrations instead of schema files on an experimental remote reset when no pg-delta config is set (pg-delta is the default)", () => { const { layer, out, conn } = setup(tmp.current, { - toml: 'project_id = "test"\n\n[db.migrations]\nschema_paths = ["schemas/*.sql"]\n\n[experimental.pgdelta]\nenabled = true\n', + 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 ();"), @@ -2877,6 +2877,42 @@ describe("db reset", () => { 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"); + const warning = out.stderrText.indexOf( + "[db.migrations].schema_paths is not applied while pg-delta is enabled", + ); + expect(warning).toBeGreaterThanOrEqual(0); + expect(warning).toBeLessThan(out.stderrText.indexOf("Resetting remote database")); + }); + }, + ); + + it.live( + "warns before the prompt that schema_paths is ignored on an experimental remote reset with migrations disabled", + () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.migrations]\nenabled = false\nschema_paths = ["schemas/*.sql"]\n', + files: { + "supabase/schemas/01_users.sql": "create table schema_users ();", + ...migrationFile("20240101000000", "create table migrated_table ();"), + }, + experimental: true, + confirm: [false], + }); + return Effect.gen(function* () { + // Declining the prompt proves the warning was printed before it. + const exit = yield* dbReset({ ...DEFAULT_FLAGS, linked: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))).toMatchObject({ + _tag: "DbResetCancelledError", + }); + expect(out.stderrText).toContain( + "[db.migrations].schema_paths is not applied while pg-delta is enabled", + ); + expect(conn.execs).toEqual([]); }); }, ); @@ -2885,7 +2921,7 @@ describe("db reset", () => { "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 ();"), @@ -2909,7 +2945,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], }); @@ -2936,7 +2972,7 @@ describe("db reset", () => { it.live("ignores a partial schema_paths glob failure once at least one pattern matches", () => { 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 seed glob's own "no files matched" warning doesn't show up here too. @@ -2956,7 +2992,7 @@ describe("db reset", () => { "attaches the 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], @@ -3028,7 +3064,7 @@ describe("db reset", () => { "does not attach the schema-file suggestion when a schema file cannot be READ on an experimental remote reset", () => { 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', experimental: true, confirm: [true], }); @@ -3066,7 +3102,7 @@ describe("db reset", () => { "fails an experimental remote reset (without silently succeeding) when a matched schema_paths directory cannot be walked", () => { 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', experimental: true, confirm: [true], }); @@ -3108,7 +3144,7 @@ describe("db reset", () => { "takes the native experimental schema-files path via SUPABASE_EXPERIMENTAL in the project .env", () => { 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 ();", @@ -3152,7 +3188,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"], @@ -3366,7 +3402,7 @@ describe("db reset", () => { "seeds from --sql-paths on an experimental remote reset, independently of the schema-files apply", () => { 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.errors.ts b/apps/cli/src/commands/db/schema/declarative/declarative.errors.ts index 8c3f4541a7..2c1792d50e 100644 --- a/apps/cli/src/commands/db/schema/declarative/declarative.errors.ts +++ b/apps/cli/src/commands/db/schema/declarative/declarative.errors.ts @@ -8,8 +8,8 @@ import { import type { DeclarativeLoadCompatibilityFinding } from "./declarative.flow.ts"; /** - * Declarative commands were invoked without `--experimental` and without - * `[experimental.pgdelta] enabled = true`; message text and suggestion are an established + * Declarative commands were invoked without `--experimental` while config sets + * `[experimental.pgdelta] enabled = false`; message text and suggestion are an established * output contract. */ export class DeclarativeNotEnabledError extends Data.TaggedError("DeclarativeNotEnabledError")<{ diff --git a/apps/cli/src/commands/db/schema/declarative/declarative.gate.ts b/apps/cli/src/commands/db/schema/declarative/declarative.gate.ts index cbaf580065..78720d6bdd 100644 --- a/apps/cli/src/commands/db/schema/declarative/declarative.gate.ts +++ b/apps/cli/src/commands/db/schema/declarative/declarative.gate.ts @@ -5,8 +5,8 @@ import { DeclarativeNotEnabledError } from "./declarative.errors.ts"; /** * Whether the declarative (pg-delta) code paths are enabled: the gate is open when either the - * global `--experimental` flag is set or `[experimental.pgdelta] enabled = true` is present in - * `config.toml`. + * global `--experimental` flag is set or `[experimental.pgdelta] enabled` resolves true (the + * default), so it closes only on an explicit `enabled = false`. */ export function isPgDeltaEnabled(experimental: boolean, pgDeltaEnabled: boolean): boolean { return experimental || pgDeltaEnabled; @@ -17,9 +17,9 @@ export function isPgDeltaEnabled(experimental: boolean, pgDeltaEnabled: boolean) * `aqua`/`bold` render plain when stderr is not a TTY. */ export function pgDeltaSuggestion(configPath: string): string { - return `Either pass ${aqua("--experimental")} or add ${aqua( - "[experimental.pgdelta]", - )} with ${aqua("enabled = true")} to ${bold(configPath)}`; + return `Pass ${aqua("--experimental")} to run declarative commands while ${aqua( + "[experimental.pgdelta] enabled = false", + )} is set in ${bold(configPath)}`; } /** diff --git a/apps/cli/src/commands/db/schema/declarative/declarative.gate.unit.test.ts b/apps/cli/src/commands/db/schema/declarative/declarative.gate.unit.test.ts index fb5c79236c..a95e629198 100644 --- a/apps/cli/src/commands/db/schema/declarative/declarative.gate.unit.test.ts +++ b/apps/cli/src/commands/db/schema/declarative/declarative.gate.unit.test.ts @@ -6,7 +6,7 @@ import { DeclarativeNotEnabledError } from "./declarative.errors.ts"; import { isPgDeltaEnabled, pgDeltaSuggestion, requirePgDelta } from "./declarative.gate.ts"; const EXPECTED_SUGGESTION = - "Either pass --experimental or add [experimental.pgdelta] with enabled = true to supabase/config.toml"; + "Pass --experimental to run declarative commands while [experimental.pgdelta] enabled = false is set in supabase/config.toml"; describe("isPgDeltaEnabled", () => { it("opens the gate when --experimental is passed even if config disables it", () => { 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 e0f920fdac..1860b0fa64 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 @@ -36,7 +36,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, 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 63560b462a..ba7ff33b3d 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 @@ -53,17 +53,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 @@ -80,7 +80,8 @@ always go to stderr, in every `--output-format`. On success: ## Notes -- Requires `--experimental` or `[experimental.pgdelta] enabled = true`. +- Open 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 f658a12608..4fb20cf237 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 @@ -382,6 +382,17 @@ const flags = ( const failError = (exit: Exit.Exit) => Exit.isFailure(exit) ? exit.cause.reasons.find(Cause.isFailReason)?.error : undefined; +const disablePgDelta = (workdir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(workdir, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); + }); + const seedMigration = (workdir: string) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -394,9 +405,24 @@ const seedMigration = (workdir: string) => describe("db schema declarative generate integration", () => { const tmp = useTempWorkdir(); - it.effect("gate: fails when neither --experimental nor config enables pg-delta", () => { + it.effect("gate is open by default: runs without --experimental or any pgdelta config", () => { + const s = setup(tmp.current, { experimental: false }); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* dbSchemaDeclarativeGenerate(flags({ local: Option.some(true) })); + expect( + yield* fs.exists( + path.join(tmp.current, "supabase", "schemas", "public", "tables", "players.sql"), + ), + ).toBe(true); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("gate: fails when config disables pg-delta", () => { const { layer } = setup(tmp.current, { experimental: false }); return Effect.gen(function* () { + yield* disablePgDelta(tmp.current); const exit = yield* Effect.exit( dbSchemaDeclarativeGenerate(flags({ local: Option.some(true) })), ); @@ -425,6 +451,7 @@ describe("db schema declarative generate integration", () => { () => { const { layer } = setup(tmp.current, { experimental: false }); return Effect.gen(function* () { + yield* disablePgDelta(tmp.current); const exit = yield* Effect.exit( dbSchemaDeclarativeGenerate( flags({ local: Option.some(true), linked: Option.some(true) }), @@ -470,6 +497,7 @@ describe("db schema declarative generate integration", () => { }); const ENV = "SUPABASE_EXPERIMENTAL"; return Effect.gen(function* () { + yield* disablePgDelta(tmp.current); const exit = yield* withEnvVar( ENV, "1", @@ -865,6 +893,8 @@ describe("db schema declarative generate integration", () => { path.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 d4f2fa008d..fea538dc0b 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. | Code | Condition | | ---- | --------------------------------------------------------------------------------------------------- | | `0` | success (migration created, applied, or "No schema changes found") | -| `1` | pg-delta not enabled | +| `1` | pg-delta disabled (`[experimental.pgdelta] enabled = false` and no `--experimental`) | | `1` | conflicting `--apply`/`--no-apply` (mutually exclusive) | | `1` | no declarative schema files found | | `1` | shadow-database / selected pg-delta engine / diff failure | @@ -76,7 +76,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 @@ -109,7 +109,8 @@ existing SQL or creates an export manifest. ## Notes -- Requires `--experimental` or `[experimental.pgdelta] enabled = true`. +- Open 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 5aecce4b9b..c8c72203bb 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 @@ -77,6 +77,10 @@ interface SetupOpts { stdinIsTty?: boolean; diffSql?: string; replannedDiffSql?: string; + /** + * Fails the first ALTER statement run against the live database. The recovery reset replays + * the migrations (including the one just written), so later ALTERs succeed. + */ applyFails?: boolean; /** * Makes the recovery reset's `resetLocalDatabase` fail immediately with @@ -227,22 +231,25 @@ function setup(workdir: string, opts: SetupOpts = {}) { // `dbExec`, which every "not yet applied" assertion expects to stay empty until real apply. const SHADOW_PORT = 54320; const dbConnectPorts: number[] = []; + let applyFailuresLeft = opts.applyFails === true ? 1 : 0; + const takeApplyFailure = (statements: ReadonlyArray) => { + const index = applyFailuresLeft > 0 ? statements.findIndex((s) => s.startsWith("ALTER")) : -1; + if (index >= 0) applyFailuresLeft--; + return index; + }; const dbConn = Layer.succeed(DbConnection, { connect: (cfg: PgConnInput) => { if (cfg.port !== SHADOW_PORT) dbConnectPorts.push(cfg.port); return Effect.succeed({ exec: (sql: string) => - opts.applyFails === true && sql.startsWith("ALTER") + takeApplyFailure([sql]) >= 0 ? 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; + const failureIndex = takeApplyFailure(sql); return failureIndex >= 0 ? Effect.fail({ _tag: "DbExecError", @@ -428,6 +435,17 @@ const seedDeclarative = (workdir: string) => yield* fs.writeFileString(path.join(dir, "public.sql"), "create table a();"); }); +const disablePgDelta = (workdir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(workdir, "supabase", "config.toml"), + "[experimental.pgdelta]\nenabled = false\n", + ); + }); + const seedMigration = (workdir: string) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -483,9 +501,19 @@ describe("db schema declarative sync integration", () => { const tmp = useTempWorkdir(); useShadowCacheDisabled(); - it.effect("gate: fails when pg-delta is not enabled", () => { + it.effect("gate is open by default: runs without --experimental or any pgdelta config", () => { + const s = setup(tmp.current, { experimental: false, diffSql: "" }); + return Effect.gen(function* () { + yield* seedDeclarative(tmp.current); + 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("gate: fails when config disables pg-delta", () => { const { layer } = setup(tmp.current, { experimental: false }); return Effect.gen(function* () { + yield* disablePgDelta(tmp.current); yield* seedDeclarative(tmp.current); const exit = yield* Effect.exit(dbSchemaDeclarativeSync(flags())); expect(failError(exit)?.constructor.name).toBe("DeclarativeNotEnabledError"); @@ -512,6 +540,7 @@ describe("db schema declarative sync integration", () => { () => { const { layer } = setup(tmp.current, { experimental: false }); return Effect.gen(function* () { + yield* disablePgDelta(tmp.current); const exit = yield* Effect.exit( dbSchemaDeclarativeSync(flags({ apply: Option.some(true), noApply: Option.some(true) })), ); @@ -555,6 +584,7 @@ describe("db schema declarative sync integration", () => { }); const ENV = "SUPABASE_EXPERIMENTAL"; return Effect.gen(function* () { + yield* disablePgDelta(tmp.current); const exit = yield* withEnvVar(ENV, "1", Effect.exit(dbSchemaDeclarativeSync(flags()))); expect(Exit.isFailure(exit)).toBe(true); expect(failError(exit)?.constructor.name).toBe("DeclarativeNotEnabledError"); 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/commands/db/shared/pgdelta-next-shadow.stack.integration.test.ts b/apps/cli/src/commands/db/shared/pgdelta-next-shadow.stack.integration.test.ts index 9b4a0acf11..86395ce276 100644 --- a/apps/cli/src/commands/db/shared/pgdelta-next-shadow.stack.integration.test.ts +++ b/apps/cli/src/commands/db/shared/pgdelta-next-shadow.stack.integration.test.ts @@ -25,7 +25,6 @@ import type { DbTomlValues } from "../../../command-internal/db-config.toml-read const toml = { projectEnv: {}, - envLookup: () => undefined, apiSchemas: ["public", "graphql_public"], port: 54321, shadowPort: 54320, diff --git a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md index cfc8743847..a2bc2fb3d7 100644 --- a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md @@ -51,8 +51,10 @@ composition reuses too — see that command's `SIDE_EFFECTS.md`): one-shot runs so user migrations see the tenant), API-privilege revocation, `[db.vault]` secret upsert, `supabase/roles.sql` seed, and finally either every pending migration + seed, OR — when `--experimental`/`SUPABASE_EXPERIMENTAL` is set AND - `[experimental.pgdelta] enabled` is false — every `db.migrations.schema_paths` file - (declarative schema files) instead of migrations, followed by seed either way + `[experimental.pgdelta] enabled = false` is set explicitly (it defaults to true) — every `db.migrations.schema_paths` file + (declarative schema files) instead of migrations (with `--experimental`, pg-delta enabled, + and non-empty `schema_paths`, a stderr warning says `schema_paths` is not applied), + followed by seed either way (`MigrateAndSeed`). Skipped IN FULL when `--from-backup` is set (not merely reduced). 8. Write `supabase/.branches/_current_branch` = `"main"` if absent — runs on EVERY path that reaches this point (fresh volume, existing volume, and a swallowed diff --git a/apps/cli/src/commands/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/pull/SIDE_EFFECTS.md index 9ac75d1cf8..1acc1a7613 100644 --- a/apps/cli/src/commands/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/pull/SIDE_EFFECTS.md @@ -141,7 +141,7 @@ Docker-dependent. Once the db and functions steps run, they consume their own established environment variables exactly as documented in their own `SIDE_EFFECTS.md` files (e.g. `SUPABASE_DB_PASSWORD`, `SUPABASE_DB_SHADOW_PORT`, `SUPABASE_SHADOW_CACHE`, `SUPABASE_NETWORK_ID`, -`SUPABASE_USE_SLIM_IMAGES`, `SUPABASE_EXPERIMENTAL_PG_DELTA` for db; +`SUPABASE_USE_SLIM_IMAGES` for db; `SUPABASE_EDGE_RUNTIME_DENO_VERSION`, `SUPABASE_INTERNAL_IMAGE_REGISTRY`, `BITBUCKET_CLONE_DIR` for functions) — `pull` does not read or override any of these itself, it only supplies the resolved `ref`. diff --git a/apps/cli/src/commands/start/SIDE_EFFECTS.md b/apps/cli/src/commands/start/SIDE_EFFECTS.md index b409be2b44..963ef1d2d3 100644 --- a/apps/cli/src/commands/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/start/SIDE_EFFECTS.md @@ -52,8 +52,9 @@ seeds `supabase/roles.sql`: the `Seeding globals from roles.sql...` stderr line prints first, whether or not the file exists — a missing file is silently tolerated (no SQL runs), any other read/exec error still fails the run. Finally runs every pending migration + seed — UNLESS `--experimental`/`SUPABASE_EXPERIMENTAL` is set and `[experimental.pgdelta] -enabled` is false, in which case `db.migrations.schema_paths` files are applied INSTEAD of -`migrations/*.sql`; seed still runs either way. +enabled = false` is set explicitly (it defaults to true), in which case `db.migrations.schema_paths` files are applied INSTEAD of +`migrations/*.sql`; seed still runs either way. With `--experimental`, pg-delta enabled, and a +non-empty `schema_paths`, a stderr warning says `schema_paths` is not applied. A failure at any step rolls back the whole `start` run (same as any other bring-up failure). `startInitCurrentBranch` (writes `supabase/.branches/_current_branch` = `"main"` if diff --git a/apps/cli/src/docs/docs-spec.tables.ts b/apps/cli/src/docs/docs-spec.tables.ts index f6ff69055a..99de0233cc 100644 --- a/apps/cli/src/docs/docs-spec.tables.ts +++ b/apps/cli/src/docs/docs-spec.tables.ts @@ -87,9 +87,9 @@ export const DOCS_EXPERIMENTAL: ReadonlySet = new Set([ ]); /** - * Leaves gated on `--experimental` OR `[experimental.pgdelta] enabled = true` - * in config.toml — the flag is documented but not marked required, since the - * config path also passes the gate. + * Leaves gated on `--experimental` OR `[experimental.pgdelta] enabled` (default true) + * in config.toml — the flag is documented but not marked required, since the gate + * is open unless config sets `enabled = false`. */ export const DOCS_EXPERIMENTAL_OPTIONAL: ReadonlySet = new Set([ "supabase-db-schema-declarative-generate", @@ -132,12 +132,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/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 0047ef3cf9..add31302c0 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -2460,8 +2460,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", @@ -4979,8 +4979,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 8086e802ed..99ddf8d1a5 100644 --- a/packages/config/src/experimental.ts +++ b/packages/config/src/experimental.ts @@ -92,11 +92,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.",