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 @@ -5,6 +5,7 @@ import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSp

import {
actionability,
causeDeclaration,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
} from "../../shared/telemetry/error-actionability.ts";
Expand All @@ -26,12 +27,13 @@ export class LocalDbRunningError extends Data.TaggedError("LocalDbRunningError")
readonly daemonDown?: boolean;
/** Set when the failure is a daemon-connection error. */
readonly suggestion?: string;
readonly cause?: unknown;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
if (this.daemonDown === true) {
return { ...actionability.dockerNotRunning, fingerprint_suffix: "docker_not_running" };
}
return actionability.startStack;
return causeDeclaration(this.cause) ?? actionability.startStack;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Output } from "../../shared/output/output.service.ts";
import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts";
import {
actionability,
causeDeclaration,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
} from "../../shared/telemetry/error-actionability.ts";
Expand Down Expand Up @@ -65,9 +66,10 @@ class ResetLocalDbNotRunningError extends Data.TaggedError("ResetLocalDbNotRunni
class ResetLocalDbFailedError extends Data.TaggedError("ResetLocalDbFailedError")<{
readonly message: string;
readonly suggestion?: string;
readonly cause?: unknown;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.dbConnection;
return causeDeclaration(this.cause) ?? actionability.dbConnection;
}
}

Expand All @@ -92,7 +94,8 @@ const notRunning = () =>
message: `${aqua("supabase start")} is not running.`,
});

const resetFailed = (message: string) => new ResetLocalDbFailedError({ message });
const resetFailed = (message: string, cause?: unknown) =>
new ResetLocalDbFailedError({ message, ...(cause === undefined ? {} : { cause }) });

const suggestionOf = (error: unknown): string | undefined =>
error instanceof StackStorageUnavailableError || error instanceof StackStorageCapabilityError
Expand Down Expand Up @@ -159,16 +162,24 @@ export const resetLocalDatabase = Effect.fn("DbBootstrap.resetLocalDatabase")(fu
yield* output.raw(`Resetting local database${toLogMessage(input.version)}\n`, "stderr");
yield* Effect.gen(function* () {
yield* opened.value.stack.composition.stop.pipe(
Effect.mapError((cause) => resetFailed(`failed to stop local stack: ${cause.message}`)),
Effect.mapError((cause) =>
resetFailed(`failed to stop local stack: ${cause.message}`, cause),
),
);
yield* opened.value.database.resetData.pipe(
Effect.mapError((cause) => resetFailed(`failed to reset local database: ${cause.message}`)),
Effect.mapError((cause) =>
resetFailed(`failed to reset local database: ${cause.message}`, cause),
),
);
yield* opened.value.database.start.pipe(
Effect.mapError((cause) => resetFailed(`failed to start local database: ${cause.message}`)),
Effect.mapError((cause) =>
resetFailed(`failed to start local database: ${cause.message}`, cause),
),
);
yield* opened.value.database.ready.pipe(
Effect.mapError((cause) => resetFailed(`failed to ready local database: ${cause.message}`)),
Effect.mapError((cause) =>
resetFailed(`failed to ready local database: ${cause.message}`, cause),
),
);
}).pipe(Effect.withSpan("DbBootstrap.resetStackDatabase"));
yield* initializeStackDatabase({
Expand Down
21 changes: 12 additions & 9 deletions apps/cli/src/command-internal/stack-local-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,15 @@ const cacheRoot = (settings: Settings, path: Path.Path) =>
path.join(settings.supabaseHome, "cache", "stack");
const notRunning = (message = "The local stack is not running.") =>
new LocalDbRunningError({ message });
const unavailable = (cause: { readonly message: string }) =>
cause instanceof LocalDbRunningError
? cause
: new LocalDbRunningError({ message: cause.message, cause });
Comment thread
avallete marked this conversation as resolved.
Resolved
const startFailed = (cause: { readonly message: string }) =>
new LocalDbRunningError({ message: `failed to start local database: ${cause.message}` });
new LocalDbRunningError({
message: `failed to start local database: ${cause.message}`,
cause,
});

type StackInstances = Effect.Success<Stack["services"]["list"]>;

Expand Down Expand Up @@ -99,7 +106,7 @@ export const stackOpenProjectBy = <E>(onFailure: (cause: { readonly message: str
openProjectStack().pipe(Effect.mapError(onFailure));

/** Returns the project stack only when its primary database is healthy. */
export const stackOpenReadyProject = stackOpenProjectBy((cause) => notRunning(cause.message)).pipe(
export const stackOpenReadyProject = stackOpenProjectBy(unavailable).pipe(
Effect.flatMap((opened) =>
Option.isNone(opened) ? Effect.succeed(Option.none()) : databaseReady(opened.value),
),
Expand Down Expand Up @@ -180,13 +187,9 @@ const stackLocalDatabaseUrl: Effect.Effect<
LocalDbRunningError,
CommandSettings | StackApi | Path.Path
> = Effect.gen(function* () {
const opened = yield* stackOpenReadyProject.pipe(
Effect.mapError((cause) => notRunning(cause.message)),
);
const opened = yield* stackOpenReadyProject.pipe(Effect.mapError(unavailable));
if (Option.isNone(opened)) return yield* notRunning();
const status = yield* opened.value.database.status.pipe(
Effect.mapError((cause) => notRunning(cause.message)),
);
const status = yield* opened.value.database.status.pipe(Effect.mapError(unavailable));
const endpoint = status.endpoints.find(({ name }) => name === "sql");
if (status.config.service !== "database")
return yield* notRunning("The local stack primary service is not a database.");
Expand All @@ -210,7 +213,7 @@ export const stackLocalDatabaseConn: Effect.Effect<
? Effect.fail(notRunning("failed to parse stack database URL"))
: Effect.succeed(conn),
),
Effect.mapError((cause) => notRunning(cause.message)),
Effect.mapError(unavailable),
);

/** Starts or resumes the primary database, initializing the catalog on first creation. */
Expand Down
4 changes: 3 additions & 1 deletion apps/cli/src/command-internal/test-db.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Data } from "effect";

import {
actionability,
causeDeclaration,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
} from "../shared/telemetry/error-actionability.ts";
Expand All @@ -21,9 +22,10 @@ export class TestDbEnablePgtapError extends Data.TaggedError("TestDbEnablePgtapE
*/
export class TestDbRunError extends Data.TaggedError("TestDbRunError")<{
readonly message: string;
readonly cause?: unknown;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.dbFinding;
return causeDeclaration(this.cause) ?? actionability.dbFinding;
}
}

Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/command-internal/test-db.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,8 @@ export const testDb = Effect.fn("test.db")(function* (flags: TestDbFlags) {
})
.pipe(
Effect.mapError(
(cause) => new TestDbRunError({ message: `pg_prove run failed: ${cause.message}` }),
(cause) =>
new TestDbRunError({ message: `pg_prove run failed: ${cause.message}`, cause }),
),
);
}
Expand Down
8 changes: 5 additions & 3 deletions apps/cli/src/commands/db/diff/diff.errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Data } from "effect";
import {
actionability,
causeDeclaration,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
} from "../../../shared/telemetry/error-actionability.ts";
Expand Down Expand Up @@ -74,14 +75,15 @@ export class DbDiffDbNotRunningError extends Data.TaggedError("DbDiffDbNotRunnin
readonly message: string;
readonly daemonDown?: boolean;
readonly suggestion?: string;
readonly cause?: unknown;
}> {
// Must stay character-identical to `LocalDbRunningError`'s classification
// (`legacy-db-bootstrap`'s equivalent local-db-not-running check). The two are kept in sync by
// hand rather than shared, since they serve separate parity targets.
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return this.daemonDown === true
? { ...actionability.dockerNotRunning, fingerprint_suffix: "docker_not_running" }
: actionability.startStack; // same preset `reset-local-database.ts` uses
if (this.daemonDown === true)
return { ...actionability.dockerNotRunning, fingerprint_suffix: "docker_not_running" };
return causeDeclaration(this.cause) ?? actionability.startStack; // same preset `reset-local-database.ts` uses
}
}

Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/commands/db/diff/diff.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) {
message: cause.message,
daemonDown: cause.daemonDown,
suggestion: cause.suggestion,
cause,
}),
),
);
Expand Down
4 changes: 3 additions & 1 deletion apps/cli/src/commands/db/dump/dump.errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Data } from "effect";
import {
actionability,
causeDeclaration,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
} from "../../../shared/telemetry/error-actionability.ts";
Expand Down Expand Up @@ -48,8 +49,9 @@ export class DbDumpRunError extends Data.TaggedError("DbDumpRunError")<{
readonly message: string;
/** Printed on stderr after the error (IPv6 pooler guidance, native client hint). */
readonly suggestion?: string;
readonly cause?: unknown;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.dbConnection;
return causeDeclaration(this.cause) ?? actionability.dbConnection;
}
}
4 changes: 2 additions & 2 deletions apps/cli/src/commands/db/dump/dump.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export const dbDump = Effect.fn("db.dump")(function* (flags: DbDumpFlags) {
const managedProject =
backend.kind === "stack" && connType === "local"
? yield* stackOpenReadyProject.pipe(
Effect.mapError((cause) => new DbDumpRunError({ message: cause.message })),
Effect.mapError((cause) => new DbDumpRunError({ message: cause.message, cause })),
Effect.map(Option.getOrUndefined),
)
: undefined;
Expand Down Expand Up @@ -288,7 +288,7 @@ export const dbDump = Effect.fn("db.dump")(function* (flags: DbDumpFlags) {
? undefined
: yield* parseConnectionString(
yield* managedProject.database.credentials({ from: "runtime" }).pipe(
Effect.mapError((cause) => new DbDumpRunError({ message: cause.message })),
Effect.mapError((cause) => new DbDumpRunError({ message: cause.message, cause })),
Effect.map((credentials) => credentials.databaseUrl ?? ""),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Data } from "effect";

import {
actionability,
causeDeclaration,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
} from "../../../../shared/telemetry/error-actionability.ts";
Expand Down Expand Up @@ -119,10 +120,14 @@ export class DeclarativeApplyError extends Data.TaggedError("DeclarativeApplyErr
* hint) when the local-reset recovery path fails, so the wrap doesn't drop it.
*/
readonly suggestion?: string;
readonly cause?: unknown;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
if (this.connect === true) {
return { ...actionability.dbConnection, fingerprint_suffix: "connect" };
const declared = causeDeclaration(this.cause);
return declared === undefined
? { ...actionability.dbConnection, fingerprint_suffix: "connect" }
: { ...declared, fingerprint_suffix: declared.fingerprint_suffix ?? "connect" };
}
Comment thread
avallete marked this conversation as resolved.
Resolved
return actionability.dbFinding;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,8 @@ export const dbSchemaDeclarativeSync = Effect.fn("db.schema.declarative.sync")(f
backend.kind === "stack"
? yield* stackLocalDatabaseConn.pipe(
Effect.mapError(
(error) => new DeclarativeApplyError({ message: error.message, connect: true }),
(error) =>
new DeclarativeApplyError({ message: error.message, connect: true, cause: error }),
),
)
: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { Data } from "effect";
import {
actionability,
causeDeclaration,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
unclassifiedStackFailureActionability,
} from "../../../../shared/telemetry/error-actionability.ts";

export class StackCommandDestroyError extends Data.TaggedError("ExperimentalStackDestroyError")<{
readonly reason:
| "flags"
| "confirmation"
| "cancelled"
| "invalid-config"
| "runtime"
| "lifecycle"
| "unknown";
readonly reason: "flags" | "confirmation" | "cancelled" | "invalid-config" | "runtime" | "stack";
readonly message: string;
readonly detail?: string;
readonly suggestion?: string;
Expand All @@ -22,17 +17,17 @@ export class StackCommandDestroyError extends Data.TaggedError("ExperimentalStac
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
switch (this.reason) {
case "flags":
return { ...actionability.provideFlags, fingerprint_suffix: "flags" };
case "confirmation":
return actionability.provideFlags;
return { ...actionability.provideFlags, fingerprint_suffix: "confirmation" };
case "cancelled":
return actionability.cancelled;
return { ...actionability.cancelled, fingerprint_suffix: "cancelled" };
case "invalid-config":
case "lifecycle":
return actionability.invalidConfig;
return { ...actionability.invalidConfig, fingerprint_suffix: "invalid_config" };
case "runtime":
return actionability.dockerNotRunning;
case "unknown":
return actionability.unknown;
return { ...actionability.dockerNotRunning, fingerprint_suffix: "docker_not_running" };
case "stack":
return causeDeclaration(this.cause) ?? unclassifiedStackFailureActionability;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const destroyError = (id: string) => (cause: StackError) =>
cause,
})
: new StackCommandDestroyError({
reason: "unknown",
reason: "stack",
message: cause.message,
cause,
});
Expand Down Expand Up @@ -192,13 +192,14 @@ export const stackDestroy = Effect.fn("experimental.stack.destroy")(function* (
const retry = `"supabase stack destroy ${failed.map(({ id }) => `--stack-id ${id}`).join(" ")} --yes"`;
const runtime = failed.every(({ error }) => error.reason === "runtime");
return yield* new StackCommandDestroyError({
reason: runtime ? "runtime" : "unknown",
reason: runtime ? "runtime" : "stack",
message: `Failed to destroy ${failed.length} managed stack(s).`,
detail: failed.map(({ id, error }) => `${id}: ${error.message}`).join("\n"),
suggestion: runtime
? `Start the container engine, then run ${retry} again; nothing was removed for those stacks.`
: `Resolve each error, then run ${retry} to retry the stacks that failed.`,
cause: failed,
// Telemetry classifies a batch by its first failure; `detail` lists every stack.
cause: failure.error,
});
}
if (batch && output.format !== "text")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ describe("stack destroy", () => {
Effect.flip,
);

expect(error).toMatchObject({ reason: "unknown", message: "engine refused" });
expect(error).toMatchObject({ reason: "stack", message: "engine refused" });
expect((yield* f.api.discover(f.locations)).map(({ definition }) => definition.id)).toEqual([
f.stack.id,
]);
Expand All @@ -489,7 +489,7 @@ describe("stack destroy", () => {
);

expect(yield* destroyWith([f.stack.id])).toMatchObject({
reason: "unknown",
reason: "stack",
suggestion: `Resolve each error, then run ${retry} to retry the stacks that failed.`,
});
expect(yield* destroyWith(ids)).toMatchObject({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class StackCommandListError extends Data.TaggedError("ExperimentalStackLi
readonly cause?: unknown;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return this.reason === "flags" ? actionability.provideFlags : actionability.invalidConfig;
return this.reason === "flags"
? { ...actionability.provideFlags, fingerprint_suffix: "flags" }
: { ...actionability.invalidConfig, fingerprint_suffix: "invalid_config" };
}
}
Loading
Loading