falkordb 6.9.1 → 6.9.3

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.
Files changed (46) hide show
  1. package/README.md +56 -1
  2. package/dist/index.d.ts +6 -1
  3. package/dist/index.js +11 -1
  4. package/dist/src/clients/client.d.ts +23 -5
  5. package/dist/src/clients/client.js +7 -0
  6. package/dist/src/clients/cluster.d.ts +16 -6
  7. package/dist/src/clients/cluster.js +41 -16
  8. package/dist/src/clients/nullClient.d.ts +4 -3
  9. package/dist/src/clients/nullClient.js +5 -2
  10. package/dist/src/clients/single.d.ts +6 -5
  11. package/dist/src/clients/single.js +17 -8
  12. package/dist/src/commands/CONSTRAINT_CREATE.js +3 -1
  13. package/dist/src/commands/CONSTRAINT_DROP.js +3 -1
  14. package/dist/src/commands/COPY.js +3 -1
  15. package/dist/src/commands/EXPLAIN.d.ts +3 -2
  16. package/dist/src/commands/EXPLAIN.js +5 -4
  17. package/dist/src/commands/MEMORY_USAGE.js +7 -4
  18. package/dist/src/commands/PROFILE.d.ts +4 -3
  19. package/dist/src/commands/PROFILE.js +7 -5
  20. package/dist/src/commands/QUERY.js +12 -4
  21. package/dist/src/commands/RO_QUERY.js +5 -1
  22. package/dist/src/commands/SLOWLOG.d.ts +1 -1
  23. package/dist/src/commands/SLOWLOG.js +3 -1
  24. package/dist/src/commands/SLOWLOG_RESET.d.ts +5 -0
  25. package/dist/src/commands/SLOWLOG_RESET.js +19 -0
  26. package/dist/src/commands/index.d.ts +10 -1
  27. package/dist/src/commands/index.js +60 -12
  28. package/dist/src/entities.d.ts +19 -0
  29. package/dist/src/entities.js +121 -0
  30. package/dist/src/execution-plan.d.ts +45 -0
  31. package/dist/src/execution-plan.js +121 -0
  32. package/dist/src/falkordb.js +31 -4
  33. package/dist/src/graph.d.ts +95 -18
  34. package/dist/src/graph.js +321 -75
  35. package/dist/src/query-statistics.d.ts +6 -0
  36. package/dist/src/query-statistics.js +66 -0
  37. package/dist/src/types/falkordb-options.d.ts +8 -0
  38. package/dist/src/types/graph-value.d.ts +11 -5
  39. package/dist/src/types/index.d.ts +2 -0
  40. package/dist/src/types/index.js +2 -0
  41. package/dist/src/types/procedures.d.ts +47 -0
  42. package/dist/src/types/procedures.js +2 -0
  43. package/dist/src/types/query-options.d.ts +6 -1
  44. package/dist/src/types/query-statistics.d.ts +20 -0
  45. package/dist/src/types/query-statistics.js +2 -0
  46. package/package.json +1 -1
package/dist/src/graph.js CHANGED
@@ -6,25 +6,107 @@ const enums_1 = require("./enums");
6
6
  Object.defineProperty(exports, "ConstraintType", { enumerable: true, get: function () { return enums_1.ConstraintType; } });
7
7
  Object.defineProperty(exports, "EntityType", { enumerable: true, get: function () { return enums_1.EntityType; } });
8
8
  const errors_1 = require("./errors");
9
+ const query_statistics_1 = require("./query-statistics");
10
+ const execution_plan_1 = require("./execution-plan");
11
+ const commands_1 = require("./commands");
12
+ // The error `CREATE INDEX` fails with when a property is already indexed
13
+ const ALREADY_INDEXED = /^Attribute '.*' is already indexed$/;
14
+ // Turns create{Node,Edge}VectorIndex's options into the index's OPTIONS map
15
+ function vectorIndexOptions(options) {
16
+ // Catches the positional `(label, dimension, similarityFunction, ...)` form
17
+ // removed in v7, which plain JavaScript callers could still use
18
+ if (options === null || typeof options !== "object") {
19
+ throw new TypeError("Vector index options must be an object, e.g. { dimension: 768, similarityFunction: 'cosine' }");
20
+ }
21
+ if (typeof options.dimension !== "number") {
22
+ throw new TypeError("Vector index options must include a numeric dimension");
23
+ }
24
+ const { similarityFunction = "euclidean", ...rest } = options;
25
+ const result = { similarityFunction };
26
+ for (const [key, value] of Object.entries(rest)) {
27
+ if (value !== undefined)
28
+ result[key] = value;
29
+ }
30
+ return result;
31
+ }
32
+ // The names GRAPH.CONSTRAINT accepts (`PROPERTY_NAME_PATTERN` in FalkorDB's
33
+ // src/commands/cmd_constraint.c), stricter than what Cypher can quote
34
+ const CONSTRAINT_NAME = /^[a-zA-Z_][a-zA-Z0-9_$]*$/;
35
+ function validateConstraintName(kind, name) {
36
+ if (!CONSTRAINT_NAME.test(name)) {
37
+ throw new TypeError(`${kind} name ${JSON.stringify(name)} is invalid for a constraint: ` +
38
+ "start with a letter or _, followed by letters, digits, _ or $");
39
+ }
40
+ }
41
+ // The pattern an index is defined on, with the label quoted so that names
42
+ // such as `Product Type` work
43
+ function indexPattern(entityType, label) {
44
+ const name = (0, commands_1.escapeIdentifier)(label, "label");
45
+ return entityType === "NODE" ? `(e:${name})` : `()-[e:${name}]->()`;
46
+ }
47
+ // export type GraphConnection = SingleGraphConnection | ClusterGraphConnection;
48
+ // A read is checked against the schema version on its first attempts, then
49
+ // sent once without a version, so that a schema changing faster than it can
50
+ // be read cannot keep it retrying. Four checked attempts cover a lagging
51
+ // replica's version, the master's, and a further change on the master.
52
+ const MAX_VERSIONED_READS = 5;
53
+ // Unknown value types already warned about, so each is reported only once
54
+ const warnedUnknownValueTypes = new Set();
55
+ function warnUnknownValueType(valueType) {
56
+ if (warnedUnknownValueTypes.has(valueType))
57
+ return;
58
+ warnedUnknownValueTypes.add(valueType);
59
+ console.warn(`FalkorDB: unknown value type ${valueType} in reply, returning null`);
60
+ }
9
61
  class Graph {
10
62
  #client;
11
63
  #name;
12
64
  #metadata;
13
65
  #schema;
14
66
  #onDelete;
67
+ #selectGraph;
68
+ // Integers are read from their exact digits and returned as bigint
69
+ #exactIntegers;
15
70
  constructor(client, name, cache) {
16
71
  this.#client = client;
17
72
  this.#name = name;
18
73
  this.#schema = cache?.schema ?? { generation: 0, version: 0 };
19
74
  this.#onDelete = cache?.onDelete;
75
+ this.#selectGraph = cache?.selectGraph;
76
+ this.#exactIntegers = cache?.integers === "bigint";
77
+ }
78
+ /** The graph's name (its Redis key) */
79
+ get name() {
80
+ return this.#name;
20
81
  }
21
82
  async query(query, options) {
22
- const reply = await this.#withSchemaVersion((version) => this.#client.query(this.#name, query, options, true, version));
83
+ const reply = await this.#withSchemaVersion((version) => this.#client.query(this.#name, query, options, true, version, this.#exactIntegers));
23
84
  return this.#parseReply(reply);
24
85
  }
25
86
  async roQuery(query, options) {
26
- const reply = await this.#withSchemaVersion((version) => this.#client.roQuery(this.#name, query, options, true, version));
27
- return this.#parseReply(reply);
87
+ // Reads may be served by any node until one of them, or the master the
88
+ // names come from, rejects the schema version
89
+ let primary = false;
90
+ for (let attempt = 1;; attempt++) {
91
+ const version = attempt < MAX_VERSIONED_READS ? this.#schema.version : undefined;
92
+ try {
93
+ const reply = await this.#client.roQuery(this.#name, query, options, true, version, primary, this.#exactIntegers);
94
+ return await this.#parseReply(reply, version);
95
+ }
96
+ catch (err) {
97
+ if (!(err instanceof errors_1.SchemaVersionMismatchError))
98
+ throw err;
99
+ // A replica can lag behind the master, even keep a graph the master
100
+ // deleted and recreated. Read from the master instead, still checked
101
+ // against the version, so that its reply matches its names even if
102
+ // the schema changes meanwhile. A version reported by a replica is
103
+ // replaced by the master's when the master rejects it in turn. Reads
104
+ // change nothing, so running one again is safe.
105
+ this.clearMetadataCache();
106
+ this.#schema.version = err.version;
107
+ primary = true;
108
+ }
109
+ }
28
110
  }
29
111
  async #withSchemaVersion(run) {
30
112
  try {
@@ -64,11 +146,20 @@ class Graph {
64
146
  this.#schema.generation++;
65
147
  this.#metadata = undefined;
66
148
  }
67
- async explain(query) {
68
- return this.#client.explain(this.#name, query);
149
+ /**
150
+ * Returns the query's execution plan as an operation tree, without running
151
+ * the query. The lines the server returned are kept in `steps`.
152
+ */
153
+ async explain(query, options) {
154
+ return new execution_plan_1.ExecutionPlan(await this.#client.explain(this.#name, query, options));
69
155
  }
70
- async profile(query) {
71
- return this.#client.profile(this.#name, query);
156
+ /**
157
+ * Runs the query and returns its execution plan as an operation tree, each
158
+ * operation annotated with its `recordsProduced` and `executionTime`. The
159
+ * lines the server returned are kept in `steps`.
160
+ */
161
+ async profile(query, options) {
162
+ return new execution_plan_1.ExecutionPlan(await this.#client.profile(this.#name, query, options));
72
163
  }
73
164
  async memoryUsage(options) {
74
165
  return this.#client.memoryUsage(this.#name, options);
@@ -76,23 +167,157 @@ class Graph {
76
167
  async slowLog() {
77
168
  return this.#client.slowLog(this.#name);
78
169
  }
170
+ /** Clears the graph's slow log (`GRAPH.SLOWLOG <graph> RESET`) */
171
+ async slowLogReset() {
172
+ return this.#client.slowLogReset(this.#name);
173
+ }
174
+ /**
175
+ * Calls a procedure, e.g. `callProcedure('db.idx.fulltext.queryNodes', ['Movie', 'Jungle'])`.
176
+ * The arguments are sent as query parameters, so they need no escaping.
177
+ * Runs as a read-only query unless `readOnly` is `false`.
178
+ */
179
+ async callProcedure(procedure, args = [], options = {}) {
180
+ const params = {};
181
+ const placeholders = args.map((arg, i) => {
182
+ params[`param${i}`] = arg;
183
+ return `$param${i}`;
184
+ });
185
+ let query = `CALL ${procedure}(${placeholders.join(",")})`;
186
+ if (options.yields?.length) {
187
+ query += ` YIELD ${options.yields.join(",")}`;
188
+ }
189
+ const queryOptions = { TIMEOUT: options.timeout };
190
+ if (args.length)
191
+ queryOptions.params = params;
192
+ return options.readOnly === false
193
+ ? this.query(query, queryOptions)
194
+ : this.roQuery(query, queryOptions);
195
+ }
196
+ /** Lists the graph's indexes (`CALL db.indexes()`) */
197
+ async listIndices() {
198
+ const reply = await this.callProcedure("db.indexes");
199
+ return (reply.data ?? []).map(({ entitytype, ...index }) => ({
200
+ ...index,
201
+ entityType: entitytype,
202
+ }));
203
+ }
204
+ /** Lists the graph's constraints and their status (`CALL db.constraints()`) */
205
+ async listConstraints() {
206
+ const reply = await this.callProcedure("db.constraints");
207
+ return (reply.data ?? []).map(({ entitytype, ...constraint }) => ({
208
+ ...constraint,
209
+ entityType: entitytype,
210
+ }));
211
+ }
212
+ /**
213
+ * Creates a constraint (`GRAPH.CONSTRAINT CREATE`).
214
+ *
215
+ * A unique constraint requires a range index on each of its properties, so
216
+ * for `ConstraintType.UNIQUE` this first creates that index on every
217
+ * property that is not indexed yet (an existing index is reused). Mandatory
218
+ * constraints need no index and create none.
219
+ *
220
+ * The constraint is enforced asynchronously: the server accepts it before
221
+ * checking the existing data, so poll `listConstraints()` until its status
222
+ * is `OPERATIONAL` (or `FAILED` if existing entities violate it).
223
+ */
79
224
  async constraintCreate(constraintType, entityType, label, ...properties) {
225
+ // Checked before the supporting indexes are created, so that a name the
226
+ // server rejects for a constraint leaves no index behind
227
+ validateConstraintName("Label", label);
228
+ properties.forEach((property) => validateConstraintName("Property", property));
229
+ if (constraintType === enums_1.ConstraintType.UNIQUE) {
230
+ await this.#ensureRangeIndexes(entityType === enums_1.EntityType.RELATIONSHIP ? "EDGE" : "NODE", label, properties);
231
+ }
80
232
  return this.#client.constraintCreate(this.#name, constraintType, entityType, label, ...properties);
81
233
  }
82
234
  async constraintDrop(constraintType, entityType, label, ...properties) {
83
235
  return this.#client.constraintDrop(this.#name, constraintType, entityType, label, ...properties);
84
236
  }
237
+ /**
238
+ * Creates a unique constraint on nodes, together with the range index it
239
+ * requires on each property. Enforced asynchronously, see `constraintCreate`.
240
+ */
241
+ async createNodeUniqueConstraint(label, ...properties) {
242
+ return this.constraintCreate(enums_1.ConstraintType.UNIQUE, enums_1.EntityType.NODE, label, ...properties);
243
+ }
244
+ /**
245
+ * Creates a unique constraint on relationships, together with the range
246
+ * index it requires on each property. Enforced asynchronously, see
247
+ * `constraintCreate`.
248
+ */
249
+ async createEdgeUniqueConstraint(relation, ...properties) {
250
+ return this.constraintCreate(enums_1.ConstraintType.UNIQUE, enums_1.EntityType.RELATIONSHIP, relation, ...properties);
251
+ }
252
+ /**
253
+ * Creates a mandatory constraint on nodes. Enforced asynchronously, see
254
+ * `constraintCreate`.
255
+ */
256
+ async createNodeMandatoryConstraint(label, ...properties) {
257
+ return this.constraintCreate(enums_1.ConstraintType.MANDATORY, enums_1.EntityType.NODE, label, ...properties);
258
+ }
259
+ /**
260
+ * Creates a mandatory constraint on relationships. Enforced asynchronously,
261
+ * see `constraintCreate`.
262
+ */
263
+ async createEdgeMandatoryConstraint(relation, ...properties) {
264
+ return this.constraintCreate(enums_1.ConstraintType.MANDATORY, enums_1.EntityType.RELATIONSHIP, relation, ...properties);
265
+ }
266
+ /** Drops a unique constraint on nodes; its supporting range index is kept */
267
+ async dropNodeUniqueConstraint(label, ...properties) {
268
+ return this.constraintDrop(enums_1.ConstraintType.UNIQUE, enums_1.EntityType.NODE, label, ...properties);
269
+ }
270
+ /** Drops a unique constraint on relationships; its supporting range index is kept */
271
+ async dropEdgeUniqueConstraint(relation, ...properties) {
272
+ return this.constraintDrop(enums_1.ConstraintType.UNIQUE, enums_1.EntityType.RELATIONSHIP, relation, ...properties);
273
+ }
274
+ async dropNodeMandatoryConstraint(label, ...properties) {
275
+ return this.constraintDrop(enums_1.ConstraintType.MANDATORY, enums_1.EntityType.NODE, label, ...properties);
276
+ }
277
+ async dropEdgeMandatoryConstraint(relation, ...properties) {
278
+ return this.constraintDrop(enums_1.ConstraintType.MANDATORY, enums_1.EntityType.RELATIONSHIP, relation, ...properties);
279
+ }
280
+ // Indexes each property separately: a single CREATE INDEX over several
281
+ // properties fails as a whole if any of them is already indexed
282
+ async #ensureRangeIndexes(entityType, label, properties) {
283
+ for (const property of properties) {
284
+ try {
285
+ await this.createTypedIndex("RANGE", entityType, label, [property]);
286
+ }
287
+ catch (err) {
288
+ if (!(err instanceof Error && ALREADY_INDEXED.test(err.message))) {
289
+ throw err;
290
+ }
291
+ }
292
+ }
293
+ }
294
+ /**
295
+ * Copies the graph to `destGraph` (`GRAPH.COPY`) and returns the copy: for
296
+ * a graph from `selectGraph`, the instance `selectGraph(destGraph)` returns.
297
+ */
85
298
  async copy(destGraph) {
86
- return this.#client.copy(this.#name, destGraph);
299
+ await this.#client.copy(this.#name, destGraph);
300
+ const copy = this.#selectGraph?.(destGraph);
301
+ if (!copy) {
302
+ return new Graph(this.#client, destGraph, {
303
+ integers: this.#exactIntegers ? "bigint" : "number",
304
+ });
305
+ }
306
+ // Names cached for a graph that had this name before do not apply
307
+ copy.clearMetadataCache();
308
+ return copy;
87
309
  }
88
310
  #pendingMetadata;
89
- #updateMetadata() {
311
+ #updateMetadata(version) {
90
312
  const generation = this.#schema.generation;
91
- // A fetch started before the metadata was invalidated is not joined
92
- if (this.#pendingMetadata?.generation !== generation) {
313
+ // A fetch started before the metadata was invalidated, or for another
314
+ // schema version, is not joined
315
+ if (this.#pendingMetadata?.generation !== generation ||
316
+ this.#pendingMetadata.version !== version) {
93
317
  const pending = {
94
318
  generation,
95
- promise: this.#setMetadata(generation).finally(() => {
319
+ version,
320
+ promise: this.#setMetadata(generation, version).finally(() => {
96
321
  if (this.#pendingMetadata === pending) {
97
322
  this.#pendingMetadata = undefined;
98
323
  }
@@ -103,11 +328,15 @@ class Graph {
103
328
  return this.#pendingMetadata.promise;
104
329
  }
105
330
  // DO NOT use directly, use #updateMetadata instead
106
- async #setMetadata(generation) {
331
+ async #setMetadata(generation, version) {
332
+ // Fetched from the master: a reply served by it (every write's) can use
333
+ // names a lagging replica does not have yet. With a version, the master
334
+ // rejects the fetch unless its schema is the one the reply was checked
335
+ // against, e.g. one a replica served from a graph since recreated.
107
336
  const [labels, relationshipTypes, propertyKeys] = await Promise.all([
108
- this.#client.roQuery(this.#name, "CALL db.labels()", undefined, false),
109
- this.#client.roQuery(this.#name, "CALL db.relationshipTypes()", undefined, false),
110
- this.#client.roQuery(this.#name, "CALL db.propertyKeys()", undefined, false),
337
+ this.#client.roQuery(this.#name, "CALL db.labels()", undefined, false, version, true),
338
+ this.#client.roQuery(this.#name, "CALL db.relationshipTypes()", undefined, false, version, true),
339
+ this.#client.roQuery(this.#name, "CALL db.propertyKeys()", undefined, false, version, true),
111
340
  ]);
112
341
  const metadata = {
113
342
  labels: this.#cleanMetadataArray(labels.data),
@@ -117,72 +346,86 @@ class Graph {
117
346
  // The caller's reply predates any reset, so it still gets these names,
118
347
  // but they are only cached if the graph was not deleted meanwhile
119
348
  if (generation === this.#schema.generation) {
120
- this.#metadata = { generation, names: metadata };
349
+ this.#metadata = { generation, version, names: metadata };
121
350
  }
122
351
  return metadata;
123
352
  }
124
353
  #cleanMetadataArray(arr) {
125
354
  return arr.map(([value]) => value);
126
355
  }
127
- #getMetadata(key, id) {
128
- const metadata = this.#metadata?.generation === this.#schema.generation
356
+ #getMetadata(key, id, version) {
357
+ const metadata = this.#metadata?.generation === this.#schema.generation &&
358
+ (version === undefined || this.#metadata.version === version)
129
359
  ? this.#metadata.names
130
360
  : undefined;
131
- return metadata?.[key][id] ?? this.#getMetadataAsync(key, id);
361
+ return metadata?.[key][id] ?? this.#getMetadataAsync(key, id, version);
132
362
  }
133
363
  // DO NOT use directly, use #getMetadata instead
134
- async #getMetadataAsync(key, id) {
135
- const value = (await this.#updateMetadata())[key][id];
364
+ async #getMetadataAsync(key, id, version) {
365
+ const value = (await this.#updateMetadata(version))[key][id];
136
366
  if (value === undefined)
137
367
  throw new Error(`Cannot find value from ${key}[${id}]`);
138
368
  return value;
139
369
  }
140
- async #parseReply(reply) {
370
+ async #parseReply(reply, version) {
371
+ // The raw statistic lines are replaced by their parsed form
372
+ const stats = (0, query_statistics_1.parseQueryStatistics)(reply.metadata);
141
373
  if (!reply.data)
142
- return reply;
143
- const promises = [], parsed = {
144
- metadata: reply.metadata,
374
+ return { stats };
375
+ // Compact replies describe each column as [type, name]
376
+ const headers = reply.headers.map(([, name]) => name);
377
+ const ctx = { promises: [], version }, parsed = {
378
+ headers,
379
+ stats,
145
380
  data: reply.data.map((row) => {
146
381
  const data = {};
147
382
  if (Array.isArray(row)) {
148
383
  for (let i = 0; i < row.length; i++) {
149
384
  const value = row[i];
150
- data[reply.headers[i][1]] = this.#parseValue(value, promises);
385
+ data[headers[i]] = this.#parseValue(value, ctx);
151
386
  }
152
387
  }
153
388
  return data;
154
389
  }),
155
390
  };
156
- if (promises.length)
157
- await Promise.all(promises);
391
+ if (ctx.promises.length)
392
+ await Promise.all(ctx.promises);
158
393
  return parsed;
159
394
  }
160
- #parseValue([valueType, value], promises) {
395
+ // With exact integers every integer in a reply, type codes and ids
396
+ // included, arrives as a string of its digits; Number() undoes that and
397
+ // leaves a number unchanged.
398
+ #parseValue([valueType, value], ctx) {
399
+ if (typeof valueType === "string") {
400
+ return this.#parseValue([Number(valueType), value], ctx);
401
+ }
161
402
  switch (valueType) {
162
403
  case enums_1.GraphValueTypes.NULL:
163
404
  return null;
164
405
  case enums_1.GraphValueTypes.STRING:
165
- case enums_1.GraphValueTypes.INTEGER:
166
406
  return value;
407
+ case enums_1.GraphValueTypes.INTEGER:
408
+ // Digits only arrive as a string when integers are read exactly
409
+ return typeof value === "string" ? BigInt(value) : value;
167
410
  case enums_1.GraphValueTypes.BOOLEAN:
168
411
  return value === "true";
169
412
  case enums_1.GraphValueTypes.DOUBLE:
170
413
  return parseFloat(value);
171
414
  case enums_1.GraphValueTypes.ARRAY:
172
- return value.map((x) => this.#parseValue(x, promises));
415
+ return value.map((x) => this.#parseValue(x, ctx));
173
416
  case enums_1.GraphValueTypes.EDGE:
174
- return this.#parseEdge(value, promises);
417
+ return this.#parseEdge(value, ctx);
175
418
  case enums_1.GraphValueTypes.NODE:
176
- return this.#parseNode(value, promises);
419
+ return this.#parseNode(value, ctx);
177
420
  case enums_1.GraphValueTypes.PATH:
178
421
  return {
179
- nodes: value[0][1].map(([, node]) => this.#parseNode(node, promises)),
180
- edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, promises)),
422
+ nodes: value[0][1].map(([, node]) => this.#parseNode(node, ctx)),
423
+ edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, ctx)),
181
424
  };
182
425
  case enums_1.GraphValueTypes.MAP: {
183
426
  const map = {};
184
427
  for (let i = 0; i < value.length; i++) {
185
- map[value[i++]] = this.#parseValue(value[i], promises);
428
+ map[value[i++]] = this.#parseValue(value[i], ctx);
186
429
  }
187
430
  return map;
188
431
  }
@@ -194,19 +437,19 @@ class Graph {
194
437
  case enums_1.GraphValueTypes.VECTORF32:
195
438
  return value.map((x) => Number(x));
196
439
  case enums_1.GraphValueTypes.DATETIME:
197
- return polyfill_1.Temporal.Instant.fromEpochMilliseconds(value * 1000)
440
+ return polyfill_1.Temporal.Instant.fromEpochMilliseconds(Number(value) * 1000)
198
441
  .toZonedDateTimeISO("UTC")
199
442
  .toPlainDateTime();
200
443
  case enums_1.GraphValueTypes.DATE:
201
- return polyfill_1.Temporal.Instant.fromEpochMilliseconds(value * 1000)
444
+ return polyfill_1.Temporal.Instant.fromEpochMilliseconds(Number(value) * 1000)
202
445
  .toZonedDateTimeISO("UTC")
203
446
  .toPlainDate();
204
447
  case enums_1.GraphValueTypes.TIME:
205
- return polyfill_1.Temporal.Instant.fromEpochMilliseconds(value * 1000)
448
+ return polyfill_1.Temporal.Instant.fromEpochMilliseconds(Number(value) * 1000)
206
449
  .toZonedDateTimeISO("UTC")
207
450
  .toPlainTime();
208
451
  case enums_1.GraphValueTypes.DURATION:
209
- const time = polyfill_1.Temporal.Instant.fromEpochMilliseconds(value * 1000);
452
+ const time = polyfill_1.Temporal.Instant.fromEpochMilliseconds(Number(value) * 1000);
210
453
  const epoch = polyfill_1.Temporal.Instant.fromEpochMilliseconds(0);
211
454
  return epoch
212
455
  .toZonedDateTimeISO("UTC")
@@ -214,48 +457,51 @@ class Graph {
214
457
  largestUnit: "years",
215
458
  });
216
459
  default:
217
- throw new Error(`unknown scalar type: ${valueType}`);
460
+ // A type this client does not know yet (e.g. one added by a newer
461
+ // server) is read as null rather than failing the whole query
462
+ warnUnknownValueType(valueType);
463
+ return null;
218
464
  }
219
465
  }
220
- #parseEdge([id, relationshipTypeId, sourceId, destinationId, properties,], promises) {
466
+ #parseEdge([id, relationshipTypeId, sourceId, destinationId, properties,], ctx) {
221
467
  const edge = {
222
- id,
223
- sourceId,
224
- destinationId,
225
- properties: this.#parseProperties(properties, promises),
468
+ id: Number(id),
469
+ sourceId: Number(sourceId),
470
+ destinationId: Number(destinationId),
471
+ properties: this.#parseProperties(properties, ctx),
226
472
  };
227
- const relationshipType = this.#getMetadata("relationshipTypes", relationshipTypeId);
473
+ const relationshipType = this.#getMetadata("relationshipTypes", Number(relationshipTypeId), ctx.version);
228
474
  if (relationshipType instanceof Promise) {
229
- promises.push(relationshipType.then((value) => (edge.relationshipType = value)));
475
+ ctx.promises.push(relationshipType.then((value) => (edge.relationshipType = value)));
230
476
  }
231
477
  else {
232
478
  edge.relationshipType = relationshipType;
233
479
  }
234
480
  return edge;
235
481
  }
236
- #parseNode([id, labelIds, properties], promises) {
482
+ #parseNode([id, labelIds, properties], ctx) {
237
483
  const labels = new Array(labelIds.length);
238
484
  for (let i = 0; i < labelIds.length; i++) {
239
- const value = this.#getMetadata("labels", labelIds[i]);
485
+ const value = this.#getMetadata("labels", Number(labelIds[i]), ctx.version);
240
486
  if (value instanceof Promise) {
241
- promises.push(value.then((value) => (labels[i] = value)));
487
+ ctx.promises.push(value.then((value) => (labels[i] = value)));
242
488
  }
243
489
  else {
244
490
  labels[i] = value;
245
491
  }
246
492
  }
247
493
  return {
248
- id,
494
+ id: Number(id),
249
495
  labels,
250
- properties: this.#parseProperties(properties, promises),
496
+ properties: this.#parseProperties(properties, ctx),
251
497
  };
252
498
  }
253
- #parseProperties(raw, promises) {
499
+ #parseProperties(raw, ctx) {
254
500
  const parsed = {};
255
501
  for (const [id, type, value] of raw) {
256
- const parsedValue = this.#parseValue([type, value], promises), key = this.#getMetadata("propertyKeys", id);
502
+ const parsedValue = this.#parseValue([type, value], ctx), key = this.#getMetadata("propertyKeys", Number(id), ctx.version);
257
503
  if (key instanceof Promise) {
258
- promises.push(key.then((key) => (parsed[key] = parsedValue)));
504
+ ctx.promises.push(key.then((key) => (parsed[key] = parsedValue)));
259
505
  }
260
506
  else {
261
507
  parsed[key] = parsedValue;
@@ -264,12 +510,12 @@ class Graph {
264
510
  return parsed;
265
511
  }
266
512
  async createTypedIndex(idxType, entityType, label, properties, options) {
267
- const pattern = entityType === "NODE" ? `(e:${label})` : `()-[e:${label}]->()`;
513
+ const pattern = indexPattern(entityType, label);
268
514
  if (idxType === "RANGE") {
269
515
  idxType = "";
270
516
  }
271
517
  let query = `CREATE ${idxType ? idxType + " " : ""}INDEX FOR ${pattern} ON (${properties
272
- .map((prop) => `e.${prop}`)
518
+ .map((prop) => `e.${(0, commands_1.escapeIdentifier)(prop, "property name")}`)
273
519
  .join(", ")})`;
274
520
  if (options) {
275
521
  const optionsMap = Object.entries(options)
@@ -277,7 +523,7 @@ class Graph {
277
523
  .join(", ");
278
524
  query += ` OPTIONS {${optionsMap}}`;
279
525
  }
280
- return this.#client.query(this.#name, query);
526
+ return this.query(query);
281
527
  }
282
528
  async createNodeRangeIndex(label, ...properties) {
283
529
  return this.createTypedIndex("RANGE", "NODE", label, properties);
@@ -285,12 +531,12 @@ class Graph {
285
531
  async createNodeFulltextIndex(label, ...properties) {
286
532
  return this.createTypedIndex("FULLTEXT", "NODE", label, properties);
287
533
  }
288
- async createNodeVectorIndex(label, dim = 0, similarityFunction = "euclidean", ...properties) {
289
- const options = {
290
- dimension: dim,
291
- similarityFunction: similarityFunction,
292
- };
293
- return await this.createTypedIndex("VECTOR", "NODE", label, properties, options);
534
+ /**
535
+ * Creates a vector index, e.g.
536
+ * `createNodeVectorIndex(label, { dimension: 768, similarityFunction: 'cosine' }, 'embedding')`.
537
+ */
538
+ async createNodeVectorIndex(label, options, ...properties) {
539
+ return this.createTypedIndex("VECTOR", "NODE", label, properties, vectorIndexOptions(options));
294
540
  }
295
541
  async createEdgeRangeIndex(label, ...properties) {
296
542
  return this.createTypedIndex("RANGE", "EDGE", label, properties);
@@ -298,20 +544,20 @@ class Graph {
298
544
  async createEdgeFulltextIndex(label, ...properties) {
299
545
  return this.createTypedIndex("FULLTEXT", "EDGE", label, properties);
300
546
  }
301
- async createEdgeVectorIndex(label, dim = 0, similarityFunction = "euclidean", ...properties) {
302
- const options = {
303
- dimension: dim,
304
- similarityFunction: similarityFunction,
305
- };
306
- return await this.createTypedIndex("VECTOR", "EDGE", label, properties, options);
547
+ /**
548
+ * Creates a vector index, e.g.
549
+ * `createEdgeVectorIndex(label, { dimension: 768, similarityFunction: 'cosine' }, 'embedding')`.
550
+ */
551
+ async createEdgeVectorIndex(label, options, ...properties) {
552
+ return this.createTypedIndex("VECTOR", "EDGE", label, properties, vectorIndexOptions(options));
307
553
  }
308
554
  async dropTypedIndex(idxType, entityType, label, attribute) {
309
- const pattern = entityType === "NODE" ? `(e:${label})` : `()-[e:${label}]->()`;
555
+ const pattern = indexPattern(entityType, label);
310
556
  if (idxType === "RANGE") {
311
557
  idxType = "";
312
558
  }
313
- let query = `DROP ${idxType ? idxType + " " : ""}INDEX FOR ${pattern} ON (e.${attribute})`;
314
- return this.#client.query(this.#name, query);
559
+ let query = `DROP ${idxType ? idxType + " " : ""}INDEX FOR ${pattern} ON (e.${(0, commands_1.escapeIdentifier)(attribute, "property name")})`;
560
+ return this.query(query);
315
561
  }
316
562
  async dropNodeRangeIndex(label, attribute) {
317
563
  return this.dropTypedIndex("RANGE", "NODE", label, attribute);
@@ -0,0 +1,6 @@
1
+ import type { QueryStatistics } from "./types";
2
+ /**
3
+ * Parses the statistic lines of a query reply's metadata, e.g.
4
+ * `"Nodes created: 1"`. Unknown or malformed lines are ignored.
5
+ */
6
+ export declare function parseQueryStatistics(metadata?: ReadonlyArray<string>): QueryStatistics;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseQueryStatistics = parseQueryStatistics;
4
+ // Statistic lines as the server words them (`ResultSet_EmitCompactStats` in
5
+ // FalkorDB's src/resultset/formatters/resultset_replycompact.c)
6
+ const COUNTERS = {
7
+ "Labels added": "labelsAdded",
8
+ "Labels removed": "labelsRemoved",
9
+ "Nodes created": "nodesCreated",
10
+ "Nodes deleted": "nodesDeleted",
11
+ "Properties set": "propertiesSet",
12
+ "Properties removed": "propertiesRemoved",
13
+ "Relationships created": "relationshipsCreated",
14
+ "Relationships deleted": "relationshipsDeleted",
15
+ "Indices created": "indicesCreated",
16
+ "Indices deleted": "indicesDeleted",
17
+ "Constraints created": "constraintsCreated",
18
+ "Constraints deleted": "constraintsDeleted",
19
+ // e.g. "Query internal execution time: 0.123456 milliseconds"
20
+ "Query internal execution time": "queryInternalExecutionTime",
21
+ };
22
+ // e.g. "1", "0.123456 milliseconds"
23
+ const STAT_VALUE = /^(\d+(?:\.\d+)?)(?: milliseconds)?$/;
24
+ /**
25
+ * Parses the statistic lines of a query reply's metadata, e.g.
26
+ * `"Nodes created: 1"`. Unknown or malformed lines are ignored.
27
+ */
28
+ function parseQueryStatistics(metadata = []) {
29
+ const stats = {
30
+ labelsAdded: 0,
31
+ labelsRemoved: 0,
32
+ nodesCreated: 0,
33
+ nodesDeleted: 0,
34
+ propertiesSet: 0,
35
+ propertiesRemoved: 0,
36
+ relationshipsCreated: 0,
37
+ relationshipsDeleted: 0,
38
+ indicesCreated: 0,
39
+ indicesDeleted: 0,
40
+ constraintsCreated: 0,
41
+ constraintsDeleted: 0,
42
+ cachedExecution: false,
43
+ queryInternalExecutionTime: 0,
44
+ };
45
+ for (const line of metadata) {
46
+ if (typeof line !== "string")
47
+ continue;
48
+ const separator = line.indexOf(":");
49
+ if (separator === -1)
50
+ continue;
51
+ const name = line.slice(0, separator).trim(),
52
+ // The whole value must be a number, optionally followed by the unit of
53
+ // the execution time, so that a malformed line is ignored
54
+ match = STAT_VALUE.exec(line.slice(separator + 1).trim());
55
+ if (!match)
56
+ continue;
57
+ const value = Number(match[1]);
58
+ if (name === "Cached execution") {
59
+ stats.cachedExecution = value !== 0;
60
+ }
61
+ else if (Object.hasOwn(COUNTERS, name)) {
62
+ stats[COUNTERS[name]] = value;
63
+ }
64
+ }
65
+ return stats;
66
+ }