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
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ surfaces before an `--apply`/`--no-apply` conflict is ever checked.
## Output

Text mode only. The generated SQL, the created-migration path, drop-statement
warnings, and apply status are written to stderr.
warnings, and apply status are written to stderr. The no-files bootstrap also
prints `Declarative schema written to <dir>` (the relative declarative dir, Go's
`GetDeclarativeDir()`) to stderr after generating, writing, and warming the
catalog cache — on both the interactive-accept and `--yes` paths.
`--no-apply` writes the migration only (never prompts/applies); `--apply` applies
without prompting; both override the global `--yes`. `--no-apply` and `--apply`
are mutually exclusive.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,15 @@ export const legacyDbSchemaDeclarativeSync = Effect.fn("legacy.db.schema.declara
);
}

// Go's `utils.GetDeclarativeDir()` — the config value verbatim (already
// `supabase/`-prefixed when relative) or the relative `supabase/database`
// default. Printed verbatim in the bootstrap's written-to line below, exactly
// as Go prints it (Go chdirs into the workdir, so its paths stay relative).
const declarativeDirRel = legacyResolveDeclarativeDir(path, toml.pgDelta);
// `path.resolve` (not `path.join`) so an absolute `declarative_schema_path` is
// used as-is, matching Go's `config.resolve` (which only prefixes the workdir onto
// a relative path). `path.join(workdir, abs)` would mangle the absolute path.
const declarativeDir = path.resolve(
cliConfig.workdir,
legacyResolveDeclarativeDir(path, toml.pgDelta),
);
const declarativeDir = path.resolve(cliConfig.workdir, declarativeDirRel);
const migrationsDir = path.join(cliConfig.workdir, "supabase", "migrations");
const tempDir = legacyPgDeltaTempPath(path, cliConfig.workdir);
const run: LegacyDeclarativeRunContext = {
Expand Down Expand Up @@ -249,6 +251,19 @@ export const legacyDbSchemaDeclarativeSync = Effect.fn("legacy.db.schema.declara
if (!run.noCache) {
yield* seam.exportCatalog({ mode: "declarative", noCache: run.noCache });
}
// Go's delegated `declarative.Generate` prints the written-to line to stderr
// after the write and the catalog warm (`declarative.go:133→138-155→156`), on
// both the interactive-accept and --yes/SUPABASE_YES bootstrap paths, and
// regardless of --no-cache (the warm is skipped, the line is not). It prints
// `utils.GetDeclarativeDir()` — the relative dir above, never a resolved
// absolute path, because Go chdirs into the workdir (CLI-1980). NOTE: the
// generate and db pull ports of this same Go line still print the absolute
// dir today — pull's is moving to relative under CLI-1978; this rendering
// is the Go-faithful one.
yield* output.raw(
`Declarative schema written to ${legacyBold(declarativeDirRel)}\n`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY (non-blocking): this is now the third literal copy of the Declarative schema written to line (generate/generate.handler.ts:275, pull/pull.handler.ts:377). Go avoids the duplication structurally — sync's bootstrap delegates to the shared declarative.Generate, so the string lives in one place per path (declarative.go:156, pull.go:119).

The PR body's rationale for deferring a hoist (the three call sites don't yet print the same rendering) applies to unifying which dir is printed, but the format helper is separable from that convergence: a one-liner in legacy-pgdelta.write.ts (which all three handlers already import) would pin the parity-critical string in one place today, with each caller passing its current dir —

export const legacyDeclarativeSchemaWrittenLine = (dir: string) =>
  `Declarative schema written to ${legacyBold(dir)}\n`;

— and would turn the CLI-1978 pull fix and the generate follow-up into one-argument changes. Fine to batch with the generate follow-up if you prefer, but worth doing before a fourth copy appears.

"stderr",
);
}

// Step 2: diff migrations state vs declarative; on error, save a debug bundle.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BunServices } from "@effect/platform-bun";
import { describe, expect, it } from "@effect/vitest";
import { Cause, Effect, Exit, Layer, Option } from "effect";

import { stripAnsi } from "../../../../../../../tests/helpers/ansi.ts";
import { mockOutput, mockStdin, mockTty } from "../../../../../../../tests/helpers/mocks.ts";
import {
mockLegacyCliConfig,
Expand Down Expand Up @@ -70,8 +71,16 @@ function setup(workdir: string, opts: SetupOpts = {}) {
const cache = mockLegacyLinkedProjectCacheTracked();
const execInheritCalls: ReadonlyArray<string>[] = [];
const localPostgresImageChecks: Array<true> = [];
// Each catalog export records how many raw chunks had been emitted when it fired,
// so tests can assert output ordering relative to the exports (e.g. the bootstrap's
// written-to line lands after the declarative warm, before the diff's exports).
const exportCatalogCalls: Array<{ mode: string; rawChunksAt: number }> = [];
const seam = Layer.succeed(LegacyDeclarativeSeam, {
exportCatalog: ({ mode }) => Effect.succeed(`supabase/.temp/pgdelta/${mode}.json`),
exportCatalog: ({ mode }) =>
Effect.sync(() => {
exportCatalogCalls.push({ mode, rawChunksAt: out.rawChunks.length });
return `supabase/.temp/pgdelta/${mode}.json`;
}),
execInherit: (args) =>
Effect.sync(() => {
execInheritCalls.push(args);
Expand Down Expand Up @@ -186,7 +195,15 @@ function setup(workdir: string, opts: SetupOpts = {}) {
}),
BunServices.layer,
);
return { layer, out, execInheritCalls, dbExec, cache, localPostgresImageChecks };
return {
layer,
out,
execInheritCalls,
dbExec,
cache,
localPostgresImageChecks,
exportCatalogCalls,
};
}

const flags = (
Expand Down Expand Up @@ -448,6 +465,92 @@ describe("legacy db schema declarative sync integration", () => {
}).pipe(Effect.provide(s.layer));
});

it.effect("bootstrap prints the declarative-schema-written line after the catalog warm", () => {
// Go's bootstrap delegates to `declarative.Generate`, which prints
// `Declarative schema written to <dir>` to stderr AFTER WriteDeclarativeSchemas
// and the catalog warm (`declarative.go:133→138-155→156`), before sync's own
// diff (step 2). It prints `utils.GetDeclarativeDir()` — the relative
// `supabase/database` default — never the absolute resolved dir (CLI-1980).
const s = setup(tmp.current, {
experimental: true,
stdinIsTty: true,
diffSql: "",
exportJson: EXPORT_JSON,
promptConfirmResponses: [true], // generate a new one? yes (no migrations → no reset prompt)
});
return Effect.gen(function* () {
yield* legacyDbSchemaDeclarativeSync(flags({ noApply: Option.some(true) }));
const line = `Declarative schema written to ${join("supabase", "database")}\n`;
const written = s.out.rawChunks
.map((c, index) => ({ text: stripAnsi(c.text), stream: c.stream, index }))
.filter((c) => c.text === line);
expect(written).toHaveLength(1);
expect(written[0]?.stream).toBe("stderr");
const lineAt = written[0]?.index ?? -1;
// The warm (first declarative-mode export) fires before the line is printed…
const warm = s.exportCatalogCalls.find((c) => c.mode === "declarative");
expect(warm?.rawChunksAt).toBeLessThanOrEqual(lineAt);
// …and the diff's first export (migrations catalog) fires after it, so the
// line sits at the end of the bootstrap, matching Go's ordering.
const diffStart = s.exportCatalogCalls.find((c) => c.mode === "migrations");
expect(diffStart?.rawChunksAt).toBeGreaterThan(lineAt);
// The generated files actually landed in the printed (resolved) dir.
expect(
existsSync(
join(tmp.current, "supabase", "database", "schemas", "public", "tables", "players.sql"),
),
).toBe(true);
}).pipe(Effect.provide(s.layer));
});

it.effect("--yes bootstrap prints the declarative-schema-written line too", () => {
// Go reaches the same delegated `declarative.Generate` print on the
// auto-confirmed (--yes / SUPABASE_YES) bootstrap as on the interactive accept.
const s = setup(tmp.current, {
experimental: true,
stdinIsTty: false,
yes: true,
diffSql: "",
exportJson: EXPORT_JSON,
});
return Effect.gen(function* () {
yield* legacyDbSchemaDeclarativeSync(flags({ noApply: Option.some(true) }));
expect(
s.out.rawChunks.map((c) => ({ text: stripAnsi(c.text), stream: c.stream })),
).toContainEqual({
text: `Declarative schema written to ${join("supabase", "database")}\n`,
stream: "stderr",
});
}).pipe(Effect.provide(s.layer));
});

it.effect("--no-cache bootstrap still prints the declarative-schema-written line", () => {
// Go's print sits OUTSIDE the `if !noCache` warm gate (`declarative.go:138-156`):
// skipping the catalog warm must not skip the line.
const s = setup(tmp.current, {
experimental: true,
stdinIsTty: false,
yes: true,
diffSql: "",
exportJson: EXPORT_JSON,
});
return Effect.gen(function* () {
yield* legacyDbSchemaDeclarativeSync(flags({ noCache: true, noApply: Option.some(true) }));
const line = `Declarative schema written to ${join("supabase", "database")}\n`;
const written = s.out.rawChunks
.map((c, index) => ({ text: stripAnsi(c.text), stream: c.stream, index }))
.filter((c) => c.text === line);
expect(written).toHaveLength(1);
expect(written[0]?.stream).toBe("stderr");
// The warm really was skipped: the only declarative-mode export is the diff's,
// which fires after the line — yet the line still printed.
const lineAt = written[0]?.index ?? -1;
const declarativeExports = s.exportCatalogCalls.filter((c) => c.mode === "declarative");
expect(declarativeExports).toHaveLength(1);
expect(declarativeExports[0]?.rawChunksAt).toBeGreaterThan(lineAt);
}).pipe(Effect.provide(s.layer));
});

it.effect("bootstrap with migrations offers the smart target choice (not local-only)", () => {
// Go delegates the no-files bootstrap to runDeclarativeGenerate; with migrations
// present it offers local/linked/custom rather than silently generating from
Expand Down
Loading