@wiscale/velesdb-sdk 1.3.0 → 1.4.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.js CHANGED
@@ -36,7 +36,9 @@ __export(index_exports, {
36
36
  ValidationError: () => ValidationError,
37
37
  VelesDB: () => VelesDB,
38
38
  VelesDBError: () => VelesDBError,
39
- WasmBackend: () => WasmBackend
39
+ VelesQLBuilder: () => VelesQLBuilder,
40
+ WasmBackend: () => WasmBackend,
41
+ velesql: () => velesql
40
42
  });
41
43
  module.exports = __toCommonJS(index_exports);
42
44
 
@@ -175,7 +177,7 @@ var WasmBackend = class {
175
177
  throw new NotFoundError(`Collection '${collectionName}'`);
176
178
  }
177
179
  for (const doc of docs) {
178
- const vectorLen = Array.isArray(doc.vector) ? doc.vector.length : doc.vector.length;
180
+ const vectorLen = doc.vector.length;
179
181
  if (vectorLen !== collection.config.dimension) {
180
182
  throw new VelesDBError(
181
183
  `Vector dimension mismatch for doc ${doc.id}: expected ${collection.config.dimension}, got ${vectorLen}`,
@@ -281,7 +283,7 @@ var WasmBackend = class {
281
283
  "NOT_SUPPORTED"
282
284
  );
283
285
  }
284
- async query(_queryString, _params) {
286
+ async query(_collection, _queryString, _params, _options) {
285
287
  throw new VelesDBError(
286
288
  "VelesQL queries are not supported in WASM backend. Use REST backend for query support.",
287
289
  "NOT_SUPPORTED"
@@ -452,6 +454,32 @@ var RestBackend = class {
452
454
  const message = typeof messageField === "string" ? messageField : void 0;
453
455
  return { code, message };
454
456
  }
457
+ /**
458
+ * Parse node ID safely to handle u64 values above Number.MAX_SAFE_INTEGER.
459
+ * Returns bigint for large values, number for safe values.
460
+ */
461
+ parseNodeId(value) {
462
+ if (value === null || value === void 0) {
463
+ return 0;
464
+ }
465
+ if (typeof value === "bigint") {
466
+ return value;
467
+ }
468
+ if (typeof value === "string") {
469
+ const num = Number(value);
470
+ if (num > Number.MAX_SAFE_INTEGER) {
471
+ return BigInt(value);
472
+ }
473
+ return num;
474
+ }
475
+ if (typeof value === "number") {
476
+ if (value > Number.MAX_SAFE_INTEGER) {
477
+ return value;
478
+ }
479
+ return value;
480
+ }
481
+ return 0;
482
+ }
455
483
  async request(method, path, body) {
456
484
  const url = `${this.baseUrl}${path}`;
457
485
  const headers = {
@@ -543,11 +571,13 @@ var RestBackend = class {
543
571
  const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
544
572
  const response = await this.request(
545
573
  "POST",
546
- `/collections/${encodeURIComponent(collection)}/vectors`,
574
+ `/collections/${encodeURIComponent(collection)}/points`,
547
575
  {
548
- id: doc.id,
549
- vector,
550
- payload: doc.payload
576
+ points: [{
577
+ id: doc.id,
578
+ vector,
579
+ payload: doc.payload
580
+ }]
551
581
  }
552
582
  );
553
583
  if (response.error) {
@@ -566,8 +596,8 @@ var RestBackend = class {
566
596
  }));
567
597
  const response = await this.request(
568
598
  "POST",
569
- `/collections/${encodeURIComponent(collection)}/vectors/batch`,
570
- { vectors }
599
+ `/collections/${encodeURIComponent(collection)}/points`,
600
+ { points: vectors }
571
601
  );
572
602
  if (response.error) {
573
603
  if (response.error.code === "NOT_FOUND") {
@@ -621,7 +651,7 @@ var RestBackend = class {
621
651
  this.ensureInitialized();
622
652
  const response = await this.request(
623
653
  "DELETE",
624
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
654
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(id))}`
625
655
  );
626
656
  if (response.error) {
627
657
  if (response.error.code === "NOT_FOUND") {
@@ -635,7 +665,7 @@ var RestBackend = class {
635
665
  this.ensureInitialized();
636
666
  const response = await this.request(
637
667
  "GET",
638
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
668
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(id))}`
639
669
  );
640
670
  if (response.error) {
641
671
  if (response.error.code === "NOT_FOUND") {
@@ -686,7 +716,7 @@ var RestBackend = class {
686
716
  }
687
717
  return response.data?.results ?? [];
688
718
  }
689
- async query(queryString, params) {
719
+ async query(collection, queryString, params, _options) {
690
720
  this.ensureInitialized();
691
721
  const response = await this.request(
692
722
  "POST",
@@ -697,9 +727,32 @@ var RestBackend = class {
697
727
  }
698
728
  );
699
729
  if (response.error) {
730
+ if (response.error.code === "NOT_FOUND") {
731
+ throw new NotFoundError(`Collection '${collection}'`);
732
+ }
700
733
  throw new VelesDBError(response.error.message, response.error.code);
701
734
  }
702
- return response.data?.results ?? [];
735
+ const rawData = response.data;
736
+ return {
737
+ results: (rawData?.results ?? []).map((r) => ({
738
+ // Server returns `id` (u64), map to nodeId with precision handling
739
+ nodeId: this.parseNodeId(r.id ?? r.node_id ?? r.nodeId),
740
+ // Server returns `score`, map to vectorScore (primary score for SELECT queries)
741
+ vectorScore: r.score ?? r.vector_score ?? r.vectorScore,
742
+ // graph_score not returned by SELECT queries, only by future MATCH queries
743
+ graphScore: r.graph_score ?? r.graphScore,
744
+ // Use score as fusedScore for compatibility
745
+ fusedScore: r.score ?? r.fused_score ?? r.fusedScore ?? 0,
746
+ // payload maps to bindings for compatibility
747
+ bindings: r.payload ?? r.bindings ?? {},
748
+ columnData: r.column_data ?? r.columnData
749
+ })),
750
+ stats: {
751
+ executionTimeMs: rawData?.timing_ms ?? 0,
752
+ strategy: "select",
753
+ scannedNodes: rawData?.rows_returned ?? 0
754
+ }
755
+ };
703
756
  }
704
757
  async multiQuerySearch(collection, vectors, options) {
705
758
  this.ensureInitialized();
@@ -1160,18 +1213,36 @@ var VelesDB = class {
1160
1213
  return this.backend.hybridSearch(collection, vector, textQuery, options);
1161
1214
  }
1162
1215
  /**
1163
- * Execute a VelesQL query
1216
+ * Execute a VelesQL multi-model query (EPIC-031 US-011)
1164
1217
  *
1218
+ * Supports hybrid vector + graph queries with VelesQL syntax.
1219
+ *
1220
+ * @param collection - Collection name
1165
1221
  * @param queryString - VelesQL query string
1166
- * @param params - Optional query parameters
1167
- * @returns Query results
1222
+ * @param params - Query parameters (vectors, scalars)
1223
+ * @param options - Query options (timeout, streaming)
1224
+ * @returns Query response with results and execution stats
1225
+ *
1226
+ * @example
1227
+ * ```typescript
1228
+ * const response = await db.query('docs', `
1229
+ * MATCH (d:Doc) WHERE vector NEAR $q LIMIT 20
1230
+ * `, { q: queryVector });
1231
+ *
1232
+ * for (const r of response.results) {
1233
+ * console.log(`Node ${r.nodeId}: ${r.fusedScore}`);
1234
+ * }
1235
+ * ```
1168
1236
  */
1169
- async query(queryString, params) {
1237
+ async query(collection, queryString, params, options) {
1170
1238
  this.ensureInitialized();
1239
+ if (!collection || typeof collection !== "string") {
1240
+ throw new ValidationError("Collection name must be a non-empty string");
1241
+ }
1171
1242
  if (!queryString || typeof queryString !== "string") {
1172
1243
  throw new ValidationError("Query string must be a non-empty string");
1173
1244
  }
1174
- return this.backend.query(queryString, params);
1245
+ return this.backend.query(collection, queryString, params, options);
1175
1246
  }
1176
1247
  /**
1177
1248
  * Multi-query fusion search combining results from multiple query vectors
@@ -1409,6 +1480,308 @@ var VelesDB = class {
1409
1480
  return this.backend.getNodeDegree(collection, nodeId);
1410
1481
  }
1411
1482
  };
1483
+
1484
+ // src/query-builder.ts
1485
+ var VelesQLBuilder = class _VelesQLBuilder {
1486
+ constructor(state) {
1487
+ this.state = {
1488
+ matchClauses: state?.matchClauses ?? [],
1489
+ whereClauses: state?.whereClauses ?? [],
1490
+ whereOperators: state?.whereOperators ?? [],
1491
+ params: state?.params ?? {},
1492
+ limitValue: state?.limitValue,
1493
+ offsetValue: state?.offsetValue,
1494
+ orderByClause: state?.orderByClause,
1495
+ returnClause: state?.returnClause,
1496
+ fusionOptions: state?.fusionOptions,
1497
+ currentNode: state?.currentNode,
1498
+ pendingRel: state?.pendingRel
1499
+ };
1500
+ }
1501
+ clone(updates) {
1502
+ return new _VelesQLBuilder({
1503
+ ...this.state,
1504
+ matchClauses: [...this.state.matchClauses],
1505
+ whereClauses: [...this.state.whereClauses],
1506
+ whereOperators: [...this.state.whereOperators],
1507
+ params: { ...this.state.params },
1508
+ ...updates
1509
+ });
1510
+ }
1511
+ /**
1512
+ * Start a MATCH clause with a node pattern
1513
+ *
1514
+ * @param alias - Node alias (e.g., 'n', 'person')
1515
+ * @param label - Optional node label(s)
1516
+ */
1517
+ match(alias, label) {
1518
+ const labelStr = this.formatLabel(label);
1519
+ const nodePattern = `(${alias}${labelStr})`;
1520
+ return this.clone({
1521
+ matchClauses: [...this.state.matchClauses, nodePattern],
1522
+ currentNode: alias
1523
+ });
1524
+ }
1525
+ /**
1526
+ * Add a relationship pattern
1527
+ *
1528
+ * @param type - Relationship type (e.g., 'KNOWS', 'FOLLOWS')
1529
+ * @param alias - Optional relationship alias
1530
+ * @param options - Relationship options (direction, hops)
1531
+ */
1532
+ rel(type, alias, options) {
1533
+ return this.clone({
1534
+ pendingRel: { type, alias, options }
1535
+ });
1536
+ }
1537
+ /**
1538
+ * Complete a relationship pattern with target node
1539
+ *
1540
+ * @param alias - Target node alias
1541
+ * @param label - Optional target node label(s)
1542
+ */
1543
+ to(alias, label) {
1544
+ if (!this.state.pendingRel) {
1545
+ throw new Error("to() must be called after rel()");
1546
+ }
1547
+ const { type, alias: relAlias, options } = this.state.pendingRel;
1548
+ const direction = options?.direction ?? "outgoing";
1549
+ const labelStr = this.formatLabel(label);
1550
+ const relPattern = this.formatRelationship(type, relAlias, options);
1551
+ const targetNode = `(${alias}${labelStr})`;
1552
+ let fullPattern;
1553
+ switch (direction) {
1554
+ case "incoming":
1555
+ fullPattern = `<-${relPattern}-${targetNode}`;
1556
+ break;
1557
+ case "both":
1558
+ fullPattern = `-${relPattern}-${targetNode}`;
1559
+ break;
1560
+ default:
1561
+ fullPattern = `-${relPattern}->${targetNode}`;
1562
+ }
1563
+ const lastMatch = this.state.matchClauses[this.state.matchClauses.length - 1];
1564
+ const updatedMatch = lastMatch + fullPattern;
1565
+ const newMatchClauses = [...this.state.matchClauses.slice(0, -1), updatedMatch];
1566
+ return this.clone({
1567
+ matchClauses: newMatchClauses,
1568
+ currentNode: alias,
1569
+ pendingRel: void 0
1570
+ });
1571
+ }
1572
+ /**
1573
+ * Add a WHERE clause
1574
+ *
1575
+ * @param condition - WHERE condition
1576
+ * @param params - Optional parameters
1577
+ */
1578
+ where(condition, params) {
1579
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1580
+ return this.clone({
1581
+ whereClauses: [...this.state.whereClauses, condition],
1582
+ whereOperators: [...this.state.whereOperators],
1583
+ params: newParams
1584
+ });
1585
+ }
1586
+ /**
1587
+ * Add an AND WHERE clause
1588
+ *
1589
+ * @param condition - WHERE condition
1590
+ * @param params - Optional parameters
1591
+ */
1592
+ andWhere(condition, params) {
1593
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1594
+ return this.clone({
1595
+ whereClauses: [...this.state.whereClauses, condition],
1596
+ whereOperators: [...this.state.whereOperators, "AND"],
1597
+ params: newParams
1598
+ });
1599
+ }
1600
+ /**
1601
+ * Add an OR WHERE clause
1602
+ *
1603
+ * @param condition - WHERE condition
1604
+ * @param params - Optional parameters
1605
+ */
1606
+ orWhere(condition, params) {
1607
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1608
+ return this.clone({
1609
+ whereClauses: [...this.state.whereClauses, condition],
1610
+ whereOperators: [...this.state.whereOperators, "OR"],
1611
+ params: newParams
1612
+ });
1613
+ }
1614
+ /**
1615
+ * Add a vector NEAR clause for similarity search
1616
+ *
1617
+ * @param paramName - Parameter name (e.g., '$query', '$embedding')
1618
+ * @param vector - Vector data
1619
+ * @param options - NEAR options (topK)
1620
+ */
1621
+ nearVector(paramName, vector, options) {
1622
+ const cleanParamName = paramName.startsWith("$") ? paramName.slice(1) : paramName;
1623
+ const topKSuffix = options?.topK ? ` TOP ${options.topK}` : "";
1624
+ const condition = `vector NEAR $${cleanParamName}${topKSuffix}`;
1625
+ const newParams = { ...this.state.params, [cleanParamName]: vector };
1626
+ if (this.state.whereClauses.length === 0) {
1627
+ return this.clone({
1628
+ whereClauses: [condition],
1629
+ params: newParams
1630
+ });
1631
+ }
1632
+ return this.clone({
1633
+ whereClauses: [...this.state.whereClauses, condition],
1634
+ whereOperators: [...this.state.whereOperators, "AND"],
1635
+ params: newParams
1636
+ });
1637
+ }
1638
+ /**
1639
+ * Add LIMIT clause
1640
+ *
1641
+ * @param value - Maximum number of results
1642
+ */
1643
+ limit(value) {
1644
+ if (value < 0) {
1645
+ throw new Error("LIMIT must be non-negative");
1646
+ }
1647
+ return this.clone({ limitValue: value });
1648
+ }
1649
+ /**
1650
+ * Add OFFSET clause
1651
+ *
1652
+ * @param value - Number of results to skip
1653
+ */
1654
+ offset(value) {
1655
+ if (value < 0) {
1656
+ throw new Error("OFFSET must be non-negative");
1657
+ }
1658
+ return this.clone({ offsetValue: value });
1659
+ }
1660
+ /**
1661
+ * Add ORDER BY clause
1662
+ *
1663
+ * @param field - Field to order by
1664
+ * @param direction - Sort direction (ASC or DESC)
1665
+ */
1666
+ orderBy(field, direction) {
1667
+ const orderClause = direction ? `${field} ${direction}` : field;
1668
+ return this.clone({ orderByClause: orderClause });
1669
+ }
1670
+ /**
1671
+ * Add RETURN clause with specific fields
1672
+ *
1673
+ * @param fields - Fields to return (array or object with aliases)
1674
+ */
1675
+ return(fields) {
1676
+ let returnClause;
1677
+ if (Array.isArray(fields)) {
1678
+ returnClause = fields.join(", ");
1679
+ } else {
1680
+ returnClause = Object.entries(fields).map(([field, alias]) => `${field} AS ${alias}`).join(", ");
1681
+ }
1682
+ return this.clone({ returnClause });
1683
+ }
1684
+ /**
1685
+ * Add RETURN * clause
1686
+ */
1687
+ returnAll() {
1688
+ return this.clone({ returnClause: "*" });
1689
+ }
1690
+ /**
1691
+ * Set fusion strategy for hybrid queries
1692
+ *
1693
+ * @param strategy - Fusion strategy
1694
+ * @param options - Fusion parameters
1695
+ */
1696
+ fusion(strategy, options) {
1697
+ return this.clone({
1698
+ fusionOptions: {
1699
+ strategy,
1700
+ ...options
1701
+ }
1702
+ });
1703
+ }
1704
+ /**
1705
+ * Get the fusion options
1706
+ */
1707
+ getFusionOptions() {
1708
+ return this.state.fusionOptions;
1709
+ }
1710
+ /**
1711
+ * Get all parameters
1712
+ */
1713
+ getParams() {
1714
+ return { ...this.state.params };
1715
+ }
1716
+ /**
1717
+ * Build the VelesQL query string
1718
+ */
1719
+ toVelesQL() {
1720
+ if (this.state.matchClauses.length === 0) {
1721
+ throw new Error("Query must have at least one MATCH clause");
1722
+ }
1723
+ const parts = [];
1724
+ parts.push(`MATCH ${this.state.matchClauses.join(", ")}`);
1725
+ if (this.state.whereClauses.length > 0) {
1726
+ const whereStr = this.buildWhereClause();
1727
+ parts.push(`WHERE ${whereStr}`);
1728
+ }
1729
+ if (this.state.orderByClause) {
1730
+ parts.push(`ORDER BY ${this.state.orderByClause}`);
1731
+ }
1732
+ if (this.state.limitValue !== void 0) {
1733
+ parts.push(`LIMIT ${this.state.limitValue}`);
1734
+ }
1735
+ if (this.state.offsetValue !== void 0) {
1736
+ parts.push(`OFFSET ${this.state.offsetValue}`);
1737
+ }
1738
+ if (this.state.returnClause) {
1739
+ parts.push(`RETURN ${this.state.returnClause}`);
1740
+ }
1741
+ if (this.state.fusionOptions) {
1742
+ parts.push(`/* FUSION ${this.state.fusionOptions.strategy} */`);
1743
+ }
1744
+ return parts.join(" ");
1745
+ }
1746
+ formatLabel(label) {
1747
+ if (!label) return "";
1748
+ if (Array.isArray(label)) {
1749
+ return label.map((l) => `:${l}`).join("");
1750
+ }
1751
+ return `:${label}`;
1752
+ }
1753
+ formatRelationship(type, alias, options) {
1754
+ const aliasStr = alias ? alias : "";
1755
+ const hopsStr = this.formatHops(options);
1756
+ if (alias) {
1757
+ return `[${aliasStr}:${type}${hopsStr}]`;
1758
+ }
1759
+ return `[:${type}${hopsStr}]`;
1760
+ }
1761
+ formatHops(options) {
1762
+ if (!options?.minHops && !options?.maxHops) return "";
1763
+ const min = options.minHops ?? 1;
1764
+ const max = options.maxHops ?? "";
1765
+ return `*${min}..${max}`;
1766
+ }
1767
+ buildWhereClause() {
1768
+ if (this.state.whereClauses.length === 0) return "";
1769
+ const first = this.state.whereClauses[0];
1770
+ if (!first) return "";
1771
+ let result = first;
1772
+ for (let i = 1; i < this.state.whereClauses.length; i++) {
1773
+ const operator = this.state.whereOperators[i - 1] ?? "AND";
1774
+ const clause = this.state.whereClauses[i];
1775
+ if (clause) {
1776
+ result += ` ${operator} ${clause}`;
1777
+ }
1778
+ }
1779
+ return result;
1780
+ }
1781
+ };
1782
+ function velesql() {
1783
+ return new VelesQLBuilder();
1784
+ }
1412
1785
  // Annotate the CommonJS export names for ESM import in node:
1413
1786
  0 && (module.exports = {
1414
1787
  ConnectionError,
@@ -1417,5 +1790,7 @@ var VelesDB = class {
1417
1790
  ValidationError,
1418
1791
  VelesDB,
1419
1792
  VelesDBError,
1420
- WasmBackend
1793
+ VelesQLBuilder,
1794
+ WasmBackend,
1795
+ velesql
1421
1796
  });