@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.
Files changed (60) hide show
  1. package/AGENTS.md +24 -9
  2. package/CLAUDE.md +24 -9
  3. package/ai-docs/package.json +2 -2
  4. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +18 -5
  5. package/ai-docs/src/01_effect/01_basics/index.md +5 -3
  6. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +1 -1
  7. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +2 -2
  8. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +2 -2
  9. package/ai-docs/src/03_stream/30_encoding.ts +5 -7
  10. package/ai-docs/src/08_observability/10_logging.ts +1 -1
  11. package/ai-docs/src/70_cli/10_basics.ts +7 -7
  12. package/ai-docs/src/71_ai/10_language-model.ts +2 -2
  13. package/ai-docs/src/71_ai/20_tools.ts +1 -1
  14. package/ai-docs/src/71_ai/30_chat.ts +1 -1
  15. package/dist/PgAuth.d.ts +24 -0
  16. package/dist/PgAuth.d.ts.map +1 -1
  17. package/dist/PgAuth.js +29 -2
  18. package/dist/PgAuth.js.map +1 -1
  19. package/dist/PgClient.d.ts +49 -82
  20. package/dist/PgClient.d.ts.map +1 -1
  21. package/dist/PgClient.js +58 -510
  22. package/dist/PgClient.js.map +1 -1
  23. package/dist/PgConnection.d.ts +177 -0
  24. package/dist/PgConnection.d.ts.map +1 -0
  25. package/dist/PgConnection.js +1786 -0
  26. package/dist/PgConnection.js.map +1 -0
  27. package/dist/PgPool.d.ts +107 -0
  28. package/dist/PgPool.d.ts.map +1 -0
  29. package/dist/PgPool.js +124 -0
  30. package/dist/PgPool.js.map +1 -0
  31. package/dist/PgProtocol.d.ts +63 -30
  32. package/dist/PgProtocol.d.ts.map +1 -1
  33. package/dist/PgProtocol.js +64 -22
  34. package/dist/PgProtocol.js.map +1 -1
  35. package/dist/PgTypes.d.ts +85 -17
  36. package/dist/PgTypes.d.ts.map +1 -1
  37. package/dist/PgTypes.js +166 -169
  38. package/dist/PgTypes.js.map +1 -1
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/connection.d.ts +2 -0
  44. package/dist/internal/connection.d.ts.map +1 -0
  45. package/dist/internal/connection.js +5 -0
  46. package/dist/internal/connection.js.map +1 -0
  47. package/dist/internal/sqlError.d.ts +10 -0
  48. package/dist/internal/sqlError.d.ts.map +1 -0
  49. package/dist/internal/sqlError.js +57 -0
  50. package/dist/internal/sqlError.js.map +1 -0
  51. package/package.json +6 -14
  52. package/src/PgAuth.ts +33 -2
  53. package/src/PgClient.ts +138 -681
  54. package/src/PgConnection.ts +2294 -0
  55. package/src/PgPool.ts +231 -0
  56. package/src/PgProtocol.ts +93 -50
  57. package/src/PgTypes.ts +254 -186
  58. package/src/index.ts +10 -0
  59. package/src/internal/connection.ts +26 -0
  60. 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
- * Feeds the next chunk of socket bytes and returns every message that is now
50
- * complete. A partial trailing message is retained until the bytes that
51
- * finish it arrive. A thrown parse or field-reader error is terminal and the
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
- * Byte fields on the returned messages - `DataRow` values, `CopyData` and
56
- * `Unknown` payloads - are views into an internal buffer that the parser
57
- * never rewrites, so they stay valid for as long as they are held. They are
58
- * meant to be consumed before the next `push`: holding one keeps its whole
59
- * buffer alive, so copy it if it has to outlive the row.
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
- * Special pre-startup replies have no type byte and are not handled here; use
68
- * `decodeSslResponse` for those.
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
- return {
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
- messages.push(decodeDataRow<A>(buffer, store, 0, body, limit, this.readField))
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
- messages.push(message as BackendMessage<A>)
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, creating a portal.
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
- * Where a value writes its wire bytes. `Bind` frames a parameter by leaving
789
- * room for its length, letting the sink fill in the body, and backfilling the
790
- * length from what was written, so a value never needs an array of its own.
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
- * Builds a `Bind` encoder that writes parameters straight into the frame,
821
- * skipping the array per parameter that `encodeBind` has to copy from.
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
- writer.reserve(6)
850
- const header = writer.bytes
851
- const headerOffset = writer.offset
852
- header[headerOffset] = 0
853
- header[headerOffset + 1] = 1
854
- header[headerOffset + 2] = 0
855
- header[headerOffset + 3] = 1
856
- header[headerOffset + 4] = count >>> 8
857
- header[headerOffset + 5] = count
858
- writer.offset = headerOffset + 6
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 out of the parser's buffer, for a parser given a
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
- * The bytes are the parser's buffer rather than a view of the field, so they
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
- * A reader runs inside the stateful parser. If it throws, that failure is
1304
- * terminal just like a `ParseError`.
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