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
13 changes: 7 additions & 6 deletions apps/cli/src/legacy/commands/db/diff/diff.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service
import type { LegacyPgConnInput } from "../../../shared/legacy-db-connection.service.ts";
import type { LegacyDbConnType } from "../../../shared/legacy-db-target-flags.ts";
import { legacyGetHostname } from "../../../shared/legacy-hostname.ts";
import { legacyMakeDir } from "../../../shared/legacy-make-dir.ts";
import { legacyToPostgresURL } from "../../../shared/legacy-postgres-url.ts";
import { legacySchemaToCsvField } from "../../../shared/legacy-schema-flags.ts";
import { legacyFindDropStatements } from "../../../shared/legacy-sql-split.ts";
Expand Down Expand Up @@ -277,9 +278,9 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy
// Create parent dirs first, matching Go's `writeOutput` → `utils.WriteFile`
// (`internal/db/diff/explicit.go`, `internal/utils/misc.go`), so a nested
// `--output tmp/diff.sql` doesn't fail when `tmp/` doesn't exist yet.
yield* fs
.makeDirectory(path.dirname(target), { recursive: true })
.pipe(Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message })));
yield* legacyMakeDir(fs, path.dirname(target)).pipe(
Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message })),
);
yield* fs
.writeFileString(target, result.sql)
.pipe(Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message })));
Expand Down Expand Up @@ -457,9 +458,9 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy
timestamp,
flags.file.value,
);
yield* fs
.makeDirectory(path.dirname(migrationPath), { recursive: true })
.pipe(Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message })));
yield* legacyMakeDir(fs, path.dirname(migrationPath)).pipe(
Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message })),
);
yield* fs
.writeFileString(migrationPath, out)
.pipe(Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message })));
Expand Down
7 changes: 4 additions & 3 deletions apps/cli/src/legacy/commands/db/pull/pull.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
legacyResolveDeclarativeDir,
} from "../../../shared/legacy-db-config.toml-read.ts";
import type { LegacyDbConnType } from "../../../shared/legacy-db-target-flags.ts";
import { legacyMakeDir } from "../../../shared/legacy-make-dir.ts";
import { legacyToPostgresURL } from "../../../shared/legacy-postgres-url.ts";
import { legacySchemaToCsvField } from "../../../shared/legacy-schema-flags.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
Expand Down Expand Up @@ -540,9 +541,9 @@ export const legacyDbPull = Effect.fn("legacy.db.pull")(function* (flags: Legacy
new LegacyDbPullInSyncError({ message: "No schema changes found" }),
);
}
yield* fs
.makeDirectory(path.dirname(migrationPath), { recursive: true })
.pipe(Effect.mapError((cause) => new LegacyDbPullWriteError({ message: cause.message })));
yield* legacyMakeDir(fs, path.dirname(migrationPath)).pipe(
Effect.mapError((cause) => new LegacyDbPullWriteError({ message: cause.message })),
);
yield* fs.writeFileString(migrationPath, out).pipe(
Effect.mapError(
(cause) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
legacyReadDbToml,
legacyResolveDeclarativeDir,
} from "../../../../../shared/legacy-db-config.toml-read.ts";
import { legacyMakeDir } from "../../../../../shared/legacy-make-dir.ts";
import { legacyApplyMigrationFile } from "../../../../../shared/legacy-migration-apply.ts";
import { legacyReadProjectRefFile } from "../../../../../shared/legacy-temp-paths.ts";
import { LegacyLinkedProjectCache } from "../../../../../telemetry/legacy-linked-project-cache.service.ts";
Expand Down Expand Up @@ -276,7 +277,7 @@ export const legacyDbSchemaDeclarativeSync = Effect.fn("legacy.db.schema.declara
// Step 5: write the timestamped migration file.
const timestamp = formatTimestamp(yield* Clock.currentTimeMillis);
const migrationPath = path.join(migrationsDir, `${timestamp}_${migrationName}.sql`);
yield* fs.makeDirectory(migrationsDir, { recursive: true });
yield* legacyMakeDir(fs, migrationsDir);
yield* fs.writeFileString(migrationPath, result.diffSQL);
yield* output.raw(`Created new migration at ${legacyBold(migrationPath)}\n`, "stderr");

Expand Down
26 changes: 26 additions & 0 deletions apps/cli/src/legacy/shared/legacy-make-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Effect, FileSystem } from "effect";
import type { PlatformError } from "effect/PlatformError";

/**
* `os.MkdirAll`-equivalent: create `dir` and any missing parents, treating an
* already-existing directory as success.
*
* Go's `os.MkdirAll` returns nil when the target is already a directory, so the
* Go CLI's migration writers never failed on a pre-existing `supabase/migrations`.
* Effect's Bun `FileSystem.makeDirectory` does not always match that: even with
* `recursive: true` it can surface an `AlreadyExists` `SystemError` for an
* existing directory on some platforms (notably Windows / OneDrive reparse
* points — see CLI-1849). Recover from that single reason so re-creating an
* existing directory is a no-op, and let every other failure propagate.
*/
export const legacyMakeDir = (
fs: FileSystem.FileSystem,
dir: string,
): Effect.Effect<void, PlatformError> =>
fs
.makeDirectory(dir, { recursive: true })
.pipe(
Effect.catchTag("PlatformError", (error) =>
error.reason._tag === "AlreadyExists" ? Effect.void : Effect.fail(error),
),
);
93 changes: 93 additions & 0 deletions apps/cli/src/legacy/shared/legacy-make-dir.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit, FileSystem, Layer, PlatformError } from "effect";

import { legacyMakeDir } from "./legacy-make-dir.ts";

const DIR = "/home/user/project/supabase/migrations";

type SystemReason = Parameters<typeof PlatformError.systemError>[0]["_tag"];

/** A FileSystem whose `makeDirectory` always fails with the given system reason. */
function failingFs(reason: SystemReason) {
const calls: Array<{ readonly path: string; readonly recursive?: boolean }> = [];
return {
calls,
layer: Layer.succeed(
FileSystem.FileSystem,
FileSystem.makeNoop({
makeDirectory: (path, opts) =>
Effect.suspend(() => {
calls.push({ path, recursive: opts?.recursive });
return Effect.fail(
PlatformError.systemError({
_tag: reason,
module: "FileSystem",
method: "makeDirectory",
description: reason,
pathOrDescriptor: path,
}),
);
}),
}),
),
};
}

const run = (dir: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* legacyMakeDir(fs, dir);
});

describe("legacyMakeDir", () => {
it.effect("creates the directory recursively, matching os.MkdirAll", () => {
const calls: Array<{ readonly path: string; readonly recursive?: boolean }> = [];
const layer = Layer.succeed(
FileSystem.FileSystem,
FileSystem.makeNoop({
makeDirectory: (path, opts) =>
Effect.sync(() => {
calls.push({ path, recursive: opts?.recursive });
}),
}),
);
return run(DIR).pipe(
Effect.tap(() =>
Effect.sync(() => {
expect(calls).toEqual([{ path: DIR, recursive: true }]);
}),
),
Effect.provide(layer),
);
});

it.effect("treats an already-existing directory as success (CLI-1849)", () => {
const fs = failingFs("AlreadyExists");
return run(DIR).pipe(
Effect.exit,
Effect.tap((exit) =>
Effect.sync(() => {
expect(Exit.isSuccess(exit)).toBe(true);
expect(fs.calls).toEqual([{ path: DIR, recursive: true }]);
}),
),
Effect.provide(fs.layer),
);
});

it.effect("propagates every other filesystem error", () => {
const fs = failingFs("PermissionDenied");
return run(DIR).pipe(
Effect.exit,
Effect.tap((exit) =>
Effect.sync(() => {
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("PermissionDenied");
}
}),
),
Effect.provide(fs.layer),
);
});
});
Loading