@effect/sql-pg 4.0.0-beta.4 → 4.0.0-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/PgClient.d.ts +57 -17
- package/dist/PgClient.d.ts.map +1 -1
- package/dist/PgClient.js +321 -172
- package/dist/PgClient.js.map +1 -1
- package/dist/PgMigrator.d.ts +8 -4
- package/dist/PgMigrator.d.ts.map +1 -1
- package/dist/PgMigrator.js +49 -56
- package/dist/PgMigrator.js.map +1 -1
- package/package.json +3 -3
- package/src/PgClient.ts +507 -234
- package/src/PgMigrator.ts +74 -60
package/src/PgClient.ts
CHANGED
|
@@ -10,16 +10,31 @@ import * as Effect from "effect/Effect"
|
|
|
10
10
|
import * as Fiber from "effect/Fiber"
|
|
11
11
|
import * as Layer from "effect/Layer"
|
|
12
12
|
import * as Number from "effect/Number"
|
|
13
|
+
import * as Option from "effect/Option"
|
|
13
14
|
import * as Queue from "effect/Queue"
|
|
14
15
|
import * as RcRef from "effect/RcRef"
|
|
15
16
|
import * as Redacted from "effect/Redacted"
|
|
16
17
|
import * as Scope from "effect/Scope"
|
|
18
|
+
import * as Semaphore from "effect/Semaphore"
|
|
17
19
|
import * as ServiceMap from "effect/ServiceMap"
|
|
18
20
|
import * as Stream from "effect/Stream"
|
|
19
21
|
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
|
20
22
|
import * as Client from "effect/unstable/sql/SqlClient"
|
|
21
23
|
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
|
22
|
-
import
|
|
24
|
+
import type * as SqlConnection from "effect/unstable/sql/SqlConnection"
|
|
25
|
+
import {
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
AuthorizationError,
|
|
28
|
+
ConnectionError,
|
|
29
|
+
ConstraintError,
|
|
30
|
+
DeadlockError,
|
|
31
|
+
LockTimeoutError,
|
|
32
|
+
SerializationError,
|
|
33
|
+
SqlError,
|
|
34
|
+
SqlSyntaxError,
|
|
35
|
+
StatementTimeoutError,
|
|
36
|
+
UnknownError
|
|
37
|
+
} from "effect/unstable/sql/SqlError"
|
|
23
38
|
import type { Custom, Fragment } from "effect/unstable/sql/Statement"
|
|
24
39
|
import * as Statement from "effect/unstable/sql/Statement"
|
|
25
40
|
import type { Duplex } from "node:stream"
|
|
@@ -28,11 +43,6 @@ import * as Pg from "pg"
|
|
|
28
43
|
import * as PgConnString from "pg-connection-string"
|
|
29
44
|
import Cursor from "pg-cursor"
|
|
30
45
|
|
|
31
|
-
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|
32
|
-
const ATTR_DB_NAMESPACE = "db.namespace"
|
|
33
|
-
const ATTR_SERVER_ADDRESS = "server.address"
|
|
34
|
-
const ATTR_SERVER_PORT = "server.port"
|
|
35
|
-
|
|
36
46
|
/**
|
|
37
47
|
* @category type ids
|
|
38
48
|
* @since 1.0.0
|
|
@@ -78,14 +88,9 @@ export interface PgClientConfig {
|
|
|
78
88
|
readonly username?: string | undefined
|
|
79
89
|
readonly password?: Redacted.Redacted | undefined
|
|
80
90
|
|
|
81
|
-
readonly
|
|
82
|
-
|
|
83
|
-
readonly idleTimeout?: Duration.DurationInput | undefined
|
|
84
|
-
readonly connectTimeout?: Duration.DurationInput | undefined
|
|
91
|
+
readonly connectTimeout?: Duration.Input | undefined
|
|
85
92
|
|
|
86
|
-
readonly
|
|
87
|
-
readonly minConnections?: number | undefined
|
|
88
|
-
readonly connectionTTL?: Duration.DurationInput | undefined
|
|
93
|
+
readonly stream?: (() => Duplex) | undefined
|
|
89
94
|
|
|
90
95
|
readonly applicationName?: string | undefined
|
|
91
96
|
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
@@ -100,9 +105,19 @@ export interface PgClientConfig {
|
|
|
100
105
|
* @category constructors
|
|
101
106
|
* @since 1.0.0
|
|
102
107
|
*/
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
export interface PgPoolConfig extends PgClientConfig {
|
|
109
|
+
readonly idleTimeout?: Duration.Input | undefined
|
|
110
|
+
|
|
111
|
+
readonly maxConnections?: number | undefined
|
|
112
|
+
readonly minConnections?: number | undefined
|
|
113
|
+
readonly connectionTTL?: Duration.Input | undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @category constructors
|
|
118
|
+
* @since 1.0.0
|
|
119
|
+
*/
|
|
120
|
+
export const make = (options: PgPoolConfig): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
106
121
|
fromPool({
|
|
107
122
|
...options,
|
|
108
123
|
acquire: Effect.gen(function*() {
|
|
@@ -116,15 +131,15 @@ export const make = (
|
|
|
116
131
|
port: options.port,
|
|
117
132
|
...(options.stream ? { stream: options.stream } : {}),
|
|
118
133
|
connectionTimeoutMillis: options.connectTimeout
|
|
119
|
-
? Duration.toMillis(Duration.
|
|
134
|
+
? Duration.toMillis(Duration.fromInputUnsafe(options.connectTimeout))
|
|
120
135
|
: undefined,
|
|
121
136
|
idleTimeoutMillis: options.idleTimeout
|
|
122
|
-
? Duration.toMillis(Duration.
|
|
137
|
+
? Duration.toMillis(Duration.fromInputUnsafe(options.idleTimeout))
|
|
123
138
|
: undefined,
|
|
124
139
|
max: options.maxConnections,
|
|
125
140
|
min: options.minConnections,
|
|
126
141
|
maxLifetimeSeconds: options.connectionTTL
|
|
127
|
-
? Duration.toSeconds(Duration.
|
|
142
|
+
? Duration.toSeconds(Duration.fromInputUnsafe(options.connectionTTL))
|
|
128
143
|
: undefined,
|
|
129
144
|
application_name: options.applicationName ?? "@effect/sql-pg",
|
|
130
145
|
types: options.types
|
|
@@ -135,7 +150,7 @@ export const make = (
|
|
|
135
150
|
yield* Effect.acquireRelease(
|
|
136
151
|
Effect.tryPromise({
|
|
137
152
|
try: () => pool.query("SELECT 1"),
|
|
138
|
-
catch: (cause) => new SqlError({ cause,
|
|
153
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
|
|
139
154
|
}),
|
|
140
155
|
() =>
|
|
141
156
|
Effect.promise(() => pool.end()).pipe(
|
|
@@ -144,11 +159,14 @@ export const make = (
|
|
|
144
159
|
).pipe(
|
|
145
160
|
Effect.timeoutOrElse({
|
|
146
161
|
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
147
|
-
|
|
162
|
+
orElse: () =>
|
|
148
163
|
Effect.fail(
|
|
149
164
|
new SqlError({
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
reason: new ConnectionError({
|
|
166
|
+
cause: new Error("Connection timed out"),
|
|
167
|
+
message: "PgClient: Connection timed out",
|
|
168
|
+
operation: "connect"
|
|
169
|
+
})
|
|
152
170
|
})
|
|
153
171
|
)
|
|
154
172
|
})
|
|
@@ -158,6 +176,63 @@ export const make = (
|
|
|
158
176
|
})
|
|
159
177
|
})
|
|
160
178
|
|
|
179
|
+
/**
|
|
180
|
+
* @category constructors
|
|
181
|
+
* @since 1.0.0
|
|
182
|
+
*/
|
|
183
|
+
export const makeClient = (
|
|
184
|
+
options: PgClientConfig & {
|
|
185
|
+
/**
|
|
186
|
+
* Whether to acquire a separate client for each sql.stream / sql.listen
|
|
187
|
+
*/
|
|
188
|
+
readonly acquireForStream?: boolean | undefined
|
|
189
|
+
}
|
|
190
|
+
): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
191
|
+
fromClient({
|
|
192
|
+
...options,
|
|
193
|
+
acquire: Effect.gen(function*() {
|
|
194
|
+
const client = new Pg.Client({
|
|
195
|
+
connectionString: options.url ? Redacted.value(options.url) : undefined,
|
|
196
|
+
user: options.username,
|
|
197
|
+
host: options.host,
|
|
198
|
+
database: options.database,
|
|
199
|
+
password: options.password ? Redacted.value(options.password) : undefined,
|
|
200
|
+
ssl: options.ssl,
|
|
201
|
+
port: options.port,
|
|
202
|
+
...(options.stream ? { stream: options.stream } : {}),
|
|
203
|
+
application_name: options.applicationName ?? "@effect/sql-pg",
|
|
204
|
+
types: options.types
|
|
205
|
+
})
|
|
206
|
+
yield* Effect.acquireRelease(
|
|
207
|
+
Effect.tryPromise({
|
|
208
|
+
try: () => client.query("SELECT 1"),
|
|
209
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
|
|
210
|
+
}),
|
|
211
|
+
() =>
|
|
212
|
+
Effect.promise(() => client.end()).pipe(
|
|
213
|
+
Effect.timeoutOption(1000)
|
|
214
|
+
)
|
|
215
|
+
).pipe(
|
|
216
|
+
Effect.timeoutOrElse({
|
|
217
|
+
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
218
|
+
orElse: () =>
|
|
219
|
+
Effect.fail(
|
|
220
|
+
new SqlError({
|
|
221
|
+
reason: new ConnectionError({
|
|
222
|
+
cause: new Error("Connection timed out"),
|
|
223
|
+
message: "PgClient: Connection timed out",
|
|
224
|
+
operation: "connect"
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
)
|
|
228
|
+
})
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return client
|
|
232
|
+
}),
|
|
233
|
+
acquireForStream: options.acquireForStream ?? false
|
|
234
|
+
})
|
|
235
|
+
|
|
161
236
|
/**
|
|
162
237
|
* @category constructors
|
|
163
238
|
* @since 1.0.0
|
|
@@ -175,167 +250,74 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
175
250
|
readonly types?: Pg.CustomTypesConfig | undefined
|
|
176
251
|
}
|
|
177
252
|
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
178
|
-
const compiler = makeCompiler(
|
|
179
|
-
options.transformQueryNames,
|
|
180
|
-
options.transformJson
|
|
181
|
-
)
|
|
182
|
-
const transformRows = options.transformResultNames ?
|
|
183
|
-
Statement.defaultTransforms(
|
|
184
|
-
options.transformResultNames,
|
|
185
|
-
options.transformJson
|
|
186
|
-
).array :
|
|
187
|
-
undefined
|
|
188
|
-
|
|
189
253
|
const pool = yield* options.acquire
|
|
190
254
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (this.pg !== undefined) {
|
|
199
|
-
return Effect.callback<A, SqlError>((resume) => {
|
|
200
|
-
f(this.pg!, resume)
|
|
201
|
-
return makeCancel(pool, this.pg!)
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
return Effect.callback<A, SqlError>((resume) => {
|
|
205
|
-
let done = false
|
|
206
|
-
let cancel: Effect.Effect<void> | undefined = undefined
|
|
207
|
-
let client: Pg.PoolClient | undefined = undefined
|
|
208
|
-
function onError(cause: Error) {
|
|
209
|
-
cleanup(cause)
|
|
210
|
-
resume(Effect.fail(new SqlError({ cause, message: "Connection error" })))
|
|
211
|
-
}
|
|
212
|
-
function cleanup(cause?: Error) {
|
|
213
|
-
if (!done) client?.release(cause)
|
|
214
|
-
done = true
|
|
215
|
-
client?.off("error", onError)
|
|
216
|
-
}
|
|
217
|
-
pool.connect((cause, client_) => {
|
|
218
|
-
if (cause) {
|
|
219
|
-
return resume(Effect.fail(new SqlError({ cause, message: "Failed to acquire connection" })))
|
|
220
|
-
} else if (!client_) {
|
|
221
|
-
return resume(
|
|
222
|
-
Effect.fail(
|
|
223
|
-
new SqlError({ message: "Failed to acquire connection", cause: new Error("No client returned") })
|
|
224
|
-
)
|
|
225
|
-
)
|
|
226
|
-
} else if (done) {
|
|
227
|
-
client_.release()
|
|
228
|
-
return
|
|
229
|
-
}
|
|
230
|
-
client = client_
|
|
231
|
-
client.once("error", onError)
|
|
232
|
-
cancel = makeCancel(pool, client)
|
|
233
|
-
f(client, (eff) => {
|
|
234
|
-
cleanup()
|
|
235
|
-
resume(eff)
|
|
255
|
+
const makeConection = (client?: Pg.PoolClient) =>
|
|
256
|
+
new ConnectionImpl(
|
|
257
|
+
function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
|
|
258
|
+
if (client !== undefined) {
|
|
259
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
260
|
+
f(client!, resume)
|
|
261
|
+
return makeCancel(pool, client!)
|
|
236
262
|
})
|
|
237
|
-
}
|
|
238
|
-
return Effect.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private run(query: string, params: ReadonlyArray<unknown>) {
|
|
249
|
-
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
250
|
-
client.query(query, params as any, (err, result) => {
|
|
251
|
-
if (err) {
|
|
252
|
-
resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
|
|
253
|
-
} else {
|
|
254
|
-
// Multi-statement queries return an array of results
|
|
255
|
-
resume(Effect.succeed(
|
|
256
|
-
Array.isArray(result)
|
|
257
|
-
? result.map((r) => r.rows ?? [])
|
|
258
|
-
: result.rows ?? []
|
|
259
|
-
))
|
|
263
|
+
}
|
|
264
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
265
|
+
let done = false
|
|
266
|
+
let cancel: Effect.Effect<void> | undefined = undefined
|
|
267
|
+
let client: Pg.PoolClient | undefined = undefined
|
|
268
|
+
function onError(cause: Error) {
|
|
269
|
+
cleanup(cause)
|
|
270
|
+
resume(Effect.fail(new SqlError({ reason: classifyError(cause, "Connection error", "acquireConnection") })))
|
|
260
271
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
execute(
|
|
266
|
-
sql: string,
|
|
267
|
-
params: ReadonlyArray<unknown>,
|
|
268
|
-
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
269
|
-
) {
|
|
270
|
-
return transformRows
|
|
271
|
-
? Effect.map(this.run(sql, params), transformRows)
|
|
272
|
-
: this.run(sql, params)
|
|
273
|
-
}
|
|
274
|
-
executeRaw(sql: string, params: ReadonlyArray<unknown>) {
|
|
275
|
-
return this.runWithClient<Pg.Result>((client, resume) => {
|
|
276
|
-
client.query(sql, params as any, (err, result) => {
|
|
277
|
-
if (err) {
|
|
278
|
-
resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
|
|
279
|
-
} else {
|
|
280
|
-
resume(Effect.succeed(result))
|
|
272
|
+
function cleanup(cause?: Error) {
|
|
273
|
+
if (!done) client?.release(cause)
|
|
274
|
+
done = true
|
|
275
|
+
client?.off("error", onError)
|
|
281
276
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
277
|
+
pool.connect((cause, client_) => {
|
|
278
|
+
if (cause) {
|
|
279
|
+
return resume(
|
|
280
|
+
Effect.fail(
|
|
281
|
+
new SqlError({
|
|
282
|
+
reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
|
|
283
|
+
})
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
} else if (!client_) {
|
|
287
|
+
return resume(
|
|
288
|
+
Effect.fail(
|
|
289
|
+
new SqlError({
|
|
290
|
+
reason: new ConnectionError({
|
|
291
|
+
message: "Failed to acquire connection",
|
|
292
|
+
cause: new Error("No client returned"),
|
|
293
|
+
operation: "acquireConnection"
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
} else if (done) {
|
|
299
|
+
client_.release()
|
|
300
|
+
return
|
|
301
301
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
sql: string,
|
|
315
|
-
params: ReadonlyArray<unknown>,
|
|
316
|
-
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
317
|
-
) {
|
|
318
|
-
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
319
|
-
const self = this
|
|
320
|
-
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
|
|
321
|
-
const client = self.pg ?? (yield* Scope.provide(reserveRaw, scope))
|
|
322
|
-
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
|
|
323
|
-
const cursor = client.query(new Cursor(sql, params as any))
|
|
324
|
-
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
325
|
-
return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
|
|
326
|
-
cursor.read(128, (err, rows) => {
|
|
327
|
-
if (err) {
|
|
328
|
-
resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
|
|
329
|
-
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
330
|
-
resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
|
|
331
|
-
} else {
|
|
332
|
-
resume(Cause.done())
|
|
302
|
+
client = client_
|
|
303
|
+
client.once("error", onError)
|
|
304
|
+
cancel = makeCancel(pool, client)
|
|
305
|
+
f(client, (eff) => {
|
|
306
|
+
cleanup()
|
|
307
|
+
resume(eff)
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
return Effect.suspend(() => {
|
|
311
|
+
if (!cancel) {
|
|
312
|
+
cleanup()
|
|
313
|
+
return Effect.void
|
|
333
314
|
}
|
|
315
|
+
return Effect.ensuring(cancel, Effect.sync(cleanup))
|
|
334
316
|
})
|
|
335
317
|
})
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
318
|
+
},
|
|
319
|
+
client ? Effect.succeed(client) : reserveRaw
|
|
320
|
+
)
|
|
339
321
|
|
|
340
322
|
const reserveRaw = Effect.callback<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
|
|
341
323
|
const fiber = Fiber.getCurrent()!
|
|
@@ -343,7 +325,17 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
343
325
|
let cause: Error | undefined = undefined
|
|
344
326
|
pool.connect((err, client, release) => {
|
|
345
327
|
if (err) {
|
|
346
|
-
resume(
|
|
328
|
+
resume(
|
|
329
|
+
Effect.fail(
|
|
330
|
+
new SqlError({
|
|
331
|
+
reason: classifyError(
|
|
332
|
+
err,
|
|
333
|
+
"Failed to acquire connection for transaction",
|
|
334
|
+
"acquireConnection"
|
|
335
|
+
)
|
|
336
|
+
})
|
|
337
|
+
)
|
|
338
|
+
)
|
|
347
339
|
} else {
|
|
348
340
|
resume(Effect.as(
|
|
349
341
|
Scope.addFinalizer(
|
|
@@ -362,10 +354,33 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
362
354
|
client!.on("error", onError)
|
|
363
355
|
})
|
|
364
356
|
})
|
|
365
|
-
const reserve = Effect.map(reserveRaw,
|
|
357
|
+
const reserve = Effect.map(reserveRaw, makeConection)
|
|
366
358
|
|
|
367
|
-
const
|
|
368
|
-
|
|
359
|
+
const onListenClientError = (_: Error) => {
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const listenAcquirer = yield* RcRef.make({
|
|
363
|
+
acquire: Effect.acquireRelease(
|
|
364
|
+
Effect.tryPromise({
|
|
365
|
+
try: async () => {
|
|
366
|
+
const client = new Pg.Client(pool.options)
|
|
367
|
+
await client.connect()
|
|
368
|
+
client.on("error", onListenClientError)
|
|
369
|
+
return client
|
|
370
|
+
},
|
|
371
|
+
catch: (cause) =>
|
|
372
|
+
new SqlError({
|
|
373
|
+
reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
|
|
374
|
+
})
|
|
375
|
+
}),
|
|
376
|
+
(client) =>
|
|
377
|
+
Effect.promise(() => {
|
|
378
|
+
client.off("error", onListenClientError)
|
|
379
|
+
return client.end()
|
|
380
|
+
}).pipe(
|
|
381
|
+
Effect.timeoutOption(1000)
|
|
382
|
+
)
|
|
383
|
+
)
|
|
369
384
|
})
|
|
370
385
|
|
|
371
386
|
let config: PgClientConfig = {
|
|
@@ -386,7 +401,7 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
386
401
|
config = {
|
|
387
402
|
...config,
|
|
388
403
|
host: config.host ?? parsed.host ?? undefined,
|
|
389
|
-
port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
|
|
404
|
+
port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
|
|
390
405
|
username: config.username ?? parsed.user ?? undefined,
|
|
391
406
|
password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
|
|
392
407
|
database: config.database ?? parsed.database ?? undefined
|
|
@@ -396,10 +411,127 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
396
411
|
}
|
|
397
412
|
}
|
|
398
413
|
|
|
414
|
+
return yield* makeWith({
|
|
415
|
+
acquirer: Effect.succeed(makeConection()),
|
|
416
|
+
transactionAcquirer: reserve,
|
|
417
|
+
listenAcquirer: RcRef.get(listenAcquirer),
|
|
418
|
+
config,
|
|
419
|
+
spanAttributes: options.spanAttributes,
|
|
420
|
+
transformResultNames: options.transformResultNames,
|
|
421
|
+
transformQueryNames: options.transformQueryNames,
|
|
422
|
+
transformJson: options.transformJson
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @category constructors
|
|
428
|
+
* @since 1.0.0
|
|
429
|
+
*/
|
|
430
|
+
export const fromClient = Effect.fnUntraced(function*(
|
|
431
|
+
options: {
|
|
432
|
+
readonly acquire: Effect.Effect<Pg.Client, SqlError, Scope.Scope>
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Whether to acquire a separate client for each sql.stream / sql.listen.
|
|
436
|
+
*/
|
|
437
|
+
readonly acquireForStream: boolean
|
|
438
|
+
|
|
439
|
+
readonly applicationName?: string | undefined
|
|
440
|
+
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
441
|
+
|
|
442
|
+
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
443
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
444
|
+
readonly transformJson?: boolean | undefined
|
|
445
|
+
readonly types?: Pg.CustomTypesConfig | undefined
|
|
446
|
+
}
|
|
447
|
+
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
448
|
+
function onError() {}
|
|
449
|
+
const acquireWithErrorHandler = options.acquire.pipe(
|
|
450
|
+
Effect.tap((client) => {
|
|
451
|
+
client.on("error", onError)
|
|
452
|
+
return Effect.addFinalizer(() => {
|
|
453
|
+
client.off("error", onError)
|
|
454
|
+
return Effect.void
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
)
|
|
458
|
+
const client = yield* acquireWithErrorHandler
|
|
459
|
+
|
|
460
|
+
const semaphore = Semaphore.makeUnsafe(1)
|
|
461
|
+
let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(
|
|
462
|
+
Effect.as(semaphore.take(1), client),
|
|
463
|
+
() => semaphore.release(1)
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
const makeConection = (client: Pg.Client) =>
|
|
467
|
+
new ConnectionImpl(
|
|
468
|
+
function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
|
|
469
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
470
|
+
f(client, resume)
|
|
471
|
+
})
|
|
472
|
+
},
|
|
473
|
+
streamClient
|
|
474
|
+
)
|
|
475
|
+
const connection = makeConection(client)
|
|
476
|
+
const acquirer = semaphore.withPermit(Effect.succeed(connection))
|
|
477
|
+
|
|
478
|
+
const config: PgClientConfig = {
|
|
479
|
+
...options,
|
|
480
|
+
host: client.host,
|
|
481
|
+
port: client.port,
|
|
482
|
+
database: client.database,
|
|
483
|
+
username: client.user,
|
|
484
|
+
password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
|
|
485
|
+
ssl: client.ssl
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return yield* makeWith({
|
|
489
|
+
acquirer,
|
|
490
|
+
transactionAcquirer: acquirer,
|
|
491
|
+
listenAcquirer: streamClient,
|
|
492
|
+
config,
|
|
493
|
+
spanAttributes: options.spanAttributes,
|
|
494
|
+
transformResultNames: options.transformResultNames,
|
|
495
|
+
transformQueryNames: options.transformQueryNames,
|
|
496
|
+
transformJson: options.transformJson
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* @category constructors
|
|
502
|
+
* @since 1.0.0
|
|
503
|
+
*/
|
|
504
|
+
export const makeWith = Effect.fnUntraced(function*(
|
|
505
|
+
options: {
|
|
506
|
+
readonly acquirer: SqlConnection.Acquirer
|
|
507
|
+
readonly transactionAcquirer: SqlConnection.Acquirer
|
|
508
|
+
readonly listenAcquirer: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
509
|
+
|
|
510
|
+
readonly config: PgClientConfig
|
|
511
|
+
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
512
|
+
|
|
513
|
+
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
514
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
515
|
+
readonly transformJson?: boolean | undefined
|
|
516
|
+
}
|
|
517
|
+
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
518
|
+
const compiler = makeCompiler(
|
|
519
|
+
options.transformQueryNames,
|
|
520
|
+
options.transformJson
|
|
521
|
+
)
|
|
522
|
+
const transformRows = options.transformResultNames ?
|
|
523
|
+
Statement.defaultTransforms(
|
|
524
|
+
options.transformResultNames,
|
|
525
|
+
options.transformJson
|
|
526
|
+
).array :
|
|
527
|
+
undefined
|
|
528
|
+
|
|
529
|
+
const config = options.config
|
|
530
|
+
|
|
399
531
|
return Object.assign(
|
|
400
532
|
yield* Client.make({
|
|
401
|
-
acquirer:
|
|
402
|
-
transactionAcquirer:
|
|
533
|
+
acquirer: options.acquirer,
|
|
534
|
+
transactionAcquirer: options.transactionAcquirer,
|
|
403
535
|
compiler,
|
|
404
536
|
spanAttributes: [
|
|
405
537
|
...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
|
|
@@ -412,11 +544,11 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
412
544
|
}),
|
|
413
545
|
{
|
|
414
546
|
[TypeId]: TypeId as TypeId,
|
|
415
|
-
config,
|
|
547
|
+
config: options.config,
|
|
416
548
|
json: (_: unknown) => Statement.fragment([PgJson(_)]),
|
|
417
549
|
listen: (channel: string) =>
|
|
418
550
|
Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
|
|
419
|
-
const client = yield*
|
|
551
|
+
const client = yield* options.listenAcquirer
|
|
420
552
|
function onNotification(msg: Pg.Notification) {
|
|
421
553
|
if (msg.channel === channel && msg.payload) {
|
|
422
554
|
Queue.offerUnsafe(queue, msg.payload)
|
|
@@ -430,24 +562,133 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
430
562
|
)
|
|
431
563
|
yield* Effect.tryPromise({
|
|
432
564
|
try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
|
|
433
|
-
catch: (cause) => new SqlError({ cause,
|
|
565
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to listen", "listen") })
|
|
434
566
|
})
|
|
435
567
|
client.on("notification", onNotification)
|
|
436
568
|
})),
|
|
437
569
|
notify: (channel: string, payload: string) =>
|
|
438
|
-
Effect.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
} else {
|
|
443
|
-
resume(Effect.void)
|
|
444
|
-
}
|
|
445
|
-
})
|
|
446
|
-
})
|
|
570
|
+
Effect.asVoid(Effect.scoped(Effect.flatMap(
|
|
571
|
+
options.acquirer,
|
|
572
|
+
(conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
|
|
573
|
+
)))
|
|
447
574
|
}
|
|
448
575
|
)
|
|
449
576
|
})
|
|
450
577
|
|
|
578
|
+
class ConnectionImpl implements Connection {
|
|
579
|
+
constructor(
|
|
580
|
+
runWithClient: <A>(
|
|
581
|
+
f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
|
|
582
|
+
) => Effect.Effect<A, SqlError>,
|
|
583
|
+
reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
584
|
+
) {
|
|
585
|
+
this.runWithClient = runWithClient
|
|
586
|
+
this.reserve = reserve
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private readonly runWithClient: <A>(
|
|
590
|
+
f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
|
|
591
|
+
) => Effect.Effect<A, SqlError>
|
|
592
|
+
private readonly reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
593
|
+
|
|
594
|
+
private run(query: string, params: ReadonlyArray<unknown>) {
|
|
595
|
+
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
596
|
+
client.query(query, params as any, (err, result) => {
|
|
597
|
+
if (err) {
|
|
598
|
+
resume(
|
|
599
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
600
|
+
)
|
|
601
|
+
} else {
|
|
602
|
+
// Multi-statement queries return an array of results
|
|
603
|
+
resume(Effect.succeed(
|
|
604
|
+
Array.isArray(result)
|
|
605
|
+
? result.map((r) => r.rows ?? [])
|
|
606
|
+
: result.rows ?? []
|
|
607
|
+
))
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
execute(
|
|
614
|
+
sql: string,
|
|
615
|
+
params: ReadonlyArray<unknown>,
|
|
616
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
617
|
+
) {
|
|
618
|
+
return transformRows
|
|
619
|
+
? Effect.map(this.run(sql, params), transformRows)
|
|
620
|
+
: this.run(sql, params)
|
|
621
|
+
}
|
|
622
|
+
executeRaw(sql: string, params: ReadonlyArray<unknown>) {
|
|
623
|
+
return this.runWithClient<Pg.Result>((client, resume) => {
|
|
624
|
+
client.query(sql, params as any, (err, result) => {
|
|
625
|
+
if (err) {
|
|
626
|
+
resume(
|
|
627
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
628
|
+
)
|
|
629
|
+
} else {
|
|
630
|
+
resume(Effect.succeed(result))
|
|
631
|
+
}
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
|
|
636
|
+
return this.run(sql, params)
|
|
637
|
+
}
|
|
638
|
+
executeValues(sql: string, params: ReadonlyArray<unknown>) {
|
|
639
|
+
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
640
|
+
client.query(
|
|
641
|
+
{
|
|
642
|
+
text: sql,
|
|
643
|
+
rowMode: "array",
|
|
644
|
+
values: params as Array<string>
|
|
645
|
+
},
|
|
646
|
+
(err, result) => {
|
|
647
|
+
if (err) {
|
|
648
|
+
resume(
|
|
649
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
650
|
+
)
|
|
651
|
+
} else {
|
|
652
|
+
resume(Effect.succeed(result.rows))
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
executeUnprepared(
|
|
659
|
+
sql: string,
|
|
660
|
+
params: ReadonlyArray<unknown>,
|
|
661
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
662
|
+
) {
|
|
663
|
+
return this.execute(sql, params, transformRows)
|
|
664
|
+
}
|
|
665
|
+
executeStream(
|
|
666
|
+
sql: string,
|
|
667
|
+
params: ReadonlyArray<unknown>,
|
|
668
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
669
|
+
) {
|
|
670
|
+
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
671
|
+
const self = this
|
|
672
|
+
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
|
|
673
|
+
const client = yield* Scope.provide(self.reserve, scope)
|
|
674
|
+
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
|
|
675
|
+
const cursor = client.query(new Cursor(sql, params as any))
|
|
676
|
+
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
677
|
+
return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
|
|
678
|
+
cursor.read(128, (err, rows) => {
|
|
679
|
+
if (err) {
|
|
680
|
+
resume(Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "stream") })))
|
|
681
|
+
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
682
|
+
resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
|
|
683
|
+
} else {
|
|
684
|
+
resume(Cause.done())
|
|
685
|
+
}
|
|
686
|
+
})
|
|
687
|
+
})
|
|
688
|
+
})))
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
451
692
|
const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
|
|
452
693
|
const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
|
|
453
694
|
if (cancelEffects.has(client)) {
|
|
@@ -474,57 +715,37 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
|
|
|
474
715
|
* @category layers
|
|
475
716
|
* @since 1.0.0
|
|
476
717
|
*/
|
|
477
|
-
export const
|
|
478
|
-
|
|
479
|
-
)
|
|
480
|
-
config: Config.Wrap<PgClientConfig>
|
|
481
|
-
): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
|
|
718
|
+
export const layerFrom = <E, R>(
|
|
719
|
+
acquire: Effect.Effect<PgClient, E, R>
|
|
720
|
+
): Layer.Layer<PgClient | Client.SqlClient, E, Exclude<R, Scope.Scope | Reactivity.Reactivity>> =>
|
|
482
721
|
Layer.effectServices(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
)
|
|
489
|
-
)
|
|
490
|
-
)
|
|
491
|
-
).pipe(Layer.provide(Reactivity.layer))
|
|
722
|
+
Effect.map(acquire, (client) =>
|
|
723
|
+
ServiceMap.make(PgClient, client).pipe(
|
|
724
|
+
ServiceMap.add(Client.SqlClient, client)
|
|
725
|
+
))
|
|
726
|
+
).pipe(Layer.provide(Reactivity.layer)) as any
|
|
492
727
|
|
|
493
728
|
/**
|
|
494
729
|
* @category layers
|
|
495
730
|
* @since 1.0.0
|
|
496
731
|
*/
|
|
497
|
-
export const
|
|
498
|
-
config:
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
)
|
|
732
|
+
export const layerConfig: (
|
|
733
|
+
config: Config.Wrap<PgPoolConfig>
|
|
734
|
+
) => Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> = (
|
|
735
|
+
config: Config.Wrap<PgPoolConfig>
|
|
736
|
+
): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
|
|
737
|
+
layerFrom(Effect.flatMap(
|
|
738
|
+
Config.unwrap(config).asEffect(),
|
|
739
|
+
make
|
|
740
|
+
))
|
|
506
741
|
|
|
507
742
|
/**
|
|
508
743
|
* @category layers
|
|
509
744
|
* @since 1.0.0
|
|
510
745
|
*/
|
|
511
|
-
export const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
readonly applicationName?: string | undefined
|
|
515
|
-
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
516
|
-
|
|
517
|
-
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
518
|
-
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
519
|
-
readonly transformJson?: boolean | undefined
|
|
520
|
-
readonly types?: Pg.CustomTypesConfig | undefined
|
|
521
|
-
}): Layer.Layer<PgClient | Client.SqlClient, SqlError> =>
|
|
522
|
-
Layer.effectServices(
|
|
523
|
-
Effect.map(fromPool(options), (client) =>
|
|
524
|
-
ServiceMap.make(PgClient, client).pipe(
|
|
525
|
-
ServiceMap.add(Client.SqlClient, client)
|
|
526
|
-
))
|
|
527
|
-
).pipe(Layer.provide(Reactivity.layer))
|
|
746
|
+
export const layer = (
|
|
747
|
+
config: PgPoolConfig
|
|
748
|
+
): Layer.Layer<PgClient | Client.SqlClient, SqlError> => layerFrom(make(config))
|
|
528
749
|
|
|
529
750
|
/**
|
|
530
751
|
* @category constructor
|
|
@@ -591,3 +812,55 @@ interface PgJson extends Custom<"PgJson", unknown> {}
|
|
|
591
812
|
* @since 1.0.0
|
|
592
813
|
*/
|
|
593
814
|
const PgJson = Statement.custom<PgJson>("PgJson")
|
|
815
|
+
|
|
816
|
+
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|
817
|
+
const ATTR_DB_NAMESPACE = "db.namespace"
|
|
818
|
+
const ATTR_SERVER_ADDRESS = "server.address"
|
|
819
|
+
const ATTR_SERVER_PORT = "server.port"
|
|
820
|
+
|
|
821
|
+
const pgCodeFromCause = (cause: unknown): string | undefined => {
|
|
822
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause)) {
|
|
823
|
+
return undefined
|
|
824
|
+
}
|
|
825
|
+
const code = cause.code
|
|
826
|
+
return typeof code === "string" ? code : undefined
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const classifyError = (
|
|
830
|
+
cause: unknown,
|
|
831
|
+
message: string,
|
|
832
|
+
operation: string
|
|
833
|
+
) => {
|
|
834
|
+
const props = { cause, message, operation }
|
|
835
|
+
const code = pgCodeFromCause(cause)
|
|
836
|
+
if (code !== undefined) {
|
|
837
|
+
if (code.startsWith("08")) {
|
|
838
|
+
return new ConnectionError(props)
|
|
839
|
+
}
|
|
840
|
+
if (code.startsWith("28")) {
|
|
841
|
+
return new AuthenticationError(props)
|
|
842
|
+
}
|
|
843
|
+
if (code === "42501") {
|
|
844
|
+
return new AuthorizationError(props)
|
|
845
|
+
}
|
|
846
|
+
if (code.startsWith("42")) {
|
|
847
|
+
return new SqlSyntaxError(props)
|
|
848
|
+
}
|
|
849
|
+
if (code.startsWith("23")) {
|
|
850
|
+
return new ConstraintError(props)
|
|
851
|
+
}
|
|
852
|
+
if (code === "40P01") {
|
|
853
|
+
return new DeadlockError(props)
|
|
854
|
+
}
|
|
855
|
+
if (code === "40001") {
|
|
856
|
+
return new SerializationError(props)
|
|
857
|
+
}
|
|
858
|
+
if (code === "55P03") {
|
|
859
|
+
return new LockTimeoutError(props)
|
|
860
|
+
}
|
|
861
|
+
if (code === "57014") {
|
|
862
|
+
return new StatementTimeoutError(props)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return new UnknownError(props)
|
|
866
|
+
}
|