@effect/sql-pg 4.0.0-rc.112 → 4.0.0-rc.113
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/AGENTS.md +24 -9
- package/CLAUDE.md +24 -9
- package/ai-docs/package.json +2 -2
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +18 -5
- package/ai-docs/src/01_effect/01_basics/index.md +5 -3
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +1 -1
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +2 -2
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +2 -2
- package/ai-docs/src/03_stream/30_encoding.ts +5 -7
- package/ai-docs/src/08_observability/10_logging.ts +1 -1
- package/ai-docs/src/70_cli/10_basics.ts +7 -7
- package/ai-docs/src/71_ai/10_language-model.ts +2 -2
- package/ai-docs/src/71_ai/20_tools.ts +1 -1
- package/ai-docs/src/71_ai/30_chat.ts +1 -1
- package/dist/PgAuth.d.ts +24 -0
- package/dist/PgAuth.d.ts.map +1 -1
- package/dist/PgAuth.js +29 -2
- package/dist/PgAuth.js.map +1 -1
- package/dist/PgClient.d.ts +49 -82
- package/dist/PgClient.d.ts.map +1 -1
- package/dist/PgClient.js +58 -510
- package/dist/PgClient.js.map +1 -1
- package/dist/PgConnection.d.ts +177 -0
- package/dist/PgConnection.d.ts.map +1 -0
- package/dist/PgConnection.js +1786 -0
- package/dist/PgConnection.js.map +1 -0
- package/dist/PgPool.d.ts +107 -0
- package/dist/PgPool.d.ts.map +1 -0
- package/dist/PgPool.js +124 -0
- package/dist/PgPool.js.map +1 -0
- package/dist/PgProtocol.d.ts +63 -30
- package/dist/PgProtocol.d.ts.map +1 -1
- package/dist/PgProtocol.js +64 -22
- package/dist/PgProtocol.js.map +1 -1
- package/dist/PgTypes.d.ts +85 -17
- package/dist/PgTypes.d.ts.map +1 -1
- package/dist/PgTypes.js +166 -169
- package/dist/PgTypes.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/connection.d.ts +2 -0
- package/dist/internal/connection.d.ts.map +1 -0
- package/dist/internal/connection.js +5 -0
- package/dist/internal/connection.js.map +1 -0
- package/dist/internal/sqlError.d.ts +10 -0
- package/dist/internal/sqlError.d.ts.map +1 -0
- package/dist/internal/sqlError.js +57 -0
- package/dist/internal/sqlError.js.map +1 -0
- package/package.json +6 -14
- package/src/PgAuth.ts +33 -2
- package/src/PgClient.ts +138 -681
- package/src/PgConnection.ts +2294 -0
- package/src/PgPool.ts +231 -0
- package/src/PgProtocol.ts +93 -50
- package/src/PgTypes.ts +254 -186
- package/src/index.ts +10 -0
- package/src/internal/connection.ts +26 -0
- package/src/internal/sqlError.ts +75 -0
package/src/PgPool.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pools of native `PgConnection` sessions.
|
|
3
|
+
*
|
|
4
|
+
* @since 4.0.0
|
|
5
|
+
*/
|
|
6
|
+
import * as Clock from "effect/Clock"
|
|
7
|
+
import * as Context from "effect/Context"
|
|
8
|
+
import * as Duration from "effect/Duration"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Pool from "effect/Pool"
|
|
11
|
+
import type * as Scope from "effect/Scope"
|
|
12
|
+
import type { SqlError } from "effect/unstable/sql/SqlError"
|
|
13
|
+
import { connectionInternals } from "./internal/connection.ts"
|
|
14
|
+
import * as PgConnection from "./PgConnection.ts"
|
|
15
|
+
|
|
16
|
+
const defaultMultiplexConcurrency = 32
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The runtime type identifier for `PgPool`.
|
|
20
|
+
*
|
|
21
|
+
* @category type IDs
|
|
22
|
+
* @since 4.0.0
|
|
23
|
+
*/
|
|
24
|
+
export const TypeId: TypeId = "~@effect/sql-pg/PgPool"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The type-level identifier for `PgPool`.
|
|
28
|
+
*
|
|
29
|
+
* @category type IDs
|
|
30
|
+
* @since 4.0.0
|
|
31
|
+
*/
|
|
32
|
+
export type TypeId = "~@effect/sql-pg/PgPool"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Connection and sizing settings for a PostgreSQL session pool.
|
|
36
|
+
*
|
|
37
|
+
* **Details**
|
|
38
|
+
*
|
|
39
|
+
* The defaults are 0 to 10 connections and a 10-second idle timeout.
|
|
40
|
+
* `connectionTTL` replaces connections that exceed the configured lifetime.
|
|
41
|
+
* Every connection is used at least once, so a TTL of zero disables reuse.
|
|
42
|
+
*
|
|
43
|
+
* With `multiplex` enabled, fibers may share pooled connections for pipelined
|
|
44
|
+
* queries. Reserved connections remain exclusive. Without multiplexing, every
|
|
45
|
+
* checkout is exclusive.
|
|
46
|
+
*
|
|
47
|
+
* @category models
|
|
48
|
+
* @since 4.0.0
|
|
49
|
+
*/
|
|
50
|
+
export interface Config extends PgConnection.Config {
|
|
51
|
+
readonly idleTimeout?: Duration.Input | undefined
|
|
52
|
+
readonly maxConnections?: number | undefined
|
|
53
|
+
readonly minConnections?: number | undefined
|
|
54
|
+
readonly connectionTTL?: Duration.Input | undefined
|
|
55
|
+
/**
|
|
56
|
+
* How many statements may share one connection when `multiplex` is on.
|
|
57
|
+
* Defaults to `32`. Statements are pipelined into one write, so a higher
|
|
58
|
+
* number means fewer round trips, but it also means a slow statement holds
|
|
59
|
+
* up more of the statements queued behind it.
|
|
60
|
+
*/
|
|
61
|
+
readonly multiplexConcurrency?: number | undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A PostgreSQL session pool.
|
|
66
|
+
*
|
|
67
|
+
* @category models
|
|
68
|
+
* @since 4.0.0
|
|
69
|
+
*/
|
|
70
|
+
export interface PgPool {
|
|
71
|
+
readonly [TypeId]: TypeId
|
|
72
|
+
readonly config: Config
|
|
73
|
+
/**
|
|
74
|
+
* Checks out a session until the scope closes. Without multiplexing the
|
|
75
|
+
* checkout is exclusive. With multiplexing the
|
|
76
|
+
* session may be shared with other fibers, so multi-statement work should
|
|
77
|
+
* use `reserve` instead.
|
|
78
|
+
*/
|
|
79
|
+
readonly get: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope>
|
|
80
|
+
/**
|
|
81
|
+
* Checks out a session for exclusive use until the scope closes. Use this for
|
|
82
|
+
* transactions and listeners on a multiplexed pool.
|
|
83
|
+
*/
|
|
84
|
+
readonly reserve: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope>
|
|
85
|
+
/**
|
|
86
|
+
* Removes a session so the pool can replace it. Fatal protocol and socket
|
|
87
|
+
* errors invalidate sessions automatically.
|
|
88
|
+
*/
|
|
89
|
+
/**
|
|
90
|
+
* Lends a session for the duration of one effect and takes it back on any
|
|
91
|
+
* exit, without opening a scope for it.
|
|
92
|
+
*
|
|
93
|
+
* **Details**
|
|
94
|
+
*
|
|
95
|
+
* For work that finishes with the effect that runs it. A lease that has to
|
|
96
|
+
* outlive its effect - a stream, a transaction - takes `get` or `reserve`.
|
|
97
|
+
*/
|
|
98
|
+
readonly use: <A, E, R>(
|
|
99
|
+
f: (connection: PgConnection.PgConnection) => Effect.Effect<A, E, R>
|
|
100
|
+
) => Effect.Effect<A, E | SqlError, R>
|
|
101
|
+
readonly invalidate: (connection: PgConnection.PgConnection) => Effect.Effect<void>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The service tag for `PgPool`.
|
|
106
|
+
*
|
|
107
|
+
* @category services
|
|
108
|
+
* @since 4.0.0
|
|
109
|
+
*/
|
|
110
|
+
export const PgPool = Context.Service<PgPool>("@effect/sql-pg/PgPool")
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a scoped PostgreSQL session pool.
|
|
114
|
+
*
|
|
115
|
+
* **Details**
|
|
116
|
+
*
|
|
117
|
+
* Connections are opened lazily up to `maxConnections` and released down to
|
|
118
|
+
* `minConnections` after `idleTimeout` without use. Closing the scope shuts
|
|
119
|
+
* the pool down and releases every session.
|
|
120
|
+
*
|
|
121
|
+
* @category constructors
|
|
122
|
+
* @since 4.0.0
|
|
123
|
+
*/
|
|
124
|
+
export const make = Effect.fnUntraced(function*(options: Config): Effect.fn.Return<PgPool, SqlError, Scope.Scope> {
|
|
125
|
+
const clock = yield* Clock.Clock
|
|
126
|
+
const runFork = Effect.runForkWith(yield* Effect.context())
|
|
127
|
+
|
|
128
|
+
const multiplex = options.multiplex ?? false
|
|
129
|
+
const connectionTTL = options.connectionTTL !== undefined
|
|
130
|
+
? Duration.toMillis(Duration.fromInputUnsafe(options.connectionTTL))
|
|
131
|
+
: undefined
|
|
132
|
+
const deadConnections = new Set<PgConnection.PgConnection>()
|
|
133
|
+
const createdAt = new WeakMap<PgConnection.PgConnection, number>()
|
|
134
|
+
const checkedOut = new WeakSet<PgConnection.PgConnection>()
|
|
135
|
+
|
|
136
|
+
// Assigned below, once the pool exists. `acquire` only runs when the pool
|
|
137
|
+
// opens a connection, which is always after that.
|
|
138
|
+
let pool: Pool.Pool<PgConnection.PgConnection, SqlError>
|
|
139
|
+
const acquire = Effect.tap(PgConnection.make(options), (connection) =>
|
|
140
|
+
Effect.sync(() => {
|
|
141
|
+
createdAt.set(connection, clock.currentTimeMillisUnsafe())
|
|
142
|
+
const internals = connectionInternals(connection)
|
|
143
|
+
internals.fatalHooks.add(() => {
|
|
144
|
+
deadConnections.add(connection)
|
|
145
|
+
// `deadConnections` is only read by the next checkout, and a checkout
|
|
146
|
+
// already waiting for this connection would never get that far. Tell
|
|
147
|
+
// the pool now so it can replace the connection and admit them.
|
|
148
|
+
runFork(Pool.invalidate(pool, connection))
|
|
149
|
+
})
|
|
150
|
+
// Pinning a shared session has to take it out of circulation, or a
|
|
151
|
+
// second checkout lands on the connection its own stream is holding.
|
|
152
|
+
if (multiplex) internals.reserve = Pool.reserve(pool, connection)
|
|
153
|
+
}))
|
|
154
|
+
|
|
155
|
+
const maxConnections = options.maxConnections ?? 10
|
|
156
|
+
pool = yield* Pool.makeWithTTL({
|
|
157
|
+
acquire,
|
|
158
|
+
min: options.minConnections ?? 0,
|
|
159
|
+
max: maxConnections,
|
|
160
|
+
concurrency: multiplex
|
|
161
|
+
? Math.max(1, options.multiplexConcurrency ?? defaultMultiplexConcurrency)
|
|
162
|
+
: 1,
|
|
163
|
+
timeToLive: options.idleTimeout ?? Duration.seconds(10),
|
|
164
|
+
timeToLiveStrategy: "usage"
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const expired = (connection: PgConnection.PgConnection): boolean => {
|
|
168
|
+
if (connectionInternals(connection).deadError() !== undefined) return true
|
|
169
|
+
if (connectionTTL === undefined) return false
|
|
170
|
+
if (!checkedOut.has(connection)) {
|
|
171
|
+
checkedOut.add(connection)
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
const openedAt = createdAt.get(connection)
|
|
175
|
+
return openedAt !== undefined && clock.currentTimeMillisUnsafe() - openedAt >= connectionTTL
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A checkout runs per statement, so the case where nothing has died and the
|
|
179
|
+
// first connection is usable stays a plain flatMap; retrying is the
|
|
180
|
+
// exception and pays for the loop.
|
|
181
|
+
const retry: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope> = Effect.gen(function*() {
|
|
182
|
+
while (true) {
|
|
183
|
+
if (deadConnections.size > 0) {
|
|
184
|
+
const dead = Array.from(deadConnections)
|
|
185
|
+
deadConnections.clear()
|
|
186
|
+
yield* Effect.forEach(dead, (connection) => Pool.invalidate(pool, connection), { discard: true })
|
|
187
|
+
}
|
|
188
|
+
const connection = yield* Pool.get(pool)
|
|
189
|
+
if (expired(connection)) {
|
|
190
|
+
yield* Pool.invalidate(pool, connection)
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
return connection
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const get: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope> = Effect.suspend(() =>
|
|
198
|
+
deadConnections.size > 0 ? retry : Effect.flatMap(Pool.get(pool), (connection) =>
|
|
199
|
+
expired(connection)
|
|
200
|
+
? Effect.andThen(Pool.invalidate(pool, connection), retry)
|
|
201
|
+
: Effect.succeed(connection))
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// `pin` reserves the pool item itself, so this needs no help.
|
|
205
|
+
const reserve = Effect.flatMap(get, (connection) => connection.pin)
|
|
206
|
+
|
|
207
|
+
// `Pool.use` cannot check the session it hands over before running the
|
|
208
|
+
// effect, so it is only taken when there is nothing to check: no session is
|
|
209
|
+
// known dead, and no lifetime can have run out. Otherwise the scoped
|
|
210
|
+
// checkout does its replacement pass first. Checking inside the callback
|
|
211
|
+
// instead would hold one lease while acquiring another, which deadlocks a
|
|
212
|
+
// pool of one.
|
|
213
|
+
const use = <A, E, R>(
|
|
214
|
+
f: (connection: PgConnection.PgConnection) => Effect.Effect<A, E, R>
|
|
215
|
+
): Effect.Effect<A, E | SqlError, R> =>
|
|
216
|
+
Effect.suspend(() =>
|
|
217
|
+
connectionTTL === undefined && deadConnections.size === 0
|
|
218
|
+
? Pool.use(pool, f)
|
|
219
|
+
: Effect.scoped(Effect.flatMap(get, f))
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const pgPool: PgPool = {
|
|
223
|
+
[TypeId]: TypeId,
|
|
224
|
+
config: options,
|
|
225
|
+
get,
|
|
226
|
+
reserve,
|
|
227
|
+
use,
|
|
228
|
+
invalidate: (connection) => Pool.invalidate(pool, connectionInternals(connection).base as PgConnection.PgConnection)
|
|
229
|
+
}
|
|
230
|
+
return pgPool
|
|
231
|
+
})
|
package/src/PgProtocol.ts
CHANGED
|
@@ -46,26 +46,35 @@ export interface Parser<A = Uint8Array | null> {
|
|
|
46
46
|
readField: FieldReader<A> | undefined
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* parser cannot be reused afterward; any messages decoded earlier in the
|
|
53
|
-
* failing push are discarded.
|
|
49
|
+
* Decodes a chunk and returns every complete message. A partial message is
|
|
50
|
+
* retained for the next call. Parse and field-reader errors are terminal and
|
|
51
|
+
* discard messages decoded earlier in the same call.
|
|
54
52
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* buffer
|
|
53
|
+
* **Details**
|
|
54
|
+
*
|
|
55
|
+
* `DataRow`, `CopyData`, and `Unknown` payloads are views into an internal
|
|
56
|
+
* buffer. Copy a payload that must outlive the current row, otherwise its
|
|
57
|
+
* entire buffer remains in memory.
|
|
60
58
|
*/
|
|
61
59
|
readonly push: (chunk: Uint8Array) => ReadonlyArray<BackendMessage<A>>
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Decodes a chunk and passes each complete message to `onMessage`
|
|
63
|
+
* immediately. This lets a `RowDescription` update `readField` before a
|
|
64
|
+
* `DataRow` later in the same chunk is decoded. The failure and
|
|
65
|
+
* buffer-lifetime rules match `push`, except messages
|
|
66
|
+
* delivered before a failure are not discarded.
|
|
67
|
+
*/
|
|
68
|
+
readonly pushEach: (chunk: Uint8Array, onMessage: (message: BackendMessage<A>) => void) => void
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
72
|
* Creates a `Parser`.
|
|
66
73
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
74
|
+
* **Details**
|
|
75
|
+
*
|
|
76
|
+
* Special pre-startup replies have no type byte. Use `decodeSslResponse` for
|
|
77
|
+
* those replies.
|
|
69
78
|
*
|
|
70
79
|
* @category constructors
|
|
71
80
|
* @since 4.0.0
|
|
@@ -114,15 +123,21 @@ export const makeParser = <A = Uint8Array | null>(options?: {
|
|
|
114
123
|
end += chunk.length
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
|
|
126
|
+
const parser: Parser<A> = {
|
|
118
127
|
readField: options?.readField,
|
|
119
128
|
push(chunk) {
|
|
129
|
+
const messages: Array<BackendMessage<A>> = []
|
|
130
|
+
parser.pushEach(chunk, (message) => {
|
|
131
|
+
messages.push(message)
|
|
132
|
+
})
|
|
133
|
+
return messages
|
|
134
|
+
},
|
|
135
|
+
pushEach(chunk, onMessage) {
|
|
120
136
|
if (failed) {
|
|
121
137
|
throw new ParseError({ message: "Parser cannot be reused after a failure" })
|
|
122
138
|
}
|
|
123
139
|
try {
|
|
124
140
|
append(chunk)
|
|
125
|
-
const messages: Array<BackendMessage<A>> = []
|
|
126
141
|
while (end - start >= 5) {
|
|
127
142
|
const length = (buffer[start + 1] << 24) | (buffer[start + 2] << 16) | (buffer[start + 3] << 8) |
|
|
128
143
|
buffer[start + 4]
|
|
@@ -141,23 +156,23 @@ export const makeParser = <A = Uint8Array | null>(options?: {
|
|
|
141
156
|
start = limit
|
|
142
157
|
if (type === BackendType.DataRow) {
|
|
143
158
|
// `buffer` always starts at byte 0 of `store`, so offsets index both.
|
|
144
|
-
|
|
159
|
+
onMessage(decodeDataRow<A>(buffer, store, 0, body, limit, parser.readField))
|
|
145
160
|
} else {
|
|
146
161
|
reader.reset(buffer, body, limit)
|
|
147
162
|
const message = decodeBackend(type, reader)
|
|
148
163
|
if (reader.offset !== limit) {
|
|
149
164
|
throw new ParseError({ message: `Message has ${limit - reader.offset} trailing byte(s)` })
|
|
150
165
|
}
|
|
151
|
-
|
|
166
|
+
onMessage(message as BackendMessage<A>)
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
|
-
return messages
|
|
155
169
|
} catch (error) {
|
|
156
170
|
failed = true
|
|
157
171
|
throw error
|
|
158
172
|
}
|
|
159
173
|
}
|
|
160
174
|
}
|
|
175
|
+
return parser
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
// -----------------------------------------------------------------------------
|
|
@@ -178,9 +193,8 @@ export interface Parse {
|
|
|
178
193
|
}
|
|
179
194
|
|
|
180
195
|
/**
|
|
181
|
-
* Binds parameter values to a prepared statement
|
|
182
|
-
*
|
|
183
|
-
* Parameters and results always use the binary format code.
|
|
196
|
+
* Binds parameter values to a prepared statement and creates a portal.
|
|
197
|
+
* Parameters and results use the binary format.
|
|
184
198
|
*
|
|
185
199
|
* @category models
|
|
186
200
|
* @since 4.0.0
|
|
@@ -714,6 +728,12 @@ const encodeParseUnsafe = (options: Omit<Parse, "_tag">): Uint8Array => {
|
|
|
714
728
|
return end()
|
|
715
729
|
}
|
|
716
730
|
|
|
731
|
+
/**
|
|
732
|
+
* Encodes a `Parse` message.
|
|
733
|
+
*
|
|
734
|
+
* @category encoding
|
|
735
|
+
* @since 4.0.0
|
|
736
|
+
*/
|
|
717
737
|
export const encodeParse = (options: Omit<Parse, "_tag">): Result.Result<Uint8Array, EncodeError> =>
|
|
718
738
|
encodeResult(() => encodeParseUnsafe(options))
|
|
719
739
|
|
|
@@ -781,15 +801,19 @@ const encodeBindUnsafe = (options: Omit<Bind, "_tag">): Uint8Array => {
|
|
|
781
801
|
return end()
|
|
782
802
|
}
|
|
783
803
|
|
|
804
|
+
/**
|
|
805
|
+
* Encodes a `Bind` message.
|
|
806
|
+
*
|
|
807
|
+
* @category encoding
|
|
808
|
+
* @since 4.0.0
|
|
809
|
+
*/
|
|
784
810
|
export const encodeBind = (options: Omit<Bind, "_tag">): Result.Result<Uint8Array, EncodeError> =>
|
|
785
811
|
encodeResult(() => encodeBindUnsafe(options))
|
|
786
812
|
|
|
787
813
|
/**
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
* `PgTypes.writeParameter` is the implementation for OID-typed values.
|
|
814
|
+
* A sink for writing parameter bytes into a `Bind` frame. The frame reserves
|
|
815
|
+
* and backfills each parameter length. `PgTypes.writeParameter` supports
|
|
816
|
+
* OID-typed values.
|
|
793
817
|
*
|
|
794
818
|
* @category models
|
|
795
819
|
* @since 4.0.0
|
|
@@ -816,24 +840,31 @@ export interface ValueSink {
|
|
|
816
840
|
readonly endLength: (token: number) => void
|
|
817
841
|
}
|
|
818
842
|
|
|
843
|
+
const valueWriterUnsafe = Symbol.for("@effect/sql-pg/PgProtocol/ValueWriter/unsafe")
|
|
844
|
+
|
|
819
845
|
/**
|
|
820
|
-
*
|
|
821
|
-
*
|
|
846
|
+
* Creates a `Bind` encoder that writes parameters directly into the frame.
|
|
847
|
+
*
|
|
848
|
+
* **Details**
|
|
849
|
+
*
|
|
850
|
+
* `textFormat` identifies parameters encoded as text. All other parameters
|
|
851
|
+
* use the binary format.
|
|
852
|
+
*
|
|
853
|
+
* **Example** (Encoding a `Bind` message)
|
|
822
854
|
*
|
|
823
855
|
* ```ts
|
|
824
856
|
* import { PgProtocol, PgTypes } from "@effect/sql-pg"
|
|
825
857
|
*
|
|
826
|
-
* const encodeBind = PgProtocol.makeBindEncoder(PgTypes.writeParameter)
|
|
858
|
+
* const encodeBind = PgProtocol.makeBindEncoder(PgTypes.writeParameter, PgTypes.isTextFormat)
|
|
827
859
|
* const frame = encodeBind({ portal: "", statement: "s1", parameters: [PgTypes.int4(1)] })
|
|
828
860
|
* ```
|
|
829
861
|
*
|
|
830
862
|
* @category encoding
|
|
831
863
|
* @since 4.0.0
|
|
832
864
|
*/
|
|
833
|
-
const valueWriterUnsafe = Symbol.for("@effect/sql-pg/PgProtocol/ValueWriter/unsafe")
|
|
834
|
-
|
|
835
865
|
export const makeBindEncoder = <A, E = never>(
|
|
836
|
-
writeParameter: (sink: ValueSink, value: A) => Result.Result<void, E
|
|
866
|
+
writeParameter: (sink: ValueSink, value: A) => Result.Result<void, E>,
|
|
867
|
+
textFormat?: (value: A) => boolean
|
|
837
868
|
) =>
|
|
838
869
|
(options: {
|
|
839
870
|
readonly portal: string
|
|
@@ -846,16 +877,32 @@ export const makeBindEncoder = <A, E = never>(
|
|
|
846
877
|
writer.cString(options.statement)
|
|
847
878
|
const parameters = options.parameters
|
|
848
879
|
const count = requireInt16Count(parameters.length, "Bind parameter")
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
880
|
+
let textCount = 0
|
|
881
|
+
if (textFormat !== undefined) {
|
|
882
|
+
for (let index = 0; index < count; index++) {
|
|
883
|
+
if (textFormat(parameters[index])) textCount++
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (textCount === 0 || textCount === count) {
|
|
887
|
+
// One format code covering every parameter: binary, or all-text.
|
|
888
|
+
const code = textCount === 0 ? 1 : 0
|
|
889
|
+
writer.reserve(6)
|
|
890
|
+
const header = writer.bytes
|
|
891
|
+
const headerOffset = writer.offset
|
|
892
|
+
header[headerOffset] = 0
|
|
893
|
+
header[headerOffset + 1] = 1
|
|
894
|
+
header[headerOffset + 2] = 0
|
|
895
|
+
header[headerOffset + 3] = code
|
|
896
|
+
header[headerOffset + 4] = count >>> 8
|
|
897
|
+
header[headerOffset + 5] = count
|
|
898
|
+
writer.offset = headerOffset + 6
|
|
899
|
+
} else {
|
|
900
|
+
writer.int16(count)
|
|
901
|
+
for (let index = 0; index < count; index++) {
|
|
902
|
+
writer.int16(textFormat!(parameters[index]) ? 0 : 1)
|
|
903
|
+
}
|
|
904
|
+
writer.int16(count)
|
|
905
|
+
}
|
|
859
906
|
const writeUnsafe = (writeParameter as any)[valueWriterUnsafe] as
|
|
860
907
|
| ((sink: ValueSink, value: A) => void)
|
|
861
908
|
| undefined
|
|
@@ -1291,17 +1338,13 @@ export interface DataRow<out A = Uint8Array | null> {
|
|
|
1291
1338
|
}
|
|
1292
1339
|
|
|
1293
1340
|
/**
|
|
1294
|
-
* Reads one `DataRow` field
|
|
1295
|
-
* `readField`. `size` is -1 for SQL NULL, and `column` is the field's position
|
|
1296
|
-
* in the row.
|
|
1341
|
+
* Reads one `DataRow` field from the parser buffer.
|
|
1297
1342
|
*
|
|
1298
|
-
*
|
|
1299
|
-
* are only the field's for `offset` to `offset + size`, and reading outside
|
|
1300
|
-
* that reads the rest of the stream. `PgTypes.makeFieldReader` is the
|
|
1301
|
-
* implementation for OID-typed columns.
|
|
1343
|
+
* **Details**
|
|
1302
1344
|
*
|
|
1303
|
-
*
|
|
1304
|
-
*
|
|
1345
|
+
* `size` is `-1` for SQL `NULL`, and `column` is the field index. Only bytes
|
|
1346
|
+
* from `offset` through `offset + size` belong to the field. A thrown error
|
|
1347
|
+
* permanently fails the parser.
|
|
1305
1348
|
*
|
|
1306
1349
|
* @category models
|
|
1307
1350
|
* @since 4.0.0
|