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
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ const runCommand = (entry: WalkedCommand, selector: "db-url" | "local" | "none")
...selectorArgs(selector),
"--workdir",
home,
"--experimental",
// `db pull` rejects `--experimental` without `--declarative` before it validates the password.
...(entry.path.join(" ") === "db pull" ? [] : ["--experimental"]),
];
const exit = yield* Command.runWith(root, { version: "0.0.0-test" })(args).pipe(
Effect.provide(
Expand Down
137 changes: 37 additions & 100 deletions apps/cli/src/command-internal/db-pull-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import {
DebugFlag,
DnsResolverFlag,
NetworkIdFlag,
resolveExperimentalWithProjectEnv,
resolveYesWithProjectEnv,
} from "./global-flags.ts";
import { CliArgs } from "../shared/cli/cli-args.service.ts";
import { Output } from "../shared/output/output.service.ts";
import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts";
import { CommandSettings } from "../config/command-settings.service.ts";
Expand All @@ -25,10 +23,7 @@ import { loadProjectEnvValues, readDbToml, resolveDeclarativeDir } from "./db-co
import type { DbConnType } from "./db-target-flags.ts";
import { makeDir } from "./make-dir.ts";
import { toPostgresURL } from "./postgres-url.ts";
import {
buildLocalDbContainerInputs,
type LocalDbContainerInputs,
} from "./db-bootstrap/local-container-inputs.ts";
import { buildLocalDbContainerInputs } from "./db-bootstrap/local-container-inputs.ts";
import { withShadowDatabase } from "./db-bootstrap/shadow-cache.ts";
import { shadowRunInputFromLocalContainerInputs } from "./db-bootstrap/shadow-database.ts";
import { LinkedProjectCache } from "../telemetry/linked-project-cache.service.ts";
Expand All @@ -38,11 +33,7 @@ import {
warnPreservedUnmanagedDeclarativeFiles,
writeDeclarativeSchemas,
} from "../commands/db/shared/pgdelta.write.ts";
import {
resolveDeclarativeFromArgs,
resolvePullDiffEngine,
schemaPathsTransitionWarning,
} from "./diff-engine.ts";
import { resolvePullDiffEngine, schemaPathsTransitionWarning } from "./diff-engine.ts";
import { diffMigra } from "../commands/db/shared/migra.ts";
import { writePgDeltaMigrations } from "../commands/db/shared/pgdelta-migrations.write.ts";
import { type DumpOptions, buildSchemaDumpEnv } from "./pg-dump.env.ts";
Expand All @@ -68,7 +59,7 @@ import { prepareShadowSource } from "../commands/db/shared/shadow-source.ts";
import { currentStackBackend } from "./stack-backend.ts";
import { stackRejectNativeDockerDiffEngine } from "./stack-local-database.ts";
import { stackPrepareShadowSource, stackWithShadowDatabase } from "./stack-shadow.ts";
import type { DbPullFlags } from "../commands/db/pull/pull.command.ts";
import type { DbPullFlags as DbPullCommandFlags } from "../commands/db/pull/pull.command.ts";
import {
DbPullDumpError,
DbPullEngineConflictError,
Expand All @@ -83,13 +74,12 @@ import {
} from "./migration-history.ts";
import { updateMigrationHistory } from "../commands/db/pull/pull.sync.ts";

// `DbPullFlags` is `runDbPull`'s own parameter type, defined alongside the standalone
// `db pull` command that also constructs it — re-exported here so an in-process caller
// (`pull`) can build one without reaching into `commands/db/**` directly.
export type { DbPullFlags };

// Established output contract; ends with a `.`.
const DEPRECATION_LINE = "Flag --use-pg-delta has been deprecated, use --declarative instead.";
/**
* `runDbPull`'s parameter type: the `db pull` command flags without the tombstoned
* `--use-pg-delta`, so an in-process caller (`pull`) can build one without reaching into
* `commands/db/**` directly.
*/
export type DbPullRunFlags = Omit<DbPullCommandFlags, "usePgDelta">;

/**
* Explains the in-sync non-zero exit instead of the generic "Try rerunning the command with
Expand All @@ -101,28 +91,15 @@ const IN_SYNC_SUGGESTION =
/** Migration-file mode for the initial pg_dump seed. */
const MIGRATION_FILE_MODE = 0o644;

// `--experimental` without `--declarative` is a deprecated path: the in-process declarative
// export covers the same per-object-files outcome. Printed only when the experimental gate
// selected this branch, not when `--declarative` already did.
const EXPERIMENTAL_STRUCTURED_DUMP_DEPRECATION_LINE =
"The --experimental structured-dump mode for `db pull` is deprecated and will be removed in a future release. Use --declarative instead to pull the remote schema as per-object files.";

export type DbPullInvoke = {
/**
* Overrides `--yes`/`SUPABASE_YES`/`supabase/.env` resolution for an
* in-process caller that already ran its own aggregated confirmation.
* `undefined` keeps the existing resolution.
*/
readonly assumeYes?: boolean;
/**
* Forces migration mode regardless of the ambient `--experimental`/`SUPABASE_EXPERIMENTAL`
* gate, for an in-process caller (`pull`) whose dirty-guard, confirmation message, and
* SIDE_EFFECTS.md all assume this step only ever writes into `supabase/migrations`. Without
* this override, an ambient experimental flag would silently switch to the declarative export
* path, writing `supabase/schemas/**` and potentially `config.toml`'s `schema_paths`
* unguarded. `undefined` keeps the existing resolution.
*/
readonly forceMigrationMode?: boolean;
/** Project `.env*` values the caller already loaded; `undefined` loads them here. */
readonly projectEnv?: Record<string, string>;
};

export type DbPullOutcome =
Expand All @@ -145,7 +122,7 @@ export type DbPullOutcome =
* owns that emission based on the returned {@link DbPullOutcome}.
*/
export const runDbPull = Effect.fn("db.pull.run")(function* (
flags: DbPullFlags,
flags: DbPullRunFlags,
invoke?: DbPullInvoke,
) {
const output = yield* Output;
Expand All @@ -159,50 +136,23 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
const path = yield* Path.Path;
const dnsResolver = yield* DnsResolverFlag;
const debug = yield* DebugFlag;
const cliArgs = yield* CliArgs;
const configValues = yield* CliConfigValues;

// `--yes` or `SUPABASE_YES`. The project `.env` is loaded before the migration
// history prompt, so a `SUPABASE_YES` set only in `supabase/.env` auto-confirms
// the native initial-migra history repair too.
const projectEnv = yield* loadProjectEnvValues(fs, path, cliSettings.workdir);
const projectEnv =
invoke?.projectEnv ?? (yield* loadProjectEnvValues(fs, path, cliSettings.workdir));
const yes = yield* resolveYesWithProjectEnv(projectEnv);
// `EXPERIMENTAL` resolves from either the global `--experimental` flag or
// `SUPABASE_EXPERIMENTAL`, reusing `resolveExperimentalWithProjectEnv`'s flag-over-env
// precedence instead of re-deriving it. Resolved once, same as `yes` above; declarative mode
// ignores it. `invoke?.forceMigrationMode` overrides this for an in-process caller that must
// never take the declarative export path — see {@link DbPullInvoke.forceMigrationMode}.
const experimental = invoke?.forceMigrationMode
? false
: yield* resolveExperimentalWithProjectEnv(projectEnv);

let linkedRefForCache: string | undefined;

return yield* Effect.gen(function* () {
const name = Option.getOrElse(flags.name, () => "remote_schema");
// `--declarative` and the deprecated `--use-pg-delta` both bind to the same
// `useDeclarative` outcome. When both are passed, the last occurrence in argv wins
// (e.g. `--declarative --use-pg-delta=false` => migration mode); since parsed Options don't
// carry order, the both-present case replays that rule off the raw argv instead of ORing the
// two, which would diverge on conflicting values.
const useDeclarative =
Option.isSome(flags.declarative) && Option.isSome(flags.usePgDelta)
? (resolveDeclarativeFromArgs(cliArgs.args) ?? false)
: Option.getOrElse(flags.declarative, () => false) ||
Option.getOrElse(flags.usePgDelta, () => false);
if (Option.isSome(flags.usePgDelta)) {
yield* output.raw(`${DEPRECATION_LINE}\n`, "stderr");
}
// Deprecated `--experimental` dump: same in-process export as `--declarative`.
const useExperimentalExport = experimental && !useDeclarative;
if (useExperimentalExport) {
yield* output.raw(`${EXPERIMENTAL_STRUCTURED_DUMP_DEPRECATION_LINE}\n`, "stderr");
}
const useDeclarativeExport = useDeclarative || useExperimentalExport;
const useDeclarative = Option.getOrElse(flags.declarative, () => false);

// Mutually exclusive flag groups: `[db-url linked local]`, `[declarative
// diff-engine]`, `[use-pg-delta diff-engine]`. "set" means the flag was
// explicitly passed.
// Mutually exclusive flag groups: `[db-url linked local]`, `[declarative diff-engine]`.
// "set" means the flag was explicitly passed.
const targetSet: Array<string> = [];
if (Option.isSome(flags.dbUrl)) targetSet.push("db-url");
if (Option.isSome(flags.linked)) targetSet.push("linked");
Expand All @@ -212,15 +162,11 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
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`,
});
}
for (const [flagName, present] of [
["declarative", Option.isSome(flags.declarative)],
["use-pg-delta", Option.isSome(flags.usePgDelta)],
] as const) {
if (present && Option.isSome(flags.diffEngine)) {
return yield* new DbPullEngineConflictError({
message: `if any flags in the group [${flagName} diff-engine] are set none of the others can be; [${[flagName, "diff-engine"].sort().join(" ")}] were all set`,
});
}
if (Option.isSome(flags.declarative) && Option.isSome(flags.diffEngine)) {
return yield* new DbPullEngineConflictError({
message:
"if any flags in the group [declarative diff-engine] are set none of the others can be; [declarative diff-engine] were all set",
});
}

const connType: DbConnType = Option.isSome(flags.dbUrl)
Expand All @@ -230,7 +176,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
: "linked";
yield* Effect.annotateCurrentSpan({
"db.conn_type": connType,
"db.pull.mode": useDeclarativeExport ? "declarative" : "migration",
"db.pull.mode": useDeclarative ? "declarative" : "migration",
});

// `--project-ref` never implies `--linked` and must not be silently
Expand Down Expand Up @@ -270,20 +216,16 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
const runtimeInfo = yield* RuntimeInfo;
const networkIdFlag = yield* NetworkIdFlag;
// Validate native shadow inputs before target resolution performs remote side effects.
const localInputs: Option.Option<LocalDbContainerInputs> = useExperimentalExport
? Option.none()
: Option.some(
yield* buildLocalDbContainerInputs(
spawner,
cliSettings.workdir,
networkIdFlag,
runtimeInfo.platform,
debug,
// So the shadow's own container spec reflects the matching `[remotes.<ref>]`
// override, same as `toml` above.
connType === "linked" ? linkedRef : undefined,
),
);
const localInputs = yield* buildLocalDbContainerInputs(
spawner,
cliSettings.workdir,
networkIdFlag,
runtimeInfo.platform,
debug,
// So the shadow's own container spec reflects the matching `[remotes.<ref>]`
// override, same as `toml` above.
connType === "linked" ? linkedRef : undefined,
);

const resolved = yield* resolver.resolve({
dbUrl: flags.dbUrl,
Expand Down Expand Up @@ -379,8 +321,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
dnsResolver,
});

// Declarative export path (`--declarative` or deprecated `--experimental`).
if (useDeclarativeExport) {
if (useDeclarative) {
yield* output.raw("Preparing declarative schema export using pg-delta...\n", "stderr");
const declarativeDirRel = resolveDeclarativeDir(path, toml.pgDelta);
const declarativeDir = path.resolve(cliSettings.workdir, declarativeDirRel);
Expand Down Expand Up @@ -460,10 +401,6 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
// is "in sync", a non-empty dump is a valid initial migration on its own.
let seedWroteBytes = false;

// `Option.getOrThrow` is safe here: this point is only reached after the
// declarative-export branch already returned, so `localInputs` was always built.
const pullLocalInputs = Option.getOrThrow(localInputs);

if (seededFromDump) {
yield* makeDir(fs, path.dirname(migrationPath)).pipe(
Effect.mapError((cause) => new DbPullWriteError({ message: cause.message })),
Expand Down Expand Up @@ -593,13 +530,13 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
const stackBackend = (yield* currentStackBackend).kind === "stack";
const resolvedPullShadowImage = stackBackend
? "stack-ephemeral"
: yield* pullLocalInputs.resolvePostgresImage;
: yield* localInputs.resolvePostgresImage;
const migrationMode: "legacy" | "pgdelta-next" = usePgDeltaDiff
? "pgdelta-next"
: "legacy";
const shadowInput = {
...shadowRunInputFromLocalContainerInputs(
pullLocalInputs,
localInputs,
resolvedPullShadowImage,
toml,
fs,
Expand All @@ -608,7 +545,7 @@ export const runDbPull = Effect.fn("db.pull.run")(function* (
targetLocal: resolved.isLocal,
migrationMode,
// `toml.schemaPathPatterns` is used here, not the raw `@supabase/config` field on
// `pullLocalInputs`, since only `toml` (via `readDbToml`) resolves the
// `localInputs`, since only `toml` (via `readDbToml`) resolves the
// `SUPABASE_DB_MIGRATIONS_SCHEMA_PATHS` env override.
schemaPaths: toml.schemaPathPatterns,
pgDelta: toml.pgDelta,
Expand Down
37 changes: 0 additions & 37 deletions apps/cli/src/command-internal/diff-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,40 +34,3 @@ export function resolvePullDiffEngine(inputs: {
}
return inputs.pgDeltaDefault;
}

/**
* Parses a boolean env var: accepts exactly `1`, `t`, `T`, `TRUE`, `true`, `True` as true, and
* every other value (including unparseable strings and unset) as false.
*/
export function parseBoolEnv(raw: string | undefined): boolean {
switch (raw) {
case "1":
case "t":
case "T":
case "TRUE":
case "true":
case "True":
return true;
default:
return false;
}
}

/**
* Resolves `db pull` declarative mode from raw argv: `--declarative` and the deprecated
* `--use-pg-delta` alias bind to the same outcome, so the last occurrence in argv wins (ORing
* the two would wrongly select declarative mode from either flag alone). A bare flag is `true`;
* `--flag=value` parses with {@link parseBoolEnv}'s true-set — a following space-separated token
* is never consumed as a value. Returns `undefined` when neither flag is present.
*/
export function resolveDeclarativeFromArgs(args: ReadonlyArray<string>): boolean | undefined {
const FLAG_PATTERN = /^--(?:declarative|use-pg-delta)(?:=(.*))?$/u;
let result: boolean | undefined;
for (const arg of args) {
if (arg === "--") break;
const match = FLAG_PATTERN.exec(arg);
if (match === null) continue;
result = match[1] === undefined ? true : parseBoolEnv(match[1]);
}
return result;
}
53 changes: 1 addition & 52 deletions apps/cli/src/command-internal/diff-engine.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { describe, expect, it } from "vitest";

import {
parseBoolEnv,
resolveDeclarativeFromArgs,
resolveDiffEngine,
resolvePullDiffEngine,
} from "./diff-engine.ts";
import { resolveDiffEngine, resolvePullDiffEngine } from "./diff-engine.ts";

describe("resolveDiffEngine", () => {
const base = {
Expand Down Expand Up @@ -63,49 +58,3 @@ describe("resolvePullDiffEngine", () => {
).toBe(false);
});
});

describe("parseBoolEnv", () => {
it("accepts only the boolean truthy strings", () => {
for (const v of ["1", "t", "T", "TRUE", "true", "True"]) {
expect(parseBoolEnv(v)).toBe(true);
}
});

it("treats every other value (including unset) as false", () => {
for (const v of ["0", "f", "FALSE", "false", "yes", "on", "2", "", "TrUe"]) {
expect(parseBoolEnv(v)).toBe(false);
}
expect(parseBoolEnv(undefined)).toBe(false);
});
});

describe("resolveDeclarativeFromArgs", () => {
it("returns undefined when neither flag is present", () => {
expect(resolveDeclarativeFromArgs(["db", "pull"])).toBeUndefined();
expect(resolveDeclarativeFromArgs([])).toBeUndefined();
});

it("treats a bare flag as true", () => {
expect(resolveDeclarativeFromArgs(["db", "pull", "--declarative"])).toBe(true);
expect(resolveDeclarativeFromArgs(["db", "pull", "--use-pg-delta"])).toBe(true);
});

it("parses an =value with boolean semantics", () => {
expect(resolveDeclarativeFromArgs(["--declarative=false"])).toBe(false);
expect(resolveDeclarativeFromArgs(["--declarative=true"])).toBe(true);
expect(resolveDeclarativeFromArgs(["--use-pg-delta=0"])).toBe(false);
expect(resolveDeclarativeFromArgs(["--use-pg-delta=1"])).toBe(true);
});

it("lets the last occurrence win across both flag names", () => {
expect(resolveDeclarativeFromArgs(["--declarative", "--use-pg-delta=false"])).toBe(false);
expect(resolveDeclarativeFromArgs(["--use-pg-delta", "--declarative=false"])).toBe(false);
expect(resolveDeclarativeFromArgs(["--declarative=false", "--use-pg-delta"])).toBe(true);
expect(resolveDeclarativeFromArgs(["--use-pg-delta=false", "--declarative"])).toBe(true);
});

it("ignores tokens after the `--` argv terminator", () => {
expect(resolveDeclarativeFromArgs(["--declarative", "--", "--use-pg-delta=false"])).toBe(true);
expect(resolveDeclarativeFromArgs(["--", "--declarative"])).toBeUndefined();
});
});
Loading
Loading