@effect/sql-pg 4.0.0-beta.8 → 4.0.0-beta.81
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 +118 -37
- package/dist/PgClient.d.ts.map +1 -1
- package/dist/PgClient.js +415 -195
- package/dist/PgClient.js.map +1 -1
- package/dist/PgMigrator.d.ts +18 -6
- package/dist/PgMigrator.d.ts.map +1 -1
- package/dist/PgMigrator.js +20 -6
- package/dist/PgMigrator.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/package.json +9 -9
- package/src/PgClient.ts +622 -268
- package/src/PgMigrator.ts +20 -7
- package/src/index.ts +3 -3
package/src/PgClient.ts
CHANGED
|
@@ -1,25 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Connects Effect SQL to PostgreSQL using the `pg` package.
|
|
3
|
+
*
|
|
4
|
+
* This module provides constructors and layers for building a PostgreSQL
|
|
5
|
+
* client from pool settings, a managed `pg.Client`, an existing `pg.Pool`, or
|
|
6
|
+
* custom connection code. The client runs Effect SQL queries against
|
|
7
|
+
* PostgreSQL, including transactions and streamed results, and adds helpers for
|
|
8
|
+
* JSON values and LISTEN/NOTIFY messages. It also maps common PostgreSQL
|
|
9
|
+
* failures, such as connection, authentication, constraint, timeout, and
|
|
10
|
+
* deadlock errors, into Effect SQL errors.
|
|
11
|
+
*
|
|
12
|
+
* @since 4.0.0
|
|
3
13
|
*/
|
|
4
14
|
import * as Arr from "effect/Array"
|
|
5
15
|
import * as Cause from "effect/Cause"
|
|
6
16
|
import * as Channel from "effect/Channel"
|
|
7
17
|
import * as Config from "effect/Config"
|
|
18
|
+
import * as Context from "effect/Context"
|
|
8
19
|
import * as Duration from "effect/Duration"
|
|
9
20
|
import * as Effect from "effect/Effect"
|
|
10
21
|
import * as Fiber from "effect/Fiber"
|
|
11
22
|
import * as Layer from "effect/Layer"
|
|
12
23
|
import * as Number from "effect/Number"
|
|
24
|
+
import * as Option from "effect/Option"
|
|
13
25
|
import * as Queue from "effect/Queue"
|
|
14
26
|
import * as RcRef from "effect/RcRef"
|
|
15
27
|
import * as Redacted from "effect/Redacted"
|
|
16
28
|
import * as Scope from "effect/Scope"
|
|
17
|
-
import * as
|
|
29
|
+
import * as Semaphore from "effect/Semaphore"
|
|
18
30
|
import * as Stream from "effect/Stream"
|
|
19
31
|
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
|
20
32
|
import * as Client from "effect/unstable/sql/SqlClient"
|
|
21
33
|
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
|
22
|
-
import
|
|
34
|
+
import type * as SqlConnection from "effect/unstable/sql/SqlConnection"
|
|
35
|
+
import {
|
|
36
|
+
AuthenticationError,
|
|
37
|
+
AuthorizationError,
|
|
38
|
+
ConnectionError,
|
|
39
|
+
ConstraintError,
|
|
40
|
+
DeadlockError,
|
|
41
|
+
LockTimeoutError,
|
|
42
|
+
SerializationError,
|
|
43
|
+
SqlError,
|
|
44
|
+
SqlSyntaxError,
|
|
45
|
+
StatementTimeoutError,
|
|
46
|
+
UniqueViolation,
|
|
47
|
+
UnknownError
|
|
48
|
+
} from "effect/unstable/sql/SqlError"
|
|
23
49
|
import type { Custom, Fragment } from "effect/unstable/sql/Statement"
|
|
24
50
|
import * as Statement from "effect/unstable/sql/Statement"
|
|
25
51
|
import type { Duplex } from "node:stream"
|
|
@@ -28,26 +54,27 @@ import * as Pg from "pg"
|
|
|
28
54
|
import * as PgConnString from "pg-connection-string"
|
|
29
55
|
import Cursor from "pg-cursor"
|
|
30
56
|
|
|
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
57
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
58
|
+
* Runtime type identifier used to mark `PgClient` values.
|
|
59
|
+
*
|
|
60
|
+
* @category type IDs
|
|
61
|
+
* @since 4.0.0
|
|
39
62
|
*/
|
|
40
63
|
export const TypeId: TypeId = "~@effect/sql-pg/PgClient"
|
|
41
64
|
|
|
42
65
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
66
|
+
* Type-level identifier used to mark `PgClient` values.
|
|
67
|
+
*
|
|
68
|
+
* @category type IDs
|
|
69
|
+
* @since 4.0.0
|
|
45
70
|
*/
|
|
46
71
|
export type TypeId = "~@effect/sql-pg/PgClient"
|
|
47
72
|
|
|
48
73
|
/**
|
|
74
|
+
* PostgreSQL client service, extending `SqlClient` with JSON parameter fragments and LISTEN/NOTIFY helpers.
|
|
75
|
+
*
|
|
49
76
|
* @category models
|
|
50
|
-
* @since
|
|
77
|
+
* @since 4.0.0
|
|
51
78
|
*/
|
|
52
79
|
export interface PgClient extends Client.SqlClient {
|
|
53
80
|
readonly [TypeId]: TypeId
|
|
@@ -58,14 +85,22 @@ export interface PgClient extends Client.SqlClient {
|
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
88
|
+
* Service tag for the PostgreSQL client service.
|
|
89
|
+
*
|
|
90
|
+
* **When to use**
|
|
91
|
+
*
|
|
92
|
+
* Use to access or provide a PostgreSQL client through the Effect context.
|
|
93
|
+
*
|
|
94
|
+
* @category services
|
|
95
|
+
* @since 4.0.0
|
|
63
96
|
*/
|
|
64
|
-
export const PgClient =
|
|
97
|
+
export const PgClient = Context.Service<PgClient>("@effect/sql-pg/PgClient")
|
|
65
98
|
|
|
66
99
|
/**
|
|
100
|
+
* Configuration for a PostgreSQL client, including connection, TLS, custom stream, application name, type parser, JSON transform, and query/result name transform options.
|
|
101
|
+
*
|
|
67
102
|
* @category constructors
|
|
68
|
-
* @since
|
|
103
|
+
* @since 4.0.0
|
|
69
104
|
*/
|
|
70
105
|
export interface PgClientConfig {
|
|
71
106
|
readonly url?: Redacted.Redacted | undefined
|
|
@@ -78,14 +113,9 @@ export interface PgClientConfig {
|
|
|
78
113
|
readonly username?: string | undefined
|
|
79
114
|
readonly password?: Redacted.Redacted | undefined
|
|
80
115
|
|
|
81
|
-
readonly stream?: (() => Duplex) | undefined
|
|
82
|
-
|
|
83
|
-
readonly idleTimeout?: Duration.Input | undefined
|
|
84
116
|
readonly connectTimeout?: Duration.Input | undefined
|
|
85
117
|
|
|
86
|
-
readonly
|
|
87
|
-
readonly minConnections?: number | undefined
|
|
88
|
-
readonly connectionTTL?: Duration.Input | undefined
|
|
118
|
+
readonly stream?: (() => Duplex) | undefined
|
|
89
119
|
|
|
90
120
|
readonly applicationName?: string | undefined
|
|
91
121
|
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
@@ -97,12 +127,26 @@ export interface PgClientConfig {
|
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
/**
|
|
130
|
+
* PostgreSQL pool configuration, extending `PgClientConfig` with idle timeout, pool size, and connection lifetime settings.
|
|
131
|
+
*
|
|
100
132
|
* @category constructors
|
|
101
|
-
* @since
|
|
133
|
+
* @since 4.0.0
|
|
102
134
|
*/
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
export interface PgPoolConfig extends PgClientConfig {
|
|
136
|
+
readonly idleTimeout?: Duration.Input | undefined
|
|
137
|
+
|
|
138
|
+
readonly maxConnections?: number | undefined
|
|
139
|
+
readonly minConnections?: number | undefined
|
|
140
|
+
readonly connectionTTL?: Duration.Input | undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates a scoped PostgreSQL client backed by a managed `pg` connection pool.
|
|
145
|
+
*
|
|
146
|
+
* @category constructors
|
|
147
|
+
* @since 4.0.0
|
|
148
|
+
*/
|
|
149
|
+
export const make = (options: PgPoolConfig): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
106
150
|
fromPool({
|
|
107
151
|
...options,
|
|
108
152
|
acquire: Effect.gen(function*() {
|
|
@@ -135,20 +179,24 @@ export const make = (
|
|
|
135
179
|
yield* Effect.acquireRelease(
|
|
136
180
|
Effect.tryPromise({
|
|
137
181
|
try: () => pool.query("SELECT 1"),
|
|
138
|
-
catch: (cause) => new SqlError({ cause,
|
|
182
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
|
|
139
183
|
}),
|
|
140
184
|
() =>
|
|
141
185
|
Effect.promise(() => pool.end()).pipe(
|
|
142
186
|
Effect.timeoutOption(1000)
|
|
143
|
-
)
|
|
187
|
+
),
|
|
188
|
+
{ interruptible: true }
|
|
144
189
|
).pipe(
|
|
145
190
|
Effect.timeoutOrElse({
|
|
146
191
|
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
147
|
-
|
|
192
|
+
orElse: () =>
|
|
148
193
|
Effect.fail(
|
|
149
194
|
new SqlError({
|
|
150
|
-
|
|
151
|
-
|
|
195
|
+
reason: new ConnectionError({
|
|
196
|
+
cause: new Error("Connection timed out"),
|
|
197
|
+
message: "PgClient: Connection timed out",
|
|
198
|
+
operation: "connect"
|
|
199
|
+
})
|
|
152
200
|
})
|
|
153
201
|
)
|
|
154
202
|
})
|
|
@@ -159,8 +207,70 @@ export const make = (
|
|
|
159
207
|
})
|
|
160
208
|
|
|
161
209
|
/**
|
|
210
|
+
* Creates a scoped PostgreSQL client backed by a managed single `pg` client, optionally acquiring a separate client for streaming and LISTEN operations.
|
|
211
|
+
*
|
|
162
212
|
* @category constructors
|
|
163
|
-
* @since
|
|
213
|
+
* @since 4.0.0
|
|
214
|
+
*/
|
|
215
|
+
export const makeClient = (
|
|
216
|
+
options: PgClientConfig & {
|
|
217
|
+
/**
|
|
218
|
+
* Whether to acquire a separate client for each sql.stream / sql.listen
|
|
219
|
+
*/
|
|
220
|
+
readonly acquireForStream?: boolean | undefined
|
|
221
|
+
}
|
|
222
|
+
): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
223
|
+
fromClient({
|
|
224
|
+
...options,
|
|
225
|
+
acquire: Effect.gen(function*() {
|
|
226
|
+
const client = new Pg.Client({
|
|
227
|
+
connectionString: options.url ? Redacted.value(options.url) : undefined,
|
|
228
|
+
user: options.username,
|
|
229
|
+
host: options.host,
|
|
230
|
+
database: options.database,
|
|
231
|
+
password: options.password ? Redacted.value(options.password) : undefined,
|
|
232
|
+
ssl: options.ssl,
|
|
233
|
+
port: options.port,
|
|
234
|
+
...(options.stream ? { stream: options.stream } : {}),
|
|
235
|
+
application_name: options.applicationName ?? "@effect/sql-pg",
|
|
236
|
+
types: options.types
|
|
237
|
+
})
|
|
238
|
+
yield* Effect.acquireRelease(
|
|
239
|
+
Effect.tryPromise({
|
|
240
|
+
try: () => client.query("SELECT 1"),
|
|
241
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
|
|
242
|
+
}),
|
|
243
|
+
() =>
|
|
244
|
+
Effect.promise(() => client.end()).pipe(
|
|
245
|
+
Effect.timeoutOption(1000)
|
|
246
|
+
),
|
|
247
|
+
{ interruptible: true }
|
|
248
|
+
).pipe(
|
|
249
|
+
Effect.timeoutOrElse({
|
|
250
|
+
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
251
|
+
orElse: () =>
|
|
252
|
+
Effect.fail(
|
|
253
|
+
new SqlError({
|
|
254
|
+
reason: new ConnectionError({
|
|
255
|
+
cause: new Error("Connection timed out"),
|
|
256
|
+
message: "PgClient: Connection timed out",
|
|
257
|
+
operation: "connect"
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
)
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return client
|
|
265
|
+
}),
|
|
266
|
+
acquireForStream: options.acquireForStream ?? false
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Builds a PostgreSQL client from a scoped `pg` pool acquisition effect, deriving transaction, streaming, and LISTEN/NOTIFY support from that pool.
|
|
271
|
+
*
|
|
272
|
+
* @category constructors
|
|
273
|
+
* @since 4.0.0
|
|
164
274
|
*/
|
|
165
275
|
export const fromPool = Effect.fnUntraced(function*(
|
|
166
276
|
options: {
|
|
@@ -175,197 +285,149 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
175
285
|
readonly types?: Pg.CustomTypesConfig | undefined
|
|
176
286
|
}
|
|
177
287
|
): 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
288
|
const pool = yield* options.acquire
|
|
190
289
|
|
|
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)
|
|
290
|
+
const makeConection = (client?: Pg.PoolClient) =>
|
|
291
|
+
new ConnectionImpl(
|
|
292
|
+
function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
|
|
293
|
+
if (client !== undefined) {
|
|
294
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
295
|
+
f(client!, resume)
|
|
296
|
+
return makeCancel(pool, client!)
|
|
236
297
|
})
|
|
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
|
-
))
|
|
298
|
+
}
|
|
299
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
300
|
+
let done = false
|
|
301
|
+
let cancel: Effect.Effect<void> | undefined = undefined
|
|
302
|
+
let client: Pg.PoolClient | undefined = undefined
|
|
303
|
+
function onError(cause: Error) {
|
|
304
|
+
cleanup(cause)
|
|
305
|
+
resume(Effect.fail(new SqlError({ reason: classifyError(cause, "Connection error", "acquireConnection") })))
|
|
260
306
|
}
|
|
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))
|
|
307
|
+
function cleanup(cause?: Error) {
|
|
308
|
+
if (!done) client?.release(cause)
|
|
309
|
+
done = true
|
|
310
|
+
client?.off("error", onError)
|
|
281
311
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
312
|
+
pool.connect((cause, client_) => {
|
|
313
|
+
if (cause) {
|
|
314
|
+
return resume(
|
|
315
|
+
Effect.fail(
|
|
316
|
+
new SqlError({
|
|
317
|
+
reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
|
|
318
|
+
})
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
} else if (!client_) {
|
|
322
|
+
return resume(
|
|
323
|
+
Effect.fail(
|
|
324
|
+
new SqlError({
|
|
325
|
+
reason: new ConnectionError({
|
|
326
|
+
message: "Failed to acquire connection",
|
|
327
|
+
cause: new Error("No client returned"),
|
|
328
|
+
operation: "acquireConnection"
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
} else if (done) {
|
|
334
|
+
client_.release()
|
|
335
|
+
return
|
|
301
336
|
}
|
|
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())
|
|
337
|
+
client = client_
|
|
338
|
+
client.once("error", onError)
|
|
339
|
+
cancel = makeCancel(pool, client)
|
|
340
|
+
f(client, (eff) => {
|
|
341
|
+
cleanup()
|
|
342
|
+
resume(eff)
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
return Effect.suspend(() => {
|
|
346
|
+
if (!cancel) {
|
|
347
|
+
cleanup()
|
|
348
|
+
return Effect.void
|
|
333
349
|
}
|
|
350
|
+
return Effect.ensuring(cancel, Effect.sync(cleanup))
|
|
334
351
|
})
|
|
335
352
|
})
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
353
|
+
},
|
|
354
|
+
client ? Effect.succeed(client) : reserveRaw
|
|
355
|
+
)
|
|
339
356
|
|
|
340
357
|
const reserveRaw = Effect.callback<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
|
|
341
358
|
const fiber = Fiber.getCurrent()!
|
|
342
|
-
const scope =
|
|
359
|
+
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
|
|
343
360
|
let cause: Error | undefined = undefined
|
|
361
|
+
function onError(cause_: Error) {
|
|
362
|
+
cause = cause_
|
|
363
|
+
}
|
|
344
364
|
pool.connect((err, client, release) => {
|
|
345
365
|
if (err) {
|
|
346
|
-
resume(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
366
|
+
return resume(
|
|
367
|
+
Effect.fail(
|
|
368
|
+
new SqlError({
|
|
369
|
+
reason: classifyError(
|
|
370
|
+
err,
|
|
371
|
+
"Failed to acquire connection for transaction",
|
|
372
|
+
"acquireConnection"
|
|
373
|
+
)
|
|
354
374
|
})
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
} else if (!client) {
|
|
378
|
+
return resume(
|
|
379
|
+
Effect.fail(
|
|
380
|
+
new SqlError({
|
|
381
|
+
reason: new ConnectionError({
|
|
382
|
+
message: "Failed to acquire connection for transaction",
|
|
383
|
+
cause: new Error("No client returned"),
|
|
384
|
+
operation: "acquireConnection"
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
)
|
|
388
|
+
)
|
|
361
389
|
}
|
|
362
|
-
client
|
|
390
|
+
client.on("error", onError)
|
|
391
|
+
resume(Effect.as(
|
|
392
|
+
Scope.addFinalizer(
|
|
393
|
+
scope,
|
|
394
|
+
Effect.sync(() => {
|
|
395
|
+
client.off("error", onError)
|
|
396
|
+
release(cause)
|
|
397
|
+
})
|
|
398
|
+
),
|
|
399
|
+
client
|
|
400
|
+
))
|
|
363
401
|
})
|
|
364
402
|
})
|
|
365
|
-
const reserve = Effect.map(reserveRaw,
|
|
403
|
+
const reserve = Effect.map(reserveRaw, makeConection)
|
|
366
404
|
|
|
367
|
-
const
|
|
368
|
-
|
|
405
|
+
const onListenClientError = (_: Error) => {
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const listenAcquirer = yield* RcRef.make({
|
|
409
|
+
acquire: Effect.acquireRelease(
|
|
410
|
+
Effect.tryPromise({
|
|
411
|
+
try: async () => {
|
|
412
|
+
const client = new Pg.Client(pool.options)
|
|
413
|
+
await client.connect()
|
|
414
|
+
client.on("error", onListenClientError)
|
|
415
|
+
return client
|
|
416
|
+
},
|
|
417
|
+
catch: (cause) =>
|
|
418
|
+
new SqlError({
|
|
419
|
+
reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
|
|
420
|
+
})
|
|
421
|
+
}),
|
|
422
|
+
(client) =>
|
|
423
|
+
Effect.promise(() => {
|
|
424
|
+
client.off("error", onListenClientError)
|
|
425
|
+
return client.end()
|
|
426
|
+
}).pipe(
|
|
427
|
+
Effect.timeoutOption(1000)
|
|
428
|
+
),
|
|
429
|
+
{ interruptible: true }
|
|
430
|
+
)
|
|
369
431
|
})
|
|
370
432
|
|
|
371
433
|
let config: PgClientConfig = {
|
|
@@ -386,7 +448,7 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
386
448
|
config = {
|
|
387
449
|
...config,
|
|
388
450
|
host: config.host ?? parsed.host ?? undefined,
|
|
389
|
-
port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
|
|
451
|
+
port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
|
|
390
452
|
username: config.username ?? parsed.user ?? undefined,
|
|
391
453
|
password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
|
|
392
454
|
database: config.database ?? parsed.database ?? undefined
|
|
@@ -396,10 +458,136 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
396
458
|
}
|
|
397
459
|
}
|
|
398
460
|
|
|
461
|
+
return yield* makeWith({
|
|
462
|
+
acquirer: Effect.succeed(makeConection()),
|
|
463
|
+
transactionAcquirer: reserve,
|
|
464
|
+
listenAcquirer: RcRef.get(listenAcquirer),
|
|
465
|
+
config,
|
|
466
|
+
spanAttributes: options.spanAttributes,
|
|
467
|
+
transformResultNames: options.transformResultNames,
|
|
468
|
+
transformQueryNames: options.transformQueryNames,
|
|
469
|
+
transformJson: options.transformJson
|
|
470
|
+
})
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Builds a PostgreSQL client from a scoped `pg` client acquisition effect, serializing access when sharing the client and optionally using separate clients for streams and LISTEN.
|
|
475
|
+
*
|
|
476
|
+
* @category constructors
|
|
477
|
+
* @since 4.0.0
|
|
478
|
+
*/
|
|
479
|
+
export const fromClient = Effect.fnUntraced(function*(
|
|
480
|
+
options: {
|
|
481
|
+
readonly acquire: Effect.Effect<Pg.Client, SqlError, Scope.Scope>
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Whether to acquire a separate client for each sql.stream / sql.listen.
|
|
485
|
+
*/
|
|
486
|
+
readonly acquireForStream: boolean
|
|
487
|
+
|
|
488
|
+
readonly applicationName?: string | undefined
|
|
489
|
+
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
490
|
+
|
|
491
|
+
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
492
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
493
|
+
readonly transformJson?: boolean | undefined
|
|
494
|
+
readonly types?: Pg.CustomTypesConfig | undefined
|
|
495
|
+
}
|
|
496
|
+
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
497
|
+
function onError() {}
|
|
498
|
+
const acquireWithErrorHandler = options.acquire.pipe(
|
|
499
|
+
Effect.tap((client) => {
|
|
500
|
+
client.on("error", onError)
|
|
501
|
+
return Effect.addFinalizer(() => {
|
|
502
|
+
client.off("error", onError)
|
|
503
|
+
return Effect.void
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
)
|
|
507
|
+
const client = yield* acquireWithErrorHandler
|
|
508
|
+
|
|
509
|
+
const semaphore = Semaphore.makeUnsafe(1)
|
|
510
|
+
let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(
|
|
511
|
+
Effect.as(semaphore.take(1), client),
|
|
512
|
+
() => semaphore.release(1)
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
const makeConection = (client: Pg.Client) =>
|
|
516
|
+
new ConnectionImpl(
|
|
517
|
+
function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
|
|
518
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
519
|
+
f(client, resume)
|
|
520
|
+
})
|
|
521
|
+
},
|
|
522
|
+
streamClient
|
|
523
|
+
)
|
|
524
|
+
const connection = makeConection(client)
|
|
525
|
+
const acquirer = semaphore.withPermit(Effect.succeed(connection))
|
|
526
|
+
|
|
527
|
+
const config: PgClientConfig = {
|
|
528
|
+
...options,
|
|
529
|
+
host: client.host,
|
|
530
|
+
port: client.port,
|
|
531
|
+
database: client.database,
|
|
532
|
+
username: client.user,
|
|
533
|
+
password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
|
|
534
|
+
ssl: client.ssl
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return yield* makeWith({
|
|
538
|
+
acquirer,
|
|
539
|
+
transactionAcquirer: acquirer,
|
|
540
|
+
listenAcquirer: streamClient,
|
|
541
|
+
config,
|
|
542
|
+
spanAttributes: options.spanAttributes,
|
|
543
|
+
transformResultNames: options.transformResultNames,
|
|
544
|
+
transformQueryNames: options.transformQueryNames,
|
|
545
|
+
transformJson: options.transformJson
|
|
546
|
+
})
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Creates a `PgClient` from SQL connection acquirers, a LISTEN acquirer, client configuration, and transformation options.
|
|
551
|
+
*
|
|
552
|
+
* **When to use**
|
|
553
|
+
*
|
|
554
|
+
* Use to build a PostgreSQL client from custom connection acquisition logic
|
|
555
|
+
* instead of the built-in pool or single-client constructors.
|
|
556
|
+
*
|
|
557
|
+
* @category constructors
|
|
558
|
+
* @since 4.0.0
|
|
559
|
+
*/
|
|
560
|
+
export const makeWith = Effect.fnUntraced(function*(
|
|
561
|
+
options: {
|
|
562
|
+
readonly acquirer: SqlConnection.Acquirer
|
|
563
|
+
readonly transactionAcquirer: SqlConnection.Acquirer
|
|
564
|
+
readonly listenAcquirer: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
565
|
+
|
|
566
|
+
readonly config: PgClientConfig
|
|
567
|
+
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
568
|
+
|
|
569
|
+
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
570
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
571
|
+
readonly transformJson?: boolean | undefined
|
|
572
|
+
}
|
|
573
|
+
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
574
|
+
const compiler = makeCompiler(
|
|
575
|
+
options.transformQueryNames,
|
|
576
|
+
options.transformJson
|
|
577
|
+
)
|
|
578
|
+
const transformRows = options.transformResultNames ?
|
|
579
|
+
Statement.defaultTransforms(
|
|
580
|
+
options.transformResultNames,
|
|
581
|
+
options.transformJson
|
|
582
|
+
).array :
|
|
583
|
+
undefined
|
|
584
|
+
|
|
585
|
+
const config = options.config
|
|
586
|
+
|
|
399
587
|
return Object.assign(
|
|
400
588
|
yield* Client.make({
|
|
401
|
-
acquirer:
|
|
402
|
-
transactionAcquirer:
|
|
589
|
+
acquirer: options.acquirer,
|
|
590
|
+
transactionAcquirer: options.transactionAcquirer,
|
|
403
591
|
compiler,
|
|
404
592
|
spanAttributes: [
|
|
405
593
|
...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
|
|
@@ -412,11 +600,11 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
412
600
|
}),
|
|
413
601
|
{
|
|
414
602
|
[TypeId]: TypeId as TypeId,
|
|
415
|
-
config,
|
|
603
|
+
config: options.config,
|
|
416
604
|
json: (_: unknown) => Statement.fragment([PgJson(_)]),
|
|
417
605
|
listen: (channel: string) =>
|
|
418
606
|
Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
|
|
419
|
-
const client = yield*
|
|
607
|
+
const client = yield* options.listenAcquirer
|
|
420
608
|
function onNotification(msg: Pg.Notification) {
|
|
421
609
|
if (msg.channel === channel && msg.payload) {
|
|
422
610
|
Queue.offerUnsafe(queue, msg.payload)
|
|
@@ -430,24 +618,133 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
430
618
|
)
|
|
431
619
|
yield* Effect.tryPromise({
|
|
432
620
|
try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
|
|
433
|
-
catch: (cause) => new SqlError({ cause,
|
|
621
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to listen", "listen") })
|
|
434
622
|
})
|
|
435
623
|
client.on("notification", onNotification)
|
|
436
624
|
})),
|
|
437
625
|
notify: (channel: string, payload: string) =>
|
|
438
|
-
Effect.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
} else {
|
|
443
|
-
resume(Effect.void)
|
|
444
|
-
}
|
|
445
|
-
})
|
|
446
|
-
})
|
|
626
|
+
Effect.asVoid(Effect.scoped(Effect.flatMap(
|
|
627
|
+
options.acquirer,
|
|
628
|
+
(conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
|
|
629
|
+
)))
|
|
447
630
|
}
|
|
448
631
|
)
|
|
449
632
|
})
|
|
450
633
|
|
|
634
|
+
class ConnectionImpl implements Connection {
|
|
635
|
+
constructor(
|
|
636
|
+
runWithClient: <A>(
|
|
637
|
+
f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
|
|
638
|
+
) => Effect.Effect<A, SqlError>,
|
|
639
|
+
reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
640
|
+
) {
|
|
641
|
+
this.runWithClient = runWithClient
|
|
642
|
+
this.reserve = reserve
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private readonly runWithClient: <A>(
|
|
646
|
+
f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
|
|
647
|
+
) => Effect.Effect<A, SqlError>
|
|
648
|
+
private readonly reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
649
|
+
|
|
650
|
+
private run(query: string, params: ReadonlyArray<unknown>) {
|
|
651
|
+
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
652
|
+
client.query(query, params as any, (err, result) => {
|
|
653
|
+
if (err) {
|
|
654
|
+
resume(
|
|
655
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
656
|
+
)
|
|
657
|
+
} else {
|
|
658
|
+
// Multi-statement queries return an array of results
|
|
659
|
+
resume(Effect.succeed(
|
|
660
|
+
Array.isArray(result)
|
|
661
|
+
? result.map((r) => r.rows ?? [])
|
|
662
|
+
: result.rows ?? []
|
|
663
|
+
))
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
})
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
execute(
|
|
670
|
+
sql: string,
|
|
671
|
+
params: ReadonlyArray<unknown>,
|
|
672
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
673
|
+
) {
|
|
674
|
+
return transformRows
|
|
675
|
+
? Effect.map(this.run(sql, params), transformRows)
|
|
676
|
+
: this.run(sql, params)
|
|
677
|
+
}
|
|
678
|
+
executeRaw(sql: string, params: ReadonlyArray<unknown>) {
|
|
679
|
+
return this.runWithClient<Pg.Result>((client, resume) => {
|
|
680
|
+
client.query(sql, params as any, (err, result) => {
|
|
681
|
+
if (err) {
|
|
682
|
+
resume(
|
|
683
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
684
|
+
)
|
|
685
|
+
} else {
|
|
686
|
+
resume(Effect.succeed(result))
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
}
|
|
691
|
+
executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
|
|
692
|
+
return this.run(sql, params)
|
|
693
|
+
}
|
|
694
|
+
executeValues(sql: string, params: ReadonlyArray<unknown>) {
|
|
695
|
+
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
696
|
+
client.query(
|
|
697
|
+
{
|
|
698
|
+
text: sql,
|
|
699
|
+
rowMode: "array",
|
|
700
|
+
values: params as Array<string>
|
|
701
|
+
},
|
|
702
|
+
(err, result) => {
|
|
703
|
+
if (err) {
|
|
704
|
+
resume(
|
|
705
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
706
|
+
)
|
|
707
|
+
} else {
|
|
708
|
+
resume(Effect.succeed(result.rows))
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
)
|
|
712
|
+
})
|
|
713
|
+
}
|
|
714
|
+
executeUnprepared(
|
|
715
|
+
sql: string,
|
|
716
|
+
params: ReadonlyArray<unknown>,
|
|
717
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
718
|
+
) {
|
|
719
|
+
return this.execute(sql, params, transformRows)
|
|
720
|
+
}
|
|
721
|
+
executeStream(
|
|
722
|
+
sql: string,
|
|
723
|
+
params: ReadonlyArray<unknown>,
|
|
724
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
725
|
+
) {
|
|
726
|
+
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
727
|
+
const self = this
|
|
728
|
+
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
|
|
729
|
+
const client = yield* Scope.provide(self.reserve, scope)
|
|
730
|
+
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
|
|
731
|
+
const cursor = client.query(new Cursor(sql, params as any))
|
|
732
|
+
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
733
|
+
return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
|
|
734
|
+
cursor.read(128, (err, rows) => {
|
|
735
|
+
if (err) {
|
|
736
|
+
resume(Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "stream") })))
|
|
737
|
+
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
738
|
+
resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
|
|
739
|
+
} else {
|
|
740
|
+
resume(Cause.done())
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
})
|
|
744
|
+
})))
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
451
748
|
const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
|
|
452
749
|
const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
|
|
453
750
|
if (cancelEffects.has(client)) {
|
|
@@ -471,64 +768,52 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
|
|
|
471
768
|
}
|
|
472
769
|
|
|
473
770
|
/**
|
|
771
|
+
* Creates a layer from an effect that acquires a `PgClient`, providing both `PgClient` and `SqlClient`.
|
|
772
|
+
*
|
|
474
773
|
* @category layers
|
|
475
|
-
* @since
|
|
774
|
+
* @since 4.0.0
|
|
476
775
|
*/
|
|
477
|
-
export const
|
|
478
|
-
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
ServiceMap.make(PgClient, client).pipe(
|
|
487
|
-
ServiceMap.add(Client.SqlClient, client)
|
|
488
|
-
)
|
|
489
|
-
)
|
|
490
|
-
)
|
|
491
|
-
).pipe(Layer.provide(Reactivity.layer))
|
|
776
|
+
export const layerFrom = <E, R>(
|
|
777
|
+
acquire: Effect.Effect<PgClient, E, R>
|
|
778
|
+
): Layer.Layer<PgClient | Client.SqlClient, E, Exclude<R, Scope.Scope | Reactivity.Reactivity>> =>
|
|
779
|
+
Layer.effectContext(
|
|
780
|
+
Effect.map(acquire, (client) =>
|
|
781
|
+
Context.make(PgClient, client).pipe(
|
|
782
|
+
Context.add(Client.SqlClient, client)
|
|
783
|
+
))
|
|
784
|
+
).pipe(Layer.provide(Reactivity.layer)) as any
|
|
492
785
|
|
|
493
786
|
/**
|
|
787
|
+
* Creates a layer from a `Config`-wrapped PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
|
|
788
|
+
*
|
|
494
789
|
* @category layers
|
|
495
|
-
* @since
|
|
790
|
+
* @since 4.0.0
|
|
496
791
|
*/
|
|
497
|
-
export const
|
|
498
|
-
config:
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
)
|
|
792
|
+
export const layerConfig: (
|
|
793
|
+
config: Config.Wrap<PgPoolConfig>
|
|
794
|
+
) => Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> = (
|
|
795
|
+
config: Config.Wrap<PgPoolConfig>
|
|
796
|
+
): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
|
|
797
|
+
layerFrom(Effect.flatMap(
|
|
798
|
+
Config.unwrap(config),
|
|
799
|
+
make
|
|
800
|
+
))
|
|
506
801
|
|
|
507
802
|
/**
|
|
803
|
+
* Creates a layer from a concrete PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
|
|
804
|
+
*
|
|
508
805
|
* @category layers
|
|
509
|
-
* @since
|
|
806
|
+
* @since 4.0.0
|
|
510
807
|
*/
|
|
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))
|
|
808
|
+
export const layer = (
|
|
809
|
+
config: PgPoolConfig
|
|
810
|
+
): Layer.Layer<PgClient | Client.SqlClient, SqlError> => layerFrom(make(config))
|
|
528
811
|
|
|
529
812
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
813
|
+
* Creates the PostgreSQL statement compiler, using `$1` placeholders, double-quoted identifiers, PostgreSQL returning clauses, and optional JSON value transformation.
|
|
814
|
+
*
|
|
815
|
+
* @category constructors
|
|
816
|
+
* @since 4.0.0
|
|
532
817
|
*/
|
|
533
818
|
export const makeCompiler = (
|
|
534
819
|
transform?: (_: string) => string,
|
|
@@ -576,18 +861,87 @@ export const makeCompiler = (
|
|
|
576
861
|
const escape = Statement.defaultEscape("\"")
|
|
577
862
|
|
|
578
863
|
/**
|
|
864
|
+
* PostgreSQL-specific custom statement fragments supported by the compiler, currently JSON parameter fragments.
|
|
865
|
+
*
|
|
579
866
|
* @category custom types
|
|
580
|
-
* @since
|
|
867
|
+
* @since 4.0.0
|
|
581
868
|
*/
|
|
582
869
|
export type PgCustom = PgJson
|
|
583
870
|
|
|
584
871
|
/**
|
|
585
872
|
* @category custom types
|
|
586
|
-
* @since
|
|
873
|
+
* @since 4.0.0
|
|
587
874
|
*/
|
|
588
875
|
interface PgJson extends Custom<"PgJson", unknown> {}
|
|
589
876
|
/**
|
|
590
877
|
* @category custom types
|
|
591
|
-
* @since
|
|
878
|
+
* @since 4.0.0
|
|
592
879
|
*/
|
|
593
880
|
const PgJson = Statement.custom<PgJson>("PgJson")
|
|
881
|
+
|
|
882
|
+
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|
883
|
+
const ATTR_DB_NAMESPACE = "db.namespace"
|
|
884
|
+
const ATTR_SERVER_ADDRESS = "server.address"
|
|
885
|
+
const ATTR_SERVER_PORT = "server.port"
|
|
886
|
+
|
|
887
|
+
const pgCodeFromCause = (cause: unknown): string | undefined => {
|
|
888
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause)) {
|
|
889
|
+
return undefined
|
|
890
|
+
}
|
|
891
|
+
const code = cause.code
|
|
892
|
+
return typeof code === "string" ? code : undefined
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const pgConstraintFromCause = (cause: unknown): string => {
|
|
896
|
+
if (typeof cause !== "object" || cause === null || !("constraint" in cause)) {
|
|
897
|
+
return "unknown"
|
|
898
|
+
}
|
|
899
|
+
const constraint = cause.constraint
|
|
900
|
+
if (typeof constraint !== "string") {
|
|
901
|
+
return "unknown"
|
|
902
|
+
}
|
|
903
|
+
const normalized = constraint.trim()
|
|
904
|
+
return normalized.length === 0 ? "unknown" : normalized
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const classifyError = (
|
|
908
|
+
cause: unknown,
|
|
909
|
+
message: string,
|
|
910
|
+
operation: string
|
|
911
|
+
) => {
|
|
912
|
+
const props = { cause, message, operation }
|
|
913
|
+
const code = pgCodeFromCause(cause)
|
|
914
|
+
if (code !== undefined) {
|
|
915
|
+
if (code.startsWith("08")) {
|
|
916
|
+
return new ConnectionError(props)
|
|
917
|
+
}
|
|
918
|
+
if (code.startsWith("28")) {
|
|
919
|
+
return new AuthenticationError(props)
|
|
920
|
+
}
|
|
921
|
+
if (code === "42501") {
|
|
922
|
+
return new AuthorizationError(props)
|
|
923
|
+
}
|
|
924
|
+
if (code.startsWith("42")) {
|
|
925
|
+
return new SqlSyntaxError(props)
|
|
926
|
+
}
|
|
927
|
+
if (code === "23505") {
|
|
928
|
+
return new UniqueViolation({ ...props, constraint: pgConstraintFromCause(cause) })
|
|
929
|
+
}
|
|
930
|
+
if (code.startsWith("23")) {
|
|
931
|
+
return new ConstraintError(props)
|
|
932
|
+
}
|
|
933
|
+
if (code === "40P01") {
|
|
934
|
+
return new DeadlockError(props)
|
|
935
|
+
}
|
|
936
|
+
if (code === "40001") {
|
|
937
|
+
return new SerializationError(props)
|
|
938
|
+
}
|
|
939
|
+
if (code === "55P03") {
|
|
940
|
+
return new LockTimeoutError(props)
|
|
941
|
+
}
|
|
942
|
+
if (code === "57014") {
|
|
943
|
+
return new StatementTimeoutError(props)
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return new UnknownError(props)
|
|
947
|
+
}
|