falkordb 6.9.2 → 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 +7 -2
  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
@@ -8,7 +8,11 @@ const _1 = require(".");
8
8
  const errors_1 = require("../errors");
9
9
  exports.FIRST_KEY_INDEX = 1;
10
10
  function parseCommand(parser, graph, query, options, compact, version) {
11
- const args = (0, _1.pushQueryArguments)(['GRAPH.QUERY'], graph, query, options, compact, version);
11
+ // The graph is pushed as a key so that cluster clients route the command
12
+ // to the graph's shard rather than a random node
13
+ const [, ...args] = (0, _1.pushQueryArguments)([], graph, query, options, compact, version);
14
+ parser.push('GRAPH.QUERY');
15
+ parser.pushKey(graph);
12
16
  parser.push(...args);
13
17
  }
14
18
  function transformArguments(graph, query, options, compact, version) {
@@ -23,7 +27,8 @@ function transformReply(reply) {
23
27
  const [first, second] = reply;
24
28
  if (first instanceof Error) {
25
29
  if (VERSION_MISMATCH.test(first.message)) {
26
- throw new errors_1.SchemaVersionMismatchError(second);
30
+ // A string when integers are returned as their exact digits
31
+ throw new errors_1.SchemaVersionMismatchError(Number(second));
27
32
  }
28
33
  throw first;
29
34
  }
@@ -8,7 +8,11 @@ var QUERY_1 = require("./QUERY");
8
8
  Object.defineProperty(exports, "FIRST_KEY_INDEX", { enumerable: true, get: function () { return QUERY_1.FIRST_KEY_INDEX; } });
9
9
  exports.IS_READ_ONLY = true;
10
10
  function parseCommand(parser, graph, query, options, compact, version) {
11
- const args = (0, _1.pushQueryArguments)(['GRAPH.RO_QUERY'], graph, query, options, compact, version);
11
+ // The graph is pushed as a key so that cluster clients route the command
12
+ // to the graph's shard rather than a random node
13
+ const [, ...args] = (0, _1.pushQueryArguments)([], graph, query, options, compact, version);
14
+ parser.push('GRAPH.RO_QUERY');
15
+ parser.pushKey(graph);
12
16
  parser.push(...args);
13
17
  }
14
18
  function transformArguments(graph, query, options, compact, version) {
@@ -1,6 +1,6 @@
1
1
  import { CommandParser } from '@redis/client';
2
2
  import type { SlowLogRawReply, SlowLogReply } from '../types';
3
- export declare const IS_READ_ONLY = true;
3
+ export declare const IS_READ_ONLY = false;
4
4
  export declare const FIRST_KEY_INDEX = 1;
5
5
  export declare function parseCommand(parser: CommandParser, key: string): void;
6
6
  export declare function transformArguments(key: string): string[];
@@ -4,7 +4,9 @@ exports.FIRST_KEY_INDEX = exports.IS_READ_ONLY = void 0;
4
4
  exports.parseCommand = parseCommand;
5
5
  exports.transformArguments = transformArguments;
6
6
  exports.transformReply = transformReply;
7
- exports.IS_READ_ONLY = true;
7
+ // Each node keeps its own slowlog and replicas don't record the queries their
8
+ // master ran, so this is sent to the master like a write
9
+ exports.IS_READ_ONLY = false;
8
10
  exports.FIRST_KEY_INDEX = 1;
9
11
  function parseCommand(parser, key) {
10
12
  parser.push('GRAPH.SLOWLOG');
@@ -0,0 +1,5 @@
1
+ import { CommandParser } from '@redis/client';
2
+ export declare const FIRST_KEY_INDEX = 1;
3
+ export declare function parseCommand(parser: CommandParser, key: string): void;
4
+ export declare function transformArguments(key: string): Array<string>;
5
+ export declare function transformReply(reply: unknown): 'OK';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FIRST_KEY_INDEX = void 0;
4
+ exports.parseCommand = parseCommand;
5
+ exports.transformArguments = transformArguments;
6
+ exports.transformReply = transformReply;
7
+ exports.FIRST_KEY_INDEX = 1;
8
+ // GRAPH.SLOWLOG key RESET
9
+ function parseCommand(parser, key) {
10
+ parser.push('GRAPH.SLOWLOG');
11
+ parser.pushKey(key);
12
+ parser.push('RESET');
13
+ }
14
+ function transformArguments(key) {
15
+ return ['GRAPH.SLOWLOG', key, 'RESET'];
16
+ }
17
+ function transformReply(reply) {
18
+ return reply;
19
+ }
@@ -19,8 +19,9 @@ import * as UDF_LOAD from './UDF_LOAD';
19
19
  import * as UDF_LIST from './UDF_LIST';
20
20
  import * as UDF_FLUSH from './UDF_FLUSH';
21
21
  import * as UDF_DELETE from './UDF_DELETE';
22
+ import * as SLOWLOG_RESET from './SLOWLOG_RESET';
22
23
  import { RedisArgument } from '@redis/client';
23
- import type { QueryOptionsBackwardCompatible } from '../types';
24
+ import type { QueryOptionsBackwardCompatible, QueryParams } from '../types';
24
25
  declare const _default: {
25
26
  CONFIG_GET: typeof CONFIG_GET;
26
27
  configGet: typeof CONFIG_GET;
@@ -64,8 +65,16 @@ declare const _default: {
64
65
  udfFlush: typeof UDF_FLUSH;
65
66
  UDF_DELETE: typeof UDF_DELETE;
66
67
  udfDelete: typeof UDF_DELETE;
68
+ SLOWLOG_RESET: typeof SLOWLOG_RESET;
69
+ slowLogReset: typeof SLOWLOG_RESET;
67
70
  };
68
71
  export default _default;
69
72
  export type { QueryOptions, QueryOptionsBackwardCompatible, QueryParam, QueryParams } from '../types';
70
73
  export declare function pushQueryArguments(args: Array<RedisArgument>, graph: RedisArgument, query: RedisArgument, options?: QueryOptionsBackwardCompatible, compact?: boolean, version?: number): Array<RedisArgument>;
74
+ /**
75
+ * Prefixes the query with a `CYPHER` header carrying the params. Without
76
+ * params the query is returned unchanged.
77
+ */
78
+ export declare function withParamsHeader<T extends RedisArgument>(query: T, params?: QueryParams): T | string;
79
+ export declare function escapeIdentifier(name: string, kind: string): string;
71
80
  export type { UdfListReply, UdfLibraryEntry } from '../types';
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.pushQueryArguments = pushQueryArguments;
37
+ exports.withParamsHeader = withParamsHeader;
38
+ exports.escapeIdentifier = escapeIdentifier;
37
39
  const CONFIG_GET = __importStar(require("./CONFIG_GET"));
38
40
  const CONFIG_SET = __importStar(require("./CONFIG_SET"));
39
41
  const DELETE = __importStar(require("./DELETE"));
@@ -55,6 +57,9 @@ const UDF_LOAD = __importStar(require("./UDF_LOAD"));
55
57
  const UDF_LIST = __importStar(require("./UDF_LIST"));
56
58
  const UDF_FLUSH = __importStar(require("./UDF_FLUSH"));
57
59
  const UDF_DELETE = __importStar(require("./UDF_DELETE"));
60
+ const SLOWLOG_RESET = __importStar(require("./SLOWLOG_RESET"));
61
+ const node_util_1 = require("node:util");
62
+ const lodash_1 = require("lodash");
58
63
  exports.default = {
59
64
  CONFIG_GET,
60
65
  configGet: CONFIG_GET,
@@ -98,6 +103,8 @@ exports.default = {
98
103
  udfFlush: UDF_FLUSH,
99
104
  UDF_DELETE,
100
105
  udfDelete: UDF_DELETE,
106
+ SLOWLOG_RESET,
107
+ slowLogReset: SLOWLOG_RESET,
101
108
  };
102
109
  function pushQueryArguments(args, graph, query, options, compact, version) {
103
110
  args.push(graph);
@@ -106,9 +113,7 @@ function pushQueryArguments(args, graph, query, options, compact, version) {
106
113
  pushTimeout(args, options);
107
114
  }
108
115
  else {
109
- args.push(options?.params ?
110
- `CYPHER ${queryParamsToString(options.params)} ${query}` :
111
- query);
116
+ args.push(withParamsHeader(query, options?.params));
112
117
  if (options?.TIMEOUT !== undefined) {
113
118
  pushTimeout(args, options.TIMEOUT);
114
119
  }
@@ -121,16 +126,43 @@ function pushQueryArguments(args, graph, query, options, compact, version) {
121
126
  }
122
127
  return args;
123
128
  }
129
+ /**
130
+ * Prefixes the query with a `CYPHER` header carrying the params. Without
131
+ * params the query is returned unchanged.
132
+ */
133
+ function withParamsHeader(query, params) {
134
+ return params ? `CYPHER ${queryParamsToString(params)} ${query}` : query;
135
+ }
124
136
  function pushTimeout(args, timeout) {
125
137
  args.push('TIMEOUT', timeout.toString());
126
138
  }
127
139
  function queryParamsToString(params) {
128
140
  const parts = [];
129
141
  for (const [key, value] of Object.entries(params)) {
130
- parts.push(`${key}=${queryParamToString(value)}`);
142
+ parts.push(`${escapeIdentifier(key, 'parameter name')}=${queryParamToString(value)}`);
131
143
  }
132
144
  return parts.join(' ');
133
145
  }
146
+ // Wraps a name (a parameter, map key, label or property) in backticks so that
147
+ // names which are not bare identifiers (e.g. `@type`, `Product Type`) are
148
+ // accepted by the parser. FalkorDB does not support escaped backticks inside
149
+ // identifiers, so names containing one are rejected, as are empty names
150
+ function escapeIdentifier(name, kind) {
151
+ if (name === '') {
152
+ throw new TypeError(`Cypher ${kind} cannot be empty`);
153
+ }
154
+ if (name.includes('`')) {
155
+ throw new TypeError(`Cypher ${kind} cannot contain a backtick: ${JSON.stringify(name)} ` +
156
+ '(FalkorDB does not support escaped backticks in identifiers)');
157
+ }
158
+ return `\`${name}\``;
159
+ }
160
+ // Names the type of a non-plain object for error messages, e.g. `Date` or a class name
161
+ function objectTypeName(value) {
162
+ const proto = Object.getPrototypeOf(value);
163
+ const name = Object.hasOwn(proto, 'constructor') ? proto.constructor?.name : undefined;
164
+ return name || 'object with a custom prototype';
165
+ }
134
166
  function queryParamToString(param) {
135
167
  if (param === null) {
136
168
  return 'null';
@@ -139,20 +171,36 @@ function queryParamToString(param) {
139
171
  case 'string':
140
172
  return `"${param.replace(/["\\]/g, '\\$&')}"`;
141
173
  case 'number':
174
+ if (!Number.isFinite(param)) {
175
+ throw new TypeError(`Unsupported param value ${param}: only finite numbers are supported`);
176
+ }
177
+ return param.toString();
178
+ case 'bigint':
142
179
  case 'boolean':
143
180
  return param.toString();
181
+ case 'object':
182
+ break;
183
+ default:
184
+ throw new TypeError(`Unsupported param type ${typeof param}`);
144
185
  }
145
186
  if (Array.isArray(param)) {
146
187
  return `[${param.map(queryParamToString).join(',')}]`;
147
188
  }
148
- else if (typeof param === 'object') {
149
- const body = [];
150
- for (const [key, value] of Object.entries(param)) {
151
- body.push(`${key}:${queryParamToString(value)}`);
152
- }
153
- return `{${body.join(',')}}`;
189
+ // The checks below also recognise values from another realm (e.g. a `vm`
190
+ // context), where `instanceof` and prototype identity do not hold
191
+ if (node_util_1.types.isTypedArray(param)) {
192
+ // Typed arrays (e.g. Float32Array vectors) are sent as lists
193
+ return `[${Array.from(param, queryParamToString).join(',')}]`;
154
194
  }
155
- else {
156
- throw new TypeError(`Unexpected param type ${typeof param} ${param}`);
195
+ // Only plain objects are sent as maps: any other object (a class instance,
196
+ // Date, Map, ...) would lose its meaning when reduced to its own properties
197
+ if (!(0, lodash_1.isPlainObject)(param)) {
198
+ throw new TypeError(`Unsupported param type ${objectTypeName(param)}: ` +
199
+ 'convert it to a plain object or another supported value first');
200
+ }
201
+ const body = [];
202
+ for (const [key, value] of Object.entries(param)) {
203
+ body.push(`${escapeIdentifier(key, 'map key')}:${queryParamToString(value)}`);
157
204
  }
205
+ return `{${body.join(',')}}`;
158
206
  }
@@ -0,0 +1,19 @@
1
+ import type { GraphEdge, GraphNode, GraphPath } from "./types";
2
+ /** The node as a Cypher pattern, e.g. `(:Person{name:"Alice"})` */
3
+ export declare function nodeToString(node: GraphNode): string;
4
+ /** The edge as a Cypher pattern, e.g. `()-[:KNOWS{since:2020}]->()` */
5
+ export declare function edgeToString(edge: GraphEdge): string;
6
+ /**
7
+ * The path as node and edge ids, each edge pointing the way it runs,
8
+ * e.g. `<(0)-[0]->(1)<-[1]-(2)>`
9
+ */
10
+ export declare function pathToString(path: GraphPath): string;
11
+ /** Whether two nodes have the same id, the same labels in order and equal properties */
12
+ export declare function nodesEqual(a: GraphNode, b: GraphNode): boolean;
13
+ /**
14
+ * Whether two edges are the same edge: equal ids, or else the same endpoints,
15
+ * relationship type and properties
16
+ */
17
+ export declare function edgesEqual(a: GraphEdge, b: GraphEdge): boolean;
18
+ /** Whether two paths have equal nodes and edges, in order */
19
+ export declare function pathsEqual(a: GraphPath, b: GraphPath): boolean;
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nodeToString = nodeToString;
4
+ exports.edgeToString = edgeToString;
5
+ exports.pathToString = pathToString;
6
+ exports.nodesEqual = nodesEqual;
7
+ exports.edgesEqual = edgesEqual;
8
+ exports.pathsEqual = pathsEqual;
9
+ /**
10
+ * Helpers for the nodes, edges and paths in query results, which are plain
11
+ * objects. They match the string forms and equality of the Python client's
12
+ * `Node`, `Edge` and `Path` classes.
13
+ */
14
+ const lodash_1 = require("lodash");
15
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+ // A label, relationship type or property key, quoted when it is not a plain
17
+ // identifier, as the query would write it. FalkorDB cannot parse a backtick
18
+ // inside a name, so no name it returns has one; should one appear anyway, it
19
+ // is doubled as openCypher escapes it, since this only formats a value for
20
+ // display and should not throw.
21
+ function nameToString(name) {
22
+ return IDENTIFIER.test(name) ? name : `\`${name.replace(/`/g, "``")}\``;
23
+ }
24
+ function valueToString(value) {
25
+ if (value === null)
26
+ return "null";
27
+ if (typeof value === "string") {
28
+ return `"${value.replace(/["\\]/g, "\\$&")}"`;
29
+ }
30
+ if (Array.isArray(value)) {
31
+ return `[${value.map(valueToString).join(",")}]`;
32
+ }
33
+ // Also maps with a null prototype or from another realm (e.g. node:vm)
34
+ if ((0, lodash_1.isPlainObject)(value)) {
35
+ const entries = Object.entries(value).map(([key, item]) => `${nameToString(key)}:${valueToString(item)}`);
36
+ return `{${entries.join(",")}}`;
37
+ }
38
+ // Numbers, booleans and dates and times (Temporal values)
39
+ return String(value);
40
+ }
41
+ // Properties sorted by key, e.g. `{age:30,name:"Alice"}`, or "" when there are none
42
+ function propertiesToString(properties) {
43
+ const keys = Object.keys(properties).sort();
44
+ if (keys.length === 0)
45
+ return "";
46
+ const entries = keys.map((key) => `${nameToString(key)}:${valueToString(properties[key])}`);
47
+ return `{${entries.join(",")}}`;
48
+ }
49
+ /** The node as a Cypher pattern, e.g. `(:Person{name:"Alice"})` */
50
+ function nodeToString(node) {
51
+ const labels = node.labels.map((label) => `:${nameToString(label)}`).join("");
52
+ return `(${labels}${propertiesToString(node.properties)})`;
53
+ }
54
+ /** The edge as a Cypher pattern, e.g. `()-[:KNOWS{since:2020}]->()` */
55
+ function edgeToString(edge) {
56
+ const type = edge.relationshipType ? `:${nameToString(edge.relationshipType)}` : "";
57
+ return `()-[${type}${propertiesToString(edge.properties)}]->()`;
58
+ }
59
+ /**
60
+ * The path as node and edge ids, each edge pointing the way it runs,
61
+ * e.g. `<(0)-[0]->(1)<-[1]-(2)>`
62
+ */
63
+ function pathToString(path) {
64
+ let result = "<";
65
+ path.edges.forEach((edge, i) => {
66
+ const node = path.nodes[i];
67
+ result += `(${node.id})`;
68
+ result += edge.sourceId === node.id ? `-[${edge.id}]->` : `<-[${edge.id}]-`;
69
+ });
70
+ return `${result}(${path.nodes[path.edges.length]?.id})>`;
71
+ }
72
+ function valuesEqual(a, b) {
73
+ // Object.is also treats two NaN as equal
74
+ if (a === b || Object.is(a, b))
75
+ return true;
76
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
77
+ return false;
78
+ }
79
+ if (Array.isArray(a) || Array.isArray(b)) {
80
+ return (Array.isArray(a) &&
81
+ Array.isArray(b) &&
82
+ a.length === b.length &&
83
+ a.every((item, i) => valuesEqual(item, b[i])));
84
+ }
85
+ // Dates and times (Temporal values) keep their fields in internal slots
86
+ if (!(0, lodash_1.isPlainObject)(a) || !(0, lodash_1.isPlainObject)(b)) {
87
+ return (!(0, lodash_1.isPlainObject)(a) &&
88
+ !(0, lodash_1.isPlainObject)(b) &&
89
+ a.constructor === b.constructor &&
90
+ String(a) === String(b));
91
+ }
92
+ const keys = Object.keys(a);
93
+ return (keys.length === Object.keys(b).length &&
94
+ keys.every((key) => Object.hasOwn(b, key) &&
95
+ valuesEqual(a[key], b[key])));
96
+ }
97
+ /** Whether two nodes have the same id, the same labels in order and equal properties */
98
+ function nodesEqual(a, b) {
99
+ return (a.id === b.id &&
100
+ valuesEqual(a.labels, b.labels) &&
101
+ valuesEqual(a.properties, b.properties));
102
+ }
103
+ /**
104
+ * Whether two edges are the same edge: equal ids, or else the same endpoints,
105
+ * relationship type and properties
106
+ */
107
+ function edgesEqual(a, b) {
108
+ if (a.id === b.id)
109
+ return true;
110
+ return (a.sourceId === b.sourceId &&
111
+ a.destinationId === b.destinationId &&
112
+ a.relationshipType === b.relationshipType &&
113
+ valuesEqual(a.properties, b.properties));
114
+ }
115
+ /** Whether two paths have equal nodes and edges, in order */
116
+ function pathsEqual(a, b) {
117
+ return (a.nodes.length === b.nodes.length &&
118
+ a.edges.length === b.edges.length &&
119
+ a.nodes.every((node, i) => nodesEqual(node, b.nodes[i])) &&
120
+ a.edges.every((edge, i) => edgesEqual(edge, b.edges[i])));
121
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Structured views of the plans returned by `GRAPH.EXPLAIN` and
3
+ * `GRAPH.PROFILE`, which describe one operation per line and convey nesting
4
+ * by indentation:
5
+ *
6
+ * Results
7
+ * Project
8
+ * Conditional Traverse | (a)->(b)
9
+ * Node By Label Scan | (a:A)
10
+ *
11
+ * Profile lines also end with the operation's runtime statistics, e.g.
12
+ * `| Records produced: 4, Execution time: 0.001200 ms`.
13
+ */
14
+ /** A single operation of an execution plan and the operations feeding it. */
15
+ export declare class Operation {
16
+ /** The operation's name, e.g. `Conditional Traverse` */
17
+ readonly name: string;
18
+ /** What follows the `|` on the plan line, e.g. `(a)->(b)`, if anything */
19
+ readonly args?: string;
20
+ /** How many records the operation produced (profile only) */
21
+ readonly recordsProduced?: number;
22
+ /** How long the operation ran, in milliseconds (profile only) */
23
+ readonly executionTime?: number;
24
+ /** The operations feeding this one, in plan order */
25
+ readonly children: Operation[];
26
+ constructor(name: string, args?: string, recordsProduced?: number, executionTime?: number);
27
+ /** Parses one plan line, ignoring its indentation. */
28
+ static fromLine(line: string): Operation;
29
+ /** The operation as it appears in an explain plan, e.g. `Filter` */
30
+ toString(): string;
31
+ }
32
+ /** The operation tree of a query, as returned by `Graph.explain`/`Graph.profile`. */
33
+ export declare class ExecutionPlan implements Iterable<Operation> {
34
+ /** The plan lines exactly as the server returned them */
35
+ readonly steps: readonly string[];
36
+ /** The operation producing the query's result */
37
+ readonly root: Operation;
38
+ constructor(steps: readonly string[]);
39
+ /** All operations named `name`, in plan order. */
40
+ collectOperations(name: string): Operation[];
41
+ /** Iterates over every operation, parents before their children. */
42
+ [Symbol.iterator](): Iterator<Operation>;
43
+ /** The plan as indented lines, without profile statistics. */
44
+ toString(): string;
45
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * Structured views of the plans returned by `GRAPH.EXPLAIN` and
4
+ * `GRAPH.PROFILE`, which describe one operation per line and convey nesting
5
+ * by indentation:
6
+ *
7
+ * Results
8
+ * Project
9
+ * Conditional Traverse | (a)->(b)
10
+ * Node By Label Scan | (a:A)
11
+ *
12
+ * Profile lines also end with the operation's runtime statistics, e.g.
13
+ * `| Records produced: 4, Execution time: 0.001200 ms`.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ExecutionPlan = exports.Operation = void 0;
17
+ const INDENT = " ";
18
+ // Matched against the text after a line's last `|` only: anchored at both
19
+ // ends so it runs in linear time on any input
20
+ const PROFILE_STATS = /^\s*Records produced: (\d+),\s*Execution time: (\d+(?:\.\d+)?) ms$/;
21
+ /** A single operation of an execution plan and the operations feeding it. */
22
+ class Operation {
23
+ /** The operation's name, e.g. `Conditional Traverse` */
24
+ name;
25
+ /** What follows the `|` on the plan line, e.g. `(a)->(b)`, if anything */
26
+ args;
27
+ /** How many records the operation produced (profile only) */
28
+ recordsProduced;
29
+ /** How long the operation ran, in milliseconds (profile only) */
30
+ executionTime;
31
+ /** The operations feeding this one, in plan order */
32
+ children = [];
33
+ constructor(name, args, recordsProduced, executionTime) {
34
+ this.name = name;
35
+ this.args = args;
36
+ this.recordsProduced = recordsProduced;
37
+ this.executionTime = executionTime;
38
+ }
39
+ /** Parses one plan line, ignoring its indentation. */
40
+ static fromLine(line) {
41
+ let text = line.trim();
42
+ let recordsProduced;
43
+ let executionTime;
44
+ const last = text.lastIndexOf("|");
45
+ const stats = last === -1 ? null : PROFILE_STATS.exec(text.slice(last + 1));
46
+ if (stats) {
47
+ recordsProduced = parseInt(stats[1], 10);
48
+ executionTime = parseFloat(stats[2]);
49
+ text = text.slice(0, last).trimEnd();
50
+ }
51
+ // Names never contain a `|`, while arguments may (e.g. `(a:A|B)`)
52
+ const separator = text.indexOf("|");
53
+ const name = (separator === -1 ? text : text.slice(0, separator)).trim();
54
+ const args = separator === -1 ? undefined : text.slice(separator + 1).trim();
55
+ return new Operation(name, args || undefined, recordsProduced, executionTime);
56
+ }
57
+ /** The operation as it appears in an explain plan, e.g. `Filter` */
58
+ toString() {
59
+ return this.args === undefined ? this.name : `${this.name} | ${this.args}`;
60
+ }
61
+ }
62
+ exports.Operation = Operation;
63
+ /** The operation tree of a query, as returned by `Graph.explain`/`Graph.profile`. */
64
+ class ExecutionPlan {
65
+ /** The plan lines exactly as the server returned them */
66
+ steps;
67
+ /** The operation producing the query's result */
68
+ root;
69
+ constructor(steps) {
70
+ if (!Array.isArray(steps) || steps.length === 0) {
71
+ throw new TypeError("An execution plan must be a non-empty array of lines");
72
+ }
73
+ this.steps = steps;
74
+ // stack[level] is the latest operation seen at that depth, so the parent
75
+ // of an operation at depth `level` is always stack[level - 1]
76
+ const stack = [];
77
+ steps.forEach((line, index) => {
78
+ const spaces = line.length - line.trimStart().length;
79
+ const level = spaces / INDENT.length;
80
+ const valid = line.trim() !== "" &&
81
+ Number.isInteger(level) &&
82
+ (index === 0 ? level === 0 : level > 0 && level <= stack.length);
83
+ if (!valid) {
84
+ throw new Error(`Malformed execution plan at line ${index}: "${line}"`);
85
+ }
86
+ const operation = Operation.fromLine(line);
87
+ if (level > 0) {
88
+ stack[level - 1].children.push(operation);
89
+ }
90
+ stack.length = level;
91
+ stack.push(operation);
92
+ });
93
+ this.root = stack[0];
94
+ }
95
+ /** All operations named `name`, in plan order. */
96
+ collectOperations(name) {
97
+ return [...this].filter((operation) => operation.name === name);
98
+ }
99
+ /** Iterates over every operation, parents before their children. */
100
+ *[Symbol.iterator]() {
101
+ const pending = [this.root];
102
+ while (pending.length > 0) {
103
+ const operation = pending.pop();
104
+ yield operation;
105
+ for (let i = operation.children.length - 1; i >= 0; i--) {
106
+ pending.push(operation.children[i]);
107
+ }
108
+ }
109
+ }
110
+ /** The plan as indented lines, without profile statistics. */
111
+ toString() {
112
+ const lines = [];
113
+ const render = (operation, level) => {
114
+ lines.push(INDENT.repeat(level) + operation.toString());
115
+ operation.children.forEach((child) => render(child, level + 1));
116
+ };
117
+ render(this.root, 0);
118
+ return lines.join("\n");
119
+ }
120
+ }
121
+ exports.ExecutionPlan = ExecutionPlan;
@@ -44,7 +44,11 @@ async function clientFactory(client, sentinelOptions) {
44
44
  return new sentinel_1.Sentinel(client, sentinelOptions);
45
45
  }
46
46
  else if (info.includes("redis_mode:cluster")) {
47
- return new cluster_1.Cluster(client);
47
+ // Connecting through a replica (or asking for `readonly`) opts in to
48
+ // serving read-only commands from replicas, see `Cluster`.
49
+ const replication = await client.info("replication");
50
+ const useReplicas = client.options?.readonly === true || /^role:slave\r?$/m.test(replication);
51
+ return new cluster_1.Cluster(client, { useReplicas });
48
52
  }
49
53
  return new single_1.Single(client);
50
54
  }
@@ -54,6 +58,7 @@ async function clientFactory(client, sentinelOptions) {
54
58
  const MAX_CACHED_GRAPHS = 1000;
55
59
  class FalkorDB extends events_1.EventEmitter {
56
60
  #client = new nullClient_1.NullClient();
61
+ #integers = 'number';
57
62
  #graphs = new Map();
58
63
  // Shared by every instance for a graph id, including ones evicted from
59
64
  // #graphs that callers still hold. Weakly held, so an entry lives only as
@@ -65,8 +70,11 @@ class FalkorDB extends events_1.EventEmitter {
65
70
  }
66
71
  });
67
72
  static async connect(options) {
68
- const { host, port, retryWritesOnConnectionLoss, ...rest } = options ?? {};
73
+ const { host, port, retryWritesOnConnectionLoss, integers = 'number', ...rest } = options ?? {};
69
74
  const redisOption = rest;
75
+ if (integers !== 'number' && integers !== 'bigint') {
76
+ throw new TypeError(`integers must be 'number' or 'bigint', not ${JSON.stringify(integers)}`);
77
+ }
70
78
  // Support the top-level `host`/`port` shorthand used by the other FalkorDB
71
79
  // clients by folding them into the socket options node-redis expects.
72
80
  // Explicit `socket` values win, and `url` takes precedence over both.
@@ -131,11 +139,28 @@ class FalkorDB extends events_1.EventEmitter {
131
139
  falkordb.emit('error', err); })
132
140
  .connect();
133
141
  // Create FalkorDB client wrapper
134
- const client = await clientFactory(redisClient, { retryWritesOnConnectionLoss });
135
- await client.init(falkordb);
142
+ let client;
143
+ try {
144
+ client = await clientFactory(redisClient, { retryWritesOnConnectionLoss });
145
+ await client.init(falkordb);
146
+ }
147
+ catch (err) {
148
+ // The caller gets no instance to close, so close what is open here
149
+ try {
150
+ if (client)
151
+ await client.disconnect();
152
+ else
153
+ redisClient.destroy();
154
+ }
155
+ catch {
156
+ // Already closed
157
+ }
158
+ throw err;
159
+ }
136
160
  connected = true;
137
161
  // Set the client to the FalkorDB instance after it was initialized
138
162
  falkordb.#client = client;
163
+ falkordb.#integers = integers;
139
164
  return falkordb;
140
165
  }
141
166
  /**
@@ -163,6 +188,8 @@ class FalkorDB extends events_1.EventEmitter {
163
188
  this.#graphs.delete(graphId);
164
189
  }
165
190
  },
191
+ selectGraph: (name) => this.selectGraph(name),
192
+ integers: this.#integers,
166
193
  });
167
194
  }
168
195
  this.#graphs.set(graphId, graph);