flowquery 1.0.21 → 1.0.23
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/.github/workflows/python-publish.yml +0 -5
- 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 +39 -0
- package/dist/graph/database.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/schema.d.ts +17 -0
- package/dist/parsing/functions/schema.d.ts.map +1 -0
- package/dist/parsing/functions/schema.js +62 -0
- package/dist/parsing/functions/schema.js.map +1 -0
- package/dist/parsing/parser.js +11 -11
- package/dist/parsing/parser.js.map +1 -1
- package/dist/tokenization/token.d.ts +2 -0
- package/dist/tokenization/token.d.ts.map +1 -1
- package/dist/tokenization/token.js +12 -0
- package/dist/tokenization/token.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 +25 -1
- package/flowquery-py/src/parsing/functions/__init__.py +2 -0
- package/flowquery-py/src/parsing/functions/schema.py +36 -0
- package/flowquery-py/src/parsing/parser.py +12 -12
- package/flowquery-py/src/tokenization/token.py +18 -0
- package/flowquery-py/tests/compute/test_runner.py +105 -1
- package/flowquery-py/tests/parsing/test_parser.py +9 -0
- package/flowquery-py/tests/tokenization/test_tokenizer.py +34 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/database.ts +30 -0
- package/src/parsing/functions/function_factory.ts +1 -0
- package/src/parsing/functions/schema.ts +36 -0
- package/src/parsing/parser.ts +11 -11
- package/src/tokenization/token.ts +16 -0
- package/tests/compute/runner.test.ts +96 -0
- package/tests/parsing/parser.test.ts +9 -0
- package/tests/tokenization/tokenizer.test.ts +34 -0
package/package.json
CHANGED
package/src/graph/database.ts
CHANGED
|
@@ -39,6 +39,36 @@ class Database {
|
|
|
39
39
|
public getRelationship(relationship: Relationship): PhysicalRelationship | null {
|
|
40
40
|
return Database.relationships.get(relationship.type!) || null;
|
|
41
41
|
}
|
|
42
|
+
public async schema(): Promise<Record<string, any>[]> {
|
|
43
|
+
const result: Record<string, any>[] = [];
|
|
44
|
+
|
|
45
|
+
for (const [label, physical] of Database.nodes) {
|
|
46
|
+
const records = await physical.data();
|
|
47
|
+
const entry: Record<string, any> = { kind: "node", label };
|
|
48
|
+
if (records.length > 0) {
|
|
49
|
+
const { id, ...sample } = records[0];
|
|
50
|
+
if (Object.keys(sample).length > 0) {
|
|
51
|
+
entry.sample = sample;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
result.push(entry);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const [type, physical] of Database.relationships) {
|
|
58
|
+
const records = await physical.data();
|
|
59
|
+
const entry: Record<string, any> = { kind: "relationship", type };
|
|
60
|
+
if (records.length > 0) {
|
|
61
|
+
const { left_id, right_id, ...sample } = records[0];
|
|
62
|
+
if (Object.keys(sample).length > 0) {
|
|
63
|
+
entry.sample = sample;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
result.push(entry);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
public async getData(element: Node | Relationship): Promise<NodeData | RelationshipData> {
|
|
43
73
|
if (element instanceof Node) {
|
|
44
74
|
const node = this.getNode(element);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Database from "../../graph/database";
|
|
2
|
+
import AsyncFunction from "./async_function";
|
|
3
|
+
import { FunctionDef } from "./function_metadata";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in function that returns the graph schema of the database.
|
|
7
|
+
*
|
|
8
|
+
* Lists all nodes and relationships with their labels/types and a sample
|
|
9
|
+
* of their data (excluding id from nodes, left_id and right_id from relationships).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```
|
|
13
|
+
* LOAD FROM schema() AS s RETURN s
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
@FunctionDef({
|
|
17
|
+
description:
|
|
18
|
+
"Returns the graph schema listing all nodes and relationships with a sample of their data.",
|
|
19
|
+
category: "async",
|
|
20
|
+
parameters: [],
|
|
21
|
+
output: {
|
|
22
|
+
description: "Schema entry with kind, label/type, and optional sample data",
|
|
23
|
+
type: "object",
|
|
24
|
+
},
|
|
25
|
+
examples: ["LOAD FROM schema() AS s RETURN s"],
|
|
26
|
+
})
|
|
27
|
+
class Schema extends AsyncFunction {
|
|
28
|
+
public async *generate(): AsyncGenerator<any> {
|
|
29
|
+
const entries = await Database.getInstance().schema();
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
yield entry;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default Schema;
|
package/src/parsing/parser.ts
CHANGED
|
@@ -353,7 +353,7 @@ class Parser extends BaseParser {
|
|
|
353
353
|
throw new Error("Expected ':' for relationship type");
|
|
354
354
|
}
|
|
355
355
|
this.setNextToken();
|
|
356
|
-
if (!this.token.
|
|
356
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
357
357
|
throw new Error("Expected relationship type identifier");
|
|
358
358
|
}
|
|
359
359
|
const type: string = this.token.value || "";
|
|
@@ -412,19 +412,19 @@ class Parser extends BaseParser {
|
|
|
412
412
|
this.setNextToken();
|
|
413
413
|
this.skipWhitespaceAndComments();
|
|
414
414
|
let identifier: string | null = null;
|
|
415
|
-
if (this.token.
|
|
415
|
+
if (this.token.isIdentifierOrKeyword()) {
|
|
416
416
|
identifier = this.token.value || "";
|
|
417
417
|
this.setNextToken();
|
|
418
418
|
}
|
|
419
419
|
this.skipWhitespaceAndComments();
|
|
420
420
|
let label: string | null = null;
|
|
421
|
-
if (!this.token.isColon() && this.peek()?.
|
|
421
|
+
if (!this.token.isColon() && this.peek()?.isIdentifierOrKeyword()) {
|
|
422
422
|
throw new Error("Expected ':' for node label");
|
|
423
423
|
}
|
|
424
|
-
if (this.token.isColon() && !this.peek()?.
|
|
424
|
+
if (this.token.isColon() && !this.peek()?.isIdentifierOrKeyword()) {
|
|
425
425
|
throw new Error("Expected node label identifier");
|
|
426
426
|
}
|
|
427
|
-
if (this.token.isColon() && this.peek()?.
|
|
427
|
+
if (this.token.isColon() && this.peek()?.isIdentifierOrKeyword()) {
|
|
428
428
|
this.setNextToken();
|
|
429
429
|
label = this.token.value || "";
|
|
430
430
|
this.setNextToken();
|
|
@@ -595,7 +595,7 @@ class Parser extends BaseParser {
|
|
|
595
595
|
}
|
|
596
596
|
this.setNextToken();
|
|
597
597
|
let variable: string | null = null;
|
|
598
|
-
if (this.token.
|
|
598
|
+
if (this.token.isIdentifierOrKeyword()) {
|
|
599
599
|
variable = this.token.value || "";
|
|
600
600
|
this.setNextToken();
|
|
601
601
|
}
|
|
@@ -603,7 +603,7 @@ class Parser extends BaseParser {
|
|
|
603
603
|
throw new Error("Expected ':' for relationship type");
|
|
604
604
|
}
|
|
605
605
|
this.setNextToken();
|
|
606
|
-
if (!this.token.
|
|
606
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
607
607
|
throw new Error("Expected relationship type identifier");
|
|
608
608
|
}
|
|
609
609
|
const type: string = this.token.value || "";
|
|
@@ -745,14 +745,14 @@ class Parser extends BaseParser {
|
|
|
745
745
|
*/
|
|
746
746
|
private parseOperand(expression: Expression): boolean {
|
|
747
747
|
this.skipWhitespaceAndComments();
|
|
748
|
-
if (this.token.
|
|
748
|
+
if (this.token.isIdentifierOrKeyword() && !this.peek()?.isLeftParenthesis()) {
|
|
749
749
|
const identifier: string = this.token.value || "";
|
|
750
750
|
const reference = new Reference(identifier, this.variables.get(identifier));
|
|
751
751
|
this.setNextToken();
|
|
752
752
|
const lookup = this.parseLookup(reference);
|
|
753
753
|
expression.addNode(lookup);
|
|
754
754
|
return true;
|
|
755
|
-
} else if (this.token.
|
|
755
|
+
} else if (this.token.isIdentifierOrKeyword() && this.peek()?.isLeftParenthesis()) {
|
|
756
756
|
const func = this.parsePredicateFunction() || this.parseFunction();
|
|
757
757
|
if (func !== null) {
|
|
758
758
|
const lookup = this.parseLookup(func);
|
|
@@ -761,7 +761,7 @@ class Parser extends BaseParser {
|
|
|
761
761
|
}
|
|
762
762
|
} else if (
|
|
763
763
|
this.token.isLeftParenthesis() &&
|
|
764
|
-
(this.peek()?.
|
|
764
|
+
(this.peek()?.isIdentifierOrKeyword() ||
|
|
765
765
|
this.peek()?.isColon() ||
|
|
766
766
|
this.peek()?.isRightParenthesis())
|
|
767
767
|
) {
|
|
@@ -857,7 +857,7 @@ class Parser extends BaseParser {
|
|
|
857
857
|
while (true) {
|
|
858
858
|
if (this.token.isDot()) {
|
|
859
859
|
this.setNextToken();
|
|
860
|
-
if (!this.token.
|
|
860
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
861
861
|
throw new Error("Expected identifier");
|
|
862
862
|
}
|
|
863
863
|
lookup = new Lookup();
|
|
@@ -104,6 +104,10 @@ class Token {
|
|
|
104
104
|
return this._type === TokenType.IDENTIFIER || this._type === TokenType.BACKTICK_STRING;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
public isIdentifierOrKeyword(): boolean {
|
|
108
|
+
return this.isIdentifier() || (this.isKeyword() && !this.isKeywordThatCannotBeIdentifier());
|
|
109
|
+
}
|
|
110
|
+
|
|
107
111
|
// String token
|
|
108
112
|
|
|
109
113
|
public static STRING(value: string, quoteChar: string = '"'): Token {
|
|
@@ -387,6 +391,18 @@ class Token {
|
|
|
387
391
|
return this._type === TokenType.KEYWORD;
|
|
388
392
|
}
|
|
389
393
|
|
|
394
|
+
public isKeywordThatCannotBeIdentifier(): boolean {
|
|
395
|
+
return (
|
|
396
|
+
this.isKeyword() &&
|
|
397
|
+
(this.isNull() ||
|
|
398
|
+
this.isCase() ||
|
|
399
|
+
this.isWhen() ||
|
|
400
|
+
this.isThen() ||
|
|
401
|
+
this.isElse() ||
|
|
402
|
+
this.isEnd())
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
390
406
|
public static get WITH(): Token {
|
|
391
407
|
return new Token(TokenType.KEYWORD, Keyword.WITH);
|
|
392
408
|
}
|
|
@@ -1460,3 +1460,99 @@ test("Test match with leftward double graph pattern", async () => {
|
|
|
1460
1460
|
expect(results[0]).toEqual({ name1: "Person 1", name2: "Person 2", name3: "Person 3" });
|
|
1461
1461
|
expect(results[1]).toEqual({ name1: "Person 2", name2: "Person 3", name3: "Person 4" });
|
|
1462
1462
|
});
|
|
1463
|
+
|
|
1464
|
+
test("Test schema() returns nodes and relationships with sample data", async () => {
|
|
1465
|
+
await new Runner(`
|
|
1466
|
+
CREATE VIRTUAL (:Animal) AS {
|
|
1467
|
+
UNWIND [
|
|
1468
|
+
{id: 1, species: 'Cat', legs: 4},
|
|
1469
|
+
{id: 2, species: 'Dog', legs: 4}
|
|
1470
|
+
] AS record
|
|
1471
|
+
RETURN record.id AS id, record.species AS species, record.legs AS legs
|
|
1472
|
+
}
|
|
1473
|
+
`).run();
|
|
1474
|
+
await new Runner(`
|
|
1475
|
+
CREATE VIRTUAL (:Animal)-[:CHASES]-(:Animal) AS {
|
|
1476
|
+
UNWIND [
|
|
1477
|
+
{left_id: 2, right_id: 1, speed: 'fast'}
|
|
1478
|
+
] AS record
|
|
1479
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id, record.speed AS speed
|
|
1480
|
+
}
|
|
1481
|
+
`).run();
|
|
1482
|
+
|
|
1483
|
+
const runner = new Runner(
|
|
1484
|
+
"CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
|
|
1485
|
+
);
|
|
1486
|
+
await runner.run();
|
|
1487
|
+
const results = runner.results;
|
|
1488
|
+
|
|
1489
|
+
const animal = results.find((r: any) => r.kind === "node" && r.label === "Animal");
|
|
1490
|
+
expect(animal).toBeDefined();
|
|
1491
|
+
expect(animal.sample).toBeDefined();
|
|
1492
|
+
expect(animal.sample).not.toHaveProperty("id");
|
|
1493
|
+
expect(animal.sample).toHaveProperty("species");
|
|
1494
|
+
expect(animal.sample).toHaveProperty("legs");
|
|
1495
|
+
|
|
1496
|
+
const chases = results.find((r: any) => r.kind === "relationship" && r.type === "CHASES");
|
|
1497
|
+
expect(chases).toBeDefined();
|
|
1498
|
+
expect(chases.sample).toBeDefined();
|
|
1499
|
+
expect(chases.sample).not.toHaveProperty("left_id");
|
|
1500
|
+
expect(chases.sample).not.toHaveProperty("right_id");
|
|
1501
|
+
expect(chases.sample).toHaveProperty("speed");
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
test("Test reserved keywords as identifiers", async () => {
|
|
1505
|
+
const runner = new Runner(`
|
|
1506
|
+
WITH 1 AS return
|
|
1507
|
+
RETURN return
|
|
1508
|
+
`);
|
|
1509
|
+
await runner.run();
|
|
1510
|
+
const results = runner.results;
|
|
1511
|
+
expect(results.length).toBe(1);
|
|
1512
|
+
expect(results[0].return).toBe(1);
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
test("Test reserved keywords as parts of identifiers", async () => {
|
|
1516
|
+
const runner = new Runner(`
|
|
1517
|
+
unwind [
|
|
1518
|
+
{from: "Alice", to: "Bob", organizer: "Charlie"},
|
|
1519
|
+
{from: "Bob", to: "Charlie", organizer: "Alice"},
|
|
1520
|
+
{from: "Charlie", to: "Alice", organizer: "Bob"}
|
|
1521
|
+
] as data
|
|
1522
|
+
return data.from as from, data.to as to, data.organizer as organizer
|
|
1523
|
+
`);
|
|
1524
|
+
await runner.run();
|
|
1525
|
+
const results = runner.results;
|
|
1526
|
+
expect(results.length).toBe(3);
|
|
1527
|
+
expect(results[0]).toEqual({ from: "Alice", to: "Bob", organizer: "Charlie" });
|
|
1528
|
+
expect(results[1]).toEqual({ from: "Bob", to: "Charlie", organizer: "Alice" });
|
|
1529
|
+
expect(results[2]).toEqual({ from: "Charlie", to: "Alice", organizer: "Bob" });
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
test("Test reserved keywords as relationship types and labels", async () => {
|
|
1533
|
+
await new Runner(`
|
|
1534
|
+
CREATE VIRTUAL (:Return) AS {
|
|
1535
|
+
unwind [
|
|
1536
|
+
{id: 1, name: 'Node 1'},
|
|
1537
|
+
{id: 2, name: 'Node 2'}
|
|
1538
|
+
] as record
|
|
1539
|
+
RETURN record.id as id, record.name as name
|
|
1540
|
+
}
|
|
1541
|
+
`).run();
|
|
1542
|
+
await new Runner(`
|
|
1543
|
+
CREATE VIRTUAL (:Return)-[:With]-(:Return) AS {
|
|
1544
|
+
unwind [
|
|
1545
|
+
{left_id: 1, right_id: 2}
|
|
1546
|
+
] as record
|
|
1547
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1548
|
+
}
|
|
1549
|
+
`).run();
|
|
1550
|
+
const match = new Runner(`
|
|
1551
|
+
MATCH (a:Return)-[:With]->(b:Return)
|
|
1552
|
+
RETURN a.name AS name1, b.name AS name2
|
|
1553
|
+
`);
|
|
1554
|
+
await match.run();
|
|
1555
|
+
const results = match.results;
|
|
1556
|
+
expect(results.length).toBe(1);
|
|
1557
|
+
expect(results[0]).toEqual({ name1: "Node 1", name2: "Node 2" });
|
|
1558
|
+
});
|
|
@@ -401,6 +401,15 @@ test("Test case statement", () => {
|
|
|
401
401
|
);
|
|
402
402
|
});
|
|
403
403
|
|
|
404
|
+
test("Test case statement with keywords as identifiers", () => {
|
|
405
|
+
const parser = new Parser();
|
|
406
|
+
const ast = parser.parse("RETURN CASE WHEN 1 THEN 2 ELSE 3 END");
|
|
407
|
+
expect(ast.print()).toContain("Case");
|
|
408
|
+
expect(ast.print()).toContain("When");
|
|
409
|
+
expect(ast.print()).toContain("Then");
|
|
410
|
+
expect(ast.print()).toContain("Else");
|
|
411
|
+
});
|
|
412
|
+
|
|
404
413
|
test("Test functions with wrong number of arguments", () => {
|
|
405
414
|
expect(() => new Parser().parse("RETURN range(1)")).toThrow(
|
|
406
415
|
"Function range expected 2 parameters, but got 1"
|
|
@@ -155,3 +155,37 @@ test("Test relationship with hops", () => {
|
|
|
155
155
|
expect(tokens).toBeDefined();
|
|
156
156
|
expect(tokens.length).toBeGreaterThan(0);
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
test("Test reserved keywords as identifiers", () => {
|
|
160
|
+
const tokenizer = new Tokenizer(`
|
|
161
|
+
WITH 1 AS return
|
|
162
|
+
RETURN return
|
|
163
|
+
`);
|
|
164
|
+
const tokens = tokenizer.tokenize();
|
|
165
|
+
expect(tokens).toBeDefined();
|
|
166
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("Test reserved keywords as part of identifiers", () => {
|
|
170
|
+
const tokenizer = new Tokenizer(`
|
|
171
|
+
unwind [
|
|
172
|
+
{from: "Alice", to: "Bob", organizer: "Charlie"},
|
|
173
|
+
{from: "Bob", to: "Charlie", organizer: "Alice"},
|
|
174
|
+
{from: "Charlie", to: "Alice", organizer: "Bob"}
|
|
175
|
+
] as data
|
|
176
|
+
return data.from, data.to
|
|
177
|
+
`);
|
|
178
|
+
const tokens = tokenizer.tokenize();
|
|
179
|
+
expect(tokens).toBeDefined();
|
|
180
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("Test reserved keywords as relationship types and labels", () => {
|
|
184
|
+
const tokenizer = new Tokenizer(`
|
|
185
|
+
MATCH (a:RETURN)-[r:WITH]->(b:RETURN)
|
|
186
|
+
RETURN a, b
|
|
187
|
+
`);
|
|
188
|
+
const tokens = tokenizer.tokenize();
|
|
189
|
+
expect(tokens).toBeDefined();
|
|
190
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
191
|
+
});
|