@wiscale/velesdb-sdk 1.3.0 → 1.5.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/dist/index.mjs CHANGED
@@ -25,6 +25,12 @@ var NotFoundError = class extends VelesDBError {
25
25
  this.name = "NotFoundError";
26
26
  }
27
27
  };
28
+ var BackpressureError = class extends VelesDBError {
29
+ constructor(message = "Server backpressure: too many requests") {
30
+ super(message, "BACKPRESSURE");
31
+ this.name = "BackpressureError";
32
+ }
33
+ };
28
34
 
29
35
  // src/backends/wasm.ts
30
36
  var WasmBackend = class {
@@ -56,6 +62,33 @@ var WasmBackend = class {
56
62
  throw new ConnectionError("WASM backend not initialized");
57
63
  }
58
64
  }
65
+ normalizeIdString(id) {
66
+ const trimmed = id.trim();
67
+ return /^\d+$/.test(trimmed) ? trimmed : null;
68
+ }
69
+ canonicalPayloadKeyFromResultId(id) {
70
+ if (typeof id === "bigint") {
71
+ return id.toString();
72
+ }
73
+ if (typeof id === "number") {
74
+ return String(Math.trunc(id));
75
+ }
76
+ const normalized = this.normalizeIdString(id);
77
+ if (normalized !== null) {
78
+ return normalized.replace(/^0+(?=\d)/, "");
79
+ }
80
+ return String(this.toNumericId(id));
81
+ }
82
+ canonicalPayloadKey(id) {
83
+ if (typeof id === "number") {
84
+ return String(Math.trunc(id));
85
+ }
86
+ const normalized = this.normalizeIdString(id);
87
+ if (normalized !== null) {
88
+ return normalized.replace(/^0+(?=\d)/, "");
89
+ }
90
+ return String(this.toNumericId(id));
91
+ }
59
92
  async createCollection(name, config) {
60
93
  this.ensureInitialized();
61
94
  if (this.collections.has(name)) {
@@ -121,9 +154,13 @@ var WasmBackend = class {
121
154
  "DIMENSION_MISMATCH"
122
155
  );
123
156
  }
124
- collection.store.insert(BigInt(id), vector);
125
157
  if (doc.payload) {
126
- collection.payloads.set(String(doc.id), doc.payload);
158
+ collection.store.insert_with_payload(BigInt(id), vector, doc.payload);
159
+ } else {
160
+ collection.store.insert(BigInt(id), vector);
161
+ }
162
+ if (doc.payload) {
163
+ collection.payloads.set(this.canonicalPayloadKey(doc.id), doc.payload);
127
164
  }
128
165
  }
129
166
  async insertBatch(collectionName, docs) {
@@ -133,7 +170,7 @@ var WasmBackend = class {
133
170
  throw new NotFoundError(`Collection '${collectionName}'`);
134
171
  }
135
172
  for (const doc of docs) {
136
- const vectorLen = Array.isArray(doc.vector) ? doc.vector.length : doc.vector.length;
173
+ const vectorLen = doc.vector.length;
137
174
  if (vectorLen !== collection.config.dimension) {
138
175
  throw new VelesDBError(
139
176
  `Vector dimension mismatch for doc ${doc.id}: expected ${collection.config.dimension}, got ${vectorLen}`,
@@ -142,14 +179,22 @@ var WasmBackend = class {
142
179
  }
143
180
  }
144
181
  collection.store.reserve(docs.length);
145
- const batch = docs.map((doc) => [
146
- BigInt(this.toNumericId(doc.id)),
147
- Array.isArray(doc.vector) ? doc.vector : Array.from(doc.vector)
148
- ]);
149
- collection.store.insert_batch(batch);
182
+ const batch = [];
183
+ for (const doc of docs) {
184
+ const id = BigInt(this.toNumericId(doc.id));
185
+ const vector = doc.vector instanceof Float32Array ? doc.vector : new Float32Array(doc.vector);
186
+ if (doc.payload) {
187
+ collection.store.insert_with_payload(id, vector, doc.payload);
188
+ } else {
189
+ batch.push([id, Array.from(vector)]);
190
+ }
191
+ }
192
+ if (batch.length > 0) {
193
+ collection.store.insert_batch(batch);
194
+ }
150
195
  for (const doc of docs) {
151
196
  if (doc.payload) {
152
- collection.payloads.set(String(doc.id), doc.payload);
197
+ collection.payloads.set(this.canonicalPayloadKey(doc.id), doc.payload);
153
198
  }
154
199
  }
155
200
  }
@@ -167,12 +212,43 @@ var WasmBackend = class {
167
212
  );
168
213
  }
169
214
  const k = options?.k ?? 10;
215
+ if (options?.sparseVector) {
216
+ const { indices, values } = this.sparseVectorToArrays(options.sparseVector);
217
+ if (queryVector.length > 0 && collection.config.dimension && collection.config.dimension > 0) {
218
+ const denseResults = collection.store.search(queryVector, k);
219
+ const sparseResults = collection.store.sparse_search(
220
+ new Uint32Array(indices),
221
+ new Float32Array(values),
222
+ k
223
+ );
224
+ const sparseArray = sparseResults;
225
+ const denseForFuse = denseResults.map(([id, score]) => [Number(id), score]);
226
+ const sparseForFuse = sparseArray.map((r) => [Number(r.doc_id), r.score]);
227
+ const fused = this.wasmModule.hybrid_search_fuse(denseForFuse, sparseForFuse, 60);
228
+ return fused.slice(0, k).map((r) => ({
229
+ id: String(r.doc_id),
230
+ score: r.score,
231
+ payload: collection.payloads.get(this.canonicalPayloadKeyFromResultId(r.doc_id))
232
+ }));
233
+ } else {
234
+ const sparseResults = collection.store.sparse_search(
235
+ new Uint32Array(indices),
236
+ new Float32Array(values),
237
+ k
238
+ );
239
+ return sparseResults.map((r) => ({
240
+ id: String(r.doc_id),
241
+ score: r.score,
242
+ payload: collection.payloads.get(this.canonicalPayloadKeyFromResultId(r.doc_id))
243
+ }));
244
+ }
245
+ }
170
246
  if (options?.filter) {
171
247
  const results = collection.store.search_with_filter(queryVector, k, options.filter);
172
248
  return results.map((r) => ({
173
249
  id: String(r.id),
174
250
  score: r.score,
175
- payload: r.payload || collection.payloads.get(String(r.id))
251
+ payload: r.payload || collection.payloads.get(this.canonicalPayloadKeyFromResultId(r.id))
176
252
  }));
177
253
  }
178
254
  const rawResults = collection.store.search(queryVector, k);
@@ -182,7 +258,7 @@ var WasmBackend = class {
182
258
  id: stringId,
183
259
  score
184
260
  };
185
- const payload = collection.payloads.get(stringId);
261
+ const payload = collection.payloads.get(this.canonicalPayloadKeyFromResultId(id));
186
262
  if (payload) {
187
263
  result.payload = payload;
188
264
  }
@@ -206,7 +282,7 @@ var WasmBackend = class {
206
282
  const numericId = this.toNumericId(id);
207
283
  const removed = collection.store.remove(BigInt(numericId));
208
284
  if (removed) {
209
- collection.payloads.delete(String(id));
285
+ collection.payloads.delete(this.canonicalPayloadKey(id));
210
286
  }
211
287
  return removed;
212
288
  }
@@ -216,38 +292,140 @@ var WasmBackend = class {
216
292
  if (!collection) {
217
293
  throw new NotFoundError(`Collection '${collectionName}'`);
218
294
  }
219
- const payload = collection.payloads.get(String(id));
220
- if (!payload) {
295
+ const numericId = this.toNumericId(id);
296
+ const point = collection.store.get(BigInt(numericId));
297
+ if (!point) {
221
298
  return null;
222
299
  }
300
+ const payload = point.payload ?? collection.payloads.get(this.canonicalPayloadKey(numericId));
223
301
  return {
224
- id,
225
- vector: [],
226
- // Not available in current WASM impl
302
+ id: String(point.id),
303
+ vector: Array.isArray(point.vector) ? point.vector : Array.from(point.vector),
227
304
  payload
228
305
  };
229
306
  }
230
307
  async textSearch(_collection, _query, _options) {
231
- throw new VelesDBError(
232
- "Text search is not supported in WASM backend. Use REST backend for BM25 search.",
233
- "NOT_SUPPORTED"
234
- );
308
+ this.ensureInitialized();
309
+ const collection = this.collections.get(_collection);
310
+ if (!collection) {
311
+ throw new NotFoundError(`Collection '${_collection}'`);
312
+ }
313
+ const k = _options?.k ?? 10;
314
+ const field = void 0;
315
+ const raw = collection.store.text_search(_query, k, field);
316
+ return raw.map((r) => {
317
+ if (Array.isArray(r)) {
318
+ const key2 = this.canonicalPayloadKeyFromResultId(r[0]);
319
+ return { id: String(r[0]), score: r[1], payload: collection.payloads.get(key2) };
320
+ }
321
+ const key = this.canonicalPayloadKeyFromResultId(r.id);
322
+ return { id: String(r.id), score: r.score, payload: r.payload ?? collection.payloads.get(key) };
323
+ });
235
324
  }
236
325
  async hybridSearch(_collection, _vector, _textQuery, _options) {
237
- throw new VelesDBError(
238
- "Hybrid search is not supported in WASM backend. Use REST backend for hybrid search.",
239
- "NOT_SUPPORTED"
326
+ this.ensureInitialized();
327
+ const collection = this.collections.get(_collection);
328
+ if (!collection) {
329
+ throw new NotFoundError(`Collection '${_collection}'`);
330
+ }
331
+ const queryVector = _vector instanceof Float32Array ? _vector : new Float32Array(_vector);
332
+ const k = _options?.k ?? 10;
333
+ const vectorWeight = _options?.vectorWeight ?? 0.5;
334
+ const raw = collection.store.hybrid_search(queryVector, _textQuery, k, vectorWeight);
335
+ return raw.map((r) => {
336
+ const key = this.canonicalPayloadKeyFromResultId(r.id);
337
+ return {
338
+ id: String(r.id),
339
+ score: r.score,
340
+ payload: r.payload ?? collection.payloads.get(key)
341
+ };
342
+ });
343
+ }
344
+ async query(_collection, _queryString, _params, _options) {
345
+ this.ensureInitialized();
346
+ const collection = this.collections.get(_collection);
347
+ if (!collection) {
348
+ throw new NotFoundError(`Collection '${_collection}'`);
349
+ }
350
+ const paramsVector = _params?.q;
351
+ if (!Array.isArray(paramsVector) && !(paramsVector instanceof Float32Array)) {
352
+ throw new VelesDBError(
353
+ "WASM query() expects params.q to contain the query embedding vector.",
354
+ "BAD_REQUEST"
355
+ );
356
+ }
357
+ const requestedK = _params?.k;
358
+ const k = typeof requestedK === "number" && Number.isInteger(requestedK) && requestedK > 0 ? requestedK : 10;
359
+ const raw = collection.store.query(
360
+ paramsVector instanceof Float32Array ? paramsVector : new Float32Array(paramsVector),
361
+ k
240
362
  );
363
+ return {
364
+ results: raw.map((r) => ({
365
+ nodeId: r.nodeId ?? r.node_id,
366
+ vectorScore: r.vectorScore ?? r.vector_score ?? null,
367
+ graphScore: r.graphScore ?? r.graph_score ?? null,
368
+ fusedScore: r.fusedScore ?? r.fused_score ?? 0,
369
+ bindings: r.bindings ?? {},
370
+ columnData: r.columnData ?? r.column_data ?? null
371
+ })),
372
+ stats: {
373
+ executionTimeMs: 0,
374
+ strategy: "wasm-query",
375
+ scannedNodes: raw.length
376
+ }
377
+ };
241
378
  }
242
- async query(_queryString, _params) {
379
+ async multiQuerySearch(_collection, _vectors, _options) {
380
+ this.ensureInitialized();
381
+ const collection = this.collections.get(_collection);
382
+ if (!collection) {
383
+ throw new NotFoundError(`Collection '${_collection}'`);
384
+ }
385
+ if (_vectors.length === 0) {
386
+ return [];
387
+ }
388
+ const numVectors = _vectors.length;
389
+ const dimension = collection.config.dimension ?? 0;
390
+ const flat = new Float32Array(numVectors * dimension);
391
+ _vectors.forEach((vector, idx) => {
392
+ const src = vector instanceof Float32Array ? vector : new Float32Array(vector);
393
+ flat.set(src, idx * dimension);
394
+ });
395
+ const strategy = _options?.fusion ?? "rrf";
396
+ if (strategy === "weighted") {
397
+ throw new VelesDBError(
398
+ "Fusion strategy 'weighted' is not supported in WASM backend.",
399
+ "NOT_SUPPORTED"
400
+ );
401
+ }
402
+ const raw = collection.store.multi_query_search(
403
+ flat,
404
+ numVectors,
405
+ _options?.k ?? 10,
406
+ strategy,
407
+ _options?.fusionParams?.k ?? 60
408
+ );
409
+ return raw.map((r) => {
410
+ if (Array.isArray(r)) {
411
+ const key2 = this.canonicalPayloadKeyFromResultId(r[0]);
412
+ return { id: String(r[0]), score: r[1], payload: collection.payloads.get(key2) };
413
+ }
414
+ const key = this.canonicalPayloadKeyFromResultId(r.id);
415
+ return { id: String(r.id), score: r.score, payload: r.payload ?? collection.payloads.get(key) };
416
+ });
417
+ }
418
+ async queryExplain(_queryString, _params) {
419
+ this.ensureInitialized();
243
420
  throw new VelesDBError(
244
- "VelesQL queries are not supported in WASM backend. Use REST backend for query support.",
421
+ "Query explain is not supported in WASM backend. Use REST backend for EXPLAIN support.",
245
422
  "NOT_SUPPORTED"
246
423
  );
247
424
  }
248
- async multiQuerySearch(_collection, _vectors, _options) {
425
+ async collectionSanity(_collection) {
426
+ this.ensureInitialized();
249
427
  throw new VelesDBError(
250
- "Multi-query fusion is not supported in WASM backend. Use REST backend for MQF search.",
428
+ "Collection sanity endpoint is not supported in WASM backend. Use REST backend for diagnostics.",
251
429
  "NOT_SUPPORTED"
252
430
  );
253
431
  }
@@ -273,13 +451,25 @@ var WasmBackend = class {
273
451
  this.collections.clear();
274
452
  this._initialized = false;
275
453
  }
454
+ sparseVectorToArrays(sv) {
455
+ const indices = [];
456
+ const values = [];
457
+ for (const [k, v] of Object.entries(sv)) {
458
+ indices.push(Number(k));
459
+ values.push(v);
460
+ }
461
+ return { indices, values };
462
+ }
276
463
  toNumericId(id) {
277
464
  if (typeof id === "number") {
278
465
  return id;
279
466
  }
280
- const parsed = parseInt(id, 10);
281
- if (!isNaN(parsed)) {
282
- return parsed;
467
+ const normalized = this.normalizeIdString(id);
468
+ if (normalized !== null) {
469
+ const parsed = Number(normalized);
470
+ if (Number.isSafeInteger(parsed)) {
471
+ return parsed;
472
+ }
283
473
  }
284
474
  let hash = 0;
285
475
  for (let i = 0; i < id.length; i++) {
@@ -343,6 +533,23 @@ var WasmBackend = class {
343
533
  "NOT_SUPPORTED"
344
534
  );
345
535
  }
536
+ // ========================================================================
537
+ // Sparse / PQ / Streaming (v1.5)
538
+ // ========================================================================
539
+ async trainPq(_collection, _options) {
540
+ this.ensureInitialized();
541
+ throw new VelesDBError(
542
+ "PQ training is not available in WASM mode. Use REST backend for PQ training.",
543
+ "NOT_SUPPORTED"
544
+ );
545
+ }
546
+ async streamInsert(_collection, _docs) {
547
+ this.ensureInitialized();
548
+ throw new VelesDBError(
549
+ "Streaming insert is not available in WASM mode. Use REST backend for streaming.",
550
+ "NOT_SUPPORTED"
551
+ );
552
+ }
346
553
  };
347
554
 
348
555
  // src/backends/rest.ts
@@ -405,11 +612,52 @@ var RestBackend = class {
405
612
  return {};
406
613
  }
407
614
  const payload = data;
408
- const code = typeof payload.code === "string" ? payload.code : void 0;
409
- const messageField = payload.message ?? payload.error;
615
+ const nestedError = payload.error && typeof payload.error === "object" ? payload.error : void 0;
616
+ const codeField = nestedError?.code ?? payload.code;
617
+ const code = typeof codeField === "string" ? codeField : void 0;
618
+ const messageField = nestedError?.message ?? payload.message ?? payload.error;
410
619
  const message = typeof messageField === "string" ? messageField : void 0;
411
620
  return { code, message };
412
621
  }
622
+ /**
623
+ * Parse node ID safely to handle u64 values above Number.MAX_SAFE_INTEGER.
624
+ * Returns bigint for large values, number for safe values.
625
+ */
626
+ parseNodeId(value) {
627
+ if (value === null || value === void 0) {
628
+ return 0;
629
+ }
630
+ if (typeof value === "bigint") {
631
+ return value;
632
+ }
633
+ if (typeof value === "string") {
634
+ const num = Number(value);
635
+ if (num > Number.MAX_SAFE_INTEGER) {
636
+ return BigInt(value);
637
+ }
638
+ return num;
639
+ }
640
+ if (typeof value === "number") {
641
+ if (value > Number.MAX_SAFE_INTEGER) {
642
+ return value;
643
+ }
644
+ return value;
645
+ }
646
+ return 0;
647
+ }
648
+ parseRestPointId(id) {
649
+ if (typeof id !== "number" || !Number.isFinite(id) || id < 0 || !Number.isInteger(id) || id > Number.MAX_SAFE_INTEGER) {
650
+ throw new ValidationError(
651
+ `REST backend requires numeric u64-compatible IDs in JS safe integer range (0..${Number.MAX_SAFE_INTEGER}). Received: ${String(id)}`
652
+ );
653
+ }
654
+ return id;
655
+ }
656
+ isLikelyAggregationQuery(query) {
657
+ return /\bGROUP\s+BY\b|\bHAVING\b|\bCOUNT\s*\(|\bSUM\s*\(|\bAVG\s*\(|\bMIN\s*\(|\bMAX\s*\(/i.test(
658
+ query
659
+ );
660
+ }
413
661
  async request(method, path, body) {
414
662
  const url = `${this.baseUrl}${path}`;
415
663
  const headers = {
@@ -498,14 +746,17 @@ var RestBackend = class {
498
746
  }
499
747
  async insert(collection, doc) {
500
748
  this.ensureInitialized();
749
+ const restId = this.parseRestPointId(doc.id);
501
750
  const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
502
751
  const response = await this.request(
503
752
  "POST",
504
- `/collections/${encodeURIComponent(collection)}/vectors`,
753
+ `/collections/${encodeURIComponent(collection)}/points`,
505
754
  {
506
- id: doc.id,
507
- vector,
508
- payload: doc.payload
755
+ points: [{
756
+ id: restId,
757
+ vector,
758
+ payload: doc.payload
759
+ }]
509
760
  }
510
761
  );
511
762
  if (response.error) {
@@ -518,14 +769,14 @@ var RestBackend = class {
518
769
  async insertBatch(collection, docs) {
519
770
  this.ensureInitialized();
520
771
  const vectors = docs.map((doc) => ({
521
- id: doc.id,
772
+ id: this.parseRestPointId(doc.id),
522
773
  vector: doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector,
523
774
  payload: doc.payload
524
775
  }));
525
776
  const response = await this.request(
526
777
  "POST",
527
- `/collections/${encodeURIComponent(collection)}/vectors/batch`,
528
- { vectors }
778
+ `/collections/${encodeURIComponent(collection)}/points`,
779
+ { points: vectors }
529
780
  );
530
781
  if (response.error) {
531
782
  if (response.error.code === "NOT_FOUND") {
@@ -534,18 +785,29 @@ var RestBackend = class {
534
785
  throw new VelesDBError(response.error.message, response.error.code);
535
786
  }
536
787
  }
788
+ sparseVectorToRestFormat(sv) {
789
+ const result = {};
790
+ for (const [k, v] of Object.entries(sv)) {
791
+ result[String(k)] = v;
792
+ }
793
+ return result;
794
+ }
537
795
  async search(collection, query, options) {
538
796
  this.ensureInitialized();
539
797
  const queryVector = query instanceof Float32Array ? Array.from(query) : query;
798
+ const body = {
799
+ vector: queryVector,
800
+ top_k: options?.k ?? 10,
801
+ filter: options?.filter,
802
+ include_vectors: options?.includeVectors ?? false
803
+ };
804
+ if (options?.sparseVector) {
805
+ body.sparse_vector = this.sparseVectorToRestFormat(options.sparseVector);
806
+ }
540
807
  const response = await this.request(
541
808
  "POST",
542
809
  `/collections/${encodeURIComponent(collection)}/search`,
543
- {
544
- vector: queryVector,
545
- k: options?.k ?? 10,
546
- filter: options?.filter,
547
- include_vectors: options?.includeVectors ?? false
548
- }
810
+ body
549
811
  );
550
812
  if (response.error) {
551
813
  if (response.error.code === "NOT_FOUND") {
@@ -553,7 +815,7 @@ var RestBackend = class {
553
815
  }
554
816
  throw new VelesDBError(response.error.message, response.error.code);
555
817
  }
556
- return response.data ?? [];
818
+ return response.data?.results ?? [];
557
819
  }
558
820
  async searchBatch(collection, searches) {
559
821
  this.ensureInitialized();
@@ -577,9 +839,10 @@ var RestBackend = class {
577
839
  }
578
840
  async delete(collection, id) {
579
841
  this.ensureInitialized();
842
+ const restId = this.parseRestPointId(id);
580
843
  const response = await this.request(
581
844
  "DELETE",
582
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
845
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(restId))}`
583
846
  );
584
847
  if (response.error) {
585
848
  if (response.error.code === "NOT_FOUND") {
@@ -591,9 +854,10 @@ var RestBackend = class {
591
854
  }
592
855
  async get(collection, id) {
593
856
  this.ensureInitialized();
857
+ const restId = this.parseRestPointId(id);
594
858
  const response = await this.request(
595
859
  "GET",
596
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
860
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(restId))}`
597
861
  );
598
862
  if (response.error) {
599
863
  if (response.error.code === "NOT_FOUND") {
@@ -644,11 +908,63 @@ var RestBackend = class {
644
908
  }
645
909
  return response.data?.results ?? [];
646
910
  }
647
- async query(queryString, params) {
911
+ async query(collection, queryString, params, options) {
648
912
  this.ensureInitialized();
913
+ const endpoint = this.isLikelyAggregationQuery(queryString) ? "/aggregate" : "/query";
649
914
  const response = await this.request(
650
915
  "POST",
651
- "/query",
916
+ endpoint,
917
+ {
918
+ query: queryString,
919
+ params: params ?? {},
920
+ collection,
921
+ timeout_ms: options?.timeoutMs,
922
+ stream: options?.stream ?? false
923
+ }
924
+ );
925
+ if (response.error) {
926
+ if (response.error.code === "NOT_FOUND") {
927
+ throw new NotFoundError(`Collection '${collection}'`);
928
+ }
929
+ throw new VelesDBError(response.error.message, response.error.code);
930
+ }
931
+ const rawData = response.data;
932
+ if (rawData && Object.prototype.hasOwnProperty.call(rawData, "result")) {
933
+ return {
934
+ result: rawData.result,
935
+ stats: {
936
+ executionTimeMs: rawData.timing_ms ?? 0,
937
+ strategy: "aggregation",
938
+ scannedNodes: 0
939
+ }
940
+ };
941
+ }
942
+ return {
943
+ results: (rawData?.results ?? []).map((r) => ({
944
+ // Server returns `id` (u64), map to nodeId with precision handling
945
+ nodeId: this.parseNodeId(r.id ?? r.node_id ?? r.nodeId),
946
+ // Server returns `score`, map to vectorScore (primary score for SELECT queries)
947
+ vectorScore: r.score ?? r.vector_score ?? r.vectorScore,
948
+ // graph_score not returned by SELECT queries, only by future MATCH queries
949
+ graphScore: r.graph_score ?? r.graphScore,
950
+ // Use score as fusedScore for compatibility
951
+ fusedScore: r.score ?? r.fused_score ?? r.fusedScore ?? 0,
952
+ // payload maps to bindings for compatibility
953
+ bindings: r.payload ?? r.bindings ?? {},
954
+ columnData: r.column_data ?? r.columnData
955
+ })),
956
+ stats: {
957
+ executionTimeMs: rawData?.timing_ms ?? 0,
958
+ strategy: "select",
959
+ scannedNodes: rawData?.rows_returned ?? 0
960
+ }
961
+ };
962
+ }
963
+ async queryExplain(queryString, params) {
964
+ this.ensureInitialized();
965
+ const response = await this.request(
966
+ "POST",
967
+ "/query/explain",
652
968
  {
653
969
  query: queryString,
654
970
  params: params ?? {}
@@ -657,7 +973,68 @@ var RestBackend = class {
657
973
  if (response.error) {
658
974
  throw new VelesDBError(response.error.message, response.error.code);
659
975
  }
660
- return response.data?.results ?? [];
976
+ const data = response.data;
977
+ return {
978
+ query: data.query,
979
+ queryType: data.query_type,
980
+ collection: data.collection,
981
+ plan: data.plan.map((step) => ({
982
+ step: step.step,
983
+ operation: step.operation,
984
+ description: step.description,
985
+ estimatedRows: step.estimated_rows
986
+ })),
987
+ estimatedCost: {
988
+ usesIndex: data.estimated_cost.uses_index,
989
+ indexName: data.estimated_cost.index_name,
990
+ selectivity: data.estimated_cost.selectivity,
991
+ complexity: data.estimated_cost.complexity
992
+ },
993
+ features: {
994
+ hasVectorSearch: data.features.has_vector_search,
995
+ hasFilter: data.features.has_filter,
996
+ hasOrderBy: data.features.has_order_by,
997
+ hasGroupBy: data.features.has_group_by,
998
+ hasAggregation: data.features.has_aggregation,
999
+ hasJoin: data.features.has_join,
1000
+ hasFusion: data.features.has_fusion,
1001
+ limit: data.features.limit,
1002
+ offset: data.features.offset
1003
+ }
1004
+ };
1005
+ }
1006
+ async collectionSanity(collection) {
1007
+ this.ensureInitialized();
1008
+ const response = await this.request(
1009
+ "GET",
1010
+ `/collections/${encodeURIComponent(collection)}/sanity`
1011
+ );
1012
+ if (response.error) {
1013
+ if (response.error.code === "NOT_FOUND") {
1014
+ throw new NotFoundError(`Collection '${collection}'`);
1015
+ }
1016
+ throw new VelesDBError(response.error.message, response.error.code);
1017
+ }
1018
+ const data = response.data;
1019
+ return {
1020
+ collection: data.collection,
1021
+ dimension: data.dimension,
1022
+ metric: data.metric,
1023
+ pointCount: data.point_count,
1024
+ isEmpty: data.is_empty,
1025
+ checks: {
1026
+ hasVectors: data.checks.has_vectors,
1027
+ searchReady: data.checks.search_ready,
1028
+ dimensionConfigured: data.checks.dimension_configured
1029
+ },
1030
+ diagnostics: {
1031
+ searchRequestsTotal: data.diagnostics.search_requests_total,
1032
+ dimensionMismatchTotal: data.diagnostics.dimension_mismatch_total,
1033
+ emptySearchResultsTotal: data.diagnostics.empty_search_results_total,
1034
+ filterParseErrorsTotal: data.diagnostics.filter_parse_errors_total
1035
+ },
1036
+ hints: data.hints ?? []
1037
+ };
661
1038
  }
662
1039
  async multiQuerySearch(collection, vectors, options) {
663
1040
  this.ensureInitialized();
@@ -672,6 +1049,9 @@ var RestBackend = class {
672
1049
  top_k: options?.k ?? 10,
673
1050
  strategy: options?.fusion ?? "rrf",
674
1051
  rrf_k: options?.fusionParams?.k ?? 60,
1052
+ avg_weight: options?.fusionParams?.avgWeight,
1053
+ max_weight: options?.fusionParams?.maxWeight,
1054
+ hit_weight: options?.fusionParams?.hitWeight,
675
1055
  filter: options?.filter
676
1056
  }
677
1057
  );
@@ -710,6 +1090,81 @@ var RestBackend = class {
710
1090
  throw new VelesDBError(response.error.message, response.error.code);
711
1091
  }
712
1092
  }
1093
+ // ========================================================================
1094
+ // Sparse / PQ / Streaming (v1.5)
1095
+ // ========================================================================
1096
+ async trainPq(collection, options) {
1097
+ this.ensureInitialized();
1098
+ const m = options?.m ?? 8;
1099
+ const k = options?.k ?? 256;
1100
+ const withClause = options?.opq ? `WITH (m=${m}, k=${k}, opq=true)` : `WITH (m=${m}, k=${k})`;
1101
+ const queryString = `TRAIN QUANTIZER ON ${collection} ${withClause}`;
1102
+ const response = await this.request(
1103
+ "POST",
1104
+ "/query",
1105
+ { query: queryString }
1106
+ );
1107
+ if (response.error) {
1108
+ throw new VelesDBError(response.error.message, response.error.code);
1109
+ }
1110
+ return response.data?.message ?? "PQ training initiated";
1111
+ }
1112
+ async streamInsert(collection, docs) {
1113
+ this.ensureInitialized();
1114
+ for (const doc of docs) {
1115
+ const restId = this.parseRestPointId(doc.id);
1116
+ const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
1117
+ const body = {
1118
+ id: restId,
1119
+ vector,
1120
+ payload: doc.payload
1121
+ };
1122
+ if (doc.sparseVector) {
1123
+ body.sparse_vector = this.sparseVectorToRestFormat(doc.sparseVector);
1124
+ }
1125
+ const url = `${this.baseUrl}/collections/${encodeURIComponent(collection)}/stream/insert`;
1126
+ const headers = {
1127
+ "Content-Type": "application/json"
1128
+ };
1129
+ if (this.apiKey) {
1130
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1131
+ }
1132
+ const controller = new AbortController();
1133
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1134
+ try {
1135
+ const response = await fetch(url, {
1136
+ method: "POST",
1137
+ headers,
1138
+ body: JSON.stringify(body),
1139
+ signal: controller.signal
1140
+ });
1141
+ clearTimeout(timeoutId);
1142
+ if (response.status === 429) {
1143
+ throw new BackpressureError();
1144
+ }
1145
+ if (!response.ok && response.status !== 202) {
1146
+ const data = await response.json().catch(() => ({}));
1147
+ const errorPayload = this.extractErrorPayload(data);
1148
+ throw new VelesDBError(
1149
+ errorPayload.message ?? `HTTP ${response.status}`,
1150
+ errorPayload.code ?? this.mapStatusToErrorCode(response.status)
1151
+ );
1152
+ }
1153
+ } catch (error) {
1154
+ clearTimeout(timeoutId);
1155
+ if (error instanceof BackpressureError || error instanceof VelesDBError) {
1156
+ throw error;
1157
+ }
1158
+ if (error instanceof Error && error.name === "AbortError") {
1159
+ throw new ConnectionError("Request timeout");
1160
+ }
1161
+ throw new ConnectionError(
1162
+ `Stream insert failed: ${error instanceof Error ? error.message : "Unknown error"}`,
1163
+ error instanceof Error ? error : void 0
1164
+ );
1165
+ }
1166
+ }
1167
+ }
713
1168
  async close() {
714
1169
  this._initialized = false;
715
1170
  }
@@ -1026,6 +1481,18 @@ var VelesDB = class {
1026
1481
  if (!Array.isArray(doc.vector) && !(doc.vector instanceof Float32Array)) {
1027
1482
  throw new ValidationError("Vector must be an array or Float32Array");
1028
1483
  }
1484
+ if (this.config.backend === "rest" && (typeof doc.id !== "number" || !Number.isInteger(doc.id) || doc.id < 0 || doc.id > Number.MAX_SAFE_INTEGER)) {
1485
+ throw new ValidationError(
1486
+ `REST backend requires numeric u64-compatible document IDs in JS safe integer range (0..${Number.MAX_SAFE_INTEGER})`
1487
+ );
1488
+ }
1489
+ }
1490
+ validateRestPointId(id) {
1491
+ if (this.config.backend === "rest" && (typeof id !== "number" || !Number.isInteger(id) || id < 0 || id > Number.MAX_SAFE_INTEGER)) {
1492
+ throw new ValidationError(
1493
+ `REST backend requires numeric u64-compatible document IDs in JS safe integer range (0..${Number.MAX_SAFE_INTEGER})`
1494
+ );
1495
+ }
1029
1496
  }
1030
1497
  /**
1031
1498
  * Search for similar vectors
@@ -1070,6 +1537,7 @@ var VelesDB = class {
1070
1537
  */
1071
1538
  async delete(collection, id) {
1072
1539
  this.ensureInitialized();
1540
+ this.validateRestPointId(id);
1073
1541
  return this.backend.delete(collection, id);
1074
1542
  }
1075
1543
  /**
@@ -1081,6 +1549,7 @@ var VelesDB = class {
1081
1549
  */
1082
1550
  async get(collection, id) {
1083
1551
  this.ensureInitialized();
1552
+ this.validateRestPointId(id);
1084
1553
  return this.backend.get(collection, id);
1085
1554
  }
1086
1555
  /**
@@ -1118,18 +1587,36 @@ var VelesDB = class {
1118
1587
  return this.backend.hybridSearch(collection, vector, textQuery, options);
1119
1588
  }
1120
1589
  /**
1121
- * Execute a VelesQL query
1590
+ * Execute a VelesQL multi-model query (EPIC-031 US-011)
1591
+ *
1592
+ * Supports hybrid vector + graph queries with VelesQL syntax.
1122
1593
  *
1594
+ * @param collection - Collection name
1123
1595
  * @param queryString - VelesQL query string
1124
- * @param params - Optional query parameters
1125
- * @returns Query results
1596
+ * @param params - Query parameters (vectors, scalars)
1597
+ * @param options - Query options (timeout, streaming)
1598
+ * @returns Query response with results and execution stats
1599
+ *
1600
+ * @example
1601
+ * ```typescript
1602
+ * const response = await db.query('docs', `
1603
+ * MATCH (d:Doc) WHERE vector NEAR $q LIMIT 20
1604
+ * `, { q: queryVector });
1605
+ *
1606
+ * for (const r of response.results) {
1607
+ * console.log(`Node ${r.nodeId}: ${r.fusedScore}`);
1608
+ * }
1609
+ * ```
1126
1610
  */
1127
- async query(queryString, params) {
1611
+ async query(collection, queryString, params, options) {
1128
1612
  this.ensureInitialized();
1613
+ if (!collection || typeof collection !== "string") {
1614
+ throw new ValidationError("Collection name must be a non-empty string");
1615
+ }
1129
1616
  if (!queryString || typeof queryString !== "string") {
1130
1617
  throw new ValidationError("Query string must be a non-empty string");
1131
1618
  }
1132
- return this.backend.query(queryString, params);
1619
+ return this.backend.query(collection, queryString, params, options);
1133
1620
  }
1134
1621
  /**
1135
1622
  * Multi-query fusion search combining results from multiple query vectors
@@ -1158,6 +1645,20 @@ var VelesDB = class {
1158
1645
  * });
1159
1646
  * ```
1160
1647
  */
1648
+ async queryExplain(queryString, params) {
1649
+ this.ensureInitialized();
1650
+ if (!queryString || typeof queryString !== "string") {
1651
+ throw new ValidationError("Query string must be a non-empty string");
1652
+ }
1653
+ return this.backend.queryExplain(queryString, params);
1654
+ }
1655
+ async collectionSanity(collection) {
1656
+ this.ensureInitialized();
1657
+ if (!collection || typeof collection !== "string") {
1658
+ throw new ValidationError("Collection name must be a non-empty string");
1659
+ }
1660
+ return this.backend.collectionSanity(collection);
1661
+ }
1161
1662
  async multiQuerySearch(collection, vectors, options) {
1162
1663
  this.ensureInitialized();
1163
1664
  if (!Array.isArray(vectors) || vectors.length === 0) {
@@ -1170,9 +1671,39 @@ var VelesDB = class {
1170
1671
  }
1171
1672
  return this.backend.multiQuerySearch(collection, vectors, options);
1172
1673
  }
1674
+ /**
1675
+ * Train Product Quantization on a collection
1676
+ *
1677
+ * @param collection - Collection name
1678
+ * @param options - PQ training options (m, k, opq)
1679
+ * @returns Server response message
1680
+ */
1681
+ async trainPq(collection, options) {
1682
+ this.ensureInitialized();
1683
+ return this.backend.trainPq(collection, options);
1684
+ }
1685
+ /**
1686
+ * Stream-insert documents with backpressure support
1687
+ *
1688
+ * Sends documents sequentially to respect server backpressure.
1689
+ * Throws BackpressureError on 429 responses.
1690
+ *
1691
+ * @param collection - Collection name
1692
+ * @param docs - Documents to insert
1693
+ */
1694
+ async streamInsert(collection, docs) {
1695
+ this.ensureInitialized();
1696
+ if (!Array.isArray(docs)) {
1697
+ throw new ValidationError("Documents must be an array");
1698
+ }
1699
+ for (const doc of docs) {
1700
+ this.validateDocument(doc);
1701
+ }
1702
+ await this.backend.streamInsert(collection, docs);
1703
+ }
1173
1704
  /**
1174
1705
  * Check if a collection is empty
1175
- *
1706
+ *
1176
1707
  * @param collection - Collection name
1177
1708
  * @returns true if empty, false otherwise
1178
1709
  */
@@ -1367,12 +1898,317 @@ var VelesDB = class {
1367
1898
  return this.backend.getNodeDegree(collection, nodeId);
1368
1899
  }
1369
1900
  };
1901
+
1902
+ // src/query-builder.ts
1903
+ var VelesQLBuilder = class _VelesQLBuilder {
1904
+ constructor(state) {
1905
+ this.state = {
1906
+ matchClauses: state?.matchClauses ?? [],
1907
+ whereClauses: state?.whereClauses ?? [],
1908
+ whereOperators: state?.whereOperators ?? [],
1909
+ params: state?.params ?? {},
1910
+ limitValue: state?.limitValue,
1911
+ offsetValue: state?.offsetValue,
1912
+ orderByClause: state?.orderByClause,
1913
+ returnClause: state?.returnClause,
1914
+ fusionOptions: state?.fusionOptions,
1915
+ currentNode: state?.currentNode,
1916
+ pendingRel: state?.pendingRel
1917
+ };
1918
+ }
1919
+ clone(updates) {
1920
+ return new _VelesQLBuilder({
1921
+ ...this.state,
1922
+ matchClauses: [...this.state.matchClauses],
1923
+ whereClauses: [...this.state.whereClauses],
1924
+ whereOperators: [...this.state.whereOperators],
1925
+ params: { ...this.state.params },
1926
+ ...updates
1927
+ });
1928
+ }
1929
+ /**
1930
+ * Start a MATCH clause with a node pattern
1931
+ *
1932
+ * @param alias - Node alias (e.g., 'n', 'person')
1933
+ * @param label - Optional node label(s)
1934
+ */
1935
+ match(alias, label) {
1936
+ const labelStr = this.formatLabel(label);
1937
+ const nodePattern = `(${alias}${labelStr})`;
1938
+ return this.clone({
1939
+ matchClauses: [...this.state.matchClauses, nodePattern],
1940
+ currentNode: alias
1941
+ });
1942
+ }
1943
+ /**
1944
+ * Add a relationship pattern
1945
+ *
1946
+ * @param type - Relationship type (e.g., 'KNOWS', 'FOLLOWS')
1947
+ * @param alias - Optional relationship alias
1948
+ * @param options - Relationship options (direction, hops)
1949
+ */
1950
+ rel(type, alias, options) {
1951
+ return this.clone({
1952
+ pendingRel: { type, alias, options }
1953
+ });
1954
+ }
1955
+ /**
1956
+ * Complete a relationship pattern with target node
1957
+ *
1958
+ * @param alias - Target node alias
1959
+ * @param label - Optional target node label(s)
1960
+ */
1961
+ to(alias, label) {
1962
+ if (!this.state.pendingRel) {
1963
+ throw new Error("to() must be called after rel()");
1964
+ }
1965
+ const { type, alias: relAlias, options } = this.state.pendingRel;
1966
+ const direction = options?.direction ?? "outgoing";
1967
+ const labelStr = this.formatLabel(label);
1968
+ const relPattern = this.formatRelationship(type, relAlias, options);
1969
+ const targetNode = `(${alias}${labelStr})`;
1970
+ let fullPattern;
1971
+ switch (direction) {
1972
+ case "incoming":
1973
+ fullPattern = `<-${relPattern}-${targetNode}`;
1974
+ break;
1975
+ case "both":
1976
+ fullPattern = `-${relPattern}-${targetNode}`;
1977
+ break;
1978
+ default:
1979
+ fullPattern = `-${relPattern}->${targetNode}`;
1980
+ }
1981
+ const lastMatch = this.state.matchClauses[this.state.matchClauses.length - 1];
1982
+ const updatedMatch = lastMatch + fullPattern;
1983
+ const newMatchClauses = [...this.state.matchClauses.slice(0, -1), updatedMatch];
1984
+ return this.clone({
1985
+ matchClauses: newMatchClauses,
1986
+ currentNode: alias,
1987
+ pendingRel: void 0
1988
+ });
1989
+ }
1990
+ /**
1991
+ * Add a WHERE clause
1992
+ *
1993
+ * @param condition - WHERE condition
1994
+ * @param params - Optional parameters
1995
+ */
1996
+ where(condition, params) {
1997
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1998
+ return this.clone({
1999
+ whereClauses: [...this.state.whereClauses, condition],
2000
+ whereOperators: [...this.state.whereOperators],
2001
+ params: newParams
2002
+ });
2003
+ }
2004
+ /**
2005
+ * Add an AND WHERE clause
2006
+ *
2007
+ * @param condition - WHERE condition
2008
+ * @param params - Optional parameters
2009
+ */
2010
+ andWhere(condition, params) {
2011
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
2012
+ return this.clone({
2013
+ whereClauses: [...this.state.whereClauses, condition],
2014
+ whereOperators: [...this.state.whereOperators, "AND"],
2015
+ params: newParams
2016
+ });
2017
+ }
2018
+ /**
2019
+ * Add an OR WHERE clause
2020
+ *
2021
+ * @param condition - WHERE condition
2022
+ * @param params - Optional parameters
2023
+ */
2024
+ orWhere(condition, params) {
2025
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
2026
+ return this.clone({
2027
+ whereClauses: [...this.state.whereClauses, condition],
2028
+ whereOperators: [...this.state.whereOperators, "OR"],
2029
+ params: newParams
2030
+ });
2031
+ }
2032
+ /**
2033
+ * Add a vector NEAR clause for similarity search
2034
+ *
2035
+ * @param paramName - Parameter name (e.g., '$query', '$embedding')
2036
+ * @param vector - Vector data
2037
+ * @param options - NEAR options (topK)
2038
+ */
2039
+ nearVector(paramName, vector, options) {
2040
+ const cleanParamName = paramName.startsWith("$") ? paramName.slice(1) : paramName;
2041
+ const topKSuffix = options?.topK ? ` TOP ${options.topK}` : "";
2042
+ const condition = `vector NEAR $${cleanParamName}${topKSuffix}`;
2043
+ const newParams = { ...this.state.params, [cleanParamName]: vector };
2044
+ if (this.state.whereClauses.length === 0) {
2045
+ return this.clone({
2046
+ whereClauses: [condition],
2047
+ params: newParams
2048
+ });
2049
+ }
2050
+ return this.clone({
2051
+ whereClauses: [...this.state.whereClauses, condition],
2052
+ whereOperators: [...this.state.whereOperators, "AND"],
2053
+ params: newParams
2054
+ });
2055
+ }
2056
+ /**
2057
+ * Add LIMIT clause
2058
+ *
2059
+ * @param value - Maximum number of results
2060
+ */
2061
+ limit(value) {
2062
+ if (value < 0) {
2063
+ throw new Error("LIMIT must be non-negative");
2064
+ }
2065
+ return this.clone({ limitValue: value });
2066
+ }
2067
+ /**
2068
+ * Add OFFSET clause
2069
+ *
2070
+ * @param value - Number of results to skip
2071
+ */
2072
+ offset(value) {
2073
+ if (value < 0) {
2074
+ throw new Error("OFFSET must be non-negative");
2075
+ }
2076
+ return this.clone({ offsetValue: value });
2077
+ }
2078
+ /**
2079
+ * Add ORDER BY clause
2080
+ *
2081
+ * @param field - Field to order by
2082
+ * @param direction - Sort direction (ASC or DESC)
2083
+ */
2084
+ orderBy(field, direction) {
2085
+ const orderClause = direction ? `${field} ${direction}` : field;
2086
+ return this.clone({ orderByClause: orderClause });
2087
+ }
2088
+ /**
2089
+ * Add RETURN clause with specific fields
2090
+ *
2091
+ * @param fields - Fields to return (array or object with aliases)
2092
+ */
2093
+ return(fields) {
2094
+ let returnClause;
2095
+ if (Array.isArray(fields)) {
2096
+ returnClause = fields.join(", ");
2097
+ } else {
2098
+ returnClause = Object.entries(fields).map(([field, alias]) => `${field} AS ${alias}`).join(", ");
2099
+ }
2100
+ return this.clone({ returnClause });
2101
+ }
2102
+ /**
2103
+ * Add RETURN * clause
2104
+ */
2105
+ returnAll() {
2106
+ return this.clone({ returnClause: "*" });
2107
+ }
2108
+ /**
2109
+ * Set fusion strategy for hybrid queries
2110
+ *
2111
+ * @param strategy - Fusion strategy
2112
+ * @param options - Fusion parameters
2113
+ */
2114
+ fusion(strategy, options) {
2115
+ return this.clone({
2116
+ fusionOptions: {
2117
+ strategy,
2118
+ ...options
2119
+ }
2120
+ });
2121
+ }
2122
+ /**
2123
+ * Get the fusion options
2124
+ */
2125
+ getFusionOptions() {
2126
+ return this.state.fusionOptions;
2127
+ }
2128
+ /**
2129
+ * Get all parameters
2130
+ */
2131
+ getParams() {
2132
+ return { ...this.state.params };
2133
+ }
2134
+ /**
2135
+ * Build the VelesQL query string
2136
+ */
2137
+ toVelesQL() {
2138
+ if (this.state.matchClauses.length === 0) {
2139
+ throw new Error("Query must have at least one MATCH clause");
2140
+ }
2141
+ const parts = [];
2142
+ parts.push(`MATCH ${this.state.matchClauses.join(", ")}`);
2143
+ if (this.state.whereClauses.length > 0) {
2144
+ const whereStr = this.buildWhereClause();
2145
+ parts.push(`WHERE ${whereStr}`);
2146
+ }
2147
+ if (this.state.orderByClause) {
2148
+ parts.push(`ORDER BY ${this.state.orderByClause}`);
2149
+ }
2150
+ if (this.state.limitValue !== void 0) {
2151
+ parts.push(`LIMIT ${this.state.limitValue}`);
2152
+ }
2153
+ if (this.state.offsetValue !== void 0) {
2154
+ parts.push(`OFFSET ${this.state.offsetValue}`);
2155
+ }
2156
+ if (this.state.returnClause) {
2157
+ parts.push(`RETURN ${this.state.returnClause}`);
2158
+ }
2159
+ if (this.state.fusionOptions) {
2160
+ parts.push(`/* FUSION ${this.state.fusionOptions.strategy} */`);
2161
+ }
2162
+ return parts.join(" ");
2163
+ }
2164
+ formatLabel(label) {
2165
+ if (!label) return "";
2166
+ if (Array.isArray(label)) {
2167
+ return label.map((l) => `:${l}`).join("");
2168
+ }
2169
+ return `:${label}`;
2170
+ }
2171
+ formatRelationship(type, alias, options) {
2172
+ const aliasStr = alias ? alias : "";
2173
+ const hopsStr = this.formatHops(options);
2174
+ if (alias) {
2175
+ return `[${aliasStr}:${type}${hopsStr}]`;
2176
+ }
2177
+ return `[:${type}${hopsStr}]`;
2178
+ }
2179
+ formatHops(options) {
2180
+ if (!options?.minHops && !options?.maxHops) return "";
2181
+ const min = options.minHops ?? 1;
2182
+ const max = options.maxHops ?? "";
2183
+ return `*${min}..${max}`;
2184
+ }
2185
+ buildWhereClause() {
2186
+ if (this.state.whereClauses.length === 0) return "";
2187
+ const first = this.state.whereClauses[0];
2188
+ if (!first) return "";
2189
+ let result = first;
2190
+ for (let i = 1; i < this.state.whereClauses.length; i++) {
2191
+ const operator = this.state.whereOperators[i - 1] ?? "AND";
2192
+ const clause = this.state.whereClauses[i];
2193
+ if (clause) {
2194
+ result += ` ${operator} ${clause}`;
2195
+ }
2196
+ }
2197
+ return result;
2198
+ }
2199
+ };
2200
+ function velesql() {
2201
+ return new VelesQLBuilder();
2202
+ }
1370
2203
  export {
2204
+ BackpressureError,
1371
2205
  ConnectionError,
1372
2206
  NotFoundError,
1373
2207
  RestBackend,
1374
2208
  ValidationError,
1375
2209
  VelesDB,
1376
2210
  VelesDBError,
1377
- WasmBackend
2211
+ VelesQLBuilder,
2212
+ WasmBackend,
2213
+ velesql
1378
2214
  };