@bzync/nextsql 0.1.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/LICENSE +21 -0
- package/README.md +36 -0
- package/client-encryption.js +634 -0
- package/nextsql.d.ts +335 -0
- package/nextsql.js +2187 -0
- package/package.json +43 -0
package/nextsql.js
ADDED
|
@@ -0,0 +1,2187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Official NextSQL Node.js driver. Speak the native NSQL v1 protocol.
|
|
4
|
+
// Encryption keys and passwords are never accepted in a URL.
|
|
5
|
+
|
|
6
|
+
const net = require('net');
|
|
7
|
+
const tls = require('tls');
|
|
8
|
+
const { Buffer } = require('buffer');
|
|
9
|
+
|
|
10
|
+
const MAGIC = Buffer.from('NSQL');
|
|
11
|
+
const VERSION = 1;
|
|
12
|
+
const HEADER = 12;
|
|
13
|
+
const MAX_PACKET = 1 << 20;
|
|
14
|
+
const MAX_SQL = 1 << 20;
|
|
15
|
+
const MAX_NAME = 256;
|
|
16
|
+
const MAX_PARAMS = 256;
|
|
17
|
+
const MAX_ENUM_LABELS = 4096;
|
|
18
|
+
const MAX_ENUM_LABEL_BYTES = 255;
|
|
19
|
+
|
|
20
|
+
const Type = {
|
|
21
|
+
Hello: 1,
|
|
22
|
+
HelloOK: 2,
|
|
23
|
+
Auth: 3,
|
|
24
|
+
AuthOK: 4,
|
|
25
|
+
Query: 5,
|
|
26
|
+
Prepare: 6,
|
|
27
|
+
PrepareOK: 7,
|
|
28
|
+
Execute: 8,
|
|
29
|
+
CloseStmt: 9,
|
|
30
|
+
CloseOK: 10,
|
|
31
|
+
FlowAck: 11,
|
|
32
|
+
Cancel: 12,
|
|
33
|
+
Terminate: 13,
|
|
34
|
+
RowDesc: 14,
|
|
35
|
+
DataBatch: 15,
|
|
36
|
+
CommandComplete: 16,
|
|
37
|
+
Error: 17,
|
|
38
|
+
Ready: 18,
|
|
39
|
+
Unlock: 19,
|
|
40
|
+
UnlockOK: 20,
|
|
41
|
+
IdempotentQuery: 21,
|
|
42
|
+
SetReadConsistency: 22,
|
|
43
|
+
NodeStatus: 23,
|
|
44
|
+
NodeStatusResp: 24,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Read-consistency mode bytes on the wire. They match the server's
|
|
48
|
+
// executor.ReadConsistency / replication.ReadConsistency ordering.
|
|
49
|
+
const ReadConsistency = {
|
|
50
|
+
Strong: 0,
|
|
51
|
+
Bounded: 1,
|
|
52
|
+
Stale: 2,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const STATUS_TTL_MS = 500;
|
|
56
|
+
|
|
57
|
+
const AuthPassword = 1;
|
|
58
|
+
const AuthPasswordKey = 2;
|
|
59
|
+
const FlagCancel = 1;
|
|
60
|
+
const FlagNull = 0x01;
|
|
61
|
+
|
|
62
|
+
const Kind = {
|
|
63
|
+
Invalid: 0,
|
|
64
|
+
UUID: 1,
|
|
65
|
+
String: 2,
|
|
66
|
+
Text: 3,
|
|
67
|
+
Decimal: 4,
|
|
68
|
+
TimestampTZ: 5,
|
|
69
|
+
JSON: 6,
|
|
70
|
+
Vector: 7,
|
|
71
|
+
Bool: 8,
|
|
72
|
+
Null: 9,
|
|
73
|
+
Point: 10,
|
|
74
|
+
Box: 11,
|
|
75
|
+
Line: 12,
|
|
76
|
+
Polygon: 13,
|
|
77
|
+
Blob: 14,
|
|
78
|
+
Int8: 15,
|
|
79
|
+
Int16: 16,
|
|
80
|
+
Int32: 17,
|
|
81
|
+
Int64: 18,
|
|
82
|
+
Uint8: 19,
|
|
83
|
+
Uint16: 20,
|
|
84
|
+
Uint32: 21,
|
|
85
|
+
Uint64: 22,
|
|
86
|
+
Date: 23,
|
|
87
|
+
Time: 24,
|
|
88
|
+
Char: 25,
|
|
89
|
+
Varchar: 26,
|
|
90
|
+
Timestamp: 27,
|
|
91
|
+
Float32: 28,
|
|
92
|
+
Float64: 29,
|
|
93
|
+
Enum: 30,
|
|
94
|
+
Interval: 31,
|
|
95
|
+
Struct: 32,
|
|
96
|
+
Array: 33,
|
|
97
|
+
Map: 34,
|
|
98
|
+
Geometry: 35,
|
|
99
|
+
Geography: 36,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const MAX_NEST_DEPTH = 8;
|
|
103
|
+
const MAX_STRUCT_FIELDS = 128;
|
|
104
|
+
const MAX_COLLECTION_LEN = 1 << 20;
|
|
105
|
+
|
|
106
|
+
class NextSQLError extends Error {
|
|
107
|
+
constructor(code, message) {
|
|
108
|
+
super(message || code);
|
|
109
|
+
this.name = 'NextSQLError';
|
|
110
|
+
this.code = code;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const {
|
|
115
|
+
FieldType,
|
|
116
|
+
MemoryFieldKeyring,
|
|
117
|
+
FileFieldKeyring,
|
|
118
|
+
decryptField,
|
|
119
|
+
encryptField,
|
|
120
|
+
generateFieldKey,
|
|
121
|
+
inspectField,
|
|
122
|
+
} = require('./client-encryption')({
|
|
123
|
+
NextSQLError,
|
|
124
|
+
Kind,
|
|
125
|
+
encodeDecimalString,
|
|
126
|
+
decodeDecimal,
|
|
127
|
+
decodeNSJB,
|
|
128
|
+
formatUUID,
|
|
129
|
+
parseUUID,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function splitHostPort(addr) {
|
|
133
|
+
if (typeof addr !== 'string' || addr.length === 0) {
|
|
134
|
+
throw new NextSQLError('invalid_argument', 'address is required');
|
|
135
|
+
}
|
|
136
|
+
if (addr.startsWith('[')) {
|
|
137
|
+
const end = addr.indexOf(']');
|
|
138
|
+
if (end < 0) {
|
|
139
|
+
throw new NextSQLError('invalid_argument', 'invalid address');
|
|
140
|
+
}
|
|
141
|
+
const host = addr.slice(1, end);
|
|
142
|
+
if (addr[end + 1] === ':') {
|
|
143
|
+
return { host, port: Number(addr.slice(end + 2)) };
|
|
144
|
+
}
|
|
145
|
+
throw new NextSQLError('invalid_argument', 'address requires a port');
|
|
146
|
+
}
|
|
147
|
+
const i = addr.lastIndexOf(':');
|
|
148
|
+
if (i < 0) {
|
|
149
|
+
throw new NextSQLError('invalid_argument', 'address requires a port');
|
|
150
|
+
}
|
|
151
|
+
return { host: addr.slice(0, i), port: Number(addr.slice(i + 1)) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isLoopback(addr) {
|
|
155
|
+
let host = addr;
|
|
156
|
+
try {
|
|
157
|
+
host = splitHostPort(addr).host;
|
|
158
|
+
} catch {
|
|
159
|
+
// bare host
|
|
160
|
+
}
|
|
161
|
+
host = String(host).trim().toLowerCase();
|
|
162
|
+
if (host === 'localhost') {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (host === '::1' || host === '0:0:0:0:0:0:0:1') {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
const m = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.exec(host);
|
|
169
|
+
return Boolean(m);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function validateConfig(cfg) {
|
|
173
|
+
if (!cfg || typeof cfg !== 'object') {
|
|
174
|
+
throw new NextSQLError('invalid_argument', 'config is required');
|
|
175
|
+
}
|
|
176
|
+
if (!cfg.address) {
|
|
177
|
+
throw new NextSQLError('invalid_argument', 'address is required');
|
|
178
|
+
}
|
|
179
|
+
const addr = String(cfg.address).toLowerCase();
|
|
180
|
+
if (addr.includes('://') || addr.includes('key=') || addr.includes('password=')) {
|
|
181
|
+
throw new NextSQLError('invalid_argument', 'keys and credentials must not be passed in a URL');
|
|
182
|
+
}
|
|
183
|
+
if (!cfg.tls && !cfg.insecureNoTLS) {
|
|
184
|
+
throw new NextSQLError('invalid_argument', 'TLS is required for remote connections');
|
|
185
|
+
}
|
|
186
|
+
if (cfg.insecureNoTLS && !isLoopback(cfg.address)) {
|
|
187
|
+
throw new NextSQLError('invalid_argument', 'plaintext is only allowed on loopback');
|
|
188
|
+
}
|
|
189
|
+
if (!cfg.user) {
|
|
190
|
+
throw new NextSQLError('invalid_argument', 'user is required');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function u16(buf, off) {
|
|
195
|
+
return buf.readUInt16LE(off);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function u32(buf, off) {
|
|
199
|
+
return buf.readUInt32LE(off);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function u64(buf, off) {
|
|
203
|
+
return buf.readBigUInt64LE(off);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function putU16(n) {
|
|
207
|
+
const b = Buffer.alloc(2);
|
|
208
|
+
b.writeUInt16LE(n, 0);
|
|
209
|
+
return b;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function putU32(n) {
|
|
213
|
+
const b = Buffer.alloc(4);
|
|
214
|
+
b.writeUInt32LE(n >>> 0, 0);
|
|
215
|
+
return b;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function putU64(n) {
|
|
219
|
+
const b = Buffer.alloc(8);
|
|
220
|
+
// BigInt.asUintN wraps a negative value to its unsigned 64-bit two's-
|
|
221
|
+
// complement bit pattern before writeBigUInt64LE, which otherwise throws
|
|
222
|
+
// a RangeError for anything outside [0, 2^64) (unlike the shared JS/Bun
|
|
223
|
+
// driver's DataView.setBigUint64, which already wraps automatically per
|
|
224
|
+
// the ECMAScript ToBigUint64 abstract operation). Found and fixed while
|
|
225
|
+
// implementing D6 (INTERVAL's nanosecond component is legitimately
|
|
226
|
+
// negative, e.g. "-1 hour") — this was already a latent, pre-existing bug
|
|
227
|
+
// for any negative epoch-nanosecond value, i.e. every pre-1970
|
|
228
|
+
// TIMESTAMPTZ/TIMESTAMP encoded through this same helper.
|
|
229
|
+
b.writeBigUInt64LE(BigInt.asUintN(64, BigInt(n)), 0);
|
|
230
|
+
return b;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function appendU16String(s, max) {
|
|
234
|
+
const buf = Buffer.from(String(s), 'utf8');
|
|
235
|
+
if (buf.length > max || buf.length > 0xffff) {
|
|
236
|
+
throw new NextSQLError('protocol', 'string exceeds limit');
|
|
237
|
+
}
|
|
238
|
+
return Buffer.concat([putU16(buf.length), buf]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function appendU32Bytes(buf, max) {
|
|
242
|
+
if (buf.length > max) {
|
|
243
|
+
throw new NextSQLError('protocol', 'bytes exceed limit');
|
|
244
|
+
}
|
|
245
|
+
return Buffer.concat([putU32(buf.length), buf]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function readU16String(buf, off, max) {
|
|
249
|
+
if (off + 2 > buf.length) {
|
|
250
|
+
throw new NextSQLError('protocol', 'truncated string length');
|
|
251
|
+
}
|
|
252
|
+
const n = u16(buf, off);
|
|
253
|
+
if (n > max) {
|
|
254
|
+
throw new NextSQLError('protocol', 'string exceeds limit');
|
|
255
|
+
}
|
|
256
|
+
if (off + 2 + n > buf.length) {
|
|
257
|
+
throw new NextSQLError('protocol', 'truncated string');
|
|
258
|
+
}
|
|
259
|
+
return { value: buf.subarray(off + 2, off + 2 + n).toString('utf8'), next: off + 2 + n };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readU32Bytes(buf, off, max) {
|
|
263
|
+
if (off + 4 > buf.length) {
|
|
264
|
+
throw new NextSQLError('protocol', 'truncated bytes length');
|
|
265
|
+
}
|
|
266
|
+
const n = u32(buf, off);
|
|
267
|
+
if (n > max) {
|
|
268
|
+
throw new NextSQLError('protocol', 'bytes exceed limit');
|
|
269
|
+
}
|
|
270
|
+
if (off + 4 + n > buf.length) {
|
|
271
|
+
throw new NextSQLError('protocol', 'truncated bytes');
|
|
272
|
+
}
|
|
273
|
+
return { value: buf.subarray(off + 4, off + 4 + n), next: off + 4 + n };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// appendEnumLabels/readEnumLabels carry an ENUM Type's declared label list on
|
|
277
|
+
// the wire, matching internal/protocol/value.go's appendEnumLabels/
|
|
278
|
+
// readEnumLabels exactly (docs/design-datatypes.md D11): ENUM is the first
|
|
279
|
+
// D-track type whose Type needs variable-length metadata beyond the fixed
|
|
280
|
+
// 5/6-byte Precision/Scale/VecElem shape every other type fits into.
|
|
281
|
+
function appendEnumLabels(labels) {
|
|
282
|
+
const parts = [putU16(labels.length)];
|
|
283
|
+
for (const l of labels) {
|
|
284
|
+
parts.push(appendU16String(l, MAX_ENUM_LABEL_BYTES));
|
|
285
|
+
}
|
|
286
|
+
return Buffer.concat(parts);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function readEnumLabels(buf, off) {
|
|
290
|
+
if (off + 2 > buf.length) {
|
|
291
|
+
throw new NextSQLError('protocol', 'truncated enum label count');
|
|
292
|
+
}
|
|
293
|
+
const n = u16(buf, off);
|
|
294
|
+
if (n > MAX_ENUM_LABELS) {
|
|
295
|
+
throw new NextSQLError('protocol', 'enum label count exceeds limit');
|
|
296
|
+
}
|
|
297
|
+
off += 2;
|
|
298
|
+
const labels = [];
|
|
299
|
+
for (let i = 0; i < n; i++) {
|
|
300
|
+
const got = readU16String(buf, off, MAX_ENUM_LABEL_BYTES);
|
|
301
|
+
labels.push(got.value);
|
|
302
|
+
off = got.next;
|
|
303
|
+
}
|
|
304
|
+
return { value: labels, next: off };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --- Collections (STRUCT / ARRAY / MAP), docs/design-collections.md ----------
|
|
308
|
+
|
|
309
|
+
function readTypeFull(buf, off, depth) {
|
|
310
|
+
if (off + 6 > buf.length) throw new NextSQLError('protocol', 'truncated type');
|
|
311
|
+
const type = {
|
|
312
|
+
kind: buf[off],
|
|
313
|
+
precision: u16(buf, off + 1),
|
|
314
|
+
scale: u16(buf, off + 3),
|
|
315
|
+
elem: buf[off + 5],
|
|
316
|
+
};
|
|
317
|
+
const next = readNestedDescriptor(buf, off + 6, type, depth);
|
|
318
|
+
return { type, next };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function readNestedDescriptor(buf, off, type, depth) {
|
|
322
|
+
if (depth > MAX_NEST_DEPTH + 1) {
|
|
323
|
+
throw new NextSQLError('protocol', 'collection type nesting too deep');
|
|
324
|
+
}
|
|
325
|
+
const kind = type.kind;
|
|
326
|
+
if (kind === Kind.Enum) {
|
|
327
|
+
const got = readEnumLabels(buf, off);
|
|
328
|
+
type.labels = got.value;
|
|
329
|
+
return got.next;
|
|
330
|
+
}
|
|
331
|
+
if (kind === Kind.Array) {
|
|
332
|
+
const e = readTypeFull(buf, off, depth + 1);
|
|
333
|
+
type.elemType = e.type;
|
|
334
|
+
return e.next;
|
|
335
|
+
}
|
|
336
|
+
if (kind === Kind.Map) {
|
|
337
|
+
const k = readTypeFull(buf, off, depth + 1);
|
|
338
|
+
const v = readTypeFull(buf, k.next, depth + 1);
|
|
339
|
+
type.keyType = k.type;
|
|
340
|
+
type.elemType = v.type;
|
|
341
|
+
return v.next;
|
|
342
|
+
}
|
|
343
|
+
if (kind === Kind.Struct) {
|
|
344
|
+
if (off + 2 > buf.length) throw new NextSQLError('protocol', 'truncated struct field count');
|
|
345
|
+
const n = u16(buf, off);
|
|
346
|
+
if (n === 0 || n > MAX_STRUCT_FIELDS) {
|
|
347
|
+
throw new NextSQLError('protocol', 'struct field count out of range');
|
|
348
|
+
}
|
|
349
|
+
off += 2;
|
|
350
|
+
const fields = [];
|
|
351
|
+
for (let i = 0; i < n; i++) {
|
|
352
|
+
const name = readU16String(buf, off, 255);
|
|
353
|
+
off = name.next;
|
|
354
|
+
const ft = readTypeFull(buf, off, depth + 1);
|
|
355
|
+
off = ft.next;
|
|
356
|
+
fields.push({ name: name.value, type: ft.type });
|
|
357
|
+
}
|
|
358
|
+
type.fields = fields;
|
|
359
|
+
return off;
|
|
360
|
+
}
|
|
361
|
+
return off;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function decodePayload(buf, off, type) {
|
|
365
|
+
const kind = type.kind;
|
|
366
|
+
if (kind === Kind.Struct || kind === Kind.Array || kind === Kind.Map) {
|
|
367
|
+
return decodeCollectionPayload(buf, off, type);
|
|
368
|
+
}
|
|
369
|
+
const header = [Buffer.from([kind, 0]), Buffer.alloc(5)];
|
|
370
|
+
if (kind === Kind.Enum) header.push(appendEnumLabels(type.labels || []));
|
|
371
|
+
const headerBytes = Buffer.concat(header);
|
|
372
|
+
const synthetic = Buffer.concat([headerBytes, buf.subarray(off)]);
|
|
373
|
+
const got = decodeValue(synthetic, 0);
|
|
374
|
+
return { value: got.value, next: off + (got.next - headerBytes.length) };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function decodeCollectionPayload(buf, off, type) {
|
|
378
|
+
if (off + 4 > buf.length) throw new NextSQLError('protocol', 'truncated collection');
|
|
379
|
+
const bodyLen = buf.readUInt32LE(off);
|
|
380
|
+
const bodyEnd = off + 4 + bodyLen;
|
|
381
|
+
if (bodyEnd > buf.length) throw new NextSQLError('protocol', 'truncated collection body');
|
|
382
|
+
let p = off + 4;
|
|
383
|
+
const n = buf.readUInt32LE(p);
|
|
384
|
+
p += 4;
|
|
385
|
+
if (n > 2 * MAX_COLLECTION_LEN + 2 || n > bodyLen) {
|
|
386
|
+
throw new NextSQLError('protocol', 'collection member count out of range');
|
|
387
|
+
}
|
|
388
|
+
const nb = (n + 7) >> 3;
|
|
389
|
+
const nulls = buf.subarray(p, p + nb);
|
|
390
|
+
p += nb;
|
|
391
|
+
const memberType = (i) => {
|
|
392
|
+
if (type.kind === Kind.Struct) return type.fields[i].type;
|
|
393
|
+
if (type.kind === Kind.Array) return type.elemType;
|
|
394
|
+
return i % 2 === 0 ? type.keyType : type.elemType;
|
|
395
|
+
};
|
|
396
|
+
const members = [];
|
|
397
|
+
for (let i = 0; i < n; i++) {
|
|
398
|
+
if (nulls[i >> 3] & (1 << (i & 7))) {
|
|
399
|
+
members.push(null);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const got = decodePayload(buf, p, memberType(i));
|
|
403
|
+
p = got.next;
|
|
404
|
+
members.push(got.value);
|
|
405
|
+
}
|
|
406
|
+
let value;
|
|
407
|
+
if (type.kind === Kind.Struct) {
|
|
408
|
+
value = {};
|
|
409
|
+
for (let i = 0; i < type.fields.length; i++) value[type.fields[i].name] = members[i];
|
|
410
|
+
} else if (type.kind === Kind.Array) {
|
|
411
|
+
value = members;
|
|
412
|
+
} else {
|
|
413
|
+
value = new Map();
|
|
414
|
+
for (let i = 0; i + 1 < members.length; i += 2) value.set(members[i], members[i + 1]);
|
|
415
|
+
}
|
|
416
|
+
return { value, next: bodyEnd, kind: type.kind };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function encodeTypeFull(type) {
|
|
420
|
+
const parts = [Buffer.from([type.kind]), Buffer.alloc(5)];
|
|
421
|
+
if (type.kind === Kind.Enum) parts.push(appendEnumLabels(type.labels || []));
|
|
422
|
+
else if (type.kind === Kind.Array) parts.push(encodeTypeFull(type.elemType));
|
|
423
|
+
else if (type.kind === Kind.Map) {
|
|
424
|
+
parts.push(encodeTypeFull(type.keyType));
|
|
425
|
+
parts.push(encodeTypeFull(type.elemType));
|
|
426
|
+
} else if (type.kind === Kind.Struct) {
|
|
427
|
+
parts.push(putU16(type.fields.length));
|
|
428
|
+
for (const f of type.fields) {
|
|
429
|
+
parts.push(appendU16String(f.name, 255));
|
|
430
|
+
parts.push(encodeTypeFull(f.type));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return Buffer.concat(parts);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function inferValue(v) {
|
|
437
|
+
if (v === null || v === undefined) return { type: { kind: Kind.String }, payload: null };
|
|
438
|
+
if (Array.isArray(v)) {
|
|
439
|
+
const els = v.map(inferValue);
|
|
440
|
+
const elemType = (els.find((e) => e.payload !== null) || {}).type || { kind: Kind.String };
|
|
441
|
+
return { type: { kind: Kind.Array, elemType }, payload: collectionPayload(els) };
|
|
442
|
+
}
|
|
443
|
+
if (v instanceof Map) {
|
|
444
|
+
const entries = [];
|
|
445
|
+
for (const [k, val] of v) {
|
|
446
|
+
entries.push(inferValue(k));
|
|
447
|
+
entries.push(inferValue(val));
|
|
448
|
+
}
|
|
449
|
+
const keyType = (entries.find((e, i) => i % 2 === 0 && e.payload !== null) || {}).type || { kind: Kind.String };
|
|
450
|
+
const elemType = (entries.find((e, i) => i % 2 === 1 && e.payload !== null) || {}).type || { kind: Kind.String };
|
|
451
|
+
return { type: { kind: Kind.Map, keyType, elemType }, payload: collectionPayload(entries) };
|
|
452
|
+
}
|
|
453
|
+
if (v && typeof v === 'object' && v.__struct) {
|
|
454
|
+
const fields = [];
|
|
455
|
+
const els = [];
|
|
456
|
+
for (const [name, fv] of v.__struct) {
|
|
457
|
+
const iv = inferValue(fv);
|
|
458
|
+
fields.push({ name, type: iv.type });
|
|
459
|
+
els.push(iv);
|
|
460
|
+
}
|
|
461
|
+
return { type: { kind: Kind.Struct, fields }, payload: collectionPayload(els) };
|
|
462
|
+
}
|
|
463
|
+
const enc = encodeParam(v);
|
|
464
|
+
const kind = enc[0];
|
|
465
|
+
let hdr = 7;
|
|
466
|
+
if (kind === Kind.Enum) {
|
|
467
|
+
const lc = u16(enc, 7);
|
|
468
|
+
hdr = 9;
|
|
469
|
+
for (let i = 0; i < lc; i++) hdr += 2 + u16(enc, hdr);
|
|
470
|
+
}
|
|
471
|
+
return { type: { kind }, payload: enc.subarray(hdr) };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function collectionPayload(members) {
|
|
475
|
+
const n = members.length;
|
|
476
|
+
const nb = (n + 7) >> 3;
|
|
477
|
+
const nulls = Buffer.alloc(nb);
|
|
478
|
+
const chunks = [];
|
|
479
|
+
members.forEach((m, i) => {
|
|
480
|
+
if (m.payload === null) nulls[i >> 3] |= 1 << (i & 7);
|
|
481
|
+
else chunks.push(m.payload);
|
|
482
|
+
});
|
|
483
|
+
const body = Buffer.concat([putU32(n), nulls, ...chunks]);
|
|
484
|
+
return Buffer.concat([putU32(body.length), body]);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function encodeCollectionParam(v) {
|
|
488
|
+
const iv = inferValue(v);
|
|
489
|
+
const typeBody = encodeTypeFull(iv.type).subarray(1);
|
|
490
|
+
return Buffer.concat([Buffer.from([iv.type.kind, 0]), typeBody, iv.payload || Buffer.alloc(0)]);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Wrap an array of [name, value] pairs as an explicit STRUCT parameter. */
|
|
494
|
+
function struct(fields) {
|
|
495
|
+
return { __struct: fields };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// --- Spatial: EWKB decode (Spatial track, docs/design-spatial.md) ----------
|
|
499
|
+
const EWKB_TYPES = {
|
|
500
|
+
1: 'Point', 2: 'LineString', 3: 'Polygon',
|
|
501
|
+
4: 'MultiPoint', 5: 'MultiLineString', 6: 'MultiPolygon',
|
|
502
|
+
7: 'GeometryCollection',
|
|
503
|
+
};
|
|
504
|
+
const EWKB_SRID_FLAG = 0x20000000;
|
|
505
|
+
|
|
506
|
+
function decodeEWKB(buf, off, depth) {
|
|
507
|
+
if (depth > 8) throw new NextSQLError('protocol', 'geometry nesting too deep');
|
|
508
|
+
if (off + 5 > buf.length) throw new NextSQLError('protocol', 'truncated geometry');
|
|
509
|
+
if (buf[off] !== 1) throw new NextSQLError('protocol', 'only little-endian EWKB is supported');
|
|
510
|
+
const tword = buf.readUInt32LE(off + 1);
|
|
511
|
+
const gtype = tword & ~EWKB_SRID_FLAG;
|
|
512
|
+
let p = off + 5;
|
|
513
|
+
let srid = 0;
|
|
514
|
+
if (tword & EWKB_SRID_FLAG) {
|
|
515
|
+
srid = buf.readUInt32LE(p);
|
|
516
|
+
p += 4;
|
|
517
|
+
}
|
|
518
|
+
const f64 = () => { const v = buf.readDoubleLE(p); p += 8; return v; };
|
|
519
|
+
const u32 = () => { const v = buf.readUInt32LE(p); p += 4; return v; };
|
|
520
|
+
const pts = (n) => {
|
|
521
|
+
const out = [];
|
|
522
|
+
for (let i = 0; i < n; i++) out.push([f64(), f64()]);
|
|
523
|
+
return out;
|
|
524
|
+
};
|
|
525
|
+
const name = EWKB_TYPES[gtype];
|
|
526
|
+
if (!name) throw new NextSQLError('protocol', 'unknown geometry type');
|
|
527
|
+
if (gtype === 1) {
|
|
528
|
+
return { g: { type: name, srid, coordinates: [f64(), f64()] }, next: p };
|
|
529
|
+
}
|
|
530
|
+
if (gtype === 2) {
|
|
531
|
+
return { g: { type: name, srid, coordinates: pts(u32()) }, next: p };
|
|
532
|
+
}
|
|
533
|
+
if (gtype === 3) {
|
|
534
|
+
const nr = u32();
|
|
535
|
+
const coordinates = [];
|
|
536
|
+
for (let r = 0; r < nr; r++) coordinates.push(pts(u32()));
|
|
537
|
+
return { g: { type: name, srid, coordinates }, next: p };
|
|
538
|
+
}
|
|
539
|
+
const np = u32();
|
|
540
|
+
const parts = [];
|
|
541
|
+
for (let i = 0; i < np; i++) {
|
|
542
|
+
const sub = decodeEWKB(buf, p, depth + 1);
|
|
543
|
+
p = sub.next;
|
|
544
|
+
parts.push(sub.g);
|
|
545
|
+
}
|
|
546
|
+
const g = { type: name, srid };
|
|
547
|
+
if (gtype === 7) g.geometries = parts;
|
|
548
|
+
else g.coordinates = parts.map((x) => x.coordinates);
|
|
549
|
+
return { g, next: p };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// geoToWKT renders a decoded { type, coordinates } object back as WKT.
|
|
553
|
+
function geoToWKT(g) {
|
|
554
|
+
const p = (xy) => `${xy[0]} ${xy[1]}`;
|
|
555
|
+
const ring = (r) => `(${r.map(p).join(', ')})`;
|
|
556
|
+
switch (g.type) {
|
|
557
|
+
case 'Point': return `POINT(${p(g.coordinates)})`;
|
|
558
|
+
case 'LineString': return `LINESTRING(${g.coordinates.map(p).join(', ')})`;
|
|
559
|
+
case 'Polygon': return `POLYGON(${g.coordinates.map(ring).join(', ')})`;
|
|
560
|
+
case 'MultiPoint': return `MULTIPOINT(${g.coordinates.map((c) => `(${p(c)})`).join(', ')})`;
|
|
561
|
+
case 'MultiLineString': return `MULTILINESTRING(${g.coordinates.map(ring).join(', ')})`;
|
|
562
|
+
case 'MultiPolygon':
|
|
563
|
+
return `MULTIPOLYGON(${g.coordinates.map((poly) => `(${poly.map(ring).join(', ')})`).join(', ')})`;
|
|
564
|
+
case 'GeometryCollection':
|
|
565
|
+
return `GEOMETRYCOLLECTION(${g.geometries.map(geoToWKT).join(', ')})`;
|
|
566
|
+
default:
|
|
567
|
+
throw new NextSQLError('invalid_argument', 'unsupported geometry type');
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function bigintToBytes(n) {
|
|
572
|
+
if (n === 0n) {
|
|
573
|
+
return Buffer.alloc(0);
|
|
574
|
+
}
|
|
575
|
+
let hex = n.toString(16);
|
|
576
|
+
if (hex.length % 2) {
|
|
577
|
+
hex = '0' + hex;
|
|
578
|
+
}
|
|
579
|
+
return Buffer.from(hex, 'hex');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function bytesToBigint(buf) {
|
|
583
|
+
let n = 0n;
|
|
584
|
+
for (const b of buf) {
|
|
585
|
+
n = (n << 8n) + BigInt(b);
|
|
586
|
+
}
|
|
587
|
+
return n;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function encodeDecimalString(s) {
|
|
591
|
+
s = String(s).trim();
|
|
592
|
+
let neg = false;
|
|
593
|
+
if (s.startsWith('+')) {
|
|
594
|
+
s = s.slice(1);
|
|
595
|
+
}
|
|
596
|
+
if (s.startsWith('-')) {
|
|
597
|
+
neg = true;
|
|
598
|
+
s = s.slice(1);
|
|
599
|
+
}
|
|
600
|
+
if (!/^\d+(\.\d+)?$/.test(s)) {
|
|
601
|
+
throw new NextSQLError('invalid_argument', 'invalid decimal');
|
|
602
|
+
}
|
|
603
|
+
const parts = s.split('.');
|
|
604
|
+
const scale = parts[1] ? parts[1].length : 0;
|
|
605
|
+
const digits = (parts[0] + (parts[1] || '')).replace(/^0+(?=\d)/, '');
|
|
606
|
+
const coef = bigintToBytes(BigInt(digits));
|
|
607
|
+
const body = Buffer.alloc(4 + coef.length);
|
|
608
|
+
body[0] = neg ? 1 : 0;
|
|
609
|
+
body.writeUInt16LE(scale, 2);
|
|
610
|
+
coef.copy(body, 4);
|
|
611
|
+
return appendU32Bytes(body, MAX_PACKET);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function decodeDecimal(body) {
|
|
615
|
+
if (body.length < 4) {
|
|
616
|
+
throw new NextSQLError('invalid_format', 'truncated decimal');
|
|
617
|
+
}
|
|
618
|
+
const neg = (body[0] & 1) !== 0;
|
|
619
|
+
const scale = u16(body, 2);
|
|
620
|
+
let s = bytesToBigint(body.subarray(4)).toString();
|
|
621
|
+
if (scale > 0) {
|
|
622
|
+
if (s.length <= scale) {
|
|
623
|
+
s = s.padStart(scale + 1, '0');
|
|
624
|
+
}
|
|
625
|
+
s = s.slice(0, s.length - scale) + '.' + s.slice(s.length - scale);
|
|
626
|
+
}
|
|
627
|
+
if (neg && s !== '0' && s !== '0.' + '0'.repeat(scale)) {
|
|
628
|
+
s = '-' + s;
|
|
629
|
+
}
|
|
630
|
+
return s;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function formatUUID(raw) {
|
|
634
|
+
const h = raw.toString('hex');
|
|
635
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function parseUUID(s) {
|
|
639
|
+
const hex = String(s).trim().replace(/-/g, '');
|
|
640
|
+
if (!/^[0-9a-fA-F]{32}$/.test(hex)) {
|
|
641
|
+
throw new NextSQLError('invalid_argument', 'invalid UUID');
|
|
642
|
+
}
|
|
643
|
+
return Buffer.from(hex, 'hex');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function decodeNSJB(doc) {
|
|
647
|
+
if (doc.length < 5 || doc.subarray(0, 4).toString() !== 'NSJB' || doc[4] !== 1) {
|
|
648
|
+
throw new NextSQLError('invalid_format', 'not binary JSON');
|
|
649
|
+
}
|
|
650
|
+
const { value, next } = readNSJB(doc, 5);
|
|
651
|
+
if (next !== doc.length) {
|
|
652
|
+
throw new NextSQLError('invalid_format', 'trailing JSON bytes');
|
|
653
|
+
}
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function readNSJB(b, off) {
|
|
658
|
+
if (off >= b.length) {
|
|
659
|
+
throw new NextSQLError('invalid_format', 'truncated JSON');
|
|
660
|
+
}
|
|
661
|
+
switch (b[off]) {
|
|
662
|
+
case 0x00:
|
|
663
|
+
return { value: null, next: off + 1 };
|
|
664
|
+
case 0x01:
|
|
665
|
+
return { value: false, next: off + 1 };
|
|
666
|
+
case 0x02:
|
|
667
|
+
return { value: true, next: off + 1 };
|
|
668
|
+
case 0x03:
|
|
669
|
+
if (off + 9 > b.length) {
|
|
670
|
+
throw new NextSQLError('invalid_format', 'truncated i64');
|
|
671
|
+
}
|
|
672
|
+
return { value: Number(b.readBigInt64LE(off + 1)), next: off + 9 };
|
|
673
|
+
case 0x04:
|
|
674
|
+
case 0x05: {
|
|
675
|
+
const n = u32(b, off + 1);
|
|
676
|
+
const end = off + 5 + n;
|
|
677
|
+
if (end > b.length) {
|
|
678
|
+
throw new NextSQLError('invalid_format', 'truncated JSON string');
|
|
679
|
+
}
|
|
680
|
+
const s = b.subarray(off + 5, end).toString('utf8');
|
|
681
|
+
if (b[off] === 0x05) {
|
|
682
|
+
const num = Number(s);
|
|
683
|
+
return { value: Number.isFinite(num) ? num : s, next: end };
|
|
684
|
+
}
|
|
685
|
+
return { value: s, next: end };
|
|
686
|
+
}
|
|
687
|
+
case 0x06: {
|
|
688
|
+
const size = u32(b, off + 1);
|
|
689
|
+
const body = off + 5;
|
|
690
|
+
const end = body + size;
|
|
691
|
+
const count = u32(b, body);
|
|
692
|
+
let cur = body + 4;
|
|
693
|
+
const arr = [];
|
|
694
|
+
for (let i = 0; i < count; i++) {
|
|
695
|
+
const got = readNSJB(b, cur);
|
|
696
|
+
arr.push(got.value);
|
|
697
|
+
cur = got.next;
|
|
698
|
+
}
|
|
699
|
+
if (cur !== end) {
|
|
700
|
+
throw new NextSQLError('invalid_format', 'array size mismatch');
|
|
701
|
+
}
|
|
702
|
+
return { value: arr, next: end };
|
|
703
|
+
}
|
|
704
|
+
case 0x07: {
|
|
705
|
+
const size = u32(b, off + 1);
|
|
706
|
+
const body = off + 5;
|
|
707
|
+
const end = body + size;
|
|
708
|
+
const count = u16(b, body);
|
|
709
|
+
let cur = body + 2;
|
|
710
|
+
const obj = {};
|
|
711
|
+
for (let i = 0; i < count; i++) {
|
|
712
|
+
const klen = u16(b, cur);
|
|
713
|
+
cur += 2;
|
|
714
|
+
const key = b.subarray(cur, cur + klen).toString('utf8');
|
|
715
|
+
cur += klen;
|
|
716
|
+
const got = readNSJB(b, cur);
|
|
717
|
+
obj[key] = got.value;
|
|
718
|
+
cur = got.next;
|
|
719
|
+
}
|
|
720
|
+
if (cur !== end) {
|
|
721
|
+
throw new NextSQLError('invalid_format', 'object size mismatch');
|
|
722
|
+
}
|
|
723
|
+
return { value: obj, next: end };
|
|
724
|
+
}
|
|
725
|
+
default:
|
|
726
|
+
throw new NextSQLError('invalid_format', 'unknown JSON tag');
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function encodeTypeMeta(kind, prec, scale, elem) {
|
|
731
|
+
const meta = Buffer.alloc(5);
|
|
732
|
+
meta.writeUInt16LE(prec || 0, 0);
|
|
733
|
+
meta.writeUInt16LE(scale || 0, 2);
|
|
734
|
+
meta[4] = elem || 0;
|
|
735
|
+
return Buffer.from([kind, 0, ...meta]);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function encodeNull() {
|
|
739
|
+
const meta = Buffer.alloc(5);
|
|
740
|
+
return Buffer.from([Kind.String, FlagNull, ...meta]);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function encodeBool(v) {
|
|
744
|
+
return Buffer.concat([encodeTypeMeta(Kind.Bool, 0, 0, 0), Buffer.from([v ? 1 : 0])]);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function encodeString(s) {
|
|
748
|
+
const raw = Buffer.from(String(s), 'utf8');
|
|
749
|
+
return Buffer.concat([Buffer.from([Kind.String, 0]), Buffer.alloc(5), appendU32Bytes(raw, MAX_PACKET)]);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function encodeUUID(raw) {
|
|
753
|
+
if (raw.length !== 16) {
|
|
754
|
+
throw new NextSQLError('invalid_argument', 'UUID must be 16 bytes');
|
|
755
|
+
}
|
|
756
|
+
return Buffer.concat([Buffer.from([Kind.UUID, 0]), Buffer.alloc(5), raw]);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function encodeBlob(raw) {
|
|
760
|
+
return Buffer.concat([Buffer.from([Kind.Blob, 0]), Buffer.alloc(5), appendU32Bytes(raw, MAX_PACKET)]);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const INT_RANGES = {
|
|
764
|
+
int8: [-0x80n, 0x7fn, Kind.Int8, 1],
|
|
765
|
+
int16: [-0x8000n, 0x7fffn, Kind.Int16, 2],
|
|
766
|
+
int32: [-0x80000000n, 0x7fffffffn, Kind.Int32, 4],
|
|
767
|
+
int64: [-0x8000000000000000n, 0x7fffffffffffffffn, Kind.Int64, 8],
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// encodeInt builds an explicit fixed-width int parameter (D2, Datatype
|
|
771
|
+
// expansion track). A bare JS number/bigint still defaults to Kind.Decimal
|
|
772
|
+
// (see encodeParam) and coerces server-side into any numeric column, so
|
|
773
|
+
// this wrapper is only needed to pin an exact wire width.
|
|
774
|
+
function encodeInt(which, value) {
|
|
775
|
+
const range = INT_RANGES[which];
|
|
776
|
+
if (!range) {
|
|
777
|
+
throw new NextSQLError('invalid_argument', 'unknown int kind: ' + which);
|
|
778
|
+
}
|
|
779
|
+
const [lo, hi, kind, width] = range;
|
|
780
|
+
const n = BigInt(value);
|
|
781
|
+
if (n < lo || n > hi) {
|
|
782
|
+
throw new NextSQLError('invalid_argument', which + ' out of range');
|
|
783
|
+
}
|
|
784
|
+
const full = Buffer.alloc(8);
|
|
785
|
+
full.writeBigInt64LE(n, 0);
|
|
786
|
+
return Buffer.concat([Buffer.from([kind, 0]), Buffer.alloc(5), full.subarray(0, width)]);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const UINT_RANGES = {
|
|
790
|
+
uint8: [0xffn, Kind.Uint8, 1],
|
|
791
|
+
uint16: [0xffffn, Kind.Uint16, 2],
|
|
792
|
+
uint32: [0xffffffffn, Kind.Uint32, 4],
|
|
793
|
+
uint64: [0xffffffffffffffffn, Kind.Uint64, 8],
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// encodeUint builds an explicit fixed-width unsigned int parameter (D3,
|
|
797
|
+
// Datatype expansion track). Mirrors encodeInt.
|
|
798
|
+
function encodeUint(which, value) {
|
|
799
|
+
const range = UINT_RANGES[which];
|
|
800
|
+
if (!range) {
|
|
801
|
+
throw new NextSQLError('invalid_argument', 'unknown uint kind: ' + which);
|
|
802
|
+
}
|
|
803
|
+
const [hi, kind, width] = range;
|
|
804
|
+
const n = BigInt(value);
|
|
805
|
+
if (n < 0n || n > hi) {
|
|
806
|
+
throw new NextSQLError('invalid_argument', which + ' out of range');
|
|
807
|
+
}
|
|
808
|
+
const full = Buffer.alloc(8);
|
|
809
|
+
full.writeBigUInt64LE(n, 0);
|
|
810
|
+
return Buffer.concat([Buffer.from([kind, 0]), Buffer.alloc(5), full.subarray(0, width)]);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// encodeEnum builds an explicit ENUM parameter (D11, Datatype expansion
|
|
814
|
+
// track). Ordinary INSERT/UPDATE params can just pass a plain JS string —
|
|
815
|
+
// the server coerces STRING -> ENUM against the destination column, same as
|
|
816
|
+
// a SQL string literal. This wrapper exists for explicit round-tripping and
|
|
817
|
+
// mirrors encodeInt/encodeUint's precedent.
|
|
818
|
+
function encodeEnum(label, labels) {
|
|
819
|
+
const ord = labels.indexOf(label);
|
|
820
|
+
if (ord < 0) {
|
|
821
|
+
throw new NextSQLError('invalid_argument', 'value is not a member of the ENUM label set');
|
|
822
|
+
}
|
|
823
|
+
return Buffer.concat([Buffer.from([Kind.Enum, 0]), Buffer.alloc(5), appendEnumLabels(labels), putU16(ord)]);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function encodeTimestamp(ns) {
|
|
827
|
+
return Buffer.concat([Buffer.from([Kind.TimestampTZ, 0]), Buffer.alloc(5), putU64(ns)]);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// encodeDate/encodeTime/encodeNaiveTimestamp/encodeFloat32/encodeFloat64 (D5/
|
|
831
|
+
// D7/D8, Datatype expansion track): explicit wrappers for types with no
|
|
832
|
+
// natural bare-value mapping (a Date is always TimestampTZ; a number always
|
|
833
|
+
// defaults to Decimal; JS has no date-only or time-only type). Wire shapes
|
|
834
|
+
// mirror internal/sql/types/row.go's encodeScalar exactly.
|
|
835
|
+
function encodeDate(dayCount) {
|
|
836
|
+
const b = Buffer.alloc(4);
|
|
837
|
+
b.writeInt32LE(dayCount, 0);
|
|
838
|
+
return Buffer.concat([Buffer.from([Kind.Date, 0]), Buffer.alloc(5), b]);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function encodeTime(nanosSinceMidnight) {
|
|
842
|
+
return Buffer.concat([Buffer.from([Kind.Time, 0]), Buffer.alloc(5), putU64(nanosSinceMidnight)]);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function encodeNaiveTimestamp(ns) {
|
|
846
|
+
return Buffer.concat([Buffer.from([Kind.Timestamp, 0]), Buffer.alloc(5), putU64(ns)]);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// NaN/+-Infinity are valid FLOAT32/FLOAT64 values (unlike the bare-number ->
|
|
850
|
+
// Decimal default path, which requires finite) — the server canonicalizes
|
|
851
|
+
// -0.0 -> +0.0 and every NaN payload to one value (docs/design-datatypes.md D8).
|
|
852
|
+
function encodeFloat32(n) {
|
|
853
|
+
if (typeof n !== 'number') {
|
|
854
|
+
throw new NextSQLError('invalid_argument', 'FLOAT32 value must be a number');
|
|
855
|
+
}
|
|
856
|
+
const b = Buffer.alloc(4);
|
|
857
|
+
b.writeFloatLE(n, 0);
|
|
858
|
+
return Buffer.concat([Buffer.from([Kind.Float32, 0]), Buffer.alloc(5), b]);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function encodeFloat64(n) {
|
|
862
|
+
if (typeof n !== 'number') {
|
|
863
|
+
throw new NextSQLError('invalid_argument', 'FLOAT64 value must be a number');
|
|
864
|
+
}
|
|
865
|
+
const b = Buffer.alloc(8);
|
|
866
|
+
b.writeDoubleLE(n, 0);
|
|
867
|
+
return Buffer.concat([Buffer.from([Kind.Float64, 0]), Buffer.alloc(5), b]);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// encodeInterval builds an explicit INTERVAL parameter (D6, Datatype
|
|
871
|
+
// expansion track): months(i32 LE) + days(i32 LE) + nanos(i64 LE) — a
|
|
872
|
+
// plain string still works as an INTERVAL param for INSERT/UPDATE column
|
|
873
|
+
// assignment (server-side Coerce) but not inside an arithmetic expression
|
|
874
|
+
// like `dur + $1`, which requires the actual wire Kind (see the shared JS/Bun
|
|
875
|
+
// driver's encodeInterval for the full explanation).
|
|
876
|
+
function encodeInterval(months, days, nanos) {
|
|
877
|
+
const mbuf = Buffer.alloc(4);
|
|
878
|
+
mbuf.writeInt32LE(months, 0);
|
|
879
|
+
const dbuf = Buffer.alloc(4);
|
|
880
|
+
dbuf.writeInt32LE(days, 0);
|
|
881
|
+
return Buffer.concat([Buffer.from([Kind.Interval, 0]), Buffer.alloc(5), mbuf, dbuf, putU64(nanos)]);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function encodePoint(lon, lat) {
|
|
885
|
+
const b = Buffer.alloc(16);
|
|
886
|
+
b.writeDoubleLE(lon, 0);
|
|
887
|
+
b.writeDoubleLE(lat, 8);
|
|
888
|
+
return Buffer.concat([Buffer.from([Kind.Point, 0]), Buffer.alloc(5), b]);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function encodeBox(w, s, e, n) {
|
|
892
|
+
const b = Buffer.alloc(32);
|
|
893
|
+
b.writeDoubleLE(w, 0);
|
|
894
|
+
b.writeDoubleLE(s, 8);
|
|
895
|
+
b.writeDoubleLE(e, 16);
|
|
896
|
+
b.writeDoubleLE(n, 24);
|
|
897
|
+
return Buffer.concat([Buffer.from([Kind.Box, 0]), Buffer.alloc(5), b]);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function encodeVector(arr) {
|
|
901
|
+
const dim = arr.length;
|
|
902
|
+
const body = Buffer.alloc(3 + dim * 4);
|
|
903
|
+
body.writeUInt16LE(dim, 0);
|
|
904
|
+
body[2] = 0;
|
|
905
|
+
for (let i = 0; i < dim; i++) {
|
|
906
|
+
const f = arr[i];
|
|
907
|
+
if (!Number.isFinite(f)) {
|
|
908
|
+
throw new NextSQLError('invalid_argument', 'VECTOR element is not finite');
|
|
909
|
+
}
|
|
910
|
+
body.writeFloatLE(f, 3 + i * 4);
|
|
911
|
+
}
|
|
912
|
+
const hdr = Buffer.alloc(7);
|
|
913
|
+
hdr[0] = Kind.Vector;
|
|
914
|
+
hdr.writeUInt16LE(dim, 2);
|
|
915
|
+
hdr[6] = 1; // VecF32
|
|
916
|
+
return Buffer.concat([hdr, body]);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function encodeParam(v) {
|
|
920
|
+
if (v === null || v === undefined) {
|
|
921
|
+
return encodeNull();
|
|
922
|
+
}
|
|
923
|
+
if (typeof v === 'boolean') {
|
|
924
|
+
return encodeBool(v);
|
|
925
|
+
}
|
|
926
|
+
if (typeof v === 'number') {
|
|
927
|
+
if (!Number.isFinite(v)) {
|
|
928
|
+
throw new NextSQLError('invalid_argument', 'parameter is not finite');
|
|
929
|
+
}
|
|
930
|
+
return Buffer.concat([Buffer.from([Kind.Decimal, 0]), Buffer.alloc(5), encodeDecimalString(Number.isInteger(v) ? String(v) : String(v))]);
|
|
931
|
+
}
|
|
932
|
+
if (typeof v === 'bigint') {
|
|
933
|
+
return Buffer.concat([Buffer.from([Kind.Decimal, 0]), Buffer.alloc(5), encodeDecimalString(v.toString())]);
|
|
934
|
+
}
|
|
935
|
+
if (typeof v === 'string') {
|
|
936
|
+
return encodeString(v);
|
|
937
|
+
}
|
|
938
|
+
if (v instanceof Date) {
|
|
939
|
+
return encodeTimestamp(BigInt(v.getTime()) * 1000000n);
|
|
940
|
+
}
|
|
941
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) {
|
|
942
|
+
// A bare 16-byte buffer keeps its pre-existing meaning (UUID) for
|
|
943
|
+
// compatibility; any other length is a BLOB. A 16-byte BLOB needs the
|
|
944
|
+
// explicit { kind: 'blob', value } wrapper below to disambiguate.
|
|
945
|
+
return v.length === 16 ? encodeUUID(v) : encodeBlob(v);
|
|
946
|
+
}
|
|
947
|
+
if (Array.isArray(v) && v.length > 0 && v.every((x) => typeof x === 'number')) {
|
|
948
|
+
return encodeVector(v);
|
|
949
|
+
}
|
|
950
|
+
if (Array.isArray(v) || v instanceof Map || (v && typeof v === 'object' && v.__struct)) {
|
|
951
|
+
return encodeCollectionParam(v);
|
|
952
|
+
}
|
|
953
|
+
if (v && typeof v === 'object') {
|
|
954
|
+
if ('lon' in v && 'lat' in v) {
|
|
955
|
+
return encodePoint(Number(v.lon), Number(v.lat));
|
|
956
|
+
}
|
|
957
|
+
if ('west' in v && 'south' in v && 'east' in v && 'north' in v) {
|
|
958
|
+
return encodeBox(Number(v.west), Number(v.south), Number(v.east), Number(v.north));
|
|
959
|
+
}
|
|
960
|
+
if (v.kind === 'uuid') {
|
|
961
|
+
return encodeUUID(parseUUID(v.value));
|
|
962
|
+
}
|
|
963
|
+
if (v.kind === 'decimal') {
|
|
964
|
+
return Buffer.concat([Buffer.from([Kind.Decimal, 0]), Buffer.alloc(5), encodeDecimalString(v.value)]);
|
|
965
|
+
}
|
|
966
|
+
if (v.kind === 'blob') {
|
|
967
|
+
return encodeBlob(Buffer.isBuffer(v.value) ? v.value : Buffer.from(v.value));
|
|
968
|
+
}
|
|
969
|
+
if (v.kind === 'int8' || v.kind === 'int16' || v.kind === 'int32' || v.kind === 'int64') {
|
|
970
|
+
return encodeInt(v.kind, v.value);
|
|
971
|
+
}
|
|
972
|
+
if (v.kind === 'uint8' || v.kind === 'uint16' || v.kind === 'uint32' || v.kind === 'uint64') {
|
|
973
|
+
return encodeUint(v.kind, v.value);
|
|
974
|
+
}
|
|
975
|
+
if (v.kind === 'enum') {
|
|
976
|
+
return encodeEnum(v.value, v.labels);
|
|
977
|
+
}
|
|
978
|
+
if (v.kind === 'date') {
|
|
979
|
+
const dayCount = v.value instanceof Date ? Math.floor(v.value.getTime() / 86400000) : Number(v.value);
|
|
980
|
+
return encodeDate(dayCount);
|
|
981
|
+
}
|
|
982
|
+
if (v.kind === 'time') {
|
|
983
|
+
return encodeTime(v.value);
|
|
984
|
+
}
|
|
985
|
+
if (v.kind === 'timestamp') {
|
|
986
|
+
const ns = v.value instanceof Date ? BigInt(v.value.getTime()) * 1000000n : BigInt(v.value);
|
|
987
|
+
return encodeNaiveTimestamp(ns);
|
|
988
|
+
}
|
|
989
|
+
if (v.kind === 'float32') {
|
|
990
|
+
return encodeFloat32(Number(v.value));
|
|
991
|
+
}
|
|
992
|
+
if (v.kind === 'float64') {
|
|
993
|
+
return encodeFloat64(Number(v.value));
|
|
994
|
+
}
|
|
995
|
+
if (v.kind === 'interval') {
|
|
996
|
+
return encodeInterval(v.months | 0, v.days | 0, v.nanos);
|
|
997
|
+
}
|
|
998
|
+
if (v.kind === 'array' || v.kind === 'map' || v.kind === 'struct') {
|
|
999
|
+
let inner = v.value;
|
|
1000
|
+
if (v.kind === 'map' && Array.isArray(inner)) inner = new Map(inner);
|
|
1001
|
+
if (v.kind === 'struct') inner = { __struct: inner };
|
|
1002
|
+
return encodeCollectionParam(inner);
|
|
1003
|
+
}
|
|
1004
|
+
if (v.kind === 'geometry' || v.kind === 'geography') {
|
|
1005
|
+
let wkt = v.wkt != null ? String(v.wkt) : geoToWKT(v);
|
|
1006
|
+
if (v.srid != null && !/^SRID=/i.test(wkt)) wkt = `SRID=${v.srid};${wkt}`;
|
|
1007
|
+
return encodeString(wkt);
|
|
1008
|
+
}
|
|
1009
|
+
// JSON object → UTF-8 text; the server coerces STRING to JSON.
|
|
1010
|
+
return encodeString(JSON.stringify(v));
|
|
1011
|
+
}
|
|
1012
|
+
throw new NextSQLError('invalid_argument', 'unsupported parameter type');
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function decodeValue(buf, off) {
|
|
1016
|
+
if (off + 7 > buf.length) {
|
|
1017
|
+
throw new NextSQLError('protocol', 'truncated value header');
|
|
1018
|
+
}
|
|
1019
|
+
const kind = buf[off];
|
|
1020
|
+
const flags = buf[off + 1];
|
|
1021
|
+
const prec = u16(buf, off + 2);
|
|
1022
|
+
const scale = u16(buf, off + 4);
|
|
1023
|
+
off += 7;
|
|
1024
|
+
let enumLabels = null;
|
|
1025
|
+
let collType = null;
|
|
1026
|
+
if (kind === Kind.Enum) {
|
|
1027
|
+
const got = readEnumLabels(buf, off);
|
|
1028
|
+
enumLabels = got.value;
|
|
1029
|
+
off = got.next;
|
|
1030
|
+
} else if (kind === Kind.Struct || kind === Kind.Array || kind === Kind.Map) {
|
|
1031
|
+
collType = { kind };
|
|
1032
|
+
off = readNestedDescriptor(buf, off, collType, 0);
|
|
1033
|
+
}
|
|
1034
|
+
if (flags & FlagNull) {
|
|
1035
|
+
return { value: null, next: off, kind, labels: enumLabels || undefined };
|
|
1036
|
+
}
|
|
1037
|
+
switch (kind) {
|
|
1038
|
+
case Kind.UUID: {
|
|
1039
|
+
if (off + 16 > buf.length) {
|
|
1040
|
+
throw new NextSQLError('protocol', 'truncated UUID');
|
|
1041
|
+
}
|
|
1042
|
+
return { value: formatUUID(buf.subarray(off, off + 16)), next: off + 16, kind };
|
|
1043
|
+
}
|
|
1044
|
+
case Kind.String:
|
|
1045
|
+
case Kind.Text:
|
|
1046
|
+
case Kind.Char:
|
|
1047
|
+
case Kind.Varchar: {
|
|
1048
|
+
const got = readU32Bytes(buf, off, MAX_PACKET);
|
|
1049
|
+
return { value: got.value.toString('utf8'), next: got.next, kind };
|
|
1050
|
+
}
|
|
1051
|
+
case Kind.Blob: {
|
|
1052
|
+
const got = readU32Bytes(buf, off, MAX_PACKET);
|
|
1053
|
+
return { value: Buffer.from(got.value), next: got.next, kind };
|
|
1054
|
+
}
|
|
1055
|
+
case Kind.JSON: {
|
|
1056
|
+
const got = readU32Bytes(buf, off, MAX_PACKET);
|
|
1057
|
+
return { value: decodeNSJB(got.value), next: got.next, kind };
|
|
1058
|
+
}
|
|
1059
|
+
case Kind.Decimal: {
|
|
1060
|
+
const got = readU32Bytes(buf, off, MAX_PACKET);
|
|
1061
|
+
return { value: decodeDecimal(got.value), next: got.next, kind };
|
|
1062
|
+
}
|
|
1063
|
+
case Kind.TimestampTZ: {
|
|
1064
|
+
if (off + 8 > buf.length) {
|
|
1065
|
+
throw new NextSQLError('protocol', 'truncated timestamp');
|
|
1066
|
+
}
|
|
1067
|
+
const ns = buf.readBigInt64LE(off);
|
|
1068
|
+
return { value: new Date(Number(ns / 1000000n)), next: off + 8, kind };
|
|
1069
|
+
}
|
|
1070
|
+
case Kind.Timestamp: {
|
|
1071
|
+
// Naive/no-timezone: same wire shape as TimestampTZ (docs/design-datatypes.md D7).
|
|
1072
|
+
if (off + 8 > buf.length) {
|
|
1073
|
+
throw new NextSQLError('protocol', 'truncated timestamp');
|
|
1074
|
+
}
|
|
1075
|
+
const ns = buf.readBigInt64LE(off);
|
|
1076
|
+
return { value: new Date(Number(ns / 1000000n)), next: off + 8, kind };
|
|
1077
|
+
}
|
|
1078
|
+
case Kind.Date: {
|
|
1079
|
+
if (off + 4 > buf.length) {
|
|
1080
|
+
throw new NextSQLError('protocol', 'truncated date');
|
|
1081
|
+
}
|
|
1082
|
+
const dayCount = buf.readInt32LE(off);
|
|
1083
|
+
return { value: new Date(dayCount * 86400000), next: off + 4, kind };
|
|
1084
|
+
}
|
|
1085
|
+
case Kind.Time: {
|
|
1086
|
+
if (off + 8 > buf.length) {
|
|
1087
|
+
throw new NextSQLError('protocol', 'truncated time');
|
|
1088
|
+
}
|
|
1089
|
+
const ns = buf.readBigUInt64LE(off);
|
|
1090
|
+
return { value: Number(ns), next: off + 8, kind };
|
|
1091
|
+
}
|
|
1092
|
+
case Kind.Float32: {
|
|
1093
|
+
if (off + 4 > buf.length) {
|
|
1094
|
+
throw new NextSQLError('protocol', 'truncated float32');
|
|
1095
|
+
}
|
|
1096
|
+
return { value: buf.readFloatLE(off), next: off + 4, kind };
|
|
1097
|
+
}
|
|
1098
|
+
case Kind.Float64: {
|
|
1099
|
+
if (off + 8 > buf.length) {
|
|
1100
|
+
throw new NextSQLError('protocol', 'truncated float64');
|
|
1101
|
+
}
|
|
1102
|
+
return { value: buf.readDoubleLE(off), next: off + 8, kind };
|
|
1103
|
+
}
|
|
1104
|
+
case Kind.Interval: {
|
|
1105
|
+
if (off + 16 > buf.length) {
|
|
1106
|
+
throw new NextSQLError('protocol', 'truncated interval');
|
|
1107
|
+
}
|
|
1108
|
+
const months = buf.readInt32LE(off);
|
|
1109
|
+
const days = buf.readInt32LE(off + 4);
|
|
1110
|
+
const nanos = buf.readBigInt64LE(off + 8);
|
|
1111
|
+
return { value: { months, days, nanos }, next: off + 16, kind };
|
|
1112
|
+
}
|
|
1113
|
+
case Kind.Bool: {
|
|
1114
|
+
if (off >= buf.length) {
|
|
1115
|
+
throw new NextSQLError('protocol', 'truncated bool');
|
|
1116
|
+
}
|
|
1117
|
+
return { value: buf[off] !== 0, next: off + 1, kind };
|
|
1118
|
+
}
|
|
1119
|
+
case Kind.Int8: {
|
|
1120
|
+
if (off + 1 > buf.length) {
|
|
1121
|
+
throw new NextSQLError('protocol', 'truncated int8');
|
|
1122
|
+
}
|
|
1123
|
+
return { value: buf.readInt8(off), next: off + 1, kind };
|
|
1124
|
+
}
|
|
1125
|
+
case Kind.Int16: {
|
|
1126
|
+
if (off + 2 > buf.length) {
|
|
1127
|
+
throw new NextSQLError('protocol', 'truncated int16');
|
|
1128
|
+
}
|
|
1129
|
+
return { value: buf.readInt16LE(off), next: off + 2, kind };
|
|
1130
|
+
}
|
|
1131
|
+
case Kind.Int32: {
|
|
1132
|
+
if (off + 4 > buf.length) {
|
|
1133
|
+
throw new NextSQLError('protocol', 'truncated int32');
|
|
1134
|
+
}
|
|
1135
|
+
return { value: buf.readInt32LE(off), next: off + 4, kind };
|
|
1136
|
+
}
|
|
1137
|
+
case Kind.Int64: {
|
|
1138
|
+
if (off + 8 > buf.length) {
|
|
1139
|
+
throw new NextSQLError('protocol', 'truncated int64');
|
|
1140
|
+
}
|
|
1141
|
+
// Exposed as BigInt, not number: the full int64 range does not fit
|
|
1142
|
+
// safely in a JS double (see docs/design-datatypes.md D2).
|
|
1143
|
+
return { value: buf.readBigInt64LE(off), next: off + 8, kind };
|
|
1144
|
+
}
|
|
1145
|
+
case Kind.Uint8: {
|
|
1146
|
+
if (off + 1 > buf.length) {
|
|
1147
|
+
throw new NextSQLError('protocol', 'truncated uint8');
|
|
1148
|
+
}
|
|
1149
|
+
return { value: buf.readUInt8(off), next: off + 1, kind };
|
|
1150
|
+
}
|
|
1151
|
+
case Kind.Uint16: {
|
|
1152
|
+
if (off + 2 > buf.length) {
|
|
1153
|
+
throw new NextSQLError('protocol', 'truncated uint16');
|
|
1154
|
+
}
|
|
1155
|
+
return { value: buf.readUInt16LE(off), next: off + 2, kind };
|
|
1156
|
+
}
|
|
1157
|
+
case Kind.Uint32: {
|
|
1158
|
+
if (off + 4 > buf.length) {
|
|
1159
|
+
throw new NextSQLError('protocol', 'truncated uint32');
|
|
1160
|
+
}
|
|
1161
|
+
return { value: buf.readUInt32LE(off), next: off + 4, kind };
|
|
1162
|
+
}
|
|
1163
|
+
case Kind.Uint64: {
|
|
1164
|
+
if (off + 8 > buf.length) {
|
|
1165
|
+
throw new NextSQLError('protocol', 'truncated uint64');
|
|
1166
|
+
}
|
|
1167
|
+
// Exposed as BigInt, not number: the full uint64 range does not fit
|
|
1168
|
+
// safely in a JS double (see docs/design-datatypes.md D3).
|
|
1169
|
+
return { value: buf.readBigUInt64LE(off), next: off + 8, kind };
|
|
1170
|
+
}
|
|
1171
|
+
case Kind.Vector: {
|
|
1172
|
+
const dim = u16(buf, off);
|
|
1173
|
+
const flag = buf[off + 2];
|
|
1174
|
+
if (flag & 1) {
|
|
1175
|
+
return { value: { ref: true, dim }, next: off + 3, kind };
|
|
1176
|
+
}
|
|
1177
|
+
if (flag & 2) {
|
|
1178
|
+
const nnz = buf.readUInt32LE(off + 3);
|
|
1179
|
+
const indices = [];
|
|
1180
|
+
const values = [];
|
|
1181
|
+
for (let i = 0; i < nnz; i++) {
|
|
1182
|
+
indices.push(buf.readUInt32LE(off + 7 + i * 8));
|
|
1183
|
+
values.push(buf.readFloatLE(off + 11 + i * 8));
|
|
1184
|
+
}
|
|
1185
|
+
return { value: { dim, indices, values }, next: off + 7 + nnz * 8, kind };
|
|
1186
|
+
}
|
|
1187
|
+
const need = dim * 4;
|
|
1188
|
+
const out = [];
|
|
1189
|
+
for (let i = 0; i < dim; i++) {
|
|
1190
|
+
out.push(buf.readFloatLE(off + 3 + i * 4));
|
|
1191
|
+
}
|
|
1192
|
+
return { value: out, next: off + 3 + need, kind };
|
|
1193
|
+
}
|
|
1194
|
+
case Kind.Point: {
|
|
1195
|
+
return {
|
|
1196
|
+
value: { lon: buf.readDoubleLE(off), lat: buf.readDoubleLE(off + 8) },
|
|
1197
|
+
next: off + 16,
|
|
1198
|
+
kind,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
case Kind.Box: {
|
|
1202
|
+
return {
|
|
1203
|
+
value: {
|
|
1204
|
+
west: buf.readDoubleLE(off),
|
|
1205
|
+
south: buf.readDoubleLE(off + 8),
|
|
1206
|
+
east: buf.readDoubleLE(off + 16),
|
|
1207
|
+
north: buf.readDoubleLE(off + 24),
|
|
1208
|
+
},
|
|
1209
|
+
next: off + 32,
|
|
1210
|
+
kind,
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
case Kind.Line: {
|
|
1214
|
+
const n = u16(buf, off);
|
|
1215
|
+
const coords = [];
|
|
1216
|
+
let p = off + 2;
|
|
1217
|
+
for (let i = 0; i < n * 2; i++) {
|
|
1218
|
+
coords.push(buf.readDoubleLE(p));
|
|
1219
|
+
p += 8;
|
|
1220
|
+
}
|
|
1221
|
+
return { value: { coords }, next: p, kind };
|
|
1222
|
+
}
|
|
1223
|
+
case Kind.Enum: {
|
|
1224
|
+
if (off + 2 > buf.length) {
|
|
1225
|
+
throw new NextSQLError('protocol', 'truncated enum');
|
|
1226
|
+
}
|
|
1227
|
+
const ord = u16(buf, off);
|
|
1228
|
+
if (ord >= enumLabels.length) {
|
|
1229
|
+
throw new NextSQLError('protocol', 'ENUM ordinal out of range');
|
|
1230
|
+
}
|
|
1231
|
+
return { value: enumLabels[ord], next: off + 2, kind, labels: enumLabels };
|
|
1232
|
+
}
|
|
1233
|
+
case Kind.Polygon: {
|
|
1234
|
+
const nr = u16(buf, off);
|
|
1235
|
+
let p = off + 2;
|
|
1236
|
+
const rings = [];
|
|
1237
|
+
for (let r = 0; r < nr; r++) {
|
|
1238
|
+
const np = u16(buf, p);
|
|
1239
|
+
p += 2;
|
|
1240
|
+
const ring = [];
|
|
1241
|
+
for (let i = 0; i < np * 2; i++) {
|
|
1242
|
+
ring.push(buf.readDoubleLE(p));
|
|
1243
|
+
p += 8;
|
|
1244
|
+
}
|
|
1245
|
+
rings.push(ring);
|
|
1246
|
+
}
|
|
1247
|
+
return { value: { rings }, next: p, kind };
|
|
1248
|
+
}
|
|
1249
|
+
case Kind.Struct:
|
|
1250
|
+
case Kind.Array:
|
|
1251
|
+
case Kind.Map:
|
|
1252
|
+
return decodeCollectionPayload(buf, off, collType);
|
|
1253
|
+
case Kind.Geometry:
|
|
1254
|
+
case Kind.Geography: {
|
|
1255
|
+
const len = buf.readUInt32LE(off);
|
|
1256
|
+
const { g } = decodeEWKB(buf, off + 4, 0);
|
|
1257
|
+
return { value: g, next: off + 4 + len, kind };
|
|
1258
|
+
}
|
|
1259
|
+
default:
|
|
1260
|
+
throw new NextSQLError('protocol', 'unsupported type');
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function encodeHello(h) {
|
|
1265
|
+
const parts = [
|
|
1266
|
+
putU16(h.version),
|
|
1267
|
+
putU16(h.flags || 0),
|
|
1268
|
+
putU64(h.secret || 0n),
|
|
1269
|
+
appendU16String(h.database || '', MAX_NAME),
|
|
1270
|
+
appendU16String(h.user || '', MAX_NAME),
|
|
1271
|
+
];
|
|
1272
|
+
// Realm is an optional trailing field (M2-2): emitted only when
|
|
1273
|
+
// selected, so a Hello with no realm is byte-identical to the pre-realm
|
|
1274
|
+
// wire shape.
|
|
1275
|
+
if (h.realm) {
|
|
1276
|
+
parts.push(appendU16String(h.realm, MAX_NAME));
|
|
1277
|
+
}
|
|
1278
|
+
return Buffer.concat(parts);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function decodeHelloOK(b) {
|
|
1282
|
+
if (b.length !== 11) {
|
|
1283
|
+
throw new NextSQLError('protocol', 'bad hello-ok length');
|
|
1284
|
+
}
|
|
1285
|
+
return { version: u16(b, 0), authMethod: b[2], secret: u64(b, 3) };
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function encodeAuth(password) {
|
|
1289
|
+
return appendU16String(password || '', MAX_NAME);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function encodeQuery(sql, params) {
|
|
1293
|
+
if (params.length > MAX_PARAMS) {
|
|
1294
|
+
throw new NextSQLError('protocol', 'too many parameters');
|
|
1295
|
+
}
|
|
1296
|
+
const sqlBuf = Buffer.from(sql, 'utf8');
|
|
1297
|
+
if (sqlBuf.length > MAX_SQL) {
|
|
1298
|
+
throw new NextSQLError('protocol', 'SQL exceeds limit');
|
|
1299
|
+
}
|
|
1300
|
+
const parts = [appendU32Bytes(sqlBuf, MAX_SQL), putU16(params.length)];
|
|
1301
|
+
for (const p of params) {
|
|
1302
|
+
parts.push(appendU16String('', MAX_NAME));
|
|
1303
|
+
parts.push(encodeParam(p));
|
|
1304
|
+
}
|
|
1305
|
+
return Buffer.concat(parts);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function encodePrepare(sql) {
|
|
1309
|
+
return appendU32Bytes(Buffer.from(sql, 'utf8'), MAX_SQL);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function encodeExecute(id, params) {
|
|
1313
|
+
if (params.length > MAX_PARAMS) {
|
|
1314
|
+
throw new NextSQLError('protocol', 'too many parameters');
|
|
1315
|
+
}
|
|
1316
|
+
const parts = [putU32(id), putU16(params.length)];
|
|
1317
|
+
for (const p of params) {
|
|
1318
|
+
parts.push(appendU16String('', MAX_NAME));
|
|
1319
|
+
parts.push(encodeParam(p));
|
|
1320
|
+
}
|
|
1321
|
+
return Buffer.concat(parts);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function decodeRowDesc(b) {
|
|
1325
|
+
const n = u16(b, 0);
|
|
1326
|
+
let off = 2;
|
|
1327
|
+
const columns = [];
|
|
1328
|
+
for (let i = 0; i < n; i++) {
|
|
1329
|
+
const name = readU16String(b, off, MAX_NAME);
|
|
1330
|
+
off = name.next;
|
|
1331
|
+
if (off + 6 > b.length) {
|
|
1332
|
+
throw new NextSQLError('protocol', 'truncated type');
|
|
1333
|
+
}
|
|
1334
|
+
const kind = b[off];
|
|
1335
|
+
const col = {
|
|
1336
|
+
name: name.value,
|
|
1337
|
+
kind,
|
|
1338
|
+
precision: u16(b, off + 1),
|
|
1339
|
+
scale: u16(b, off + 3),
|
|
1340
|
+
};
|
|
1341
|
+
off += 6;
|
|
1342
|
+
if (kind === Kind.Enum) {
|
|
1343
|
+
const got = readEnumLabels(b, off);
|
|
1344
|
+
col.labels = got.value;
|
|
1345
|
+
off = got.next;
|
|
1346
|
+
} else if (kind === Kind.Struct || kind === Kind.Array || kind === Kind.Map) {
|
|
1347
|
+
const t = { kind };
|
|
1348
|
+
off = readNestedDescriptor(b, off, t, 0);
|
|
1349
|
+
col.collType = t;
|
|
1350
|
+
}
|
|
1351
|
+
columns.push(col);
|
|
1352
|
+
}
|
|
1353
|
+
return columns;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function decodeDataBatch(b) {
|
|
1357
|
+
const nrows = u32(b, 0);
|
|
1358
|
+
let off = 4;
|
|
1359
|
+
const rows = [];
|
|
1360
|
+
for (let i = 0; i < nrows; i++) {
|
|
1361
|
+
const ncols = u16(b, off);
|
|
1362
|
+
off += 2;
|
|
1363
|
+
const row = [];
|
|
1364
|
+
for (let j = 0; j < ncols; j++) {
|
|
1365
|
+
const got = decodeValue(b, off);
|
|
1366
|
+
row.push(got.value);
|
|
1367
|
+
off = got.next;
|
|
1368
|
+
}
|
|
1369
|
+
rows.push(row);
|
|
1370
|
+
}
|
|
1371
|
+
return rows;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function decodeError(b) {
|
|
1375
|
+
const code = readU16String(b, 0, MAX_NAME);
|
|
1376
|
+
const msg = readU16String(b, code.next, MAX_NAME);
|
|
1377
|
+
return new NextSQLError(code.value, msg.value);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function decodeCommandComplete(b) {
|
|
1381
|
+
if (b.length !== 8) {
|
|
1382
|
+
throw new NextSQLError('protocol', 'bad command-complete length');
|
|
1383
|
+
}
|
|
1384
|
+
return Number(b.readBigUInt64LE(0));
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function encodeSetReadConsistency(mode, maxStalenessMs) {
|
|
1388
|
+
if (mode !== ReadConsistency.Strong && mode !== ReadConsistency.Bounded && mode !== ReadConsistency.Stale) {
|
|
1389
|
+
throw new NextSQLError('invalid_argument', 'unknown read consistency mode');
|
|
1390
|
+
}
|
|
1391
|
+
let ms = 0;
|
|
1392
|
+
if (maxStalenessMs && maxStalenessMs > 0) {
|
|
1393
|
+
// Keep a sub-millisecond bound positive so the server does not read it as
|
|
1394
|
+
// "use the default window" (0). Real staleness bounds are seconds.
|
|
1395
|
+
ms = Math.floor(maxStalenessMs);
|
|
1396
|
+
if (ms === 0) {
|
|
1397
|
+
ms = 1;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const buf = Buffer.alloc(9);
|
|
1401
|
+
buf[0] = mode;
|
|
1402
|
+
buf.writeBigUInt64LE(BigInt(ms), 1);
|
|
1403
|
+
return buf;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function decodeNodeStatus(b) {
|
|
1407
|
+
const role = readU16String(b, 0, MAX_NAME);
|
|
1408
|
+
const off = role.next;
|
|
1409
|
+
if (b.length - off !== 25) {
|
|
1410
|
+
throw new NextSQLError('protocol', 'bad node-status length');
|
|
1411
|
+
}
|
|
1412
|
+
const flags = b[off];
|
|
1413
|
+
return {
|
|
1414
|
+
role: role.value,
|
|
1415
|
+
hasLeader: (flags & 1) !== 0,
|
|
1416
|
+
healthy: (flags & 2) !== 0,
|
|
1417
|
+
appliedLSN: b.readBigUInt64LE(off + 1),
|
|
1418
|
+
lastContactMs: b.readBigInt64LE(off + 9),
|
|
1419
|
+
applyBacklog: b.readBigUInt64LE(off + 17),
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function txnControl(sql) {
|
|
1424
|
+
const up = String(sql).replace(/^[\s(]+/, '').toUpperCase();
|
|
1425
|
+
const begin = up.startsWith('BEGIN') || up.startsWith('START TRANSACTION');
|
|
1426
|
+
const end = up.startsWith('COMMIT') || up.startsWith('ROLLBACK');
|
|
1427
|
+
return { begin, end };
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// isReadOnlySQL is a conservative check: a false negative only costs a leader
|
|
1431
|
+
// round trip, and a false positive on a write self-corrects (the follower
|
|
1432
|
+
// rejects it as not-leader and the caller retries on the leader). EXPLAIN is
|
|
1433
|
+
// excluded because EXPLAIN ANALYZE executes its statement.
|
|
1434
|
+
function isReadOnlySQL(sql) {
|
|
1435
|
+
let s = String(sql).replace(/^[\s(]+/, '');
|
|
1436
|
+
while (s.startsWith('--')) {
|
|
1437
|
+
const i = s.indexOf('\n');
|
|
1438
|
+
if (i < 0) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
s = s.slice(i + 1).replace(/^[\s(]+/, '');
|
|
1442
|
+
}
|
|
1443
|
+
const up = s.toUpperCase();
|
|
1444
|
+
if (up.startsWith('SELECT') || up.startsWith('SHOW')) {
|
|
1445
|
+
return true;
|
|
1446
|
+
}
|
|
1447
|
+
if (up.startsWith('WITH')) {
|
|
1448
|
+
return !up.includes('INSERT') && !up.includes('UPDATE') &&
|
|
1449
|
+
!up.includes('DELETE') && !up.includes('UPSERT');
|
|
1450
|
+
}
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
class Wire {
|
|
1455
|
+
constructor(socket) {
|
|
1456
|
+
this.socket = socket;
|
|
1457
|
+
this.buf = Buffer.alloc(0);
|
|
1458
|
+
this.closed = false;
|
|
1459
|
+
this.err = null;
|
|
1460
|
+
this.wait = null;
|
|
1461
|
+
socket.on('data', (chunk) => {
|
|
1462
|
+
this.buf = Buffer.concat([this.buf, chunk]);
|
|
1463
|
+
this.pump();
|
|
1464
|
+
});
|
|
1465
|
+
socket.on('error', (err) => {
|
|
1466
|
+
this.err = err;
|
|
1467
|
+
this.pump();
|
|
1468
|
+
});
|
|
1469
|
+
socket.on('close', () => {
|
|
1470
|
+
this.closed = true;
|
|
1471
|
+
this.pump();
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
pump() {
|
|
1476
|
+
if (!this.wait) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (this.err) {
|
|
1480
|
+
const { reject } = this.wait;
|
|
1481
|
+
this.wait = null;
|
|
1482
|
+
reject(new NextSQLError('io', this.err.message));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (this.buf.length >= this.wait.n) {
|
|
1486
|
+
const out = this.buf.subarray(0, this.wait.n);
|
|
1487
|
+
this.buf = this.buf.subarray(this.wait.n);
|
|
1488
|
+
const { resolve } = this.wait;
|
|
1489
|
+
this.wait = null;
|
|
1490
|
+
resolve(out);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (this.closed) {
|
|
1494
|
+
const { reject } = this.wait;
|
|
1495
|
+
this.wait = null;
|
|
1496
|
+
reject(new NextSQLError('unavailable', 'connection closed'));
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
readExact(n) {
|
|
1501
|
+
if (this.wait) {
|
|
1502
|
+
return Promise.reject(new NextSQLError('internal', 'overlapping read'));
|
|
1503
|
+
}
|
|
1504
|
+
return new Promise((resolve, reject) => {
|
|
1505
|
+
this.wait = { n, resolve, reject };
|
|
1506
|
+
this.pump();
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
async readFrame() {
|
|
1511
|
+
const hdr = await this.readExact(HEADER);
|
|
1512
|
+
if (!hdr.subarray(0, 4).equals(MAGIC)) {
|
|
1513
|
+
throw new NextSQLError('protocol', 'bad magic');
|
|
1514
|
+
}
|
|
1515
|
+
if (u16(hdr, 4) !== VERSION) {
|
|
1516
|
+
throw new NextSQLError('protocol', 'unsupported protocol version');
|
|
1517
|
+
}
|
|
1518
|
+
const typ = hdr[6];
|
|
1519
|
+
if (typ === 0) {
|
|
1520
|
+
throw new NextSQLError('protocol', 'invalid message type');
|
|
1521
|
+
}
|
|
1522
|
+
const n = u32(hdr, 8);
|
|
1523
|
+
if (n > MAX_PACKET) {
|
|
1524
|
+
throw new NextSQLError('protocol', 'packet exceeds limit');
|
|
1525
|
+
}
|
|
1526
|
+
const payload = n === 0 ? Buffer.alloc(0) : await this.readExact(n);
|
|
1527
|
+
return { type: typ, payload };
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
writeFrame(typ, payload) {
|
|
1531
|
+
payload = payload || Buffer.alloc(0);
|
|
1532
|
+
if (payload.length > MAX_PACKET) {
|
|
1533
|
+
throw new NextSQLError('protocol', 'payload exceeds packet limit');
|
|
1534
|
+
}
|
|
1535
|
+
const hdr = Buffer.alloc(HEADER);
|
|
1536
|
+
MAGIC.copy(hdr, 0);
|
|
1537
|
+
hdr.writeUInt16LE(VERSION, 4);
|
|
1538
|
+
hdr[6] = typ;
|
|
1539
|
+
hdr.writeUInt32LE(payload.length, 8);
|
|
1540
|
+
return new Promise((resolve, reject) => {
|
|
1541
|
+
this.socket.write(Buffer.concat([hdr, payload]), (err) => {
|
|
1542
|
+
if (err) {
|
|
1543
|
+
reject(new NextSQLError('io', err.message));
|
|
1544
|
+
} else {
|
|
1545
|
+
resolve();
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
close() {
|
|
1552
|
+
this.socket.destroy();
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function connectSocket(cfg) {
|
|
1557
|
+
const { host, port } = splitHostPort(cfg.address);
|
|
1558
|
+
return new Promise((resolve, reject) => {
|
|
1559
|
+
const onErr = (err) => reject(new NextSQLError('io', err.message));
|
|
1560
|
+
if (cfg.tls) {
|
|
1561
|
+
const opts = {
|
|
1562
|
+
host,
|
|
1563
|
+
port,
|
|
1564
|
+
minVersion: 'TLSv1.3',
|
|
1565
|
+
servername: cfg.tls.servername || host,
|
|
1566
|
+
};
|
|
1567
|
+
if (cfg.tls.ca) {
|
|
1568
|
+
opts.ca = cfg.tls.ca;
|
|
1569
|
+
}
|
|
1570
|
+
if (cfg.tls.rejectUnauthorized === false) {
|
|
1571
|
+
opts.rejectUnauthorized = false;
|
|
1572
|
+
}
|
|
1573
|
+
const sock = tls.connect(opts, () => resolve(sock));
|
|
1574
|
+
sock.on('error', onErr);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const sock = net.connect({ host, port }, () => resolve(sock));
|
|
1578
|
+
sock.on('error', onErr);
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
class Rows {
|
|
1583
|
+
constructor(conn, columns) {
|
|
1584
|
+
this.conn = conn;
|
|
1585
|
+
this.columns = columns ? columns.map((c) => c.name) : [];
|
|
1586
|
+
this.columnTypes = columns || [];
|
|
1587
|
+
this.affected = 0;
|
|
1588
|
+
this._batch = [];
|
|
1589
|
+
this._i = 0;
|
|
1590
|
+
this._done = columns == null;
|
|
1591
|
+
this._closed = columns == null;
|
|
1592
|
+
this._err = null;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async next() {
|
|
1596
|
+
if (this._closed || this._err) {
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
if (this._i < this._batch.length) {
|
|
1600
|
+
this._i++;
|
|
1601
|
+
return true;
|
|
1602
|
+
}
|
|
1603
|
+
if (this._done) {
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
try {
|
|
1607
|
+
await this._fill();
|
|
1608
|
+
} catch (err) {
|
|
1609
|
+
this._err = err;
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
if (this._i < this._batch.length) {
|
|
1613
|
+
this._i++;
|
|
1614
|
+
return true;
|
|
1615
|
+
}
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
values() {
|
|
1620
|
+
if (this._i <= 0 || this._i > this._batch.length) {
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
return this._batch[this._i - 1];
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
async _fill() {
|
|
1627
|
+
const c = this.conn;
|
|
1628
|
+
if (!this._done && this._batch.length > 0) {
|
|
1629
|
+
await c.wire.writeFrame(Type.FlowAck, null);
|
|
1630
|
+
}
|
|
1631
|
+
const msg = await c.wire.readFrame();
|
|
1632
|
+
if (msg.type === Type.DataBatch) {
|
|
1633
|
+
this._batch = decodeDataBatch(msg.payload);
|
|
1634
|
+
this._i = 0;
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (msg.type === Type.CommandComplete) {
|
|
1638
|
+
this.affected = decodeCommandComplete(msg.payload);
|
|
1639
|
+
this._done = true;
|
|
1640
|
+
this._batch = [];
|
|
1641
|
+
this._i = 0;
|
|
1642
|
+
await c.expectReady();
|
|
1643
|
+
this._finish();
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
throw await c.unexpected(msg);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
async close() {
|
|
1650
|
+
while (await this.next()) {
|
|
1651
|
+
// drain so the session returns to Ready
|
|
1652
|
+
}
|
|
1653
|
+
if (!this._closed) {
|
|
1654
|
+
this._finish();
|
|
1655
|
+
}
|
|
1656
|
+
if (this._err) {
|
|
1657
|
+
throw this._err;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
_finish() {
|
|
1662
|
+
if (this.conn && !this._closed) {
|
|
1663
|
+
this.conn.busy = false;
|
|
1664
|
+
}
|
|
1665
|
+
this._closed = true;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
async *[Symbol.asyncIterator]() {
|
|
1669
|
+
try {
|
|
1670
|
+
while (await this.next()) {
|
|
1671
|
+
yield this.values();
|
|
1672
|
+
}
|
|
1673
|
+
if (this._err) {
|
|
1674
|
+
throw this._err;
|
|
1675
|
+
}
|
|
1676
|
+
} finally {
|
|
1677
|
+
if (!this._closed) {
|
|
1678
|
+
try {
|
|
1679
|
+
await this.close();
|
|
1680
|
+
} catch {
|
|
1681
|
+
// already surfaced via _err
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
class Stmt {
|
|
1689
|
+
constructor(conn, id) {
|
|
1690
|
+
this.conn = conn;
|
|
1691
|
+
this.id = id;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
async query(params) {
|
|
1695
|
+
return this.conn._execute(this.id, params || []);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
async exec(params) {
|
|
1699
|
+
const rows = await this.query(params);
|
|
1700
|
+
return collect(rows);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
async close() {
|
|
1704
|
+
const c = this.conn;
|
|
1705
|
+
if (!c.wire || !this.id) {
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
if (c.busy) {
|
|
1709
|
+
throw new NextSQLError('conflict', 'connection is busy');
|
|
1710
|
+
}
|
|
1711
|
+
await c.wire.writeFrame(Type.CloseStmt, putU32(this.id));
|
|
1712
|
+
const msg = await c.wire.readFrame();
|
|
1713
|
+
if (msg.type !== Type.CloseOK) {
|
|
1714
|
+
throw await c.unexpected(msg);
|
|
1715
|
+
}
|
|
1716
|
+
await c.expectReady();
|
|
1717
|
+
this.id = 0;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
class Conn {
|
|
1722
|
+
constructor(cfg, wire) {
|
|
1723
|
+
this.cfg = cfg;
|
|
1724
|
+
this.wire = wire;
|
|
1725
|
+
this.secret = 0n;
|
|
1726
|
+
this.busy = false;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
async handshake() {
|
|
1730
|
+
await this.wire.writeFrame(Type.Hello, encodeHello({
|
|
1731
|
+
version: VERSION,
|
|
1732
|
+
database: this.cfg.database || '',
|
|
1733
|
+
user: this.cfg.user,
|
|
1734
|
+
realm: this.cfg.realm || '',
|
|
1735
|
+
}));
|
|
1736
|
+
let msg = await this.wire.readFrame();
|
|
1737
|
+
if (msg.type !== Type.HelloOK) {
|
|
1738
|
+
throw await this.unexpected(msg);
|
|
1739
|
+
}
|
|
1740
|
+
const ok = decodeHelloOK(msg.payload);
|
|
1741
|
+
this.secret = ok.secret;
|
|
1742
|
+
await this.wire.writeFrame(Type.Auth, encodeAuth(this.cfg.password || ''));
|
|
1743
|
+
msg = await this.wire.readFrame();
|
|
1744
|
+
if (msg.type !== Type.AuthOK) {
|
|
1745
|
+
throw await this.unexpected(msg);
|
|
1746
|
+
}
|
|
1747
|
+
if (ok.authMethod === AuthPasswordKey) {
|
|
1748
|
+
if (!this.cfg.key) {
|
|
1749
|
+
throw new NextSQLError('unauthorized', 'server requires a client-held key');
|
|
1750
|
+
}
|
|
1751
|
+
const key = Buffer.isBuffer(this.cfg.key) ? this.cfg.key : Buffer.from(this.cfg.key);
|
|
1752
|
+
if (key.length !== 32) {
|
|
1753
|
+
throw new NextSQLError('invalid_argument', 'client key must be 32 bytes');
|
|
1754
|
+
}
|
|
1755
|
+
const mat = Buffer.alloc(36);
|
|
1756
|
+
mat.writeUInt32LE(this.cfg.keyVersion || 1, 0);
|
|
1757
|
+
key.copy(mat, 4);
|
|
1758
|
+
await this.wire.writeFrame(Type.Unlock, mat);
|
|
1759
|
+
msg = await this.wire.readFrame();
|
|
1760
|
+
if (msg.type !== Type.UnlockOK) {
|
|
1761
|
+
throw await this.unexpected(msg);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
msg = await this.wire.readFrame();
|
|
1765
|
+
if (msg.type !== Type.Ready) {
|
|
1766
|
+
throw await this.unexpected(msg);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Decodes an out-of-band Error frame (or reports a genuine protocol
|
|
1771
|
+
// violation) for a call site checking "did I get what I expected?".
|
|
1772
|
+
// writeErrReady on the server always sends Error then Ready — every call
|
|
1773
|
+
// site funnels through here specifically so that trailing Ready is
|
|
1774
|
+
// always drained in one place, rather than each of
|
|
1775
|
+
// query/prepare/closeStatement/etc. having to remember to do it
|
|
1776
|
+
// individually (a per-call-site version of this was exactly the shape of
|
|
1777
|
+
// a real bug: _readRows/prepare/close never drained it, leaving the
|
|
1778
|
+
// connection permanently desynced after the first query error).
|
|
1779
|
+
async unexpected(msg) {
|
|
1780
|
+
if (msg.type === Type.Error) {
|
|
1781
|
+
const err = decodeError(msg.payload);
|
|
1782
|
+
try {
|
|
1783
|
+
await this.expectReady();
|
|
1784
|
+
} catch {
|
|
1785
|
+
// Best-effort: surface the original application error even if
|
|
1786
|
+
// draining the trailing Ready itself fails (e.g. the connection
|
|
1787
|
+
// is now genuinely broken).
|
|
1788
|
+
}
|
|
1789
|
+
return err;
|
|
1790
|
+
}
|
|
1791
|
+
return new NextSQLError('protocol', 'unexpected message type');
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
async expectReady() {
|
|
1795
|
+
const msg = await this.wire.readFrame();
|
|
1796
|
+
if (msg.type !== Type.Ready) {
|
|
1797
|
+
throw await this.unexpected(msg);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// readAck reads a single control acknowledgement: Ready, or Error
|
|
1802
|
+
// (unexpected() drains the trailing Ready so the session stays usable).
|
|
1803
|
+
async readAck() {
|
|
1804
|
+
const msg = await this.wire.readFrame();
|
|
1805
|
+
if (msg.type === Type.Ready) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
throw await this.unexpected(msg);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// setReadConsistency sets this connection's read-consistency mode for
|
|
1812
|
+
// subsequent statements. maxStalenessMs applies only to Bounded (0 or omitted
|
|
1813
|
+
// selects the server default window).
|
|
1814
|
+
async setReadConsistency(mode, maxStalenessMs) {
|
|
1815
|
+
if (!this.wire) {
|
|
1816
|
+
throw new NextSQLError('unavailable', 'connection closed');
|
|
1817
|
+
}
|
|
1818
|
+
if (this.busy) {
|
|
1819
|
+
throw new NextSQLError('conflict', 'connection is busy');
|
|
1820
|
+
}
|
|
1821
|
+
await this.wire.writeFrame(Type.SetReadConsistency, encodeSetReadConsistency(mode, maxStalenessMs));
|
|
1822
|
+
await this.readAck();
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// nodeStatus asks the connected server for its key-free replication health.
|
|
1826
|
+
async nodeStatus() {
|
|
1827
|
+
if (!this.wire) {
|
|
1828
|
+
throw new NextSQLError('unavailable', 'connection closed');
|
|
1829
|
+
}
|
|
1830
|
+
if (this.busy) {
|
|
1831
|
+
throw new NextSQLError('conflict', 'connection is busy');
|
|
1832
|
+
}
|
|
1833
|
+
await this.wire.writeFrame(Type.NodeStatus, null);
|
|
1834
|
+
const msg = await this.wire.readFrame();
|
|
1835
|
+
if (msg.type !== Type.NodeStatusResp) {
|
|
1836
|
+
throw await this.unexpected(msg);
|
|
1837
|
+
}
|
|
1838
|
+
const st = decodeNodeStatus(msg.payload);
|
|
1839
|
+
await this.expectReady();
|
|
1840
|
+
return st;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
async _readRows() {
|
|
1844
|
+
const msg = await this.wire.readFrame();
|
|
1845
|
+
if (msg.type === Type.RowDesc) {
|
|
1846
|
+
return new Rows(this, decodeRowDesc(msg.payload));
|
|
1847
|
+
}
|
|
1848
|
+
if (msg.type === Type.CommandComplete) {
|
|
1849
|
+
const rows = new Rows(this, null);
|
|
1850
|
+
rows.affected = decodeCommandComplete(msg.payload);
|
|
1851
|
+
await this.expectReady();
|
|
1852
|
+
this.busy = false;
|
|
1853
|
+
rows._closed = true;
|
|
1854
|
+
rows._done = true;
|
|
1855
|
+
return rows;
|
|
1856
|
+
}
|
|
1857
|
+
this.busy = false;
|
|
1858
|
+
throw await this.unexpected(msg);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
async query(sql, params) {
|
|
1862
|
+
if (!this.wire) {
|
|
1863
|
+
throw new NextSQLError('unavailable', 'connection closed');
|
|
1864
|
+
}
|
|
1865
|
+
if (this.busy) {
|
|
1866
|
+
throw new NextSQLError('conflict', 'connection is busy');
|
|
1867
|
+
}
|
|
1868
|
+
this.busy = true;
|
|
1869
|
+
try {
|
|
1870
|
+
await this.wire.writeFrame(Type.Query, encodeQuery(sql, params || []));
|
|
1871
|
+
return await this._readRows();
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
this.busy = false;
|
|
1874
|
+
throw err;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
async exec(sql, params) {
|
|
1879
|
+
const rows = await this.query(sql, params);
|
|
1880
|
+
return collect(rows);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
async encryptField(table, column, type, value) {
|
|
1884
|
+
return encryptField(this.cfg.fieldKeys, this.cfg.database || '', table, column, type, value);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
async decryptField(table, column, type, ciphertext) {
|
|
1888
|
+
return decryptField(this.cfg.fieldKeys, this.cfg.database || '', table, column, type, ciphertext);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
async prepare(sql) {
|
|
1892
|
+
if (!this.wire) {
|
|
1893
|
+
throw new NextSQLError('unavailable', 'connection closed');
|
|
1894
|
+
}
|
|
1895
|
+
if (this.busy) {
|
|
1896
|
+
throw new NextSQLError('conflict', 'connection is busy');
|
|
1897
|
+
}
|
|
1898
|
+
await this.wire.writeFrame(Type.Prepare, encodePrepare(sql));
|
|
1899
|
+
const msg = await this.wire.readFrame();
|
|
1900
|
+
if (msg.type !== Type.PrepareOK) {
|
|
1901
|
+
throw await this.unexpected(msg);
|
|
1902
|
+
}
|
|
1903
|
+
if (msg.payload.length !== 4) {
|
|
1904
|
+
throw new NextSQLError('protocol', 'bad prepare-ok length');
|
|
1905
|
+
}
|
|
1906
|
+
const id = u32(msg.payload, 0);
|
|
1907
|
+
await this.expectReady();
|
|
1908
|
+
return new Stmt(this, id);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
async _execute(id, params) {
|
|
1912
|
+
if (!this.wire) {
|
|
1913
|
+
throw new NextSQLError('unavailable', 'connection closed');
|
|
1914
|
+
}
|
|
1915
|
+
if (this.busy) {
|
|
1916
|
+
throw new NextSQLError('conflict', 'connection is busy');
|
|
1917
|
+
}
|
|
1918
|
+
this.busy = true;
|
|
1919
|
+
try {
|
|
1920
|
+
await this.wire.writeFrame(Type.Execute, encodeExecute(id, params));
|
|
1921
|
+
return await this._readRows();
|
|
1922
|
+
} catch (err) {
|
|
1923
|
+
this.busy = false;
|
|
1924
|
+
throw err;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
async cancel() {
|
|
1929
|
+
if (!this.secret) {
|
|
1930
|
+
throw new NextSQLError('unavailable', 'not connected');
|
|
1931
|
+
}
|
|
1932
|
+
const sock = await connectSocket(this.cfg);
|
|
1933
|
+
const side = new Wire(sock);
|
|
1934
|
+
try {
|
|
1935
|
+
await side.writeFrame(Type.Hello, encodeHello({
|
|
1936
|
+
version: VERSION,
|
|
1937
|
+
flags: FlagCancel,
|
|
1938
|
+
secret: this.secret,
|
|
1939
|
+
database: '',
|
|
1940
|
+
user: '',
|
|
1941
|
+
}));
|
|
1942
|
+
const msg = await side.readFrame();
|
|
1943
|
+
if (msg.type !== Type.Ready) {
|
|
1944
|
+
throw await this.unexpected(msg);
|
|
1945
|
+
}
|
|
1946
|
+
} finally {
|
|
1947
|
+
side.close();
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
async close() {
|
|
1952
|
+
if (!this.wire) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
await this.wire.writeFrame(Type.Terminate, null);
|
|
1957
|
+
} catch {
|
|
1958
|
+
// ignore
|
|
1959
|
+
}
|
|
1960
|
+
this.wire.close();
|
|
1961
|
+
this.wire = null;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
async function collect(rows) {
|
|
1966
|
+
const out = { columns: rows.columns, rows: [], affected: 0 };
|
|
1967
|
+
try {
|
|
1968
|
+
while (await rows.next()) {
|
|
1969
|
+
out.rows.push(rows.values());
|
|
1970
|
+
}
|
|
1971
|
+
if (rows._err) {
|
|
1972
|
+
throw rows._err;
|
|
1973
|
+
}
|
|
1974
|
+
} finally {
|
|
1975
|
+
if (!rows._closed) {
|
|
1976
|
+
await rows.close();
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
out.affected = rows.affected;
|
|
1980
|
+
return out;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
async function openConn(cfg) {
|
|
1984
|
+
const sock = await connectSocket(cfg);
|
|
1985
|
+
const wire = new Wire(sock);
|
|
1986
|
+
const conn = new Conn(cfg, wire);
|
|
1987
|
+
try {
|
|
1988
|
+
await conn.handshake();
|
|
1989
|
+
const mode = cfg.readConsistency;
|
|
1990
|
+
if (mode !== undefined && mode !== null && mode !== ReadConsistency.Strong) {
|
|
1991
|
+
await conn.setReadConsistency(mode, cfg.maxStalenessMs);
|
|
1992
|
+
}
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
wire.close();
|
|
1995
|
+
throw err;
|
|
1996
|
+
}
|
|
1997
|
+
return conn;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
async function connect(cfg) {
|
|
2001
|
+
validateConfig(cfg);
|
|
2002
|
+
return openConn(cfg);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// Cluster is a routing client over every node of a NextSQL HA cluster.
|
|
2006
|
+
//
|
|
2007
|
+
// With cfg.readConsistency set to Bounded or Stale it sends eligible read-only
|
|
2008
|
+
// statements to a healthy follower and everything else — writes, DDL,
|
|
2009
|
+
// transaction control, and Strong reads — to the leader. With the default
|
|
2010
|
+
// Strong consistency every statement goes to the leader and Cluster is just a
|
|
2011
|
+
// leader-failover wrapper. A Cluster is for sequential use.
|
|
2012
|
+
class Cluster {
|
|
2013
|
+
constructor(cfg, conns) {
|
|
2014
|
+
this.cfg = cfg;
|
|
2015
|
+
this._conns = conns; // [{ addr, conn, status, seen }]
|
|
2016
|
+
this._rr = 0;
|
|
2017
|
+
this._inTxn = false;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
async close() {
|
|
2021
|
+
let err = null;
|
|
2022
|
+
for (const cc of this._conns) {
|
|
2023
|
+
try {
|
|
2024
|
+
await cc.conn.close();
|
|
2025
|
+
} catch (e) {
|
|
2026
|
+
if (!err) {
|
|
2027
|
+
err = e;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
if (err) {
|
|
2032
|
+
throw err;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
async nodes() {
|
|
2037
|
+
await this._refresh();
|
|
2038
|
+
return this._conns.map((cc) => cc.status).filter((s) => s != null);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
async exec(sql, params) {
|
|
2042
|
+
const rows = await this.query(sql, params);
|
|
2043
|
+
return collect(rows);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
async query(sql, params) {
|
|
2047
|
+
const { begin, end } = txnControl(sql);
|
|
2048
|
+
const routable = !this._inTxn && !begin && !end &&
|
|
2049
|
+
this.cfg.readConsistency !== undefined &&
|
|
2050
|
+
this.cfg.readConsistency !== null &&
|
|
2051
|
+
this.cfg.readConsistency !== ReadConsistency.Strong &&
|
|
2052
|
+
isReadOnlySQL(sql);
|
|
2053
|
+
|
|
2054
|
+
if (routable) {
|
|
2055
|
+
const fc = await this._followerConn();
|
|
2056
|
+
if (fc) {
|
|
2057
|
+
try {
|
|
2058
|
+
return await fc.query(sql, params || []);
|
|
2059
|
+
} catch (err) {
|
|
2060
|
+
if (!(err instanceof NextSQLError) || err.code !== 'unavailable') {
|
|
2061
|
+
throw err;
|
|
2062
|
+
}
|
|
2063
|
+
// Follower lost the leader or fell outside the bound; fall through.
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
const lc = await this._leaderConn();
|
|
2069
|
+
const rows = await lc.query(sql, params || []);
|
|
2070
|
+
if (begin || end) {
|
|
2071
|
+
this._inTxn = begin;
|
|
2072
|
+
}
|
|
2073
|
+
return rows;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
async _refresh() {
|
|
2077
|
+
const now = Date.now();
|
|
2078
|
+
const targets = this._conns.filter((cc) => now - (cc.seen || 0) >= STATUS_TTL_MS);
|
|
2079
|
+
for (const cc of targets) {
|
|
2080
|
+
try {
|
|
2081
|
+
cc.status = await cc.conn.nodeStatus();
|
|
2082
|
+
cc.seen = Date.now();
|
|
2083
|
+
} catch {
|
|
2084
|
+
// keep the last known status
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
async _leaderConn() {
|
|
2090
|
+
await this._refresh();
|
|
2091
|
+
for (const cc of this._conns) {
|
|
2092
|
+
const role = cc.status && cc.status.role;
|
|
2093
|
+
if (role === 'leader' || role === 'standalone') {
|
|
2094
|
+
return cc.conn;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
throw new NextSQLError('unavailable', 'no reachable leader');
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
async _followerConn() {
|
|
2101
|
+
await this._refresh();
|
|
2102
|
+
const followers = [];
|
|
2103
|
+
const others = [];
|
|
2104
|
+
for (const cc of this._conns) {
|
|
2105
|
+
if (!cc.status || !cc.status.healthy) {
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
if (cc.status.role === 'follower') {
|
|
2109
|
+
followers.push(cc);
|
|
2110
|
+
} else if (cc.status.role === 'leader' || cc.status.role === 'standalone') {
|
|
2111
|
+
others.push(cc);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
const pick = followers.length > 0 ? followers : others;
|
|
2115
|
+
if (pick.length === 0) {
|
|
2116
|
+
return null;
|
|
2117
|
+
}
|
|
2118
|
+
const cc = pick[this._rr % pick.length];
|
|
2119
|
+
this._rr++;
|
|
2120
|
+
return cc.conn;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
async function connectCluster(cfg) {
|
|
2125
|
+
let addrs = Array.isArray(cfg && cfg.nodes) ? cfg.nodes.slice() : [];
|
|
2126
|
+
if (addrs.length === 0 && cfg && cfg.address) {
|
|
2127
|
+
addrs = [cfg.address];
|
|
2128
|
+
}
|
|
2129
|
+
if (addrs.length === 0) {
|
|
2130
|
+
throw new NextSQLError('invalid_argument', 'at least one node address is required');
|
|
2131
|
+
}
|
|
2132
|
+
for (const a of addrs) {
|
|
2133
|
+
validateConfig({ ...cfg, address: a, nodes: undefined });
|
|
2134
|
+
}
|
|
2135
|
+
const conns = [];
|
|
2136
|
+
let firstErr = null;
|
|
2137
|
+
for (const a of addrs) {
|
|
2138
|
+
try {
|
|
2139
|
+
const conn = await openConn({ ...cfg, address: a, nodes: undefined });
|
|
2140
|
+
conns.push({ addr: a, conn, status: null, seen: 0 });
|
|
2141
|
+
} catch (err) {
|
|
2142
|
+
if (!firstErr) {
|
|
2143
|
+
firstErr = err;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
if (conns.length === 0) {
|
|
2148
|
+
throw firstErr;
|
|
2149
|
+
}
|
|
2150
|
+
return new Cluster(cfg, conns);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
module.exports = {
|
|
2154
|
+
connect,
|
|
2155
|
+
connectCluster,
|
|
2156
|
+
Cluster,
|
|
2157
|
+
NextSQLError,
|
|
2158
|
+
Kind,
|
|
2159
|
+
Type,
|
|
2160
|
+
ReadConsistency,
|
|
2161
|
+
validateConfig,
|
|
2162
|
+
isLoopback,
|
|
2163
|
+
isReadOnlySQL,
|
|
2164
|
+
txnControl,
|
|
2165
|
+
encodeParam,
|
|
2166
|
+
decodeValue,
|
|
2167
|
+
struct,
|
|
2168
|
+
encodeHello,
|
|
2169
|
+
decodeHelloOK,
|
|
2170
|
+
encodeQuery,
|
|
2171
|
+
encodeSetReadConsistency,
|
|
2172
|
+
decodeNodeStatus,
|
|
2173
|
+
decodeRowDesc,
|
|
2174
|
+
decodeDataBatch,
|
|
2175
|
+
decodeError,
|
|
2176
|
+
decodeNSJB,
|
|
2177
|
+
formatUUID,
|
|
2178
|
+
encodeDecimalString,
|
|
2179
|
+
decodeDecimal,
|
|
2180
|
+
FieldType,
|
|
2181
|
+
MemoryFieldKeyring,
|
|
2182
|
+
FileFieldKeyring,
|
|
2183
|
+
decryptField,
|
|
2184
|
+
encryptField,
|
|
2185
|
+
generateFieldKey,
|
|
2186
|
+
inspectField,
|
|
2187
|
+
};
|