@cognica-io/uqa-wasm 0.1.6

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/index.mjs ADDED
@@ -0,0 +1,1543 @@
1
+ //
2
+ // Unified Query Algebra
3
+ //
4
+ // Copyright (c) 2023-2026 Cognica, Inc.
5
+ //
6
+
7
+ // TypeScript-facing wrapper over the uqa_call dispatch ABI exported by
8
+ // the emscripten module (see ../src/main.rs). Databases live under
9
+ // PERSIST_DIR on the emscripten virtual filesystem; in browsers that
10
+ // directory is an IDBFS mount, so `UQA.persist()` flushes every
11
+ // database into IndexedDB and `UQA.load()` restores them on startup.
12
+
13
+ import createUQAModule from "./uqa.js";
14
+
15
+ const PERSIST_DIR = "/uqa";
16
+
17
+ let modulePromise = null;
18
+ const sqlCallbacks = new Map();
19
+ let nextSQLCallbackId = 1;
20
+ let nextAggregateStateId = 1;
21
+ let sqlCallbackDepth = 0;
22
+
23
+ function hasIndexedDB() {
24
+ return typeof indexedDB !== "undefined";
25
+ }
26
+
27
+ async function loadModule() {
28
+ if (modulePromise === null) {
29
+ modulePromise = (async () => {
30
+ const module = await createUQAModule();
31
+ installCallbackBridge(module);
32
+ module.FS.mkdirTree(PERSIST_DIR);
33
+ if (hasIndexedDB()) {
34
+ module.FS.mount(module.IDBFS, {}, PERSIST_DIR);
35
+ await syncFS(module, true);
36
+ }
37
+ return module;
38
+ })();
39
+ }
40
+ return modulePromise;
41
+ }
42
+
43
+ function installCallbackBridge(module) {
44
+ module.uqaInvokeCallback = (callbackId, requestText) => {
45
+ try {
46
+ const entry = sqlCallbacks.get(callbackId);
47
+ if (entry === undefined) {
48
+ throw new Error(`unknown JavaScript SQL callback ID ${callbackId}`);
49
+ }
50
+ const request = JSON.parse(requestText);
51
+ const result = invokeSQLCallback(entry, request);
52
+ return JSON.stringify({ ok: encodeValue(result) });
53
+ } catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ return JSON.stringify({ error: message });
56
+ }
57
+ };
58
+ }
59
+
60
+ function invokeSQLCallback(entry, request) {
61
+ sqlCallbackDepth += 1;
62
+ try {
63
+ const args = decodeValue(request.args ?? []);
64
+ switch (request.operation) {
65
+ case "scalar":
66
+ requireCallbackKind(entry, "scalar");
67
+ return synchronousResult(entry.callback(...args), "scalar SQL callback");
68
+ case "table":
69
+ requireCallbackKind(entry, "table");
70
+ return synchronousResult(entry.callback(...args), "table SQL callback");
71
+ case "aggregateCreate":
72
+ requireCallbackKind(entry, "aggregate");
73
+ return createAggregateState(entry);
74
+ case "aggregateObserve":
75
+ requireCallbackKind(entry, "aggregate");
76
+ return observeAggregateState(entry, request.stateId, args);
77
+ case "aggregateFinish":
78
+ requireCallbackKind(entry, "aggregate");
79
+ return finishAggregateState(entry, request.stateId);
80
+ case "aggregateDrop":
81
+ requireCallbackKind(entry, "aggregate");
82
+ entry.states.delete(request.stateId);
83
+ return null;
84
+ default:
85
+ throw new Error(`unknown JavaScript SQL callback operation ${request.operation}`);
86
+ }
87
+ } finally {
88
+ sqlCallbackDepth -= 1;
89
+ }
90
+ }
91
+
92
+ function assertEngineCallAllowed() {
93
+ if (sqlCallbackDepth !== 0) {
94
+ throw new Error("Engine methods cannot be called from a JavaScript SQL callback");
95
+ }
96
+ }
97
+
98
+ function guardEngineMethods(engineClass) {
99
+ const prototype = engineClass.prototype;
100
+ for (const name of Object.getOwnPropertyNames(prototype)) {
101
+ if (name === "constructor") {
102
+ continue;
103
+ }
104
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
105
+ if (descriptor === undefined || typeof descriptor.value !== "function") {
106
+ continue;
107
+ }
108
+ const method = descriptor.value;
109
+ Object.defineProperty(prototype, name, {
110
+ ...descriptor,
111
+ value(...args) {
112
+ assertEngineCallAllowed();
113
+ return Reflect.apply(method, this, args);
114
+ },
115
+ });
116
+ }
117
+ }
118
+
119
+ function requireCallbackKind(entry, expected) {
120
+ if (entry.kind !== expected) {
121
+ throw new Error(`SQL callback kind mismatch: expected ${expected}, got ${entry.kind}`);
122
+ }
123
+ }
124
+
125
+ function synchronousResult(value, label) {
126
+ if (value !== null && (typeof value === "object" || typeof value === "function")) {
127
+ if (typeof value.then === "function") {
128
+ throw new Error(`${label} must return synchronously; Promise results are not supported`);
129
+ }
130
+ }
131
+ return value === undefined ? null : value;
132
+ }
133
+
134
+ function createAggregateState(entry) {
135
+ const state = synchronousResult(entry.callback(), "SQL aggregate factory");
136
+ if (state === null || typeof state !== "object") {
137
+ throw new Error("SQL aggregate factory must return an object");
138
+ }
139
+ const observe = state.observe ?? state.step;
140
+ const finish = state.finish ?? state.finalize;
141
+ if (typeof observe !== "function") {
142
+ throw new Error("SQL aggregate state needs an observe or step method");
143
+ }
144
+ if (typeof finish !== "function") {
145
+ throw new Error("SQL aggregate state needs a finish or finalize method");
146
+ }
147
+ const stateId = allocateAggregateStateId();
148
+ entry.states.set(stateId, {
149
+ observe: observe.bind(state),
150
+ finish: finish.bind(state),
151
+ });
152
+ return stateId;
153
+ }
154
+
155
+ function aggregateState(entry, stateId) {
156
+ const state = entry.states.get(stateId);
157
+ if (state === undefined) {
158
+ throw new Error(`unknown JavaScript SQL aggregate state ID ${stateId}`);
159
+ }
160
+ return state;
161
+ }
162
+
163
+ function observeAggregateState(entry, stateId, args) {
164
+ const state = aggregateState(entry, stateId);
165
+ synchronousResult(state.observe(...args), "SQL aggregate observe method");
166
+ return null;
167
+ }
168
+
169
+ function finishAggregateState(entry, stateId) {
170
+ const state = aggregateState(entry, stateId);
171
+ try {
172
+ return synchronousResult(state.finish(), "SQL aggregate finish method");
173
+ } finally {
174
+ entry.states.delete(stateId);
175
+ }
176
+ }
177
+
178
+ function allocateAggregateStateId() {
179
+ if (nextAggregateStateId > 0xffffffff) {
180
+ throw new Error("JavaScript SQL aggregate state ID space is exhausted");
181
+ }
182
+ const stateId = nextAggregateStateId;
183
+ nextAggregateStateId += 1;
184
+ return stateId;
185
+ }
186
+
187
+ function createCallbackGroup() {
188
+ return { references: 1, registrations: new Map() };
189
+ }
190
+
191
+ function retainCallbackGroup(group) {
192
+ group.references += 1;
193
+ }
194
+
195
+ function releaseCallbackGroup(group) {
196
+ group.references -= 1;
197
+ if (group.references !== 0) {
198
+ return;
199
+ }
200
+ for (const callbackId of group.registrations.values()) {
201
+ sqlCallbacks.delete(callbackId);
202
+ }
203
+ group.registrations.clear();
204
+ }
205
+
206
+ function registerSQLCallback(group, kind, name, callback, registerNative) {
207
+ if (typeof callback !== "function") {
208
+ throw new TypeError(`${kind} SQL callback must be a function`);
209
+ }
210
+ const normalizedName = String(name).trim().toLowerCase();
211
+ if (normalizedName.length === 0) {
212
+ throw new TypeError("SQL function name cannot be empty");
213
+ }
214
+ if (nextSQLCallbackId > 0xffffffff) {
215
+ throw new Error("JavaScript SQL callback ID space is exhausted");
216
+ }
217
+ const callbackId = nextSQLCallbackId;
218
+ nextSQLCallbackId += 1;
219
+ sqlCallbacks.set(callbackId, {
220
+ kind,
221
+ callback,
222
+ states: kind === "aggregate" ? new Map() : null,
223
+ });
224
+ try {
225
+ registerNative(callbackId);
226
+ } catch (error) {
227
+ sqlCallbacks.delete(callbackId);
228
+ throw error;
229
+ }
230
+ const key = `${kind}:${normalizedName}`;
231
+ const previous = group.registrations.get(key);
232
+ group.registrations.set(key, callbackId);
233
+ if (previous !== undefined) {
234
+ sqlCallbacks.delete(previous);
235
+ }
236
+ }
237
+
238
+ function syncFS(module, populate) {
239
+ return new Promise((resolve, reject) => {
240
+ module.FS.syncfs(populate, (error) => {
241
+ if (error) {
242
+ reject(error);
243
+ } else {
244
+ resolve();
245
+ }
246
+ });
247
+ });
248
+ }
249
+
250
+ function rawCall(module, handle, method, args) {
251
+ const request = JSON.stringify({ method, args });
252
+ const ptr = module.ccall("uqa_call", "number", ["number", "string"], [handle, request]);
253
+ if (ptr === 0) {
254
+ throw new Error("uqa_call could not allocate a response");
255
+ }
256
+ let text;
257
+ try {
258
+ text = module.UTF8ToString(ptr);
259
+ } finally {
260
+ module.ccall("uqa_free", null, ["number"], [ptr]);
261
+ }
262
+ const response = JSON.parse(text);
263
+ if (response.error !== undefined) {
264
+ throw new Error(response.error);
265
+ }
266
+ return decodeValue(response.ok);
267
+ }
268
+
269
+ // {"$bytes": base64} payloads become Uint8Array on the way out;
270
+ // Uint8Array/ArrayBuffer arguments become {"$bytes"} on the way in.
271
+ function decodeValue(value) {
272
+ if (Array.isArray(value)) {
273
+ return value.map(decodeValue);
274
+ }
275
+ if (value !== null && typeof value === "object") {
276
+ const keys = Object.keys(value);
277
+ if (keys.length === 1 && keys[0] === "$bytes") {
278
+ return base64ToBytes(value.$bytes);
279
+ }
280
+ const out = {};
281
+ for (const key of keys) {
282
+ defineOwnValue(out, key, decodeValue(value[key]));
283
+ }
284
+ return out;
285
+ }
286
+ return value;
287
+ }
288
+
289
+ function encodeValue(value) {
290
+ if (value === undefined) {
291
+ return null;
292
+ }
293
+ if (typeof value === "number") {
294
+ if (!Number.isFinite(value)) {
295
+ throw new Error(`non-finite numbers cannot cross the JSON bridge: ${value}`);
296
+ }
297
+ if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
298
+ throw new Error(`integer exceeds JavaScript's safe range: ${value}`);
299
+ }
300
+ return value;
301
+ }
302
+ if (value instanceof Uint8Array) {
303
+ return { $bytes: bytesToBase64(value) };
304
+ }
305
+ if (value instanceof ArrayBuffer) {
306
+ return { $bytes: bytesToBase64(new Uint8Array(value)) };
307
+ }
308
+ if (value instanceof Float32Array || value instanceof Float64Array) {
309
+ return Array.from(value);
310
+ }
311
+ if (typeof value === "bigint") {
312
+ if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < -BigInt(Number.MAX_SAFE_INTEGER)) {
313
+ throw new Error("BigInt values beyond Number.MAX_SAFE_INTEGER are not supported in the browser binding");
314
+ }
315
+ return Number(value);
316
+ }
317
+ if (Array.isArray(value)) {
318
+ return value.map(encodeValue);
319
+ }
320
+ if (value !== null && typeof value === "object") {
321
+ const out = {};
322
+ for (const key of Object.keys(value)) {
323
+ defineOwnValue(out, key, encodeValue(value[key]));
324
+ }
325
+ return out;
326
+ }
327
+ return value;
328
+ }
329
+
330
+ function bytesToBase64(bytes) {
331
+ let binary = "";
332
+ for (const byte of bytes) {
333
+ binary += String.fromCharCode(byte);
334
+ }
335
+ return btoa(binary);
336
+ }
337
+
338
+ function base64ToBytes(encoded) {
339
+ const binary = atob(encoded);
340
+ const bytes = new Uint8Array(binary.length);
341
+ for (let index = 0; index < binary.length; index += 1) {
342
+ bytes[index] = binary.charCodeAt(index);
343
+ }
344
+ return bytes;
345
+ }
346
+
347
+ function encodeParams(params) {
348
+ if (params === undefined || params === null) {
349
+ return undefined;
350
+ }
351
+ return params.map((param) => {
352
+ if (param instanceof SQLParam) {
353
+ return param.payload;
354
+ }
355
+ return encodeValue(param);
356
+ });
357
+ }
358
+
359
+ const MAX_HTTP_JSON_BYTES = 65 * 1024 * 1024;
360
+ const MAX_HTTP_ERROR_BYTES = 64 * 1024;
361
+ const MAX_HTTP_STREAM_FRAME_BYTES = 64 * 1024 * 1024;
362
+
363
+ /** Redacted local/Cloud HTTP client error. */
364
+ export class HttpEngineError extends Error {
365
+ constructor(message, { code, status, requestId } = {}) {
366
+ super(message);
367
+ this.name = "HttpEngineError";
368
+ this.code = code;
369
+ this.status = status;
370
+ this.requestId = requestId;
371
+ }
372
+ }
373
+
374
+ function httpBaseURL(source) {
375
+ let url;
376
+ try {
377
+ url = new URL(source);
378
+ } catch {
379
+ throw new HttpEngineError("UQA data-plane URL is invalid");
380
+ }
381
+ const exactOrigin = url.username === "" && url.password === "" && url.pathname === "/"
382
+ && url.search === "" && url.hash === "";
383
+ if (!exactOrigin || (url.protocol !== "http:" && url.protocol !== "https:")) {
384
+ throw new HttpEngineError("UQA data-plane URL is invalid");
385
+ }
386
+ if (url.protocol === "http:" && !isLoopbackHostname(url.hostname)) {
387
+ throw new HttpEngineError("plain HTTP UQA URLs must resolve to loopback");
388
+ }
389
+ return url;
390
+ }
391
+
392
+ function isLoopbackHostname(hostname) {
393
+ const normalized = hostname.toLowerCase();
394
+ if (normalized === "localhost" || normalized === "[::1]" || normalized === "::1") {
395
+ return true;
396
+ }
397
+ const octets = normalized.split(".");
398
+ return octets.length === 4
399
+ && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255)
400
+ && Number(octets[0]) === 127;
401
+ }
402
+
403
+ function encodeHTTPStatement(query, params) {
404
+ if (typeof query !== "string" || query.trim() === "") {
405
+ throw new HttpEngineError("SQL text must not be empty");
406
+ }
407
+ return {
408
+ sql: query,
409
+ params: (params ?? []).map(encodeHTTPParameter),
410
+ };
411
+ }
412
+
413
+ function encodeHTTPParameter(parameter) {
414
+ if (parameter instanceof SQLParam) {
415
+ if (parameter.httpKind === "bytes") {
416
+ return { type: "bytes", hex: bytesToHex(base64ToBytes(parameter.payload.$bytes)) };
417
+ }
418
+ if (parameter.httpKind === "vector") {
419
+ return { type: "vector", value: finiteHTTPVector(parameter.payload.$vector) };
420
+ }
421
+ if (parameter.httpKind === "tensor") {
422
+ return {
423
+ type: "tensor",
424
+ value: parameter.payload.$tensor.map(finiteHTTPVector),
425
+ };
426
+ }
427
+ return encodeHTTPScalar(parameter.payload);
428
+ }
429
+ return encodeHTTPScalar(parameter);
430
+ }
431
+
432
+ function encodeHTTPScalar(value) {
433
+ if (value === undefined || value === null) {
434
+ return { type: "null" };
435
+ }
436
+ if (typeof value === "boolean") {
437
+ return { type: "boolean", value };
438
+ }
439
+ if (typeof value === "bigint") {
440
+ if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < -BigInt(Number.MAX_SAFE_INTEGER)) {
441
+ throw new HttpEngineError("SQL integer exceeds the browser safe range");
442
+ }
443
+ return { type: "int64", value: Number(value) };
444
+ }
445
+ if (typeof value === "number") {
446
+ if (!Number.isFinite(value)) {
447
+ throw new HttpEngineError("SQL parameter cannot be represented by the HTTP protocol");
448
+ }
449
+ if (Number.isInteger(value)) {
450
+ if (!Number.isSafeInteger(value)) {
451
+ throw new HttpEngineError("SQL integer exceeds the browser safe range");
452
+ }
453
+ return { type: "int64", value };
454
+ }
455
+ return { type: "float64", value };
456
+ }
457
+ if (typeof value === "string") {
458
+ return { type: "text", value };
459
+ }
460
+ if (value instanceof Uint8Array) {
461
+ return { type: "bytes", hex: bytesToHex(value) };
462
+ }
463
+ if (value instanceof ArrayBuffer) {
464
+ return { type: "bytes", hex: bytesToHex(new Uint8Array(value)) };
465
+ }
466
+ return { type: "json", value: encodeHTTPJSONValue(value) };
467
+ }
468
+
469
+ function encodeHTTPJSONValue(value) {
470
+ if (value === undefined || value === null) {
471
+ return null;
472
+ }
473
+ if (typeof value === "boolean" || typeof value === "string") {
474
+ return value;
475
+ }
476
+ if (typeof value === "bigint") {
477
+ if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < -BigInt(Number.MAX_SAFE_INTEGER)) {
478
+ throw new HttpEngineError("JSON integer exceeds the browser safe range");
479
+ }
480
+ return Number(value);
481
+ }
482
+ if (typeof value === "number") {
483
+ if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))) {
484
+ throw new HttpEngineError("JSON number cannot be represented by the HTTP protocol");
485
+ }
486
+ return value;
487
+ }
488
+ if (value instanceof Uint8Array) {
489
+ return { $uqa_type: "bytes", hex: bytesToHex(value) };
490
+ }
491
+ if (value instanceof ArrayBuffer) {
492
+ return { $uqa_type: "bytes", hex: bytesToHex(new Uint8Array(value)) };
493
+ }
494
+ if (value instanceof Float32Array || value instanceof Float64Array) {
495
+ return Array.from(value, encodeHTTPJSONValue);
496
+ }
497
+ if (Array.isArray(value)) {
498
+ return value.map(encodeHTTPJSONValue);
499
+ }
500
+ if (typeof value === "object") {
501
+ const encoded = {};
502
+ for (const key of Object.keys(value)) {
503
+ defineOwnValue(encoded, key, encodeHTTPJSONValue(value[key]));
504
+ }
505
+ return encoded;
506
+ }
507
+ throw new HttpEngineError("SQL parameter cannot be represented by the HTTP protocol");
508
+ }
509
+
510
+ function defineOwnValue(object, key, value) {
511
+ Object.defineProperty(object, key, {
512
+ configurable: true,
513
+ enumerable: true,
514
+ value,
515
+ writable: true,
516
+ });
517
+ }
518
+
519
+ function finiteHTTPVector(values) {
520
+ return Array.from(values, (value) => {
521
+ const number = Number(value);
522
+ if (!Number.isFinite(number)) {
523
+ throw new HttpEngineError("SQL parameter cannot be represented by the HTTP protocol");
524
+ }
525
+ return number;
526
+ });
527
+ }
528
+
529
+ function bytesToHex(bytes) {
530
+ let encoded = "";
531
+ for (const byte of bytes) {
532
+ encoded += byte.toString(16).padStart(2, "0");
533
+ }
534
+ return encoded;
535
+ }
536
+
537
+ function hexToBytes(encoded) {
538
+ if (typeof encoded !== "string" || encoded.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(encoded)) {
539
+ throw new HttpEngineError("UQA response body is not valid JSON");
540
+ }
541
+ const bytes = new Uint8Array(encoded.length / 2);
542
+ for (let index = 0; index < bytes.length; index += 1) {
543
+ bytes[index] = Number.parseInt(encoded.slice(index * 2, index * 2 + 2), 16);
544
+ }
545
+ return bytes;
546
+ }
547
+
548
+ function decodeHTTPValue(value) {
549
+ if (Array.isArray(value)) {
550
+ return value.map(decodeHTTPValue);
551
+ }
552
+ if (typeof value === "number") {
553
+ if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))) {
554
+ throw new HttpEngineError("UQA response integer exceeds the browser safe range");
555
+ }
556
+ return value;
557
+ }
558
+ if (value === null || typeof value !== "object") {
559
+ return value;
560
+ }
561
+ const kind = value.$uqa_type;
562
+ if (kind === "bytes" && exactHTTPObject(value, ["$uqa_type", "hex"])
563
+ && validHTTPHex(value.hex)) {
564
+ return hexToBytes(value.hex);
565
+ }
566
+ if (kind === "decimal" && canonicalHTTPDecimal(value.value)) {
567
+ return value.value;
568
+ }
569
+ if (kind === "fixed_char" && exactHTTPObject(value, ["$uqa_type", "value"])
570
+ && typeof value.value === "string") {
571
+ return value.value;
572
+ }
573
+ if ((kind === "json" || kind === "jsonb")
574
+ && exactHTTPObject(value, ["$uqa_type", "value"])
575
+ && typeof value.value === "string") {
576
+ try {
577
+ return validateHTTPJSONDocument(JSON.parse(value.value));
578
+ } catch {
579
+ throw new HttpEngineError("UQA response body is not valid JSON");
580
+ }
581
+ }
582
+ if (kind === "array" && validHTTPTaggedArrayShape(value) !== undefined) {
583
+ return value.values.map(decodeHTTPValue);
584
+ }
585
+ if (kind === "row" && exactHTTPObject(value, ["$uqa_type", "values"])
586
+ && Array.isArray(value.values)) {
587
+ return value.values.map(decodeHTTPValue);
588
+ }
589
+ if (kind === "record" && exactHTTPObject(value, ["$uqa_type", "fields"])
590
+ && Array.isArray(value.fields)
591
+ && value.fields.every((field) => Array.isArray(field)
592
+ && field.length === 2 && typeof field[0] === "string")) {
593
+ return Object.fromEntries(value.fields.map(([key, item]) => [key, decodeHTTPValue(item)]));
594
+ }
595
+ if (kind === "date" && exactHTTPObject(value, ["$uqa_type", "days"])
596
+ && isHTTPInt32(value.days)) {
597
+ return formatHTTPDate(value.days);
598
+ }
599
+ if (kind === "time" && exactHTTPObject(value, ["$uqa_type", "micros"])
600
+ && Number.isSafeInteger(value.micros)) {
601
+ return formatHTTPTime(value.micros);
602
+ }
603
+ if (kind === "time_tz"
604
+ && exactHTTPObject(value, ["$uqa_type", "micros", "offset_minutes"])
605
+ && Number.isSafeInteger(value.micros) && isHTTPInt32(value.offset_minutes)) {
606
+ return `${formatHTTPTime(value.micros)}${formatHTTPOffset(value.offset_minutes)}`;
607
+ }
608
+ if ((kind === "timestamp" || kind === "timestamp_tz")
609
+ && exactHTTPObject(value, ["$uqa_type", "micros"])
610
+ && Number.isSafeInteger(value.micros)) {
611
+ return formatHTTPTimestamp(value.micros, kind === "timestamp_tz");
612
+ }
613
+ if (kind === "interval"
614
+ && exactHTTPObject(value, ["$uqa_type", "months", "days", "micros"])
615
+ && isHTTPInt32(value.months) && isHTTPInt32(value.days)
616
+ && Number.isSafeInteger(value.micros)) {
617
+ return formatHTTPInterval(value.months, value.days, value.micros);
618
+ }
619
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, decodeHTTPValue(item)]));
620
+ }
621
+
622
+ function exactHTTPObject(value, keys) {
623
+ const actual = Object.keys(value);
624
+ return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
625
+ }
626
+
627
+ function validHTTPHex(value) {
628
+ return typeof value === "string" && value.length % 2 === 0 && /^[0-9a-f]*$/i.test(value);
629
+ }
630
+
631
+ function canonicalHTTPDecimal(value) {
632
+ return typeof value === "string"
633
+ && /^(?:NaN|Infinity|-Infinity|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?)$/.test(value)
634
+ && !/^-0(?:\.0+)?$/.test(value);
635
+ }
636
+
637
+ function isHTTPInt32(value) {
638
+ return Number.isInteger(value) && value >= -2_147_483_648 && value <= 2_147_483_647;
639
+ }
640
+
641
+ function validHTTPTaggedArrayShape(value) {
642
+ if (value === null || typeof value !== "object" || Array.isArray(value)
643
+ || value.$uqa_type !== "array"
644
+ || !exactHTTPObject(value, ["$uqa_type", "lower_bounds", "values"])
645
+ || !Array.isArray(value.lower_bounds)
646
+ || !value.lower_bounds.every(isHTTPInt32)
647
+ || !Array.isArray(value.values)) {
648
+ return undefined;
649
+ }
650
+ const shape = httpArrayShape(value.values);
651
+ if (shape === undefined) {
652
+ return undefined;
653
+ }
654
+ const normalizedShape = shape[0] === 0 ? [] : shape;
655
+ return normalizedShape.length === value.lower_bounds.length ? shape : undefined;
656
+ }
657
+
658
+ function httpArrayShape(values) {
659
+ const dimensions = [values.length];
660
+ let nestedShape;
661
+ let hasScalar = false;
662
+ for (const value of values) {
663
+ const shape = Array.isArray(value)
664
+ ? httpArrayShape(value)
665
+ : validHTTPTaggedArrayShape(value);
666
+ if (shape === undefined) {
667
+ if (nestedShape !== undefined) {
668
+ return undefined;
669
+ }
670
+ hasScalar = true;
671
+ continue;
672
+ }
673
+ if (hasScalar || (nestedShape !== undefined && !sameHTTPShape(nestedShape, shape))) {
674
+ return undefined;
675
+ }
676
+ nestedShape = shape;
677
+ }
678
+ if (nestedShape !== undefined) {
679
+ dimensions.push(...nestedShape);
680
+ }
681
+ return dimensions;
682
+ }
683
+
684
+ function sameHTTPShape(left, right) {
685
+ return left.length === right.length && left.every((value, index) => value === right[index]);
686
+ }
687
+
688
+ function validateHTTPJSONDocument(value) {
689
+ if (Array.isArray(value)) {
690
+ for (const item of value) {
691
+ validateHTTPJSONDocument(item);
692
+ }
693
+ return value;
694
+ }
695
+ if (typeof value === "number") {
696
+ if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))) {
697
+ throw new HttpEngineError("UQA response integer exceeds the browser safe range");
698
+ }
699
+ return value;
700
+ }
701
+ if (value !== null && typeof value === "object") {
702
+ for (const item of Object.values(value)) {
703
+ validateHTTPJSONDocument(item);
704
+ }
705
+ }
706
+ return value;
707
+ }
708
+
709
+ function requireSafeHTTPInteger(value) {
710
+ if (!Number.isSafeInteger(value)) {
711
+ throw new HttpEngineError("UQA response integer exceeds the browser safe range");
712
+ }
713
+ return value;
714
+ }
715
+
716
+ function formatHTTPDate(days) {
717
+ requireSafeHTTPInteger(days);
718
+ const date = new Date(days * 86_400_000);
719
+ if (Number.isNaN(date.valueOf())) {
720
+ return String(days);
721
+ }
722
+ const year = date.getUTCFullYear();
723
+ if (year < -262_143 || year > 262_142) {
724
+ return String(days);
725
+ }
726
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
727
+ const day = String(date.getUTCDate()).padStart(2, "0");
728
+ return `${formatHTTPYear(year)}-${month}-${day}`;
729
+ }
730
+
731
+ function formatHTTPYear(year) {
732
+ const magnitude = String(Math.abs(year)).padStart(4, "0");
733
+ return year < 0 ? `-${magnitude}` : year > 9_999 ? `+${magnitude}` : magnitude;
734
+ }
735
+
736
+ function formatHTTPTime(source) {
737
+ const day = 86_400_000_000;
738
+ const micros = ((requireSafeHTTPInteger(source) % day) + day) % day;
739
+ const hours = Math.floor(micros / 3_600_000_000);
740
+ const minutes = Math.floor((micros % 3_600_000_000) / 60_000_000);
741
+ const seconds = Math.floor((micros % 60_000_000) / 1_000_000);
742
+ const fraction = micros % 1_000_000;
743
+ let output = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
744
+ if (fraction !== 0) {
745
+ output += `.${String(fraction).padStart(6, "0").replace(/0+$/, "")}`;
746
+ }
747
+ return output;
748
+ }
749
+
750
+ function formatHTTPOffset(source) {
751
+ const minutes = requireSafeHTTPInteger(source);
752
+ const sign = minutes < 0 ? "-" : "+";
753
+ const absolute = Math.abs(minutes);
754
+ return `${sign}${String(Math.floor(absolute / 60)).padStart(2, "0")}:${String(absolute % 60).padStart(2, "0")}`;
755
+ }
756
+
757
+ function formatHTTPTimestamp(source, utc) {
758
+ const micros = requireSafeHTTPInteger(source);
759
+ const date = new Date(Math.floor(micros / 1000));
760
+ if (Number.isNaN(date.valueOf())) {
761
+ return String(micros);
762
+ }
763
+ const year = date.getUTCFullYear();
764
+ if (year < -262_143 || year > 262_142) {
765
+ return String(micros);
766
+ }
767
+ const datePart = `${formatHTTPYear(year)}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
768
+ const timePart = [date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()]
769
+ .map((value) => String(value).padStart(2, "0"))
770
+ .join(":");
771
+ let output = `${datePart} ${timePart}`;
772
+ const fraction = ((micros % 1_000_000) + 1_000_000) % 1_000_000;
773
+ if (fraction !== 0) {
774
+ output += `.${String(fraction).padStart(6, "0").replace(/0+$/, "")}`;
775
+ }
776
+ return utc ? `${output}+00` : output;
777
+ }
778
+
779
+ function formatHTTPInterval(monthsSource, daysSource, microsSource) {
780
+ const monthsTotal = requireSafeHTTPInteger(monthsSource);
781
+ const days = requireSafeHTTPInteger(daysSource);
782
+ const micros = requireSafeHTTPInteger(microsSource);
783
+ const fields = [];
784
+ let negativeFieldSeen = false;
785
+ const years = Math.trunc(monthsTotal / 12);
786
+ const months = monthsTotal % 12;
787
+ for (const [value, singular] of [[years, "year"], [months, "mon"], [days, "day"]]) {
788
+ if (value !== 0) {
789
+ const sign = negativeFieldSeen && value > 0 ? "+" : "";
790
+ fields.push(`${sign}${value} ${singular}${value === 1 ? "" : "s"}`);
791
+ negativeFieldSeen ||= value < 0;
792
+ }
793
+ }
794
+ if (micros !== 0 || fields.length === 0) {
795
+ const sign = micros < 0 ? "-" : negativeFieldSeen ? "+" : "";
796
+ const absolute = Math.abs(micros);
797
+ const hours = Math.floor(absolute / 3_600_000_000);
798
+ const minutes = Math.floor((absolute % 3_600_000_000) / 60_000_000);
799
+ const seconds = Math.floor((absolute % 60_000_000) / 1_000_000);
800
+ const fraction = absolute % 1_000_000;
801
+ let time = `${sign}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
802
+ if (fraction !== 0) {
803
+ time += `.${String(fraction).padStart(6, "0").replace(/0+$/, "")}`;
804
+ }
805
+ fields.push(time);
806
+ }
807
+ return fields.join(" ");
808
+ }
809
+
810
+ function validateHTTPContentType(response, expected) {
811
+ const actual = response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
812
+ if (actual !== expected) {
813
+ throw new HttpEngineError("UQA response content type is invalid");
814
+ }
815
+ }
816
+
817
+ function httpRequestId(response) {
818
+ const requestId = response.headers.get("x-request-id");
819
+ if (requestId === null || requestId === "") {
820
+ throw new HttpEngineError("UQA response is missing its request ID");
821
+ }
822
+ return requestId;
823
+ }
824
+
825
+ async function readBoundedHTTPBody(response, maximumBytes) {
826
+ const declared = response.headers.get("content-length");
827
+ if (declared !== null && Number(declared) > maximumBytes) {
828
+ throw new HttpEngineError("UQA response exceeded the client safety limit");
829
+ }
830
+ if (response.body === null) {
831
+ return new Uint8Array();
832
+ }
833
+ const reader = response.body.getReader();
834
+ const chunks = [];
835
+ let length = 0;
836
+ for (;;) {
837
+ const { value, done } = await readHTTPChunk(reader);
838
+ if (done) {
839
+ break;
840
+ }
841
+ length += value.byteLength;
842
+ if (length > maximumBytes) {
843
+ try {
844
+ await reader.cancel();
845
+ } catch {
846
+ // The bounded error remains the stable public diagnostic.
847
+ }
848
+ throw new HttpEngineError("UQA response exceeded the client safety limit");
849
+ }
850
+ chunks.push(value);
851
+ }
852
+ const body = new Uint8Array(length);
853
+ let offset = 0;
854
+ for (const chunk of chunks) {
855
+ body.set(chunk, offset);
856
+ offset += chunk.byteLength;
857
+ }
858
+ return body;
859
+ }
860
+
861
+ async function readHTTPChunk(reader) {
862
+ try {
863
+ return await reader.read();
864
+ } catch {
865
+ throw new HttpEngineError("UQA HTTP transport failed");
866
+ }
867
+ }
868
+
869
+ async function decodeHTTPJSONResponse(response) {
870
+ const requestId = httpRequestId(response);
871
+ validateHTTPContentType(response, "application/json");
872
+ const maximumBytes = response.ok ? MAX_HTTP_JSON_BYTES : MAX_HTTP_ERROR_BYTES;
873
+ const bytes = await readBoundedHTTPBody(response, maximumBytes);
874
+ let body;
875
+ try {
876
+ body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
877
+ } catch {
878
+ throw new HttpEngineError("UQA response body is not valid JSON");
879
+ }
880
+ if (!response.ok) {
881
+ const code = typeof body?.error?.code === "string" ? body.error.code : "HTTP_ERROR";
882
+ if (body?.request_id !== undefined && body.request_id !== requestId) {
883
+ throw new HttpEngineError("UQA response request IDs do not match");
884
+ }
885
+ throw new HttpEngineError(`UQA returned ${response.status} with code ${code}`, {
886
+ code,
887
+ status: response.status,
888
+ requestId,
889
+ });
890
+ }
891
+ if (body?.request_id !== requestId) {
892
+ throw new HttpEngineError("UQA response request IDs do not match");
893
+ }
894
+ return { body, requestId };
895
+ }
896
+
897
+ function decodeHTTPSQLResult(body) {
898
+ if (!Array.isArray(body?.columns) || !Array.isArray(body?.rows)
899
+ || !body.columns.every((column) => typeof column === "string")
900
+ || !Number.isSafeInteger(body?.affected_rows) || body.affected_rows < 0) {
901
+ throw new HttpEngineError("UQA response body is not valid JSON");
902
+ }
903
+ return {
904
+ columns: body.columns,
905
+ rows: body.rows.map(decodeHTTPRow),
906
+ affectedRows: body.affected_rows,
907
+ };
908
+ }
909
+
910
+ function decodeHTTPRow(row) {
911
+ if (row === null || Array.isArray(row) || typeof row !== "object") {
912
+ throw new HttpEngineError("UQA response body is not valid JSON");
913
+ }
914
+ const decoded = {};
915
+ for (const [key, value] of Object.entries(row)) {
916
+ defineOwnValue(decoded, key, decodeHTTPValue(value));
917
+ }
918
+ return decoded;
919
+ }
920
+
921
+ /** Direct authenticated SQL over the local or Cloud UQA HTTP data plane. */
922
+ export class HttpEngine {
923
+ #baseURL;
924
+
925
+ #token;
926
+
927
+ constructor(url, token) {
928
+ this.#baseURL = httpBaseURL(url);
929
+ if (typeof token !== "string" || token.length === 0) {
930
+ throw new HttpEngineError("UQA project token must not be empty");
931
+ }
932
+ this.#token = token;
933
+ }
934
+
935
+ static fromEnv(environment = globalThis.process?.env) {
936
+ if (environment?.UQA_URL === undefined) {
937
+ throw new HttpEngineError("required UQA connection environment variable UQA_URL is missing");
938
+ }
939
+ if (environment?.UQA_TOKEN === undefined) {
940
+ throw new HttpEngineError("required UQA connection environment variable UQA_TOKEN is missing");
941
+ }
942
+ return new HttpEngine(environment.UQA_URL, environment.UQA_TOKEN);
943
+ }
944
+
945
+ async #request(path, body, accept = "application/json") {
946
+ if (typeof globalThis.fetch !== "function") {
947
+ throw new HttpEngineError("Fetch API is unavailable in this JavaScript runtime");
948
+ }
949
+ let response;
950
+ try {
951
+ response = await globalThis.fetch(new URL(path, this.#baseURL), {
952
+ method: "POST",
953
+ headers: {
954
+ accept,
955
+ authorization: `Bearer ${this.#token}`,
956
+ "content-type": "application/json",
957
+ },
958
+ body: JSON.stringify(body),
959
+ cache: "no-store",
960
+ credentials: "omit",
961
+ redirect: "error",
962
+ referrerPolicy: "no-referrer",
963
+ });
964
+ } catch {
965
+ throw new HttpEngineError("UQA HTTP transport failed");
966
+ }
967
+ return response;
968
+ }
969
+
970
+ async sql(query, params) {
971
+ return (await this.sqlWithMetadata(query, params)).result;
972
+ }
973
+
974
+ async sqlWithMetadata(query, params) {
975
+ const response = await this.#request("v1/sql", encodeHTTPStatement(query, params));
976
+ const { body, requestId } = await decodeHTTPJSONResponse(response);
977
+ return { result: decodeHTTPSQLResult(body), requestId };
978
+ }
979
+
980
+ async sqlBatch(statements) {
981
+ return (await this.sqlBatchWithMetadata(statements)).results;
982
+ }
983
+
984
+ async sqlBatchWithMetadata(statements) {
985
+ const encoded = statements.map(([query, params]) => encodeHTTPStatement(query, params));
986
+ const response = await this.#request("v1/sql/batch", { statements: encoded });
987
+ const { body, requestId } = await decodeHTTPJSONResponse(response);
988
+ if (!Array.isArray(body.results)) {
989
+ throw new HttpEngineError("UQA response body is not valid JSON");
990
+ }
991
+ return { results: body.results.map(decodeHTTPSQLResult), requestId };
992
+ }
993
+
994
+ async sqlStream(query, params) {
995
+ const response = await this.#request(
996
+ "v1/sql/stream",
997
+ encodeHTTPStatement(query, params),
998
+ "application/x-ndjson",
999
+ );
1000
+ if (!response.ok) {
1001
+ await decodeHTTPJSONResponse(response);
1002
+ }
1003
+ validateHTTPContentType(response, "application/x-ndjson");
1004
+ const requestId = httpRequestId(response);
1005
+ return new HttpSQLStream(response, requestId);
1006
+ }
1007
+ }
1008
+
1009
+ /** Incremental reader for one authenticated UQA NDJSON SQL response. */
1010
+ export class HttpSQLStream {
1011
+ constructor(response, requestId) {
1012
+ if (response.body === null) {
1013
+ throw new HttpEngineError("UQA NDJSON stream ended before a terminal frame");
1014
+ }
1015
+ this.reader = response.body.getReader();
1016
+ this.requestId = requestId;
1017
+ this.chunks = [];
1018
+ this.bufferedBytes = 0;
1019
+ this.newlineOffset = null;
1020
+ this.phase = "metadata";
1021
+ this.bodyFinished = false;
1022
+ }
1023
+
1024
+ async nextFrame() {
1025
+ for (;;) {
1026
+ if (this.phase === "finished") {
1027
+ return null;
1028
+ }
1029
+ if (this.phase === "terminal") {
1030
+ return this.#finish();
1031
+ }
1032
+ const bufferedLine = this.#takeLine();
1033
+ if (bufferedLine !== null) {
1034
+ const line = stripHTTPStreamCR(bufferedLine);
1035
+ if (line.byteLength === 0) {
1036
+ continue;
1037
+ }
1038
+ return this.decodeFrame(line);
1039
+ }
1040
+ if (this.bodyFinished) {
1041
+ if (this.bufferedBytes === 0) {
1042
+ throw new HttpEngineError("UQA NDJSON stream ended before a terminal frame");
1043
+ }
1044
+ const line = stripHTTPStreamCR(this.#takeRemainder());
1045
+ if (line.byteLength === 0) {
1046
+ continue;
1047
+ }
1048
+ return this.decodeFrame(line);
1049
+ }
1050
+ await this.#readChunk();
1051
+ }
1052
+ }
1053
+
1054
+ async #finish() {
1055
+ for (;;) {
1056
+ const bufferedLine = this.#takeLine();
1057
+ if (bufferedLine !== null) {
1058
+ if (stripHTTPStreamCR(bufferedLine).byteLength !== 0) {
1059
+ throw new HttpEngineError("UQA NDJSON stream frame order is invalid");
1060
+ }
1061
+ continue;
1062
+ }
1063
+ if (this.bodyFinished) {
1064
+ if (stripHTTPStreamCR(this.#takeRemainder()).byteLength !== 0) {
1065
+ throw new HttpEngineError("UQA NDJSON stream frame order is invalid");
1066
+ }
1067
+ this.phase = "finished";
1068
+ return null;
1069
+ }
1070
+ await this.#readChunk();
1071
+ }
1072
+ }
1073
+
1074
+ async #readChunk() {
1075
+ const { value, done } = await readHTTPChunk(this.reader);
1076
+ if (done) {
1077
+ this.bodyFinished = true;
1078
+ return;
1079
+ }
1080
+ if (value.byteLength === 0) {
1081
+ return;
1082
+ }
1083
+ if (this.newlineOffset === null) {
1084
+ const newline = value.indexOf(10);
1085
+ if (newline !== -1) {
1086
+ this.newlineOffset = this.bufferedBytes + newline;
1087
+ }
1088
+ }
1089
+ this.chunks.push(value);
1090
+ this.bufferedBytes += value.byteLength;
1091
+ this.#validateBufferedFrameSize();
1092
+ }
1093
+
1094
+ #takeLine() {
1095
+ if (this.newlineOffset === null) {
1096
+ return null;
1097
+ }
1098
+ const lineLength = this.newlineOffset;
1099
+ if (lineLength > MAX_HTTP_STREAM_FRAME_BYTES) {
1100
+ throw new HttpEngineError("UQA NDJSON stream frame exceeded the client safety limit");
1101
+ }
1102
+ const line = new Uint8Array(lineLength);
1103
+ let lineOffset = 0;
1104
+ let remaining = lineLength + 1;
1105
+ while (remaining !== 0) {
1106
+ const chunk = this.chunks.shift();
1107
+ const consumed = Math.min(remaining, chunk.byteLength);
1108
+ const copied = Math.min(consumed, lineLength - lineOffset);
1109
+ if (copied !== 0) {
1110
+ line.set(chunk.subarray(0, copied), lineOffset);
1111
+ lineOffset += copied;
1112
+ }
1113
+ if (consumed !== chunk.byteLength) {
1114
+ this.chunks.unshift(chunk.subarray(consumed));
1115
+ }
1116
+ this.bufferedBytes -= consumed;
1117
+ remaining -= consumed;
1118
+ }
1119
+ this.#indexNewline();
1120
+ this.#validateBufferedFrameSize();
1121
+ return line;
1122
+ }
1123
+
1124
+ #takeRemainder() {
1125
+ const output = new Uint8Array(this.bufferedBytes);
1126
+ let offset = 0;
1127
+ for (const chunk of this.chunks) {
1128
+ output.set(chunk, offset);
1129
+ offset += chunk.byteLength;
1130
+ }
1131
+ this.chunks = [];
1132
+ this.bufferedBytes = 0;
1133
+ this.newlineOffset = null;
1134
+ return output;
1135
+ }
1136
+
1137
+ #indexNewline() {
1138
+ this.newlineOffset = null;
1139
+ let offset = 0;
1140
+ for (const chunk of this.chunks) {
1141
+ const newline = chunk.indexOf(10);
1142
+ if (newline !== -1) {
1143
+ this.newlineOffset = offset + newline;
1144
+ return;
1145
+ }
1146
+ offset += chunk.byteLength;
1147
+ }
1148
+ }
1149
+
1150
+ #validateBufferedFrameSize() {
1151
+ const frameBytes = this.newlineOffset ?? this.bufferedBytes;
1152
+ if (frameBytes > MAX_HTTP_STREAM_FRAME_BYTES) {
1153
+ throw new HttpEngineError("UQA NDJSON stream frame exceeded the client safety limit");
1154
+ }
1155
+ }
1156
+
1157
+ decodeFrame(line) {
1158
+ let frame;
1159
+ try {
1160
+ frame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(line));
1161
+ } catch {
1162
+ throw new HttpEngineError("UQA response body is not valid JSON");
1163
+ }
1164
+ if (frame.request_id !== undefined && frame.request_id !== this.requestId) {
1165
+ throw new HttpEngineError("UQA NDJSON stream request ID does not match its HTTP response");
1166
+ }
1167
+ if (this.phase === "metadata" && frame.type === "metadata") {
1168
+ if (!Array.isArray(frame.columns)
1169
+ || !frame.columns.every((column) => typeof column === "string")
1170
+ || !Number.isSafeInteger(frame.row_count) || frame.row_count < 0
1171
+ || typeof frame.spilled_to_disk !== "boolean"
1172
+ || typeof frame.request_id !== "string" || frame.request_id === "") {
1173
+ throw new HttpEngineError("UQA response body is not valid JSON");
1174
+ }
1175
+ this.phase = "rows";
1176
+ return {
1177
+ type: "metadata",
1178
+ columns: frame.columns,
1179
+ rowCount: frame.row_count,
1180
+ spilledToDisk: frame.spilled_to_disk,
1181
+ requestId: frame.request_id,
1182
+ };
1183
+ }
1184
+ if (this.phase === "rows" && frame.type === "row") {
1185
+ return { type: "row", row: decodeHTTPRow(frame.row) };
1186
+ }
1187
+ if ((this.phase === "metadata" || this.phase === "rows") && frame.type === "error") {
1188
+ if (typeof frame.code !== "string" || typeof frame.message !== "string"
1189
+ || typeof frame.request_id !== "string" || frame.request_id === "") {
1190
+ throw new HttpEngineError("UQA response body is not valid JSON");
1191
+ }
1192
+ this.phase = "terminal";
1193
+ return {
1194
+ type: "error",
1195
+ code: frame.code,
1196
+ message: frame.message,
1197
+ requestId: frame.request_id,
1198
+ };
1199
+ }
1200
+ if (this.phase === "rows" && frame.type === "complete") {
1201
+ if (!Number.isSafeInteger(frame.row_count) || frame.row_count < 0
1202
+ || typeof frame.request_id !== "string" || frame.request_id === "") {
1203
+ throw new HttpEngineError("UQA response body is not valid JSON");
1204
+ }
1205
+ this.phase = "terminal";
1206
+ return { type: "complete", rowCount: frame.row_count, requestId: frame.request_id };
1207
+ }
1208
+ throw new HttpEngineError("UQA NDJSON stream frame order is invalid");
1209
+ }
1210
+
1211
+ async *[Symbol.asyncIterator]() {
1212
+ for (;;) {
1213
+ const frame = await this.nextFrame();
1214
+ if (frame === null) {
1215
+ return;
1216
+ }
1217
+ yield frame;
1218
+ }
1219
+ }
1220
+ }
1221
+
1222
+ function stripHTTPStreamCR(line) {
1223
+ return line.at(-1) === 13 ? line.subarray(0, line.byteLength - 1) : line;
1224
+ }
1225
+
1226
+ /** Tagged SQL parameter (vector / tensor); scalars pass directly. */
1227
+ export class SQLParam {
1228
+ constructor(payload, httpKind = "scalar") {
1229
+ this.payload = payload;
1230
+ Object.defineProperty(this, "httpKind", { value: httpKind });
1231
+ }
1232
+
1233
+ static scalar(value) {
1234
+ const bytes = value instanceof Uint8Array || value instanceof ArrayBuffer;
1235
+ return new SQLParam(encodeValue(value), bytes ? "bytes" : "scalar");
1236
+ }
1237
+
1238
+ static vector(values) {
1239
+ return new SQLParam({ $vector: Array.from(values) }, "vector");
1240
+ }
1241
+
1242
+ static tensor(values) {
1243
+ return new SQLParam({ $tensor: values.map((row) => Array.from(row)) }, "tensor");
1244
+ }
1245
+ }
1246
+
1247
+ export function vector(values) {
1248
+ return SQLParam.vector(values);
1249
+ }
1250
+
1251
+ export function tensor(values) {
1252
+ return SQLParam.tensor(values);
1253
+ }
1254
+
1255
+ /** Namespace for module-wide operations. */
1256
+ export const UQA = {
1257
+ /** Preload the WASM module and restore persisted databases. */
1258
+ async load() {
1259
+ await loadModule();
1260
+ },
1261
+
1262
+ /** Flush every persistent database to IndexedDB; rejects when IndexedDB is unavailable. */
1263
+ async persist() {
1264
+ const module = await loadModule();
1265
+ if (!hasIndexedDB()) {
1266
+ throw new Error("cannot persist UQA databases because IndexedDB is unavailable");
1267
+ }
1268
+ await syncFS(module, false);
1269
+ },
1270
+
1271
+ /** Directory on the virtual filesystem that persists to IndexedDB. */
1272
+ persistDir: PERSIST_DIR,
1273
+
1274
+ async detectDatabaseFile(path) {
1275
+ const module = await loadModule();
1276
+ return rawCall(module, 0, "detectDatabaseFile", { path });
1277
+ },
1278
+ };
1279
+
1280
+ export class Engine {
1281
+ constructor(module, handle, callbackGroup = createCallbackGroup()) {
1282
+ this.module = module;
1283
+ this.handle = handle;
1284
+ this.callbackGroup = callbackGroup;
1285
+ this.closed = false;
1286
+ }
1287
+
1288
+ static async inMemory() {
1289
+ const module = await loadModule();
1290
+ return new Engine(module, rawCall(module, 0, "new", {}));
1291
+ }
1292
+
1293
+ static async open(path) {
1294
+ const module = await loadModule();
1295
+ return new Engine(module, rawCall(module, 0, "open", { path }));
1296
+ }
1297
+
1298
+ static async openAuto(path) {
1299
+ const module = await loadModule();
1300
+ return new Engine(module, rawCall(module, 0, "openAuto", { path }));
1301
+ }
1302
+
1303
+ static async openCompressed(path, options) {
1304
+ const module = await loadModule();
1305
+ return new Engine(module, rawCall(module, 0, "openCompressed", { path, ...options }));
1306
+ }
1307
+
1308
+ call(method, args = {}) {
1309
+ if (this.closed) {
1310
+ throw new Error("engine is closed");
1311
+ }
1312
+ return rawCall(this.module, this.handle, method, args);
1313
+ }
1314
+
1315
+ async newSession() {
1316
+ const handle = this.call("newSession", {});
1317
+ retainCallbackGroup(this.callbackGroup);
1318
+ return new Engine(this.module, handle, this.callbackGroup);
1319
+ }
1320
+
1321
+ async sql(query, params) {
1322
+ return this.call("sql", { query, params: encodeParams(params) });
1323
+ }
1324
+
1325
+ async sqlBatch(statements) {
1326
+ return this.call("sqlBatch", {
1327
+ statements: statements.map(([sql, params]) => [sql, encodeParams(params) ?? []]),
1328
+ });
1329
+ }
1330
+
1331
+ async registerScalarFunction(name, callback, options) {
1332
+ registerSQLCallback(this.callbackGroup, "scalar", name, callback, (callbackId) => {
1333
+ this.call("registerScalarFunction", { name, callbackId, options });
1334
+ });
1335
+ }
1336
+
1337
+ async registerTableFunction(name, callback, options) {
1338
+ registerSQLCallback(this.callbackGroup, "table", name, callback, (callbackId) => {
1339
+ this.call("registerTableFunction", { name, callbackId, options });
1340
+ });
1341
+ }
1342
+
1343
+ async registerAggregateFunction(name, factory, options) {
1344
+ registerSQLCallback(this.callbackGroup, "aggregate", name, factory, (callbackId) => {
1345
+ this.call("registerAggregateFunction", { name, callbackId, options });
1346
+ });
1347
+ }
1348
+
1349
+ async createDefaultTable(name, ftsFields) {
1350
+ return this.call("createDefaultTable", { name, ftsFields });
1351
+ }
1352
+
1353
+ async createVectorField(table, field, dimensions) {
1354
+ return this.call("createVectorField", { table, field, dimensions });
1355
+ }
1356
+
1357
+ async addDocument(table, docId, document) {
1358
+ return this.call("addDocument", { table, docId, document: encodeValue(document) });
1359
+ }
1360
+
1361
+ async addDocumentWithVectors(table, docId, document, vectors) {
1362
+ return this.call("addDocumentWithVectors", {
1363
+ table,
1364
+ docId,
1365
+ document: encodeValue(document),
1366
+ vectors: encodeValue(vectors),
1367
+ });
1368
+ }
1369
+
1370
+ async addVector(table, docId, field, vector) {
1371
+ return this.call("addVector", { table, docId, field, vector: Array.from(vector) });
1372
+ }
1373
+
1374
+ async addVectorValues(table, docId, field, vectors) {
1375
+ return this.call("addVectorValues", {
1376
+ table,
1377
+ docId,
1378
+ field,
1379
+ vectors: vectors.map((row) => Array.from(row)),
1380
+ });
1381
+ }
1382
+
1383
+ async getDocument(table, docId) {
1384
+ return this.call("getDocument", { table, docId });
1385
+ }
1386
+
1387
+ async deleteDocument(table, docId) {
1388
+ return this.call("deleteDocument", { table, docId });
1389
+ }
1390
+
1391
+ async documentCount(table) {
1392
+ return this.call("documentCount", { table });
1393
+ }
1394
+
1395
+ async search(table, field, query, topK, scoring) {
1396
+ return this.call("search", { table, field, query, topK, scoring });
1397
+ }
1398
+
1399
+ async knnSearch(table, field, vector, topK) {
1400
+ return this.call("knnSearch", { table, field, vector: Array.from(vector), topK });
1401
+ }
1402
+
1403
+ async vectorSimilaritySearch(table, field, vector, threshold) {
1404
+ return this.call("vectorSimilaritySearch", {
1405
+ table,
1406
+ field,
1407
+ vector: Array.from(vector),
1408
+ threshold,
1409
+ });
1410
+ }
1411
+
1412
+ async hybridSearch(table, textField, textQuery, vectorField, queryVector, topK, knnPool) {
1413
+ return this.call("hybridSearch", {
1414
+ table,
1415
+ textField,
1416
+ textQuery,
1417
+ vectorField,
1418
+ queryVector: Array.from(queryVector),
1419
+ topK,
1420
+ knnPool,
1421
+ });
1422
+ }
1423
+
1424
+ async robustHybridSearch(table, textField, textQuery, vectorField, queryVector, topK, knnPool, alpha) {
1425
+ return this.call("robustHybridSearch", {
1426
+ table,
1427
+ textField,
1428
+ textQuery,
1429
+ vectorField,
1430
+ queryVector: Array.from(queryVector),
1431
+ topK,
1432
+ knnPool,
1433
+ alpha,
1434
+ });
1435
+ }
1436
+
1437
+ async estimateScoringParams(table, field, nSamples, tokensPerQuery, seed) {
1438
+ return this.call("estimateScoringParams", { table, field, nSamples, tokensPerQuery, seed });
1439
+ }
1440
+
1441
+ async learnScoringParams(table, field, query, labels) {
1442
+ return this.call("learnScoringParams", { table, field, query, labels });
1443
+ }
1444
+
1445
+ async updateScoringParams(table, field, score, label) {
1446
+ return this.call("updateScoringParams", { table, field, score, label });
1447
+ }
1448
+
1449
+ async calibrationReport(table, field, query, labels) {
1450
+ return this.call("calibrationReport", { table, field, query, labels });
1451
+ }
1452
+
1453
+ async saveScoringParams(name, params) {
1454
+ return this.call("saveScoringParams", { name, params });
1455
+ }
1456
+
1457
+ async loadScoringParams(name) {
1458
+ return this.call("loadScoringParams", { name });
1459
+ }
1460
+
1461
+ async loadAllScoringParams() {
1462
+ return this.call("loadAllScoringParams", {});
1463
+ }
1464
+
1465
+ async dropScoringParams(name) {
1466
+ return this.call("dropScoringParams", { name });
1467
+ }
1468
+
1469
+ async runCypher(graph, query, params) {
1470
+ return this.call("runCypher", { graph, query, params: encodeValue(params ?? null) });
1471
+ }
1472
+
1473
+ async createGraph(name) {
1474
+ return this.call("createGraph", { name });
1475
+ }
1476
+
1477
+ async dropGraph(name) {
1478
+ return this.call("dropGraph", { name });
1479
+ }
1480
+
1481
+ async listGraphs() {
1482
+ return this.call("listGraphs", {});
1483
+ }
1484
+
1485
+ async listPathIndexes() {
1486
+ return this.call("listPathIndexes", {});
1487
+ }
1488
+
1489
+ async tableNames() {
1490
+ return this.call("tableNames", {});
1491
+ }
1492
+
1493
+ async listViews() {
1494
+ return this.call("listViews", {});
1495
+ }
1496
+
1497
+ async listSchemas() {
1498
+ return this.call("listSchemas", {});
1499
+ }
1500
+
1501
+ async listSequences() {
1502
+ return this.call("listSequences", {});
1503
+ }
1504
+
1505
+ async listNamedAnalyzers() {
1506
+ return this.call("listNamedAnalyzers", {});
1507
+ }
1508
+
1509
+ async listForeignServers() {
1510
+ return this.call("listForeignServers", {});
1511
+ }
1512
+
1513
+ async listForeignTables() {
1514
+ return this.call("listForeignTables", {});
1515
+ }
1516
+
1517
+ async takeSQLNotices() {
1518
+ return this.call("takeSQLNotices", {});
1519
+ }
1520
+
1521
+ async sqlFunctionDepthLimit() {
1522
+ return this.call("sqlFunctionDepthLimit", {});
1523
+ }
1524
+
1525
+ async setSQLFunctionDepthLimit(limit) {
1526
+ return this.call("setSQLFunctionDepthLimit", { limit });
1527
+ }
1528
+
1529
+ async cancel() {
1530
+ return this.call("cancel", {});
1531
+ }
1532
+
1533
+ async close() {
1534
+ if (this.closed) {
1535
+ return;
1536
+ }
1537
+ this.call("close", {});
1538
+ this.closed = true;
1539
+ releaseCallbackGroup(this.callbackGroup);
1540
+ }
1541
+ }
1542
+
1543
+ guardEngineMethods(Engine);