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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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;
|
package/dist/src/falkordb.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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);
|