From 37ff443fa6eb249c40ec401df4d287ab3d86591a Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 28 Jul 2026 11:55:15 +0100 Subject: [PATCH] fix(cli): print declarative-schema-written line in sync bootstrap (CLI-1980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go's sync bootstrap (no declarative files -> user agrees to generate) delegates to declarative.Generate, which prints 'Declarative schema written to ' to stderr after writing the files and warming the catalog cache (declarative.go:133->138-155->156). The TS bootstrap inlined the generate+write+warm but never emitted the line. Emit it after the catalog warm, on both the interactive-accept and --yes/SUPABASE_YES paths, and regardless of --no-cache — printing the relative dir (Go's GetDeclarativeDir(): the config value verbatim or the supabase/database default), never the resolved absolute path. --- .../schema/declarative/sync/SIDE_EFFECTS.md | 5 +- .../schema/declarative/sync/sync.handler.ts | 23 +++- .../declarative/sync/sync.integration.test.ts | 107 +++++++++++++++++- 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md index e2182911be..ba4ee2562b 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md @@ -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 ` (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. diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts index 8ab78569fe..7dda0c611f 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts @@ -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 = { @@ -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`, + "stderr", + ); } // Step 2: diff migrations state vs declarative; on error, save a debug bundle. diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts index d4b0df08aa..a5acb0655e 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts @@ -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, @@ -70,8 +71,16 @@ function setup(workdir: string, opts: SetupOpts = {}) { const cache = mockLegacyLinkedProjectCacheTracked(); const execInheritCalls: ReadonlyArray[] = []; const localPostgresImageChecks: Array = []; + // 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); @@ -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 = ( @@ -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 ` 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