| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Effect } from "effect" |
| 3 | import * as Reactivity from "effect/reactivity/Reactivity" |
| 4 | import type * as SqlError from "effect/sql/SqlError" |
| 5 | import { vi } from "vitest" |
| 6 | |
| 7 | const state: { |
| 8 | connectCause: unknown |
| 9 | } = { |
| 10 | connectCause: null |
| 11 | } |
| 12 | |
| 13 | vi.mock("mysql2", () => ({ |
| 14 | createPool: () => ({ |
| 15 | query: (_sql: string, cb: (cause: unknown) => void) => cb(state.connectCause), |
| 16 | end: (cb: () => void) => cb(), |
| 17 | getConnection: (_cb: () => void) => undefined |
| 18 | }) |
| 19 | })) |
| 20 | |
| 21 | const connectFailureReason = (cause: unknown) => |
| 22 | Effect.gen(function*() { |
| 23 | state.connectCause = cause |
| 24 | const { MysqlClient } = yield* Effect.promise(() => import("@effect/sql-mysql2")) |
| 25 | const error = yield* Effect.flip(MysqlClient.make({})) |
| 26 | return error.reason |
| 27 | }).pipe( |
| 28 | Effect.scoped, |
| 29 | Effect.provide(Reactivity.layer) |
| 30 | ) |
| 31 | |
| 32 | const connectFailureReasonTag = (errno: number) => Effect.map(connectFailureReason({ errno }), (reason) => reason._tag) |
| 33 | |
| 34 | const assertUniqueViolation = (reason: SqlError.SqlErrorReason, constraint: string) => { |
| 35 | assert.strictEqual(reason._tag, "UniqueViolation") |
| 36 | if (reason._tag === "UniqueViolation") { |
| 37 | assert.strictEqual(reason.constraint, constraint) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | describe("MysqlClient SqlError classification", { concurrent: false }, () => { |
| 42 | it.effect("maps representative errno codes to reasons", () => |
| 43 | Effect.gen(function*() { |
| 44 | const cases = [ |
| 45 | [1040, "ConnectionError"], |
| 46 | [1045, "AuthenticationError"], |
| 47 | [1142, "AuthorizationError"], |
| 48 | [1064, "SqlSyntaxError"], |
| 49 | [1062, "UniqueViolation"], |
| 50 | [1048, "ConstraintError"], |
| 51 | [1213, "DeadlockError"], |
| 52 | [1205, "LockTimeoutError"], |
| 53 | [3024, "StatementTimeoutError"] |
| 54 | ] as const |
| 55 | |
| 56 | for (const [errno, expectedTag] of cases) { |
| 57 | const tag = yield* connectFailureReasonTag(errno) |
| 58 | assert.strictEqual(tag, expectedTag) |
| 59 | } |
| 60 | })) |
| 61 | |
| 62 | it.effect("falls back to UnknownError for unmapped errno", () => |
| 63 | Effect.gen(function*() { |
| 64 | const tag = yield* connectFailureReasonTag(9999) |
| 65 | assert.strictEqual(tag, "UnknownError") |
| 66 | })) |
| 67 | |
| 68 | it.effect("classifies duplicate-entry errno 1062 as UniqueViolation", () => |
| 69 | Effect.gen(function*() { |
| 70 | const reason = yield* connectFailureReason({ errno: 1062 }) |
| 71 | assertUniqueViolation(reason, "unknown") |
| 72 | })) |
| 73 | |
| 74 | it.effect("extracts duplicate-entry constraints from structured fields and common messages", () => |
| 75 | Effect.gen(function*() { |
| 76 | const explicit = yield* connectFailureReason({ |
| 77 | errno: 1062, |
| 78 | constraint: " users_email_key ", |
| 79 | sqlMessage: "Duplicate entry 'user@example.com' for key 'ignored_key'" |
| 80 | }) |
| 81 | assertUniqueViolation(explicit, "users_email_key") |
| 82 | |
| 83 | const singleQuoted = yield* connectFailureReason({ |
| 84 | errno: 1062, |
| 85 | sqlMessage: "Duplicate entry 'user@example.com' for key 'users_email_key'" |
| 86 | }) |
| 87 | assertUniqueViolation(singleQuoted, "users_email_key") |
| 88 | |
| 89 | const backtickQuoted = yield* connectFailureReason({ |
| 90 | errno: 1062, |
| 91 | message: "Duplicate entry '1' for key `PRIMARY`" |
| 92 | }) |
| 93 | assertUniqueViolation(backtickQuoted, "PRIMARY") |
| 94 | |
| 95 | const unquoted = yield* connectFailureReason({ |
| 96 | errno: 1062, |
| 97 | sqlMessage: "Duplicate entry 'user@example.com' for key users_email_key" |
| 98 | }) |
| 99 | assertUniqueViolation(unquoted, "users_email_key") |
| 100 | })) |
| 101 | |
| 102 | it.effect("uses unknown for blank, missing, malformed, or non-string duplicate-entry metadata", () => |
| 103 | Effect.gen(function*() { |
| 104 | const missing = yield* connectFailureReason({ errno: 1062 }) |
| 105 | assertUniqueViolation(missing, "unknown") |
| 106 | |
| 107 | const blank = yield* connectFailureReason({ |
| 108 | errno: 1062, |
| 109 | constraint: " ", |
| 110 | sqlMessage: "Duplicate entry 'user@example.com' for key ' '" |
| 111 | }) |
| 112 | assertUniqueViolation(blank, "unknown") |
| 113 | |
| 114 | const malformed = yield* connectFailureReason({ |
| 115 | errno: 1062, |
| 116 | sqlMessage: "Duplicate entry 'user@example.com'" |
| 117 | }) |
| 118 | assertUniqueViolation(malformed, "unknown") |
| 119 | |
| 120 | const nonString = yield* connectFailureReason({ |
| 121 | errno: 1062, |
| 122 | sqlMessage: 1062, |
| 123 | message: { text: "Duplicate entry 'user@example.com' for key 'users_email_key'" } |
| 124 | }) |
| 125 | assertUniqueViolation(nonString, "unknown") |
| 126 | })) |
| 127 | |
| 128 | it.effect("keeps non-unique constraint errno values classified as ConstraintError", () => |
| 129 | Effect.gen(function*() { |
| 130 | const tag = yield* connectFailureReasonTag(1452) |
| 131 | assert.strictEqual(tag, "ConstraintError") |
| 132 | })) |
| 133 | }) |