1import { assert, describe, it } from "@effect/vitest"
2import { Effect } from "effect"
3import * as Reactivity from "effect/reactivity/Reactivity"
4import type * as SqlError from "effect/sql/SqlError"
5import { vi } from "vitest"
6
7const state: {
8 connectCause: unknown
9} = {
10 connectCause: null
11}
12
13vi.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
21const 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
32const connectFailureReasonTag = (errno: number) => Effect.map(connectFailureReason({ errno }), (reason) => reason._tag)
33
34const 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
41describe("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})