Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/cli/src/legacy/commands/db/diff/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ on release). A cache anomaly never fails the command — a warm-path anomaly col
a cold export failure only warns and leaves the run uncached (one exception: a shadow that
fails to come back up after the snapshot fails the run rather than reporting a false success). See `shared/db-bootstrap/
shadow-cache.ts`'s doc comment for the mechanics. `--use-pgadmin` shares this cache and these
snapshots: its shadow runs the same forced-on Webhooks/`pg_net` baseline
(`legacyMigrateShadowDatabase`), so it keys to the same tars as the native branch.
snapshots: its shadow input states the same forced-on Webhooks/`pg_net` policy
(`webhooks: "enabled"`) the native branch's legacy-engine runs do, so it keys to the same tars.

### `--use-pgadmin` parity quirks and deliberate divergence (CLI-1968)

Expand Down
201 changes: 98 additions & 103 deletions apps/cli/src/legacy/commands/db/diff/diff.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { legacyWithShadowDatabase } from "../../../shared/db-bootstrap/shadow-ca
import {
legacyMigrateShadowDatabase,
legacyShadowRunInputFromLocalContainerInputs,
type LegacyShadowWebhooksPolicy,
} from "../../../shared/db-bootstrap/shadow-database.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
Expand Down Expand Up @@ -62,7 +63,10 @@ import {
legacyIsPgDeltaDebugEnabled,
legacyResolvePgDeltaProjectId,
} from "../../../shared/legacy-pgdelta.ts";
import { legacyPrepareShadowSource } from "../shared/legacy-shadow-source.ts";
import {
legacyPrepareShadowSource,
legacyShadowSourceWebhooksPolicy,
} from "../shared/legacy-shadow-source.ts";
import type { LegacyDbDiffFlags } from "./diff.command.ts";
import { legacyClassifyExplicitRef, legacyUnknownTargetMessage } from "./diff.explicit.ts";
import {
Expand Down Expand Up @@ -581,13 +585,18 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy
// Shared by both branches below (pgAdmin's `shadowBase` and the migra/pg-delta
// `shadowInput`'s own spread) — resolving the image is the actual provisioning work each
// branch's own "Creating shadow database..." banner announces, so every call site still
// emits its banner FIRST and only then invokes this.
const resolveShadowRunInput = Effect.fnUntraced(function* () {
// emits its banner FIRST and only then invokes this. `webhooks` is each branch's own
// baseline policy, which lands on `setup.webhooks` — the single field the provisioner
// applies and the shadow baseline cache keys on.
const resolveShadowRunInput = Effect.fnUntraced(function* (
webhooks: LegacyShadowWebhooksPolicy,
) {
const resolvedShadowImage = yield* localInputs.resolvePostgresImage;
return legacyShadowRunInputFromLocalContainerInputs(
localInputs,
resolvedShadowImage,
cfg,
webhooks,
fs,
path,
);
Expand Down Expand Up @@ -629,7 +638,10 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy
);
}
yield* emitStatus("Creating shadow database...");
const shadowBase = yield* resolveShadowRunInput();
// Forced-on `pg_net`, the baseline `legacyMigrateShadowDatabase` has always run for the
// legacy engine — same policy the native branch's legacy-engine runs use, so the two
// share snapshots rather than keying a second, pgAdmin-only set.
const shadowBase = yield* resolveShadowRunInput("enabled");
const shadowConnConfig: LegacyPgConnInput = {
host: shadowBase.hostname,
port: shadowBase.shadowPort,
Expand All @@ -642,57 +654,49 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy
// `shadow-cache.ts`) — see that call site's comment for the full rationale. The migrate
// step is untouched: it still receives the whole local migration set through the SAME
// `legacyMigrateShadowDatabase`, only now told (via `handle`) whether the cluster already
// carries the platform baseline. `webhooks: "enabled"` matches that function's own forced
// `pg_net` baseline, so this shares the snapshots the native branch below keys for the SAME
// forced-on baseline (its legacy-engine runs) rather than a second, pgAdmin-only set. It
// deliberately does NOT share with next's config-following migrate — see that branch's own
// `migrationMode`-conditional `webhooks` opt.
const sql = yield* legacyWithShadowDatabase(
spawner,
shadowBase,
(handle) =>
Effect.gen(function* () {
yield* legacyWaitForShadowReady(spawner, handle.containerId, shadowConnConfig, {
timeoutSeconds: shadowBase.healthTimeoutSeconds,
image: shadowBase.image,
});
yield* legacyMigrateShadowDatabase(
spawner,
{
fs,
path,
workdir: cliConfig.workdir,
projectId: shadowBase.projectId,
container: handle.containerId,
networkId: shadowBase.networkId,
connConfig: shadowConnConfig,
setup: shadowBase.setup,
},
handle,
);
yield* emitStatus("Diffing local database with current migrations...");
return yield* legacyDiffSchemaPgAdmin({
// `source`/`target` are INVERTED relative to the migra/pg-delta path below:
// `source` is the USER'S db, `target` is the SHADOW.
source: targetUrl,
// Deliberately hardcoded, not built via `legacyToPostgresURL`: this ignores
// `SUPABASE_SERVICES_HOSTNAME`/`[db] password` by design — not a bug to fix.
target: `postgresql://postgres:postgres@127.0.0.1:${shadowBase.shadowPort}/postgres`,
schema: flags.schema,
// carries the platform baseline.
const sql = yield* legacyWithShadowDatabase(spawner, shadowBase, (handle) =>
Effect.gen(function* () {
yield* legacyWaitForShadowReady(spawner, handle.containerId, shadowConnConfig, {
timeoutSeconds: shadowBase.healthTimeoutSeconds,
image: shadowBase.image,
});
yield* legacyMigrateShadowDatabase(
spawner,
{
fs,
path,
workdir: cliConfig.workdir,
projectId: shadowBase.projectId,
container: handle.containerId,
networkId: shadowBase.networkId,
extraHosts: shadowBase.extraHosts,
emitStatus,
});
}),
{ webhooks: "enabled" },
connConfig: shadowConnConfig,
setup: shadowBase.setup,
},
handle,
);
yield* emitStatus("Diffing local database with current migrations...");
return yield* legacyDiffSchemaPgAdmin({
// `source`/`target` are INVERTED relative to the migra/pg-delta path below:
// `source` is the USER'S db, `target` is the SHADOW.
source: targetUrl,
// Deliberately hardcoded, not built via `legacyToPostgresURL`: this ignores
// `SUPABASE_SERVICES_HOSTNAME`/`[db] password` by design — not a bug to fix.
target: `postgresql://postgres:postgres@127.0.0.1:${shadowBase.shadowPort}/postgres`,
schema: flags.schema,
projectId: shadowBase.projectId,
networkId: shadowBase.networkId,
extraHosts: shadowBase.extraHosts,
emitStatus,
});
}),
);
diffResult = { sql, files: undefined };
} else {
yield* output.raw("Creating shadow database...\n", "stderr");
const migrationMode: "legacy" | "pgdelta-next" = usesPgDeltaNext ? "pgdelta-next" : "legacy";
const shadowInput = {
...(yield* resolveShadowRunInput()),
...(yield* resolveShadowRunInput(legacyShadowSourceWebhooksPolicy(migrationMode))),
targetLocal: resolved.isLocal,
usePgDelta: useDelta,
migrationMode,
Expand All @@ -711,65 +715,56 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy
// why the cache seam sits here (with `SUPABASE_SHADOW_CACHE` unset it IS today's
// create/remove pair; otherwise a key-matching PGDATA snapshot is restored into the fresh
// container in a few seconds instead of cold-provisioning the baseline in ~15s).
// The `webhooks` policy MUST describe the baseline the `use` callback below actually
// provisions, because that is what the cache key hashes: `legacyPrepareShadowSource`
// dispatches on `migrationMode`, running `legacyMigrateShadowDatabase` (forced `pg_net`) for
// the legacy engine but `legacyMigrateNextShadowDatabase` (config-following) for pg-delta
// next. Hardcoding `"enabled"` for both would make a next-mode cold run on a
// webhooks-disabled project publish a `pg_net`-less cluster under the
// `webhooks_enabled=true` key that the pgAdmin branch above (whose baseline really is
// forced-on) could then warm-restore, and vice versa.
diffResult = yield* legacyWithShadowDatabase(
spawner,
shadowInput,
(handle) =>
Effect.gen(function* () {
const shadow = yield* legacyPrepareShadowSource(spawner, handle, shadowInput);
const target = shadow.targetUrlOverride ?? targetUrl;
yield* output.raw(
flags.schema.length > 0
? `Diffing schemas: ${flags.schema.join(",")}\n`
: "Diffing schemas...\n",
"stderr",
);
if (useDelta) {
const result = yield* pgDelta.diffDatabase({
context: ctx,
source: {
kind: "database",
ref: shadow.sourceUrl,
connectOptions: { isLocal: true, dnsResolver: "native" },
},
target: {
kind: "database",
ref: target,
...(shadow.targetUrlOverride === undefined ? { connection: resolved.conn } : {}),
connectOptions: {
isLocal: shadow.targetUrlOverride !== undefined || resolved.isLocal,
dnsResolver,
},
// The engine's Webhooks policy rides on `shadowInput.setup.webhooks` above, which is both
// what `legacyPrepareShadowSource`'s baseline applies and what the cache key hashes — the
// two cannot describe different clusters.
diffResult = yield* legacyWithShadowDatabase(spawner, shadowInput, (handle) =>
Effect.gen(function* () {
const shadow = yield* legacyPrepareShadowSource(spawner, handle, shadowInput);
const target = shadow.targetUrlOverride ?? targetUrl;
yield* output.raw(
flags.schema.length > 0
? `Diffing schemas: ${flags.schema.join(",")}\n`
: "Diffing schemas...\n",
"stderr",
);
if (useDelta) {
const result = yield* pgDelta.diffDatabase({
context: ctx,
source: {
kind: "database",
ref: shadow.sourceUrl,
connectOptions: { isLocal: true, dnsResolver: "native" },
},
target: {
kind: "database",
ref: target,
...(shadow.targetUrlOverride === undefined ? { connection: resolved.conn } : {}),
connectOptions: {
isLocal: shadow.targetUrlOverride !== undefined || resolved.isLocal,
dnsResolver,
},
schema: flags.schema,
formatOptions,
debug: legacyIsPgDeltaDebugEnabled(),
strictCoverage: flags.strictCoverage,
});
// Keep the per-unit plan files so a multi-unit plan can be written as one
// migration file each; `sql` stays the flattened join for stdout review +
// machine payloads.
return { sql: result.sql, files: result.files, hazards: result.hazards };
}
const sql = yield* legacyDiffMigra(ctx, {
source: shadow.sourceUrl,
target,
},
schema: flags.schema,
connectOptions: { isLocal: resolved.isLocal, dnsResolver },
formatOptions,
debug: legacyIsPgDeltaDebugEnabled(),
strictCoverage: flags.strictCoverage,
});
// The migra engine has no execution-aware plan units, so it always writes a
// single migration file.
return { sql, files: undefined };
}),
migrationMode === "pgdelta-next" ? {} : { webhooks: "enabled" },
// Keep the per-unit plan files so a multi-unit plan can be written as one
// migration file each; `sql` stays the flattened join for stdout review +
// machine payloads.
return { sql: result.sql, files: result.files, hazards: result.hazards };
}
const sql = yield* legacyDiffMigra(ctx, {
source: shadow.sourceUrl,
target,
schema: flags.schema,
connectOptions: { isLocal: resolved.isLocal, dnsResolver },
});
// The migra engine has no execution-aware plan units, so it always writes a
// single migration file.
return { sql, files: undefined };
}),
);
}
const out = diffResult.sql;
Expand Down
11 changes: 5 additions & 6 deletions apps/cli/src/legacy/commands/db/diff/diff.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1136,12 +1136,11 @@ describe("legacy db diff", () => {
);

// The two native branches provision DIFFERENT clusters on a webhooks-disabled project:
// `--use-pgadmin` migrates through `legacyMigrateShadowDatabase`, whose baseline installs
// `pg_net` unconditionally, while pg-delta next migrates through
// `legacyMigrateNextShadowDatabase`, which follows `config.toml` (webhooks absent = off). If the
// cache key described the caller's literal `webhooks` opt rather than the baseline the run
// actually builds, one would warm-restore the other's snapshot and silently diff against a
// cluster with the wrong extension set.
// `--use-pgadmin` states `webhooks: "enabled"` on its shadow input, so its baseline installs
// `pg_net` unconditionally, while pg-delta next states `"config"` and follows `config.toml`
// (webhooks absent = off). That one field is what the cache key hashes, so neither can
// warm-restore the other's snapshot and silently diff against a cluster with the wrong
// extension set.
it.effect(
"--use-pgadmin does not reuse the pg-delta next baseline when config leaves webhooks disabled",
() =>
Expand Down
Loading