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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 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 @@ -28,8 +28,12 @@ the native pg-delta or migra engine (both run inside Docker via edge-runtime). T

## Docker

- Edge-runtime container (pg-delta / migra diff scripts).
- Shadow Postgres container (provisioned + torn down via the Go `db __shadow` seam).
- Edge-runtime container (pg-delta / migra diff scripts; also runs the pg-delta
catalog-export script for explicit `--from/--to migrations` on a cache miss —
CLI-1959, native, no longer the hidden Go `__catalog` seam).
- Shadow Postgres container (provisioned + torn down via the Go `db __shadow` seam;
explicit `--from/--to migrations` reuses this same seam call — `mode: "diff"` —
on a cache miss, rather than a second, `__catalog`-specific shadow).
- `supabase/migra` container — the migra OOM bash fallback only.

## API Routes (linked path, via the db-config resolver)
Expand Down Expand Up @@ -82,6 +86,12 @@ Progress strings still go to stderr; stdout carries a single structured envelope
binary (their side effects are Go's); the Go child's telemetry is disabled so the
single `cli_command_executed` event comes from this TS command.
- Explicit `--from`/`--to` mode always uses pg-delta and writes to `--output` (or stdout).
- The explicit `migrations` target resolves natively (CLI-1959): a bare
migrations-content hash cache lookup (`<workdir>/supabase/.temp/pgdelta/catalog-local-migrations-<hash>-<ts>.json`,
shared with `db push`'s post-apply cache write), and on a miss, the existing
`db __shadow --mode diff` seam call (unchanged — still Go, out of scope for
CLI-1959) plus a native pg-delta catalog export. No hidden Go
`db schema declarative __catalog` subprocess runs for this path any more.

### `--use-pg-schema` is deprecated (CLI-1960) — keep-in-Go exception

Expand Down
34 changes: 24 additions & 10 deletions apps/cli/src/legacy/commands/db/diff/diff.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
legacyGetMigrationPath,
} from "../../../shared/legacy-migration-file.ts";
import { legacyDiffMigra } from "../shared/legacy-migra.ts";
import { legacyResolveMigrationsCatalogRef } from "../shared/legacy-pgdelta.cache.ts";
import { legacyWritePgDeltaMigrations } from "../shared/legacy-pgdelta-migrations.write.ts";
import { type LegacyPgDeltaContext, legacyDiffPgDelta } from "../shared/legacy-pgdelta.ts";
import { LegacyDeclarativeSeam } from "../shared/legacy-pgdelta.seam.service.ts";
Expand Down Expand Up @@ -229,16 +230,29 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy
}
return legacyToPostgresURL(resolved.conn);
}
case "migrations":
return yield* seam.exportCatalog({
mode: "migrations",
noCache: false,
// Pass the linked ref only if one resolved earlier in the cascade,
// so the `__catalog` child merges the same remote override Go's
// in-process migrations catalog sees (`explicit.go:88-126`). Absent
// otherwise → base config, matching Go's resolution order.
...(mergedLinkedRef !== undefined ? { projectRef: mergedLinkedRef } : {}),
});
case "migrations": {
// Native (CLI-1959): mirrors Go's `resolveMigrationsCatalogRef`
// (`explicit.go:88-126`) exactly — see `legacyResolveMigrationsCatalogRef`'s
// doc comment. The pg-delta context is built from whatever `cfg` is
// current at this point in the cascade (possibly re-merged by an
// earlier "linked" ref above), matching Go's stateful pre-run.
const migrationsCtx: LegacyPgDeltaContext = {
projectId: Option.getOrElse(cliConfig.projectId, () => ""),
cwd: cliConfig.workdir,
npmVersion: Option.getOrUndefined(cfg.pgDelta.npmVersion),
denoVersion: cfg.denoVersion,
};
// Pass the linked ref only if one resolved earlier in the cascade, so
// the shadow merges the same remote override Go's in-process
// migrations catalog sees (`explicit.go:88-126`). Absent otherwise →
// base config, matching Go's resolution order.
return yield* legacyResolveMigrationsCatalogRef(
fs,
path,
migrationsCtx,
mergedLinkedRef !== undefined ? { projectRef: mergedLinkedRef } : {},
);
}
case "url":
return ref;
default:
Expand Down
54 changes: 47 additions & 7 deletions apps/cli/src/legacy/commands/db/diff/diff.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { BunServices } from "@effect/platform-bun";
Expand Down Expand Up @@ -685,16 +686,55 @@ describe("legacy db diff", () => {
},
);

it.effect("explicit --from migrations resolves a shadow catalog via the seam", () => {
it.effect("explicit --from migrations resolves a shadow catalog natively", () => {
// CLI-1959: the migrations ref now resolves via `provisionShadow` (Go's
// unchanged `db __shadow --mode diff`) + a native pg-delta catalog export,
// instead of the retired `exportCatalog({mode:"migrations"})` seam call.
const s = setup(tmp.current, { diffSql: "create table m ();\n" });
return Effect.gen(function* () {
yield* legacyDbDiff(flags({ from: Option.some("migrations"), to: Option.some("local") }));
expect(s.exportCalls).toEqual(["migrations"]);
expect(s.exportCalls).toEqual([]);
expect(s.provisionCalls).toEqual([
{ mode: "diff", targetLocal: false, usePgDelta: false, projectRef: undefined },
]);
// `resolveMigrationsCatalogRef` (Go's `explicit.go:88-126`) calls the shadow
// primitives directly, without `DiffDatabase`'s own progress line — unlike
// `db schema declarative sync`'s `getMigrationsCatalogRef`, which DOES print
// it (`legacy-pgdelta.cache.ts`'s `legacyGetMigrationsCatalogRef`). This
// stderr asymmetry is the parity fix CLI-1959 makes; pin it here even though
// a shadow was actually provisioned on this cache miss.
expect(s.out.stderrText).not.toContain("Creating shadow database...");
}).pipe(Effect.provide(s.layer));
});

it.effect(
"explicit --from linked --to migrations exports the catalog with the linked ref",
"explicit --from migrations reuses an already-cached catalog without provisioning a shadow",
() => {
// A cache pre-warmed by a prior `db push` (`legacyTryCacheMigrationsCatalog`)
// or `db diff --from migrations` run keys off the BARE migrations hash
// (`pgcache.HashMigrations` — no setup-inputs token; see
// `legacyResolveMigrationsCatalogRef`'s doc comment), so it must be reused
// here without spinning up a new shadow database at all.
const noMigrationsHash = createHash("sha256").digest("hex");
const tempDir = join(tmp.current, "supabase", ".temp", "pgdelta");
mkdirSync(tempDir, { recursive: true });
const cachedPath = join(tempDir, `catalog-local-migrations-${noMigrationsHash}-1000.json`);
writeFileSync(cachedPath, '{"cached":true}');
const s = setup(tmp.current, { diffSql: "create table m ();\n" });
return Effect.gen(function* () {
yield* legacyDbDiff(flags({ from: Option.some("migrations"), to: Option.some("local") }));
expect(s.provisionCalls).toEqual([]);
expect(s.exportCalls).toEqual([]);
const diffCall = s.edgeCalls.find((c) => c.script.includes("renderPlanFiles"));
expect(diffCall?.env["SOURCE"]).toBe(
`/workspace/${join("supabase", ".temp", "pgdelta", `catalog-local-migrations-${noMigrationsHash}-1000.json`)}`,
);
}).pipe(Effect.provide(s.layer));
},
);

it.effect(
"explicit --from linked --to migrations provisions the shadow with the linked ref",
() => {
// Go resolves linked first (LoadConfig merges [remotes.<ref>]), so the later
// migrations catalog is built from the remote-merged config (explicit.go).
Expand All @@ -705,13 +745,13 @@ describe("legacy db diff", () => {
});
return Effect.gen(function* () {
yield* legacyDbDiff(flags({ from: Option.some("linked"), to: Option.some("migrations") }));
const migrations = s.exportCatalogCalls.find((c) => c.mode === "migrations");
const migrations = s.provisionCalls.find((c) => c.mode === "diff" && !c.targetLocal);
expect(migrations?.projectRef).toBe("abcdefghijklmnopqrst");
}).pipe(Effect.provide(s.layer));
},
);

it.effect("explicit --from migrations --to linked exports the catalog with base config", () => {
it.effect("explicit --from migrations --to linked provisions the shadow with base config", () => {
// Migrations is resolved BEFORE linked here, so Go's LoadConfig(ref) hasn't run
// yet — the catalog must use base config (no ref forwarded), matching order.
const s = setup(tmp.current, {
Expand All @@ -721,7 +761,7 @@ describe("legacy db diff", () => {
});
return Effect.gen(function* () {
yield* legacyDbDiff(flags({ from: Option.some("migrations"), to: Option.some("linked") }));
const migrations = s.exportCatalogCalls.find((c) => c.mode === "migrations");
const migrations = s.provisionCalls.find((c) => c.mode === "diff" && !c.targetLocal);
expect(migrations?.projectRef).toBeUndefined();
}).pipe(Effect.provide(s.layer));
});
Expand All @@ -744,7 +784,7 @@ describe("legacy db diff", () => {
linked: Option.some(true),
}),
);
const migrations = s.exportCatalogCalls.find((c) => c.mode === "migrations");
const migrations = s.provisionCalls.find((c) => c.mode === "diff" && !c.targetLocal);
expect(migrations?.projectRef).toBe("abcdefghijklmnopqrst");
}).pipe(Effect.provide(s.layer));
});
Expand Down
Loading
Loading