1import { assert, describe, it } from "@effect/vitest"
2import { Effect } from "effect"
3import * as Reactivity from "effect/reactivity/Reactivity"
4import { SqlError } from "effect/sql/SqlError"
5import { vi } from "vitest"
6
7const state: {
8 connectCause: unknown
9 queryCause: unknown
10 resultJsonCause: unknown
11} = {
12 connectCause: null,
13 queryCause: null,
14 resultJsonCause: null
15}
16
17vi.mock("@clickhouse/client", () => ({
18 createClient: () => ({
19 ping: () =>
20 state.connectCause
21 ? Promise.resolve({ success: false, error: state.connectCause })
22 : Promise.resolve({ success: true }),
23 close: () => Promise.resolve(),
24 query: () =>
25 state.queryCause
26 ? Promise.reject(state.queryCause)
27 : Promise.resolve({
28 json: () => state.resultJsonCause ? Promise.reject(state.resultJsonCause) : Promise.resolve({ data: [] })
29 }),
30 command: () => Promise.resolve({}),
31 insert: () => Promise.resolve({})
32 })
33}))
34
35const connectFailureReasonTag = (code: number) =>
36 Effect.gen(function*() {
37 state.connectCause = { code }
38 state.queryCause = null
39 state.resultJsonCause = null
40 const { ClickhouseClient } = yield* Effect.promise(() => import("@effect/sql-clickhouse"))
41 const error = yield* Effect.flip(ClickhouseClient.make({ url: "http://localhost:8123" }))
42 return error.reason._tag
43 }).pipe(
44 Effect.scoped,
45 Effect.provide(Reactivity.layer)
46 )
47
48const queryFailureReasonTag = (code: number) =>
49 Effect.gen(function*() {
50 state.connectCause = null
51 state.queryCause = { code }
52 state.resultJsonCause = null
53 const { ClickhouseClient } = yield* Effect.promise(() => import("@effect/sql-clickhouse"))
54 const client = yield* ClickhouseClient.make({ url: "http://localhost:8123" })
55 const error = yield* Effect.flip(client`SELECT 1`)
56 return error.reason._tag
57 }).pipe(
58 Effect.scoped,
59 Effect.provide(Reactivity.layer)
60 )
61
62describe("ClickhouseClient SqlError classification", { concurrent: false }, () => {
63 it.effect("maps representative native codes to reasons", () =>
64 Effect.gen(function*() {
65 const cases = [
66 [516, "AuthenticationError"],
67 [497, "AuthorizationError"],
68 [62, "SqlSyntaxError"],
69 [159, "StatementTimeoutError"]
70 ] as const
71
72 for (const [code, expectedTag] of cases) {
73 const tag = yield* connectFailureReasonTag(code)
74 assert.strictEqual(tag, expectedTag)
75 }
76 }))
77
78 it.effect("falls back to UnknownError for unmapped codes during execution", () =>
79 Effect.gen(function*() {
80 const tag = yield* queryFailureReasonTag(999)
81 assert.strictEqual(tag, "UnknownError")
82 }))
83
84 it.effect("maps result decoding failures to SqlError", () =>
85 Effect.gen(function*() {
86 state.connectCause = null
87 state.queryCause = null
88 state.resultJsonCause = new SyntaxError("invalid JSON response")
89 const { ClickhouseClient } = yield* Effect.promise(() => import("@effect/sql-clickhouse"))
90 const client = yield* ClickhouseClient.make({ url: "http://localhost:8123" })
91 const error = yield* Effect.flip(client`SELECT 1`)
92
93 assert(error instanceof SqlError)
94 assert.strictEqual(error.reason._tag, "UnknownError")
95 assert.strictEqual(error.reason.cause, state.resultJsonCause)
96 }).pipe(
97 Effect.scoped,
98 Effect.provide(Reactivity.layer)
99 ))
100})