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.
Files changed (40) hide show
  1. package/.github/workflows/python-publish.yml +0 -5
  2. package/dist/flowquery.min.js +1 -1
  3. package/dist/graph/database.d.ts +1 -0
  4. package/dist/graph/database.d.ts.map +1 -1
  5. package/dist/graph/database.js +39 -0
  6. package/dist/graph/database.js.map +1 -1
  7. package/dist/parsing/functions/function_factory.d.ts +1 -0
  8. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  9. package/dist/parsing/functions/function_factory.js +1 -0
  10. package/dist/parsing/functions/function_factory.js.map +1 -1
  11. package/dist/parsing/functions/schema.d.ts +17 -0
  12. package/dist/parsing/functions/schema.d.ts.map +1 -0
  13. package/dist/parsing/functions/schema.js +62 -0
  14. package/dist/parsing/functions/schema.js.map +1 -0
  15. package/dist/parsing/parser.js +11 -11
  16. package/dist/parsing/parser.js.map +1 -1
  17. package/dist/tokenization/token.d.ts +2 -0
  18. package/dist/tokenization/token.d.ts.map +1 -1
  19. package/dist/tokenization/token.js +12 -0
  20. package/dist/tokenization/token.js.map +1 -1
  21. package/docs/flowquery.min.js +1 -1
  22. package/flowquery-py/pyproject.toml +1 -1
  23. package/flowquery-py/src/graph/database.py +25 -1
  24. package/flowquery-py/src/parsing/functions/__init__.py +2 -0
  25. package/flowquery-py/src/parsing/functions/schema.py +36 -0
  26. package/flowquery-py/src/parsing/parser.py +12 -12
  27. package/flowquery-py/src/tokenization/token.py +18 -0
  28. package/flowquery-py/tests/compute/test_runner.py +105 -1
  29. package/flowquery-py/tests/parsing/test_parser.py +9 -0
  30. package/flowquery-py/tests/tokenization/test_tokenizer.py +34 -0
  31. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  32. package/package.json +1 -1
  33. package/src/graph/database.ts +30 -0
  34. package/src/parsing/functions/function_factory.ts +1 -0
  35. package/src/parsing/functions/schema.ts +36 -0
  36. package/src/parsing/parser.ts +11 -11
  37. package/src/tokenization/token.ts +16 -0
  38. package/tests/compute/runner.test.ts +96 -0
  39. package/tests/parsing/parser.test.ts +9 -0
  40. package/tests/tokenization/tokenizer.test.ts +34 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowquery",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "A declarative query language for data processing pipelines.",
5
5
  "main": "dist/index.node.js",
6
6
  "types": "dist/index.node.d.ts",
@@ -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);
@@ -18,6 +18,7 @@ import "./rand";
18
18
  import "./range";
19
19
  import "./replace";
20
20
  import "./round";
21
+ import "./schema";
21
22
  import "./size";
22
23
  import "./split";
23
24
  import "./stringify";
@@ -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;
@@ -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.isIdentifier()) {
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.isIdentifier()) {
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()?.isIdentifier()) {
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()?.isIdentifier()) {
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()?.isIdentifier()) {
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.isIdentifier()) {
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.isIdentifier()) {
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.isIdentifier() && !this.peek()?.isLeftParenthesis()) {
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.isIdentifier() && this.peek()?.isLeftParenthesis()) {
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()?.isIdentifier() ||
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.isIdentifier() && !this.token.isKeyword()) {
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
+ });