falkordb 6.9.2 → 7.0.0
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/README.md +56 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +11 -1
- package/dist/src/clients/client.d.ts +23 -5
- package/dist/src/clients/client.js +7 -0
- package/dist/src/clients/cluster.d.ts +16 -6
- package/dist/src/clients/cluster.js +41 -16
- package/dist/src/clients/nullClient.d.ts +4 -3
- package/dist/src/clients/nullClient.js +5 -2
- package/dist/src/clients/single.d.ts +6 -5
- package/dist/src/clients/single.js +17 -8
- package/dist/src/commands/CONSTRAINT_CREATE.js +3 -1
- package/dist/src/commands/CONSTRAINT_DROP.js +3 -1
- package/dist/src/commands/COPY.js +3 -1
- package/dist/src/commands/EXPLAIN.d.ts +3 -2
- package/dist/src/commands/EXPLAIN.js +5 -4
- package/dist/src/commands/MEMORY_USAGE.js +7 -4
- package/dist/src/commands/PROFILE.d.ts +4 -3
- package/dist/src/commands/PROFILE.js +7 -5
- package/dist/src/commands/QUERY.js +7 -2
- package/dist/src/commands/RO_QUERY.js +5 -1
- package/dist/src/commands/SLOWLOG.d.ts +1 -1
- package/dist/src/commands/SLOWLOG.js +3 -1
- package/dist/src/commands/SLOWLOG_RESET.d.ts +5 -0
- package/dist/src/commands/SLOWLOG_RESET.js +19 -0
- package/dist/src/commands/index.d.ts +10 -1
- package/dist/src/commands/index.js +60 -12
- package/dist/src/entities.d.ts +19 -0
- package/dist/src/entities.js +121 -0
- package/dist/src/execution-plan.d.ts +45 -0
- package/dist/src/execution-plan.js +121 -0
- package/dist/src/falkordb.js +31 -4
- package/dist/src/graph.d.ts +95 -18
- package/dist/src/graph.js +321 -75
- package/dist/src/query-statistics.d.ts +6 -0
- package/dist/src/query-statistics.js +66 -0
- package/dist/src/types/falkordb-options.d.ts +8 -0
- package/dist/src/types/graph-value.d.ts +11 -5
- package/dist/src/types/index.d.ts +2 -0
- package/dist/src/types/index.js +2 -0
- package/dist/src/types/procedures.d.ts +47 -0
- package/dist/src/types/procedures.js +2 -0
- package/dist/src/types/query-options.d.ts +6 -1
- package/dist/src/types/query-statistics.d.ts +20 -0
- package/dist/src/types/query-statistics.js +2 -0
- 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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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[
|
|
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
|
-
|
|
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,
|
|
415
|
+
return value.map((x) => this.#parseValue(x, ctx));
|
|
173
416
|
case enums_1.GraphValueTypes.EDGE:
|
|
174
|
-
return this.#parseEdge(value,
|
|
417
|
+
return this.#parseEdge(value, ctx);
|
|
175
418
|
case enums_1.GraphValueTypes.NODE:
|
|
176
|
-
return this.#parseNode(value,
|
|
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,
|
|
180
|
-
edges: value[1][1].map(([, edge]) => this.#parseEdge(edge,
|
|
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],
|
|
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
|
-
|
|
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,],
|
|
466
|
+
#parseEdge([id, relationshipTypeId, sourceId, destinationId, properties,], ctx) {
|
|
221
467
|
const edge = {
|
|
222
|
-
id,
|
|
223
|
-
sourceId,
|
|
224
|
-
destinationId,
|
|
225
|
-
properties: this.#parseProperties(properties,
|
|
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],
|
|
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,
|
|
496
|
+
properties: this.#parseProperties(properties, ctx),
|
|
251
497
|
};
|
|
252
498
|
}
|
|
253
|
-
#parseProperties(raw,
|
|
499
|
+
#parseProperties(raw, ctx) {
|
|
254
500
|
const parsed = {};
|
|
255
501
|
for (const [id, type, value] of raw) {
|
|
256
|
-
const parsedValue = this.#parseValue([type, value],
|
|
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
|
|
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
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
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
|
|
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
|
|
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
|
+
}
|