@bzync/nextsql 0.0.1

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