postgrejs 3.12.2 → 3.14.0
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 +49 -0
- package/connection/connection.js +7 -2
- package/connection/intl-connection.d.ts +1 -0
- package/connection/intl-connection.js +4 -3
- package/constants.d.ts +1 -1
- package/constants.js +1 -1
- package/data-types/date-type.js +8 -1
- package/package.json +1 -1
- package/protocol/frontend.d.ts +14 -1
- package/protocol/frontend.js +39 -5
- package/protocol/pg-socket.d.ts +27 -3
- package/protocol/pg-socket.js +132 -34
- package/protocol/smart-buffer.d.ts +5 -4
- package/protocol/smart-buffer.js +21 -1
- package/protocol/workerd-socket.d.ts +87 -0
- package/protocol/workerd-socket.js +245 -0
- package/util/resolved-bind-type.d.ts +15 -0
- package/util/resolved-bind-type.js +85 -0
- package/util/runtime.d.ts +6 -0
- package/util/runtime.js +19 -0
- package/util/stringify-arrayliteral.js +9 -2
- package/util/unspecified-param.d.ts +14 -0
- package/util/unspecified-param.js +40 -0
- package/util/unspecified-value.d.ts +26 -0
- package/util/unspecified-value.js +63 -0
package/protocol/smart-buffer.js
CHANGED
|
@@ -6,9 +6,29 @@ import * as os from 'os';
|
|
|
6
6
|
* length-prefixed string where a `null`/`undefined` value is written as a
|
|
7
7
|
* length of -1 with no bytes following.
|
|
8
8
|
*/
|
|
9
|
+
/** The ceiling on `maxSizeFor`, and its answer when there is no machine to measure. */
|
|
10
|
+
const HARD_MAX = 1024 * 1024 * 1024 * 2; // 2 GB
|
|
11
|
+
/**
|
|
12
|
+
* The most a buffer may ever hold, given how much memory the machine has.
|
|
13
|
+
*
|
|
14
|
+
* @param totalMemory Bytes of memory, or 0 when that cannot be known.
|
|
15
|
+
* @returns Half of it, capped - or the cap alone when it is unknown.
|
|
16
|
+
*/
|
|
17
|
+
/* `os.totalmem()` answers 0 where there is no machine to ask about:
|
|
18
|
+
measured under Cloudflare's workerd, where `node:os` resolves and
|
|
19
|
+
returns zeros rather than throwing. Taking the minimum of that was a
|
|
20
|
+
maximum size of zero, which worked only because flexy-buffer reads a
|
|
21
|
+
falsy `maxLength` as "use the default" - an accident, and one that
|
|
22
|
+
would have become a buffer refusing every write the day that default
|
|
23
|
+
changed. */
|
|
24
|
+
export function maxSizeFor(totalMemory) {
|
|
25
|
+
return totalMemory > 0
|
|
26
|
+
? Math.min(Math.floor(totalMemory / 2), HARD_MAX)
|
|
27
|
+
: HARD_MAX;
|
|
28
|
+
}
|
|
9
29
|
export class SmartBuffer extends FlexyBuffer {
|
|
10
30
|
static DEFAULT_PAGE_SIZE = 512;
|
|
11
|
-
static DEFAULT_MAX_SIZE =
|
|
31
|
+
static DEFAULT_MAX_SIZE = maxSizeFor(typeof os.totalmem === 'function' ? os.totalmem() : 0);
|
|
12
32
|
constructor(cfg) {
|
|
13
33
|
super({
|
|
14
34
|
pageSize: cfg?.pageSize || SmartBuffer.DEFAULT_PAGE_SIZE,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
2
|
+
/** What `cloudflare:sockets` hands back, as much of it as is used here. */
|
|
3
|
+
interface CfSocket {
|
|
4
|
+
readable: ReadableStream<Uint8Array>;
|
|
5
|
+
writable: WritableStream<Uint8Array>;
|
|
6
|
+
closed: Promise<void>;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
startTls(options: {
|
|
9
|
+
expectedServerHostname?: string;
|
|
10
|
+
}): CfSocket;
|
|
11
|
+
}
|
|
12
|
+
/** The one function `cloudflare:sockets` is reached for. */
|
|
13
|
+
export type CfConnect = (address: string, options?: {
|
|
14
|
+
secureTransport?: 'off' | 'on' | 'starttls';
|
|
15
|
+
}) => CfSocket;
|
|
16
|
+
/**
|
|
17
|
+
* A `net.Socket`-shaped view of a Cloudflare socket, for the part of it
|
|
18
|
+
* this client uses: `connect`, `write`, `destroy`, and `data`/`error`/
|
|
19
|
+
* `close` events, plus `startTls()`, which is the one thing Node's
|
|
20
|
+
* `tls.connect({ socket })` cannot do on that runtime.
|
|
21
|
+
*
|
|
22
|
+
* Reads are a loop over the stream rather than a `'data'` event the
|
|
23
|
+
* runtime emits, so the loop is what turns one into the other.
|
|
24
|
+
*/
|
|
25
|
+
export declare class WorkerdSocket extends SafeEventEmitter {
|
|
26
|
+
private readonly _wantsTls;
|
|
27
|
+
private readonly _connect;
|
|
28
|
+
private _cf?;
|
|
29
|
+
private _writer?;
|
|
30
|
+
private _reader?;
|
|
31
|
+
private _upgrading;
|
|
32
|
+
private _destroyed;
|
|
33
|
+
private _paused?;
|
|
34
|
+
private _resume?;
|
|
35
|
+
constructor(_wantsTls: boolean, _connect?: () => Promise<CfConnect>);
|
|
36
|
+
/** Whether a TLS upgrade has been asked for and completed. */
|
|
37
|
+
get secure(): boolean;
|
|
38
|
+
private _secure;
|
|
39
|
+
/** Whether `destroy()` has been called. */
|
|
40
|
+
get destroyed(): boolean;
|
|
41
|
+
/** Whether writes are still accepted. */
|
|
42
|
+
get writable(): boolean;
|
|
43
|
+
cork(): void;
|
|
44
|
+
uncork(): void;
|
|
45
|
+
setNoDelay(): this;
|
|
46
|
+
setKeepAlive(): this;
|
|
47
|
+
setTimeout(): this;
|
|
48
|
+
/** Stops reading, so the sender is made to wait. */
|
|
49
|
+
pause(): this;
|
|
50
|
+
/** Starts reading again. */
|
|
51
|
+
resume(): this;
|
|
52
|
+
/** Closes the connection once anything already written has gone. */
|
|
53
|
+
end(): this;
|
|
54
|
+
/**
|
|
55
|
+
* Opens the connection and emits `connect` once it is writable.
|
|
56
|
+
*
|
|
57
|
+
* @param port The port to reach.
|
|
58
|
+
* @param host The host to reach.
|
|
59
|
+
*/
|
|
60
|
+
connect(port: number, host: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Reads one chunk and hands it over, without starting the loop.
|
|
63
|
+
*
|
|
64
|
+
* @param cb Called with the bytes, or with nothing if the connection
|
|
65
|
+
* ended first.
|
|
66
|
+
*/
|
|
67
|
+
readOnce(cb: (data?: Buffer) => void): void;
|
|
68
|
+
/**
|
|
69
|
+
* Upgrades the connection in place.
|
|
70
|
+
*
|
|
71
|
+
* @param servername The name to check the certificate against.
|
|
72
|
+
*/
|
|
73
|
+
startTls(servername?: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Writes to the connection.
|
|
76
|
+
*
|
|
77
|
+
* @param data The bytes to send.
|
|
78
|
+
* @param cb Called once they are written, or with the error.
|
|
79
|
+
* @returns True - the runtime offers no "buffer is full" answer.
|
|
80
|
+
*/
|
|
81
|
+
write(data: Uint8Array, cb?: (err?: Error | null) => void): boolean;
|
|
82
|
+
/** Closes the connection; `close` follows once the runtime agrees. */
|
|
83
|
+
destroy(): this;
|
|
84
|
+
protected _attach(cf: CfSocket, readLoop: boolean): void;
|
|
85
|
+
protected _read(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
2
|
+
/* Imported where it is called rather than at the top of the file: the
|
|
3
|
+
specifier does not resolve off workerd, and this module is reachable
|
|
4
|
+
from a Node build through `pg-socket.ts`. Nothing awaits it unless a
|
|
5
|
+
connection is actually being made on that runtime. */
|
|
6
|
+
const importCfConnect = async () => (await import(/* webpackIgnore: true */ 'cloudflare:sockets'))
|
|
7
|
+
.connect;
|
|
8
|
+
/**
|
|
9
|
+
* A `net.Socket`-shaped view of a Cloudflare socket, for the part of it
|
|
10
|
+
* this client uses: `connect`, `write`, `destroy`, and `data`/`error`/
|
|
11
|
+
* `close` events, plus `startTls()`, which is the one thing Node's
|
|
12
|
+
* `tls.connect({ socket })` cannot do on that runtime.
|
|
13
|
+
*
|
|
14
|
+
* Reads are a loop over the stream rather than a `'data'` event the
|
|
15
|
+
* runtime emits, so the loop is what turns one into the other.
|
|
16
|
+
*/
|
|
17
|
+
/* Written here rather than taken from `pg-cloudflare`, which does the
|
|
18
|
+
same job for `pg`: the adapter is a hundred lines whose lifecycle this
|
|
19
|
+
client would otherwise not control, and only the upgrade is actually
|
|
20
|
+
needed - the plain path on workerd already works through `node:net`.
|
|
21
|
+
|
|
22
|
+
The setNoDelay/setKeepAlive/setTimeout trio answers nothing because
|
|
23
|
+
the runtime exposes no equivalent; they are here so the connect path
|
|
24
|
+
above can treat both socket kinds the same way. */
|
|
25
|
+
export class WorkerdSocket extends SafeEventEmitter {
|
|
26
|
+
_wantsTls;
|
|
27
|
+
_connect;
|
|
28
|
+
_cf;
|
|
29
|
+
_writer;
|
|
30
|
+
_reader;
|
|
31
|
+
/* True between startTls() and the old socket's `closed` settling: that
|
|
32
|
+
promise resolves because the socket was replaced, not because the
|
|
33
|
+
connection went away, and reporting it as a close would end the
|
|
34
|
+
session in the middle of its handshake. */
|
|
35
|
+
_upgrading = false;
|
|
36
|
+
_destroyed = false;
|
|
37
|
+
_paused;
|
|
38
|
+
_resume;
|
|
39
|
+
constructor(_wantsTls, _connect = importCfConnect) {
|
|
40
|
+
super();
|
|
41
|
+
this._wantsTls = _wantsTls;
|
|
42
|
+
this._connect = _connect;
|
|
43
|
+
}
|
|
44
|
+
/** Whether a TLS upgrade has been asked for and completed. */
|
|
45
|
+
get secure() {
|
|
46
|
+
return this._secure;
|
|
47
|
+
}
|
|
48
|
+
_secure = false;
|
|
49
|
+
/** Whether `destroy()` has been called. */
|
|
50
|
+
get destroyed() {
|
|
51
|
+
return this._destroyed;
|
|
52
|
+
}
|
|
53
|
+
/** Whether writes are still accepted. */
|
|
54
|
+
get writable() {
|
|
55
|
+
return !this._destroyed && !!this._writer;
|
|
56
|
+
}
|
|
57
|
+
/* `cork`/`uncork` gather several small writes into one syscall on
|
|
58
|
+
Node. There is no syscall to gather here - a write is a promise
|
|
59
|
+
against a stream - so the pair is accepted and does nothing, which
|
|
60
|
+
is what it means on this runtime. */
|
|
61
|
+
cork() {
|
|
62
|
+
/* nothing to gather */
|
|
63
|
+
}
|
|
64
|
+
uncork() {
|
|
65
|
+
/* nothing to gather */
|
|
66
|
+
}
|
|
67
|
+
setNoDelay() {
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
setKeepAlive() {
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
setTimeout() {
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/** Stops reading, so the sender is made to wait. */
|
|
77
|
+
/* Real backpressure rather than a no-op: the read loop is what pulls
|
|
78
|
+
from the stream, so not pulling is the whole of it. `pause()` during
|
|
79
|
+
a COPY TO is the only caller, and a stream left unread is how the
|
|
80
|
+
runtime learns to stop. */
|
|
81
|
+
pause() {
|
|
82
|
+
if (!this._paused)
|
|
83
|
+
this._paused = new Promise(r => (this._resume = r));
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
/** Starts reading again. */
|
|
87
|
+
resume() {
|
|
88
|
+
const go = this._resume;
|
|
89
|
+
this._paused = undefined;
|
|
90
|
+
this._resume = undefined;
|
|
91
|
+
go?.();
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
/** Closes the connection once anything already written has gone. */
|
|
95
|
+
end() {
|
|
96
|
+
return this.destroy();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Opens the connection and emits `connect` once it is writable.
|
|
100
|
+
*
|
|
101
|
+
* @param port The port to reach.
|
|
102
|
+
* @param host The host to reach.
|
|
103
|
+
*/
|
|
104
|
+
connect(port, host) {
|
|
105
|
+
void (async () => {
|
|
106
|
+
try {
|
|
107
|
+
const connect = await this._connect();
|
|
108
|
+
this._cf = connect(`${host}:${port}`, {
|
|
109
|
+
// `starttls` leaves the connection in the clear and allows a
|
|
110
|
+
// later upgrade, which is what the SSLRequest exchange needs.
|
|
111
|
+
secureTransport: this._wantsTls ? 'starttls' : 'off',
|
|
112
|
+
});
|
|
113
|
+
/* No read loop yet when a TLS upgrade is coming: `releaseLock()`
|
|
114
|
+
throws on a reader with an outstanding read, and a loop parked
|
|
115
|
+
in `reader.read()` is exactly that. The negotiation asks for
|
|
116
|
+
the one byte it expects through `readOnce()`, and the loop
|
|
117
|
+
starts on the upgraded socket. */
|
|
118
|
+
this._attach(this._cf, !this._wantsTls);
|
|
119
|
+
await this._writer.ready;
|
|
120
|
+
this.emit('connect');
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
this.emit('error', e);
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Reads one chunk and hands it over, without starting the loop.
|
|
129
|
+
*
|
|
130
|
+
* @param cb Called with the bytes, or with nothing if the connection
|
|
131
|
+
* ended first.
|
|
132
|
+
*/
|
|
133
|
+
/* What the SSLRequest exchange needs: a single answer, read while no
|
|
134
|
+
loop holds the reader, so the lock can be released for the upgrade
|
|
135
|
+
that follows. */
|
|
136
|
+
readOnce(cb) {
|
|
137
|
+
const reader = this._reader;
|
|
138
|
+
if (!reader)
|
|
139
|
+
return cb();
|
|
140
|
+
reader.read().then(({ done, value }) => cb(done || !value ? undefined : Buffer.from(value)), (e) => {
|
|
141
|
+
this.emit('error', e);
|
|
142
|
+
cb();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Upgrades the connection in place.
|
|
147
|
+
*
|
|
148
|
+
* @param servername The name to check the certificate against.
|
|
149
|
+
*/
|
|
150
|
+
/* The locks have to go back before the socket is replaced, and the new
|
|
151
|
+
one needs its own reader and writer and its own read loop. */
|
|
152
|
+
startTls(servername) {
|
|
153
|
+
const cf = this._cf;
|
|
154
|
+
if (!cf) {
|
|
155
|
+
this.emit('error', new Error('Cannot upgrade a socket that is not connected'));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
this._writer.releaseLock();
|
|
160
|
+
this._reader.releaseLock();
|
|
161
|
+
this._upgrading = true;
|
|
162
|
+
this._cf = cf.startTls(servername ? { expectedServerHostname: servername } : {});
|
|
163
|
+
this._attach(this._cf, true);
|
|
164
|
+
void this._writer.ready.then(() => {
|
|
165
|
+
this._secure = true;
|
|
166
|
+
this.emit('secureConnect');
|
|
167
|
+
}, (e) => this.emit('error', e));
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
this.emit('error', e);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Writes to the connection.
|
|
175
|
+
*
|
|
176
|
+
* @param data The bytes to send.
|
|
177
|
+
* @param cb Called once they are written, or with the error.
|
|
178
|
+
* @returns True - the runtime offers no "buffer is full" answer.
|
|
179
|
+
*/
|
|
180
|
+
write(data, cb) {
|
|
181
|
+
const writer = this._writer;
|
|
182
|
+
if (!writer) {
|
|
183
|
+
cb?.(new Error('Socket is not connected'));
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
writer.write(data).then(() => cb?.(), (e) => {
|
|
187
|
+
cb?.(e);
|
|
188
|
+
this.emit('error', e);
|
|
189
|
+
});
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/** Closes the connection; `close` follows once the runtime agrees. */
|
|
193
|
+
destroy() {
|
|
194
|
+
if (this._destroyed)
|
|
195
|
+
return this;
|
|
196
|
+
this._destroyed = true;
|
|
197
|
+
const cf = this._cf;
|
|
198
|
+
this._cf = undefined;
|
|
199
|
+
cf?.close().catch(() => undefined);
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
_attach(cf, readLoop) {
|
|
203
|
+
this._writer = cf.writable.getWriter();
|
|
204
|
+
this._reader = cf.readable.getReader();
|
|
205
|
+
if (readLoop)
|
|
206
|
+
void this._read(this._reader);
|
|
207
|
+
cf.closed.then(() => {
|
|
208
|
+
if (this._upgrading) {
|
|
209
|
+
this._upgrading = false;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.emit('close');
|
|
213
|
+
}, (e) => {
|
|
214
|
+
if (this._upgrading) {
|
|
215
|
+
this._upgrading = false;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.emit('error', e);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async _read(reader) {
|
|
222
|
+
try {
|
|
223
|
+
for (;;) {
|
|
224
|
+
if (this._paused)
|
|
225
|
+
await this._paused;
|
|
226
|
+
const { done, value } = await reader.read();
|
|
227
|
+
if (done)
|
|
228
|
+
return;
|
|
229
|
+
/* Asked again on the way out: a pause() during a read already in
|
|
230
|
+
flight cannot un-await it, and delivering that chunk anyway
|
|
231
|
+
would be data arriving after the consumer said stop. */
|
|
232
|
+
if (this._paused)
|
|
233
|
+
await this._paused;
|
|
234
|
+
if (value?.length)
|
|
235
|
+
this.emit('data', Buffer.from(value));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
// A read that ends because the socket was replaced is the upgrade
|
|
240
|
+
// happening, not a failure to report.
|
|
241
|
+
if (!this._upgrading && !this._destroyed)
|
|
242
|
+
this.emit('error', e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Maybe, OID } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The types to encode a Bind's parameters against, given what the caller
|
|
4
|
+
* declared and what the server resolved.
|
|
5
|
+
*
|
|
6
|
+
* Returns `declared` unchanged unless some parameter can be improved, so
|
|
7
|
+
* the common call allocates nothing.
|
|
8
|
+
*
|
|
9
|
+
* @param declared What went into Parse - `0`/undefined where this client
|
|
10
|
+
* declared nothing and left the type to the server.
|
|
11
|
+
* @param params The values about to be written.
|
|
12
|
+
* @param resolved What the server answered for this statement, in order.
|
|
13
|
+
* @returns The types to hand `getBindMessage`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolvedBindTypes(declared: Maybe<Maybe<OID>[]>, params: Maybe<Maybe<any>[]>, resolved: Maybe<OID[]>): Maybe<Maybe<OID>[]>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { DataTypeOIDs } from '../constants.js';
|
|
2
|
+
import { arrayLeaf } from './array-leaf.js';
|
|
3
|
+
/**
|
|
4
|
+
* The types a `Date` is allowed to be encoded into binary against, by
|
|
5
|
+
* whether the value is an array.
|
|
6
|
+
*/
|
|
7
|
+
/* Deliberately short. `time` and `timetz` are left out because
|
|
8
|
+
TimeType.encodeBinary() multiplies the whole epoch by 1000 and
|
|
9
|
+
overflows for any ordinary Date - measured, every one of four test
|
|
10
|
+
instants answered 22008 "time out of range", where the text path
|
|
11
|
+
stores the wall clock correctly. Nothing else here may be reached
|
|
12
|
+
until it has been checked the same way. */
|
|
13
|
+
const DATE_SCALARS = new Set([
|
|
14
|
+
DataTypeOIDs.timestamptz,
|
|
15
|
+
DataTypeOIDs.timestamp,
|
|
16
|
+
DataTypeOIDs.date,
|
|
17
|
+
]);
|
|
18
|
+
const DATE_ARRAYS = new Set([
|
|
19
|
+
DataTypeOIDs._timestamptz,
|
|
20
|
+
DataTypeOIDs._timestamp,
|
|
21
|
+
DataTypeOIDs._date,
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* The types to encode a Bind's parameters against, given what the caller
|
|
25
|
+
* declared and what the server resolved.
|
|
26
|
+
*
|
|
27
|
+
* Returns `declared` unchanged unless some parameter can be improved, so
|
|
28
|
+
* the common call allocates nothing.
|
|
29
|
+
*
|
|
30
|
+
* @param declared What went into Parse - `0`/undefined where this client
|
|
31
|
+
* declared nothing and left the type to the server.
|
|
32
|
+
* @param params The values about to be written.
|
|
33
|
+
* @param resolved What the server answered for this statement, in order.
|
|
34
|
+
* @returns The types to hand `getBindMessage`.
|
|
35
|
+
*/
|
|
36
|
+
/* A Date goes out as text because nothing but the server knows whether
|
|
37
|
+
the column is an instant or a wall clock - see `isUnspecifiedParam()`.
|
|
38
|
+
Once the statement has a name, the server has already said which, and
|
|
39
|
+
saying it does not have to be repeated: a prepared statement's
|
|
40
|
+
parameter types are fixed at Parse, so a later Bind may send binary
|
|
41
|
+
against them with no re-Parse. Checked on the wire: Parse with no
|
|
42
|
+
declared types resolves `$1` to 1184, and a Bind of eight binary bytes
|
|
43
|
+
against that statement reads back correctly.
|
|
44
|
+
|
|
45
|
+
Worth it only because the text is wide and the binary is not. Measured
|
|
46
|
+
over a loopback connection, 20 000 elements:
|
|
47
|
+
|
|
48
|
+
timestamptz[] text 625 KB / 11.61ms binary 234 KB / 3.79ms
|
|
49
|
+
|
|
50
|
+
The same measurement is why this is not done for numbers: an int4[] of
|
|
51
|
+
ordinary values is 380 KB of text against 781 KB of binary, because
|
|
52
|
+
the binary array format carries a 4-byte length per element whatever
|
|
53
|
+
its magnitude - there, text is both smaller and twice as fast.
|
|
54
|
+
|
|
55
|
+
Binary and text store the same value, which is the thing that had to
|
|
56
|
+
be true before any of this: checked for timestamptz, timestamp and
|
|
57
|
+
date, over four instants spanning a DST boundary and including
|
|
58
|
+
milliseconds, and for the array forms of two of them. */
|
|
59
|
+
export function resolvedBindTypes(declared, params, resolved) {
|
|
60
|
+
if (!resolved || !params)
|
|
61
|
+
return declared;
|
|
62
|
+
const l = params.length;
|
|
63
|
+
let out;
|
|
64
|
+
let i;
|
|
65
|
+
for (i = 0; i < l; i++) {
|
|
66
|
+
// A type the caller named is an answer, not a guess - left alone.
|
|
67
|
+
if (declared?.[i])
|
|
68
|
+
continue;
|
|
69
|
+
const oid = resolved[i];
|
|
70
|
+
if (!oid)
|
|
71
|
+
continue;
|
|
72
|
+
const v = params[i];
|
|
73
|
+
if (!(arrayLeaf(v) instanceof Date))
|
|
74
|
+
continue;
|
|
75
|
+
// An array value needs an array type and a scalar needs a scalar
|
|
76
|
+
// one: getBindMessage() picks its encoder from the type alone and
|
|
77
|
+
// would hand an array to a scalar encoder.
|
|
78
|
+
if (!(Array.isArray(v) ? DATE_ARRAYS : DATE_SCALARS).has(oid))
|
|
79
|
+
continue;
|
|
80
|
+
if (!out)
|
|
81
|
+
out = declared ? declared.slice() : new Array(l).fill(0);
|
|
82
|
+
out[i] = oid;
|
|
83
|
+
}
|
|
84
|
+
return out || declared;
|
|
85
|
+
}
|
package/util/runtime.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this client is running inside a Cloudflare Worker.
|
|
3
|
+
*
|
|
4
|
+
* @returns True on workerd, false on Node.js, Bun and anything else.
|
|
5
|
+
*/
|
|
6
|
+
/* The documented tell, and the one `pg` uses: since the
|
|
7
|
+
`global_navigator` compatibility flag went on, workerd defines
|
|
8
|
+
`navigator.userAgent` and sets it to exactly this. Node and Bun define
|
|
9
|
+
`navigator` too, with a userAgent of their own, so the test is for the
|
|
10
|
+
whole string rather than a substring of it.
|
|
11
|
+
|
|
12
|
+
Asked on each call rather than read once at load: it is reached once
|
|
13
|
+
per connection, where the cost is nothing, and a value frozen at
|
|
14
|
+
module load cannot be tested from both sides. */
|
|
15
|
+
export function isWorkerd() {
|
|
16
|
+
return (typeof navigator === 'object' &&
|
|
17
|
+
navigator !== null &&
|
|
18
|
+
navigator.userAgent === 'Cloudflare-Workers');
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { arrayCalculateDim } from './array-calculatedim.js';
|
|
2
2
|
import { arrayLeaf } from './array-leaf.js';
|
|
3
|
+
import { unspecifiedText } from './unspecified-value.js';
|
|
3
4
|
/**
|
|
4
5
|
* What a number's own text looks like: digits, one optional dot, an
|
|
5
6
|
* optional exponent, an optional leading minus. An element that matches
|
|
@@ -66,7 +67,11 @@ export function stringifyArrayLiteral(value, options, encode) {
|
|
|
66
67
|
const isNumber = typeof x === 'number' || typeof x === 'bigint';
|
|
67
68
|
if (encode)
|
|
68
69
|
x = encode(x, options || {});
|
|
69
|
-
|
|
70
|
+
// With an encoder the element's text is the type's to produce;
|
|
71
|
+
// without one this is an undeclared array, where a value that
|
|
72
|
+
// knows how to write itself does, and a plain object goes out as
|
|
73
|
+
// JSON rather than as `[object Object]`.
|
|
74
|
+
const s = encode ? '' + x : unspecifiedText(x);
|
|
70
75
|
out += isNumber && BARE_NUMBER.test(s) ? s : escapeArrayItem(s);
|
|
71
76
|
}
|
|
72
77
|
return out + '}';
|
|
@@ -241,7 +246,9 @@ export function writeArrayLiteral(io, value, options, encode) {
|
|
|
241
246
|
// of its spelling - the same rule the string path states: a
|
|
242
247
|
// string's own text is quoted whatever it looks like.
|
|
243
248
|
const isNumber = typeof x === 'number' || typeof x === 'bigint';
|
|
244
|
-
|
|
249
|
+
// This lane is only entered with no element encoder, so the same
|
|
250
|
+
// rules the string path applies without one apply here.
|
|
251
|
+
s = unspecifiedText(x);
|
|
245
252
|
const bare = isNumber && BARE_NUMBER.test(s);
|
|
246
253
|
if (!bare)
|
|
247
254
|
s =
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
* bigint[] = integer[]`, and `numeric[] = double precision[]` the
|
|
31
31
|
* same. The scalars are left declared because they do have those
|
|
32
32
|
* operators - `1::int8 = $1` and `1.5::numeric = $1` both resolve.
|
|
33
|
+
* That is true of the operators *inside* the numeric family and not
|
|
34
|
+
* of the casts out of it, which is what `needsNumericParam()` below
|
|
35
|
+
* exists for.
|
|
33
36
|
*
|
|
34
37
|
* A scalar number, a boolean, a Buffer, and an array of anything but
|
|
35
38
|
* numbers keep their declared types: an array of booleans, of Buffers
|
|
@@ -64,3 +67,14 @@
|
|
|
64
67
|
* answer rather than a guess.
|
|
65
68
|
*/
|
|
66
69
|
export declare function isUnspecifiedParam(v: any): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether a number must be declared `numeric` rather than the `float8`
|
|
72
|
+
* its value suggests.
|
|
73
|
+
*
|
|
74
|
+
* True for a finite non-integer, false for everything else - an integer,
|
|
75
|
+
* a bigint, a non-finite number, and anything that is not a number.
|
|
76
|
+
*
|
|
77
|
+
* @param v The value a parameter is about to be declared for.
|
|
78
|
+
* @returns Whether to declare `numeric` instead of asking `determine()`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function needsNumericParam(v: any): boolean;
|
|
@@ -31,6 +31,9 @@ import { arrayLeaf } from './array-leaf.js';
|
|
|
31
31
|
* bigint[] = integer[]`, and `numeric[] = double precision[]` the
|
|
32
32
|
* same. The scalars are left declared because they do have those
|
|
33
33
|
* operators - `1::int8 = $1` and `1.5::numeric = $1` both resolve.
|
|
34
|
+
* That is true of the operators *inside* the numeric family and not
|
|
35
|
+
* of the casts out of it, which is what `needsNumericParam()` below
|
|
36
|
+
* exists for.
|
|
34
37
|
*
|
|
35
38
|
* A scalar number, a boolean, a Buffer, and an array of anything but
|
|
36
39
|
* numbers keep their declared types: an array of booleans, of Buffers
|
|
@@ -70,3 +73,40 @@ export function isUnspecifiedParam(v) {
|
|
|
70
73
|
return true;
|
|
71
74
|
return Array.isArray(v) && (typeof x === 'number' || typeof x === 'bigint');
|
|
72
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether a number must be declared `numeric` rather than the `float8`
|
|
78
|
+
* its value suggests.
|
|
79
|
+
*
|
|
80
|
+
* True for a finite non-integer, false for everything else - an integer,
|
|
81
|
+
* a bigint, a non-finite number, and anything that is not a number.
|
|
82
|
+
*
|
|
83
|
+
* @param v The value a parameter is about to be declared for.
|
|
84
|
+
* @returns Whether to declare `numeric` instead of asking `determine()`.
|
|
85
|
+
*/
|
|
86
|
+
/* `determine()` answers by value, so 12 is int4 and 12.34 is float8 -
|
|
87
|
+
and `int4 -> money` is an assignment cast while `float8 -> money` has
|
|
88
|
+
no pg_cast row at all. So `select ($1::money)::text` worked with 12
|
|
89
|
+
and failed 42846 with 12.34: the same SQL, the same column, decided by
|
|
90
|
+
whether the amount happened to have a fractional part, which for money
|
|
91
|
+
it usually does. A test written with 12 passed and production failed
|
|
92
|
+
on 12.34. `numeric -> money` is an assignment cast, which an explicit
|
|
93
|
+
`::money` accepts, and `numeric -> float8` is implicit, so a float8
|
|
94
|
+
column is unaffected.
|
|
95
|
+
|
|
96
|
+
Measured, inserting into a float8 column over a loopback connection,
|
|
97
|
+
three interleaved rounds of 3000: 0.197ms a call against float8's
|
|
98
|
+
0.198, and 51 bytes on the socket against 47. Round-tripping a double
|
|
99
|
+
through numeric is exact - checked against float8 for 0.1, 5e-324,
|
|
100
|
+
1.7976931348623157e308, 0.30000000000000004 and pi, among others.
|
|
101
|
+
|
|
102
|
+
Non-finite values keep float8 deliberately: `numeric` only grew
|
|
103
|
+
Infinity in PostgreSQL 14, and NaN would be the only one of the three
|
|
104
|
+
that worked on 12 and 13.
|
|
105
|
+
|
|
106
|
+
This does not close `$1::interval` from a number, which fails the same
|
|
107
|
+
way - there is no cast to interval from any numeric type, so only an
|
|
108
|
+
undeclared parameter reaches it. An integer scalar is left declared,
|
|
109
|
+
and `new BindParam(0, 5)` is the way to say it. */
|
|
110
|
+
export function needsNumericParam(v) {
|
|
111
|
+
return typeof v === 'number' && Number.isFinite(v) && !Number.isInteger(v);
|
|
112
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follows a value's `toPostgres()` until it reaches one that has none.
|
|
3
|
+
*
|
|
4
|
+
* `toPostgres()` is `pg`'s extension point for a type that knows how to
|
|
5
|
+
* write itself: a library with its own geometry, money or domain class
|
|
6
|
+
* implements it, and the value goes out as that class says rather than
|
|
7
|
+
* as whatever `String()` makes of it. The result is followed rather than
|
|
8
|
+
* returned, because what it hands back may itself be a `Date`, an array,
|
|
9
|
+
* a `Buffer` or another such value.
|
|
10
|
+
*
|
|
11
|
+
* @param v The value a parameter is about to be written from.
|
|
12
|
+
* @returns The value to write, which is `v` itself unless it had one.
|
|
13
|
+
* @throws TypeError if the values form a cycle.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveToPostgres(v: any): any;
|
|
16
|
+
/**
|
|
17
|
+
* The text an undeclared parameter - or one element of an undeclared
|
|
18
|
+
* array - goes out as.
|
|
19
|
+
*
|
|
20
|
+
* Follows `toPostgres()` first, then writes a plain object as JSON and
|
|
21
|
+
* anything else as its own text.
|
|
22
|
+
*
|
|
23
|
+
* @param v The value to render.
|
|
24
|
+
* @returns What to put on the wire, before any array quoting.
|
|
25
|
+
*/
|
|
26
|
+
export declare function unspecifiedText(v: any): string;
|