flowquery 1.0.34 → 1.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/database.d.ts +1 -0
- package/dist/graph/database.d.ts.map +1 -1
- package/dist/graph/database.js +43 -6
- package/dist/graph/database.js.map +1 -1
- package/dist/graph/relationship.d.ts +3 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +12 -4
- package/dist/graph/relationship.js.map +1 -1
- package/dist/graph/relationship_data.js +1 -1
- package/dist/graph/relationship_data.js.map +1 -1
- package/dist/graph/relationship_match_collector.d.ts.map +1 -1
- package/dist/graph/relationship_match_collector.js +6 -3
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/dist/graph/relationship_reference.js +1 -1
- package/dist/graph/relationship_reference.js.map +1 -1
- package/dist/parsing/functions/function_factory.d.ts +1 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +1 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/predicate_sum.d.ts.map +1 -1
- package/dist/parsing/functions/predicate_sum.js +13 -10
- package/dist/parsing/functions/predicate_sum.js.map +1 -1
- package/dist/parsing/functions/schema.d.ts +5 -2
- package/dist/parsing/functions/schema.d.ts.map +1 -1
- package/dist/parsing/functions/schema.js +7 -4
- package/dist/parsing/functions/schema.js.map +1 -1
- package/dist/parsing/functions/trim.d.ts +7 -0
- package/dist/parsing/functions/trim.d.ts.map +1 -0
- package/dist/parsing/functions/trim.js +37 -0
- package/dist/parsing/functions/trim.js.map +1 -0
- package/dist/parsing/operations/group_by.d.ts.map +1 -1
- package/dist/parsing/operations/group_by.js +4 -2
- package/dist/parsing/operations/group_by.js.map +1 -1
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +15 -2
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/database.py +44 -11
- package/flowquery-py/src/graph/relationship.py +11 -3
- package/flowquery-py/src/graph/relationship_data.py +2 -1
- package/flowquery-py/src/graph/relationship_match_collector.py +7 -1
- package/flowquery-py/src/graph/relationship_reference.py +2 -2
- package/flowquery-py/src/parsing/functions/__init__.py +2 -0
- package/flowquery-py/src/parsing/functions/predicate_sum.py +3 -6
- package/flowquery-py/src/parsing/functions/schema.py +9 -5
- package/flowquery-py/src/parsing/functions/trim.py +35 -0
- package/flowquery-py/src/parsing/operations/group_by.py +2 -0
- package/flowquery-py/src/parsing/parser.py +12 -2
- package/flowquery-py/tests/compute/test_runner.py +249 -4
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/database.ts +42 -4
- package/src/graph/relationship.ts +12 -4
- package/src/graph/relationship_data.ts +1 -1
- package/src/graph/relationship_match_collector.ts +6 -2
- package/src/graph/relationship_reference.ts +1 -1
- package/src/parsing/functions/function_factory.ts +1 -0
- package/src/parsing/functions/predicate_sum.ts +17 -12
- package/src/parsing/functions/schema.ts +7 -4
- package/src/parsing/functions/trim.ts +25 -0
- package/src/parsing/operations/group_by.ts +4 -1
- package/src/parsing/parser.ts +15 -2
- package/tests/compute/runner.test.ts +279 -3
- package/tests/parsing/parser.test.ts +37 -0
package/package.json
CHANGED
package/src/graph/database.ts
CHANGED
|
@@ -34,20 +34,34 @@ class Database {
|
|
|
34
34
|
}
|
|
35
35
|
const physical = new PhysicalRelationship(null, relationship.type);
|
|
36
36
|
physical.statement = statement;
|
|
37
|
+
physical.source = relationship.source;
|
|
38
|
+
physical.target = relationship.target;
|
|
37
39
|
Database.relationships.set(relationship.type, physical);
|
|
38
40
|
}
|
|
39
41
|
public getRelationship(relationship: Relationship): PhysicalRelationship | null {
|
|
40
42
|
return Database.relationships.get(relationship.type!) || null;
|
|
41
43
|
}
|
|
44
|
+
public getRelationships(relationship: Relationship): PhysicalRelationship[] {
|
|
45
|
+
const result: PhysicalRelationship[] = [];
|
|
46
|
+
for (const type of relationship.types) {
|
|
47
|
+
const physical = Database.relationships.get(type);
|
|
48
|
+
if (physical) {
|
|
49
|
+
result.push(physical);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
42
54
|
public async schema(): Promise<Record<string, any>[]> {
|
|
43
55
|
const result: Record<string, any>[] = [];
|
|
44
56
|
|
|
45
57
|
for (const [label, physical] of Database.nodes) {
|
|
46
58
|
const records = await physical.data();
|
|
47
|
-
const entry: Record<string, any> = { kind: "
|
|
59
|
+
const entry: Record<string, any> = { kind: "Node", label };
|
|
48
60
|
if (records.length > 0) {
|
|
49
61
|
const { id, ...sample } = records[0];
|
|
50
|
-
|
|
62
|
+
const properties = Object.keys(sample);
|
|
63
|
+
if (properties.length > 0) {
|
|
64
|
+
entry.properties = properties;
|
|
51
65
|
entry.sample = sample;
|
|
52
66
|
}
|
|
53
67
|
}
|
|
@@ -56,10 +70,17 @@ class Database {
|
|
|
56
70
|
|
|
57
71
|
for (const [type, physical] of Database.relationships) {
|
|
58
72
|
const records = await physical.data();
|
|
59
|
-
const entry: Record<string, any> = {
|
|
73
|
+
const entry: Record<string, any> = {
|
|
74
|
+
kind: "Relationship",
|
|
75
|
+
type,
|
|
76
|
+
from_label: physical.source?.label || null,
|
|
77
|
+
to_label: physical.target?.label || null,
|
|
78
|
+
};
|
|
60
79
|
if (records.length > 0) {
|
|
61
80
|
const { left_id, right_id, ...sample } = records[0];
|
|
62
|
-
|
|
81
|
+
const properties = Object.keys(sample);
|
|
82
|
+
if (properties.length > 0) {
|
|
83
|
+
entry.properties = properties;
|
|
63
84
|
entry.sample = sample;
|
|
64
85
|
}
|
|
65
86
|
}
|
|
@@ -78,6 +99,23 @@ class Database {
|
|
|
78
99
|
const data = await node.data();
|
|
79
100
|
return new NodeData(data as NodeRecord[]);
|
|
80
101
|
} else if (element instanceof Relationship) {
|
|
102
|
+
if (element.types.length > 1) {
|
|
103
|
+
const physicals = this.getRelationships(element);
|
|
104
|
+
if (physicals.length === 0) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`No physical relationships found for types ${element.types.join(", ")}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const allRecords: RelationshipRecord[] = [];
|
|
110
|
+
for (let i = 0; i < physicals.length; i++) {
|
|
111
|
+
const records = (await physicals[i].data()) as RelationshipRecord[];
|
|
112
|
+
const typeName = element.types[i];
|
|
113
|
+
for (const record of records) {
|
|
114
|
+
allRecords.push({ ...record, _type: typeName });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return new RelationshipData(allRecords);
|
|
118
|
+
}
|
|
81
119
|
const relationship = this.getRelationship(element);
|
|
82
120
|
if (relationship === null) {
|
|
83
121
|
throw new Error(`Physical relationship not found for type ${element.type}`);
|
|
@@ -9,7 +9,7 @@ import RelationshipMatchCollector, {
|
|
|
9
9
|
|
|
10
10
|
class Relationship extends ASTNode {
|
|
11
11
|
protected _identifier: string | null = null;
|
|
12
|
-
protected
|
|
12
|
+
protected _types: string[] = [];
|
|
13
13
|
protected _properties: Map<string, Expression> = new Map();
|
|
14
14
|
protected _hops: Hops = new Hops();
|
|
15
15
|
|
|
@@ -25,7 +25,9 @@ class Relationship extends ASTNode {
|
|
|
25
25
|
constructor(identifier: string | null = null, type: string | null = null) {
|
|
26
26
|
super();
|
|
27
27
|
this._identifier = identifier;
|
|
28
|
-
|
|
28
|
+
if (type !== null) {
|
|
29
|
+
this._types = [type];
|
|
30
|
+
}
|
|
29
31
|
}
|
|
30
32
|
public set identifier(identifier: string) {
|
|
31
33
|
this._identifier = identifier;
|
|
@@ -34,10 +36,16 @@ class Relationship extends ASTNode {
|
|
|
34
36
|
return this._identifier;
|
|
35
37
|
}
|
|
36
38
|
public set type(type: string) {
|
|
37
|
-
this.
|
|
39
|
+
this._types = [type];
|
|
38
40
|
}
|
|
39
41
|
public get type(): string | null {
|
|
40
|
-
return this.
|
|
42
|
+
return this._types.length > 0 ? this._types[0] : null;
|
|
43
|
+
}
|
|
44
|
+
public set types(types: string[]) {
|
|
45
|
+
this._types = types;
|
|
46
|
+
}
|
|
47
|
+
public get types(): string[] {
|
|
48
|
+
return this._types;
|
|
41
49
|
}
|
|
42
50
|
public get properties(): Map<string, Expression> {
|
|
43
51
|
return this._properties;
|
|
@@ -18,7 +18,7 @@ class RelationshipData extends Data {
|
|
|
18
18
|
public properties(): Record<string, any> | null {
|
|
19
19
|
const current = this.current();
|
|
20
20
|
if (current) {
|
|
21
|
-
const { left_id, right_id, ...props } = current;
|
|
21
|
+
const { left_id, right_id, _type, ...props } = current;
|
|
22
22
|
return props;
|
|
23
23
|
}
|
|
24
24
|
return null;
|
|
@@ -12,11 +12,15 @@ class RelationshipMatchCollector {
|
|
|
12
12
|
private _nodeIds: Array<string> = [];
|
|
13
13
|
|
|
14
14
|
public push(relationship: Relationship, traversalId: string): RelationshipMatchRecord {
|
|
15
|
+
const data = relationship.getData();
|
|
16
|
+
const currentRecord = data?.current();
|
|
17
|
+
const actualType =
|
|
18
|
+
currentRecord && "_type" in currentRecord ? currentRecord["_type"] : relationship.type!;
|
|
15
19
|
const match: RelationshipMatchRecord = {
|
|
16
|
-
type:
|
|
20
|
+
type: actualType,
|
|
17
21
|
startNode: relationship.source?.value() || {},
|
|
18
22
|
endNode: null,
|
|
19
|
-
properties:
|
|
23
|
+
properties: data?.properties() as Record<string, any>,
|
|
20
24
|
};
|
|
21
25
|
this._matches.push(match);
|
|
22
26
|
this._nodeIds.push(traversalId);
|
|
@@ -6,7 +6,7 @@ class RelationshipReference extends Relationship {
|
|
|
6
6
|
constructor(base: Relationship, reference: Relationship) {
|
|
7
7
|
super();
|
|
8
8
|
this._identifier = base.identifier;
|
|
9
|
-
this.
|
|
9
|
+
this._types = base.types;
|
|
10
10
|
this._hops = base.hops!;
|
|
11
11
|
this._source = base.source;
|
|
12
12
|
this._target = base.target;
|
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import PredicateFunction from "./predicate_function";
|
|
2
1
|
import { FunctionDef } from "./function_metadata";
|
|
2
|
+
import PredicateFunction from "./predicate_function";
|
|
3
3
|
|
|
4
4
|
@FunctionDef({
|
|
5
|
-
description:
|
|
5
|
+
description:
|
|
6
|
+
"Calculates the sum of values in an array with optional filtering. Uses list comprehension syntax: sum(variable IN array [WHERE condition] | expression)",
|
|
6
7
|
category: "predicate",
|
|
7
8
|
parameters: [
|
|
8
9
|
{ name: "variable", description: "Variable name to bind each element", type: "string" },
|
|
9
10
|
{ name: "array", description: "Array to iterate over", type: "array" },
|
|
10
11
|
{ name: "expression", description: "Expression to sum for each element", type: "any" },
|
|
11
|
-
{
|
|
12
|
+
{
|
|
13
|
+
name: "where",
|
|
14
|
+
description: "Optional filter condition",
|
|
15
|
+
type: "boolean",
|
|
16
|
+
required: false,
|
|
17
|
+
},
|
|
12
18
|
],
|
|
13
19
|
output: { description: "Sum of the evaluated expressions", type: "number", example: 6 },
|
|
14
|
-
examples: [
|
|
20
|
+
examples: [
|
|
21
|
+
"WITH [1, 2, 3] AS nums RETURN sum(n IN nums | n)",
|
|
22
|
+
"WITH [1, 2, 3, 4] AS nums RETURN sum(n IN nums WHERE n > 1 | n * 2)",
|
|
23
|
+
],
|
|
15
24
|
})
|
|
16
25
|
class PredicateSum extends PredicateFunction {
|
|
17
26
|
constructor() {
|
|
@@ -24,19 +33,15 @@ class PredicateSum extends PredicateFunction {
|
|
|
24
33
|
if (array === null || !Array.isArray(array)) {
|
|
25
34
|
throw new Error("Invalid array for sum function");
|
|
26
35
|
}
|
|
27
|
-
let _sum:
|
|
28
|
-
for(let i = 0; i < array.length; i++) {
|
|
36
|
+
let _sum: number = 0;
|
|
37
|
+
for (let i = 0; i < array.length; i++) {
|
|
29
38
|
this._valueHolder.holder = array[i];
|
|
30
39
|
if (this.where === null || this.where.value()) {
|
|
31
|
-
|
|
32
|
-
_sum = this._return.value();
|
|
33
|
-
} else {
|
|
34
|
-
_sum += this._return.value();
|
|
35
|
-
}
|
|
40
|
+
_sum += this._return.value();
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
return _sum;
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
export default PredicateSum;
|
|
47
|
+
export default PredicateSum;
|
|
@@ -5,8 +5,11 @@ import { FunctionDef } from "./function_metadata";
|
|
|
5
5
|
/**
|
|
6
6
|
* Built-in function that returns the graph schema of the database.
|
|
7
7
|
*
|
|
8
|
-
* Lists all nodes and relationships with their labels/types
|
|
9
|
-
* of their data (excluding id from nodes, left_id and right_id from relationships).
|
|
8
|
+
* Lists all nodes and relationships with their labels/types, properties,
|
|
9
|
+
* and a sample of their data (excluding id from nodes, left_id and right_id from relationships).
|
|
10
|
+
*
|
|
11
|
+
* Nodes: {label, properties, sample}
|
|
12
|
+
* Relationships: {type, from_label, to_label, properties, sample}
|
|
10
13
|
*
|
|
11
14
|
* @example
|
|
12
15
|
* ```
|
|
@@ -15,11 +18,11 @@ import { FunctionDef } from "./function_metadata";
|
|
|
15
18
|
*/
|
|
16
19
|
@FunctionDef({
|
|
17
20
|
description:
|
|
18
|
-
"Returns the graph schema listing all nodes and relationships with a sample of their data.",
|
|
21
|
+
"Returns the graph schema listing all nodes and relationships with their properties and a sample of their data.",
|
|
19
22
|
category: "async",
|
|
20
23
|
parameters: [],
|
|
21
24
|
output: {
|
|
22
|
-
description: "Schema entry with
|
|
25
|
+
description: "Schema entry with label/type, properties, and optional sample data",
|
|
23
26
|
type: "object",
|
|
24
27
|
},
|
|
25
28
|
examples: ["LOAD FROM schema() AS s RETURN s"],
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Function from "./function";
|
|
2
|
+
import { FunctionDef } from "./function_metadata";
|
|
3
|
+
|
|
4
|
+
@FunctionDef({
|
|
5
|
+
description: "Removes leading and trailing whitespace from a string",
|
|
6
|
+
category: "scalar",
|
|
7
|
+
parameters: [{ name: "text", description: "String to trim", type: "string" }],
|
|
8
|
+
output: { description: "Trimmed string", type: "string", example: "hello" },
|
|
9
|
+
examples: ["WITH ' hello ' AS s RETURN trim(s)", "WITH '\\tfoo\\n' AS s RETURN trim(s)"],
|
|
10
|
+
})
|
|
11
|
+
class Trim extends Function {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("trim");
|
|
14
|
+
this._expectedParameterCount = 1;
|
|
15
|
+
}
|
|
16
|
+
public value(): any {
|
|
17
|
+
const val = this.getChildren()[0].value();
|
|
18
|
+
if (typeof val !== "string") {
|
|
19
|
+
throw new Error("Invalid argument for trim function: expected a string");
|
|
20
|
+
}
|
|
21
|
+
return val.trim();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Trim;
|
|
@@ -106,7 +106,10 @@ class GroupBy extends Projection {
|
|
|
106
106
|
yield* this.generate_results(mapperIndex + 1, child);
|
|
107
107
|
}
|
|
108
108
|
} else {
|
|
109
|
-
node.elements
|
|
109
|
+
if (node.elements === null) {
|
|
110
|
+
node.elements = this.reducers.map((reducer) => reducer.element());
|
|
111
|
+
}
|
|
112
|
+
node.elements.forEach((element, reducerIndex) => {
|
|
110
113
|
this.reducers[reducerIndex].overridden = element.value;
|
|
111
114
|
});
|
|
112
115
|
const record: Record<string, any> = {};
|
package/src/parsing/parser.ts
CHANGED
|
@@ -420,6 +420,8 @@ class Parser extends BaseParser {
|
|
|
420
420
|
}
|
|
421
421
|
relationship = new Relationship();
|
|
422
422
|
relationship.type = type;
|
|
423
|
+
relationship.source = node;
|
|
424
|
+
relationship.target = target;
|
|
423
425
|
}
|
|
424
426
|
this.expectAndSkipWhitespaceAndComments();
|
|
425
427
|
if (!this.token.isAs()) {
|
|
@@ -673,8 +675,19 @@ class Parser extends BaseParser {
|
|
|
673
675
|
if (!this.token.isIdentifierOrKeyword()) {
|
|
674
676
|
throw new Error("Expected relationship type identifier");
|
|
675
677
|
}
|
|
676
|
-
const
|
|
678
|
+
const types: string[] = [this.token.value || ""];
|
|
677
679
|
this.setNextToken();
|
|
680
|
+
while (this.token.isPipe()) {
|
|
681
|
+
this.setNextToken();
|
|
682
|
+
if (this.token.isColon()) {
|
|
683
|
+
this.setNextToken();
|
|
684
|
+
}
|
|
685
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
686
|
+
throw new Error("Expected relationship type identifier after '|'");
|
|
687
|
+
}
|
|
688
|
+
types.push(this.token.value || "");
|
|
689
|
+
this.setNextToken();
|
|
690
|
+
}
|
|
678
691
|
const hops: Hops | null = this.parseRelationshipHops();
|
|
679
692
|
const properties: Map<string, Expression> = new Map(this.parseProperties());
|
|
680
693
|
if (!this.token.isClosingBracket()) {
|
|
@@ -711,7 +724,7 @@ class Parser extends BaseParser {
|
|
|
711
724
|
if (hops !== null) {
|
|
712
725
|
relationship.hops = hops;
|
|
713
726
|
}
|
|
714
|
-
relationship.
|
|
727
|
+
relationship.types = types;
|
|
715
728
|
return relationship;
|
|
716
729
|
}
|
|
717
730
|
|
|
@@ -613,6 +613,38 @@ test("Test toLower function with all uppercase", async () => {
|
|
|
613
613
|
expect(results[0]).toEqual({ result: "foo bar" });
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
+
test("Test trim function", async () => {
|
|
617
|
+
const runner = new Runner('RETURN trim(" hello ") as result');
|
|
618
|
+
await runner.run();
|
|
619
|
+
const results = runner.results;
|
|
620
|
+
expect(results.length).toBe(1);
|
|
621
|
+
expect(results[0]).toEqual({ result: "hello" });
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("Test trim function with tabs and newlines", async () => {
|
|
625
|
+
const runner = new Runner('WITH "\tfoo\n" AS s RETURN trim(s) as result');
|
|
626
|
+
await runner.run();
|
|
627
|
+
const results = runner.results;
|
|
628
|
+
expect(results.length).toBe(1);
|
|
629
|
+
expect(results[0]).toEqual({ result: "foo" });
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("Test trim function with no whitespace", async () => {
|
|
633
|
+
const runner = new Runner('RETURN trim("hello") as result');
|
|
634
|
+
await runner.run();
|
|
635
|
+
const results = runner.results;
|
|
636
|
+
expect(results.length).toBe(1);
|
|
637
|
+
expect(results[0]).toEqual({ result: "hello" });
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("Test trim function with empty string", async () => {
|
|
641
|
+
const runner = new Runner('RETURN trim("") as result');
|
|
642
|
+
await runner.run();
|
|
643
|
+
const results = runner.results;
|
|
644
|
+
expect(results.length).toBe(1);
|
|
645
|
+
expect(results[0]).toEqual({ result: "" });
|
|
646
|
+
});
|
|
647
|
+
|
|
616
648
|
test("Test associative array with key which is keyword", async () => {
|
|
617
649
|
const runner = new Runner("RETURN {return: 1} as aa");
|
|
618
650
|
await runner.run();
|
|
@@ -1964,20 +1996,24 @@ test("Test schema() returns nodes and relationships with sample data", async ()
|
|
|
1964
1996
|
`).run();
|
|
1965
1997
|
|
|
1966
1998
|
const runner = new Runner(
|
|
1967
|
-
"CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
|
|
1999
|
+
"CALL schema() YIELD kind, label, type, from_label, to_label, properties, sample RETURN kind, label, type, from_label, to_label, properties, sample"
|
|
1968
2000
|
);
|
|
1969
2001
|
await runner.run();
|
|
1970
2002
|
const results = runner.results;
|
|
1971
2003
|
|
|
1972
|
-
const animal = results.find((r: any) => r.kind === "
|
|
2004
|
+
const animal = results.find((r: any) => r.kind === "Node" && r.label === "Animal");
|
|
1973
2005
|
expect(animal).toBeDefined();
|
|
2006
|
+
expect(animal.properties).toEqual(["species", "legs"]);
|
|
1974
2007
|
expect(animal.sample).toBeDefined();
|
|
1975
2008
|
expect(animal.sample).not.toHaveProperty("id");
|
|
1976
2009
|
expect(animal.sample).toHaveProperty("species");
|
|
1977
2010
|
expect(animal.sample).toHaveProperty("legs");
|
|
1978
2011
|
|
|
1979
|
-
const chases = results.find((r: any) => r.kind === "
|
|
2012
|
+
const chases = results.find((r: any) => r.kind === "Relationship" && r.type === "CHASES");
|
|
1980
2013
|
expect(chases).toBeDefined();
|
|
2014
|
+
expect(chases.from_label).toBe("Animal");
|
|
2015
|
+
expect(chases.to_label).toBe("Animal");
|
|
2016
|
+
expect(chases.properties).toEqual(["speed"]);
|
|
1981
2017
|
expect(chases.sample).toBeDefined();
|
|
1982
2018
|
expect(chases.sample).not.toHaveProperty("left_id");
|
|
1983
2019
|
expect(chases.sample).not.toHaveProperty("right_id");
|
|
@@ -2690,3 +2726,243 @@ test("Test UNION with empty right side", async () => {
|
|
|
2690
2726
|
expect(results.length).toBe(1);
|
|
2691
2727
|
expect(results).toEqual([{ x: 1 }]);
|
|
2692
2728
|
});
|
|
2729
|
+
|
|
2730
|
+
test("Test language name hits query with virtual graph", async () => {
|
|
2731
|
+
// Create Language nodes
|
|
2732
|
+
await new Runner(`
|
|
2733
|
+
CREATE VIRTUAL (:Language) AS {
|
|
2734
|
+
UNWIND [
|
|
2735
|
+
{id: 1, name: 'Python'},
|
|
2736
|
+
{id: 2, name: 'JavaScript'},
|
|
2737
|
+
{id: 3, name: 'TypeScript'}
|
|
2738
|
+
] AS record
|
|
2739
|
+
RETURN record.id AS id, record.name AS name
|
|
2740
|
+
}
|
|
2741
|
+
`).run();
|
|
2742
|
+
|
|
2743
|
+
// Create Chat nodes with messages
|
|
2744
|
+
await new Runner(`
|
|
2745
|
+
CREATE VIRTUAL (:Chat) AS {
|
|
2746
|
+
UNWIND [
|
|
2747
|
+
{id: 1, name: 'Dev Discussion', messages: [
|
|
2748
|
+
{From: 'Alice', SentDateTime: '2025-01-01T10:00:00', Content: 'I love Python and JavaScript'},
|
|
2749
|
+
{From: 'Bob', SentDateTime: '2025-01-01T10:05:00', Content: 'What languages do you prefer?'}
|
|
2750
|
+
]},
|
|
2751
|
+
{id: 2, name: 'General', messages: [
|
|
2752
|
+
{From: 'Charlie', SentDateTime: '2025-01-02T09:00:00', Content: 'The weather is nice today'},
|
|
2753
|
+
{From: 'Alice', SentDateTime: '2025-01-02T09:05:00', Content: 'TypeScript is great for language tooling'}
|
|
2754
|
+
]}
|
|
2755
|
+
] AS record
|
|
2756
|
+
RETURN record.id AS id, record.name AS name, record.messages AS messages
|
|
2757
|
+
}
|
|
2758
|
+
`).run();
|
|
2759
|
+
|
|
2760
|
+
// Create User nodes
|
|
2761
|
+
await new Runner(`
|
|
2762
|
+
CREATE VIRTUAL (:User) AS {
|
|
2763
|
+
UNWIND [
|
|
2764
|
+
{id: 1, displayName: 'Alice'},
|
|
2765
|
+
{id: 2, displayName: 'Bob'},
|
|
2766
|
+
{id: 3, displayName: 'Charlie'}
|
|
2767
|
+
] AS record
|
|
2768
|
+
RETURN record.id AS id, record.displayName AS displayName
|
|
2769
|
+
}
|
|
2770
|
+
`).run();
|
|
2771
|
+
|
|
2772
|
+
// Create PARTICIPATES_IN relationships
|
|
2773
|
+
await new Runner(`
|
|
2774
|
+
CREATE VIRTUAL (:User)-[:PARTICIPATES_IN]-(:Chat) AS {
|
|
2775
|
+
UNWIND [
|
|
2776
|
+
{left_id: 1, right_id: 1},
|
|
2777
|
+
{left_id: 2, right_id: 1},
|
|
2778
|
+
{left_id: 3, right_id: 2},
|
|
2779
|
+
{left_id: 1, right_id: 2}
|
|
2780
|
+
] AS record
|
|
2781
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id
|
|
2782
|
+
}
|
|
2783
|
+
`).run();
|
|
2784
|
+
|
|
2785
|
+
// Run the original query (using 'sender' alias since 'from' is a reserved keyword)
|
|
2786
|
+
const runner = new Runner(`
|
|
2787
|
+
MATCH (l:Language)
|
|
2788
|
+
WITH collect(distinct l.name) AS langs
|
|
2789
|
+
MATCH (c:Chat)
|
|
2790
|
+
UNWIND c.messages AS msg
|
|
2791
|
+
WITH c, msg, langs,
|
|
2792
|
+
sum(lang IN langs | 1 where toLower(msg.Content) CONTAINS toLower(lang)) AS langNameHits
|
|
2793
|
+
WHERE toLower(msg.Content) CONTAINS "language"
|
|
2794
|
+
OR toLower(msg.Content) CONTAINS "languages"
|
|
2795
|
+
OR langNameHits > 0
|
|
2796
|
+
OPTIONAL MATCH (u:User)-[:PARTICIPATES_IN]->(c)
|
|
2797
|
+
RETURN
|
|
2798
|
+
c.name AS chat,
|
|
2799
|
+
collect(distinct u.displayName) AS participants,
|
|
2800
|
+
msg.From AS sender,
|
|
2801
|
+
msg.SentDateTime AS sentDateTime,
|
|
2802
|
+
msg.Content AS message
|
|
2803
|
+
`);
|
|
2804
|
+
await runner.run();
|
|
2805
|
+
const results = runner.results;
|
|
2806
|
+
|
|
2807
|
+
// Messages that mention a language name or the word "language(s)":
|
|
2808
|
+
// 1. "I love Python and JavaScript" - langNameHits=2 (matches Python and JavaScript)
|
|
2809
|
+
// 2. "What languages do you prefer?" - contains "languages"
|
|
2810
|
+
// 3. "TypeScript is great for language tooling" - langNameHits=1, also contains "language"
|
|
2811
|
+
expect(results.length).toBe(3);
|
|
2812
|
+
expect(results[0].chat).toBe("Dev Discussion");
|
|
2813
|
+
expect(results[0].message).toBe("I love Python and JavaScript");
|
|
2814
|
+
expect(results[0].sender).toBe("Alice");
|
|
2815
|
+
expect(results[1].chat).toBe("Dev Discussion");
|
|
2816
|
+
expect(results[1].message).toBe("What languages do you prefer?");
|
|
2817
|
+
expect(results[1].sender).toBe("Bob");
|
|
2818
|
+
expect(results[2].chat).toBe("General");
|
|
2819
|
+
expect(results[2].message).toBe("TypeScript is great for language tooling");
|
|
2820
|
+
expect(results[2].sender).toBe("Alice");
|
|
2821
|
+
});
|
|
2822
|
+
|
|
2823
|
+
test("Test sum with empty collected array", async () => {
|
|
2824
|
+
// Reproduces the original bug: collect on empty input should yield []
|
|
2825
|
+
// and sum over that empty array should return 0, not throw
|
|
2826
|
+
const runner = new Runner(`
|
|
2827
|
+
UNWIND [] AS lang
|
|
2828
|
+
WITH collect(distinct lang) AS langs
|
|
2829
|
+
UNWIND ['hello', 'world'] AS msg
|
|
2830
|
+
WITH msg, langs, sum(l IN langs | 1 where toLower(msg) CONTAINS toLower(l)) AS hits
|
|
2831
|
+
RETURN msg, hits
|
|
2832
|
+
`);
|
|
2833
|
+
await runner.run();
|
|
2834
|
+
const results = runner.results;
|
|
2835
|
+
expect(results.length).toBe(2);
|
|
2836
|
+
expect(results[0]).toEqual({ msg: "hello", hits: 0 });
|
|
2837
|
+
expect(results[1]).toEqual({ msg: "world", hits: 0 });
|
|
2838
|
+
});
|
|
2839
|
+
|
|
2840
|
+
test("Test sum where all elements filtered returns 0", async () => {
|
|
2841
|
+
const runner = new Runner("RETURN sum(n in [1, 2, 3] | n where n > 100) as sum");
|
|
2842
|
+
await runner.run();
|
|
2843
|
+
const results = runner.results;
|
|
2844
|
+
expect(results.length).toBe(1);
|
|
2845
|
+
expect(results[0]).toEqual({ sum: 0 });
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
test("Test sum over empty array returns 0", async () => {
|
|
2849
|
+
const runner = new Runner("WITH [] AS arr RETURN sum(n in arr | n) as sum");
|
|
2850
|
+
await runner.run();
|
|
2851
|
+
const results = runner.results;
|
|
2852
|
+
expect(results.length).toBe(1);
|
|
2853
|
+
expect(results[0]).toEqual({ sum: 0 });
|
|
2854
|
+
});
|
|
2855
|
+
|
|
2856
|
+
test("Test match with ORed relationship types", async () => {
|
|
2857
|
+
await new Runner(`
|
|
2858
|
+
CREATE VIRTUAL (:Person) AS {
|
|
2859
|
+
unwind [
|
|
2860
|
+
{id: 1, name: 'Alice'},
|
|
2861
|
+
{id: 2, name: 'Bob'},
|
|
2862
|
+
{id: 3, name: 'Charlie'}
|
|
2863
|
+
] as record
|
|
2864
|
+
RETURN record.id as id, record.name as name
|
|
2865
|
+
}
|
|
2866
|
+
`).run();
|
|
2867
|
+
await new Runner(`
|
|
2868
|
+
CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
|
|
2869
|
+
unwind [
|
|
2870
|
+
{left_id: 1, right_id: 2}
|
|
2871
|
+
] as record
|
|
2872
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2873
|
+
}
|
|
2874
|
+
`).run();
|
|
2875
|
+
await new Runner(`
|
|
2876
|
+
CREATE VIRTUAL (:Person)-[:FOLLOWS]-(:Person) AS {
|
|
2877
|
+
unwind [
|
|
2878
|
+
{left_id: 2, right_id: 3}
|
|
2879
|
+
] as record
|
|
2880
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2881
|
+
}
|
|
2882
|
+
`).run();
|
|
2883
|
+
const match = new Runner(`
|
|
2884
|
+
MATCH (a:Person)-[:KNOWS|FOLLOWS]->(b:Person)
|
|
2885
|
+
RETURN a.name AS name1, b.name AS name2
|
|
2886
|
+
`);
|
|
2887
|
+
await match.run();
|
|
2888
|
+
const results = match.results;
|
|
2889
|
+
expect(results.length).toBe(2);
|
|
2890
|
+
expect(results[0]).toEqual({ name1: "Alice", name2: "Bob" });
|
|
2891
|
+
expect(results[1]).toEqual({ name1: "Bob", name2: "Charlie" });
|
|
2892
|
+
});
|
|
2893
|
+
|
|
2894
|
+
test("Test match with ORed relationship types with optional colon syntax", async () => {
|
|
2895
|
+
await new Runner(`
|
|
2896
|
+
CREATE VIRTUAL (:Animal) AS {
|
|
2897
|
+
unwind [
|
|
2898
|
+
{id: 1, name: 'Cat'},
|
|
2899
|
+
{id: 2, name: 'Dog'},
|
|
2900
|
+
{id: 3, name: 'Fish'}
|
|
2901
|
+
] as record
|
|
2902
|
+
RETURN record.id as id, record.name as name
|
|
2903
|
+
}
|
|
2904
|
+
`).run();
|
|
2905
|
+
await new Runner(`
|
|
2906
|
+
CREATE VIRTUAL (:Animal)-[:CHASES]-(:Animal) AS {
|
|
2907
|
+
unwind [
|
|
2908
|
+
{left_id: 1, right_id: 2}
|
|
2909
|
+
] as record
|
|
2910
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2911
|
+
}
|
|
2912
|
+
`).run();
|
|
2913
|
+
await new Runner(`
|
|
2914
|
+
CREATE VIRTUAL (:Animal)-[:EATS]-(:Animal) AS {
|
|
2915
|
+
unwind [
|
|
2916
|
+
{left_id: 1, right_id: 3}
|
|
2917
|
+
] as record
|
|
2918
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2919
|
+
}
|
|
2920
|
+
`).run();
|
|
2921
|
+
const match = new Runner(`
|
|
2922
|
+
MATCH (a:Animal)-[:CHASES|:EATS]->(b:Animal)
|
|
2923
|
+
RETURN a.name AS name1, b.name AS name2
|
|
2924
|
+
`);
|
|
2925
|
+
await match.run();
|
|
2926
|
+
const results = match.results;
|
|
2927
|
+
expect(results.length).toBe(2);
|
|
2928
|
+
expect(results[0]).toEqual({ name1: "Cat", name2: "Dog" });
|
|
2929
|
+
expect(results[1]).toEqual({ name1: "Cat", name2: "Fish" });
|
|
2930
|
+
});
|
|
2931
|
+
|
|
2932
|
+
test("Test match with ORed relationship types returns correct type in relationship variable", async () => {
|
|
2933
|
+
await new Runner(`
|
|
2934
|
+
CREATE VIRTUAL (:City) AS {
|
|
2935
|
+
unwind [
|
|
2936
|
+
{id: 1, name: 'NYC'},
|
|
2937
|
+
{id: 2, name: 'LA'},
|
|
2938
|
+
{id: 3, name: 'Chicago'}
|
|
2939
|
+
] as record
|
|
2940
|
+
RETURN record.id as id, record.name as name
|
|
2941
|
+
}
|
|
2942
|
+
`).run();
|
|
2943
|
+
await new Runner(`
|
|
2944
|
+
CREATE VIRTUAL (:City)-[:FLIGHT]-(:City) AS {
|
|
2945
|
+
unwind [
|
|
2946
|
+
{left_id: 1, right_id: 2, airline: 'Delta'}
|
|
2947
|
+
] as record
|
|
2948
|
+
RETURN record.left_id as left_id, record.right_id as right_id, record.airline as airline
|
|
2949
|
+
}
|
|
2950
|
+
`).run();
|
|
2951
|
+
await new Runner(`
|
|
2952
|
+
CREATE VIRTUAL (:City)-[:TRAIN]-(:City) AS {
|
|
2953
|
+
unwind [
|
|
2954
|
+
{left_id: 1, right_id: 3, line: 'Amtrak'}
|
|
2955
|
+
] as record
|
|
2956
|
+
RETURN record.left_id as left_id, record.right_id as right_id, record.line as line
|
|
2957
|
+
}
|
|
2958
|
+
`).run();
|
|
2959
|
+
const match = new Runner(`
|
|
2960
|
+
MATCH (a:City)-[r:FLIGHT|TRAIN]->(b:City)
|
|
2961
|
+
RETURN a.name AS from, b.name AS to, r.type AS type
|
|
2962
|
+
`);
|
|
2963
|
+
await match.run();
|
|
2964
|
+
const results = match.results;
|
|
2965
|
+
expect(results.length).toBe(2);
|
|
2966
|
+
expect(results[0]).toEqual({ from: "NYC", to: "LA", type: "FLIGHT" });
|
|
2967
|
+
expect(results[1]).toEqual({ from: "NYC", to: "Chicago", type: "TRAIN" });
|
|
2968
|
+
});
|