flowquery 1.0.33 → 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.
Files changed (78) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/graph/database.d.ts +1 -0
  3. package/dist/graph/database.d.ts.map +1 -1
  4. package/dist/graph/database.js +43 -6
  5. package/dist/graph/database.js.map +1 -1
  6. package/dist/graph/relationship.d.ts +3 -1
  7. package/dist/graph/relationship.d.ts.map +1 -1
  8. package/dist/graph/relationship.js +12 -4
  9. package/dist/graph/relationship.js.map +1 -1
  10. package/dist/graph/relationship_data.js +1 -1
  11. package/dist/graph/relationship_data.js.map +1 -1
  12. package/dist/graph/relationship_match_collector.d.ts.map +1 -1
  13. package/dist/graph/relationship_match_collector.js +6 -3
  14. package/dist/graph/relationship_match_collector.js.map +1 -1
  15. package/dist/graph/relationship_reference.js +1 -1
  16. package/dist/graph/relationship_reference.js.map +1 -1
  17. package/dist/parsing/functions/function_factory.d.ts +3 -0
  18. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  19. package/dist/parsing/functions/function_factory.js +3 -0
  20. package/dist/parsing/functions/function_factory.js.map +1 -1
  21. package/dist/parsing/functions/predicate_sum.d.ts.map +1 -1
  22. package/dist/parsing/functions/predicate_sum.js +13 -10
  23. package/dist/parsing/functions/predicate_sum.js.map +1 -1
  24. package/dist/parsing/functions/schema.d.ts +5 -2
  25. package/dist/parsing/functions/schema.d.ts.map +1 -1
  26. package/dist/parsing/functions/schema.js +7 -4
  27. package/dist/parsing/functions/schema.js.map +1 -1
  28. package/dist/parsing/functions/to_lower.d.ts +7 -0
  29. package/dist/parsing/functions/to_lower.d.ts.map +1 -0
  30. package/dist/parsing/functions/to_lower.js +37 -0
  31. package/dist/parsing/functions/to_lower.js.map +1 -0
  32. package/dist/parsing/functions/to_string.d.ts +7 -0
  33. package/dist/parsing/functions/to_string.d.ts.map +1 -0
  34. package/dist/parsing/functions/to_string.js +44 -0
  35. package/dist/parsing/functions/to_string.js.map +1 -0
  36. package/dist/parsing/functions/trim.d.ts +7 -0
  37. package/dist/parsing/functions/trim.d.ts.map +1 -0
  38. package/dist/parsing/functions/trim.js +37 -0
  39. package/dist/parsing/functions/trim.js.map +1 -0
  40. package/dist/parsing/operations/group_by.d.ts.map +1 -1
  41. package/dist/parsing/operations/group_by.js +4 -2
  42. package/dist/parsing/operations/group_by.js.map +1 -1
  43. package/dist/parsing/parser.d.ts.map +1 -1
  44. package/dist/parsing/parser.js +15 -2
  45. package/dist/parsing/parser.js.map +1 -1
  46. package/docs/flowquery.min.js +1 -1
  47. package/flowquery-py/pyproject.toml +1 -1
  48. package/flowquery-py/src/graph/database.py +44 -11
  49. package/flowquery-py/src/graph/relationship.py +11 -3
  50. package/flowquery-py/src/graph/relationship_data.py +2 -1
  51. package/flowquery-py/src/graph/relationship_match_collector.py +7 -1
  52. package/flowquery-py/src/graph/relationship_reference.py +2 -2
  53. package/flowquery-py/src/parsing/functions/__init__.py +6 -0
  54. package/flowquery-py/src/parsing/functions/predicate_sum.py +3 -6
  55. package/flowquery-py/src/parsing/functions/schema.py +9 -5
  56. package/flowquery-py/src/parsing/functions/to_lower.py +35 -0
  57. package/flowquery-py/src/parsing/functions/to_string.py +41 -0
  58. package/flowquery-py/src/parsing/functions/trim.py +35 -0
  59. package/flowquery-py/src/parsing/operations/group_by.py +2 -0
  60. package/flowquery-py/src/parsing/parser.py +12 -2
  61. package/flowquery-py/tests/compute/test_runner.py +294 -4
  62. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  63. package/package.json +1 -1
  64. package/src/graph/database.ts +42 -4
  65. package/src/graph/relationship.ts +12 -4
  66. package/src/graph/relationship_data.ts +1 -1
  67. package/src/graph/relationship_match_collector.ts +6 -2
  68. package/src/graph/relationship_reference.ts +1 -1
  69. package/src/parsing/functions/function_factory.ts +3 -0
  70. package/src/parsing/functions/predicate_sum.ts +17 -12
  71. package/src/parsing/functions/schema.ts +7 -4
  72. package/src/parsing/functions/to_lower.ts +25 -0
  73. package/src/parsing/functions/to_string.ts +32 -0
  74. package/src/parsing/functions/trim.ts +25 -0
  75. package/src/parsing/operations/group_by.ts +4 -1
  76. package/src/parsing/parser.ts +15 -2
  77. package/tests/compute/runner.test.ts +319 -3
  78. package/tests/parsing/parser.test.ts +37 -0
@@ -573,6 +573,78 @@ test("Test stringify function", async () => {
573
573
  });
574
574
  });
575
575
 
576
+ test("Test toString function with number", async () => {
577
+ const runner = new Runner("RETURN toString(42) as result");
578
+ await runner.run();
579
+ const results = runner.results;
580
+ expect(results.length).toBe(1);
581
+ expect(results[0]).toEqual({ result: "42" });
582
+ });
583
+
584
+ test("Test toString function with boolean", async () => {
585
+ const runner = new Runner("RETURN toString(true) as result");
586
+ await runner.run();
587
+ const results = runner.results;
588
+ expect(results.length).toBe(1);
589
+ expect(results[0]).toEqual({ result: "true" });
590
+ });
591
+
592
+ test("Test toString function with object", async () => {
593
+ const runner = new Runner("RETURN toString({a: 1}) as result");
594
+ await runner.run();
595
+ const results = runner.results;
596
+ expect(results.length).toBe(1);
597
+ expect(results[0]).toEqual({ result: '{"a":1}' });
598
+ });
599
+
600
+ test("Test toLower function", async () => {
601
+ const runner = new Runner('RETURN toLower("Hello World") as result');
602
+ await runner.run();
603
+ const results = runner.results;
604
+ expect(results.length).toBe(1);
605
+ expect(results[0]).toEqual({ result: "hello world" });
606
+ });
607
+
608
+ test("Test toLower function with all uppercase", async () => {
609
+ const runner = new Runner('RETURN toLower("FOO BAR") as result');
610
+ await runner.run();
611
+ const results = runner.results;
612
+ expect(results.length).toBe(1);
613
+ expect(results[0]).toEqual({ result: "foo bar" });
614
+ });
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
+
576
648
  test("Test associative array with key which is keyword", async () => {
577
649
  const runner = new Runner("RETURN {return: 1} as aa");
578
650
  await runner.run();
@@ -1924,20 +1996,24 @@ test("Test schema() returns nodes and relationships with sample data", async ()
1924
1996
  `).run();
1925
1997
 
1926
1998
  const runner = new Runner(
1927
- "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"
1928
2000
  );
1929
2001
  await runner.run();
1930
2002
  const results = runner.results;
1931
2003
 
1932
- const animal = results.find((r: any) => r.kind === "node" && r.label === "Animal");
2004
+ const animal = results.find((r: any) => r.kind === "Node" && r.label === "Animal");
1933
2005
  expect(animal).toBeDefined();
2006
+ expect(animal.properties).toEqual(["species", "legs"]);
1934
2007
  expect(animal.sample).toBeDefined();
1935
2008
  expect(animal.sample).not.toHaveProperty("id");
1936
2009
  expect(animal.sample).toHaveProperty("species");
1937
2010
  expect(animal.sample).toHaveProperty("legs");
1938
2011
 
1939
- const chases = results.find((r: any) => r.kind === "relationship" && r.type === "CHASES");
2012
+ const chases = results.find((r: any) => r.kind === "Relationship" && r.type === "CHASES");
1940
2013
  expect(chases).toBeDefined();
2014
+ expect(chases.from_label).toBe("Animal");
2015
+ expect(chases.to_label).toBe("Animal");
2016
+ expect(chases.properties).toEqual(["speed"]);
1941
2017
  expect(chases.sample).toBeDefined();
1942
2018
  expect(chases.sample).not.toHaveProperty("left_id");
1943
2019
  expect(chases.sample).not.toHaveProperty("right_id");
@@ -2650,3 +2726,243 @@ test("Test UNION with empty right side", async () => {
2650
2726
  expect(results.length).toBe(1);
2651
2727
  expect(results).toEqual([{ x: 1 }]);
2652
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
+ });
@@ -750,6 +750,43 @@ test("Match with graph pattern including relationships", () => {
750
750
  expect(target.label).toBe("Person");
751
751
  });
752
752
 
753
+ test("Match with ORed relationship types", () => {
754
+ const parser = new Parser();
755
+ const ast = parser.parse("MATCH (a:Person)-[:KNOWS|FOLLOWS]->(b:Person) RETURN a, b");
756
+ const match = ast.firstChild() as Match;
757
+ expect(match.patterns[0].chain.length).toBe(3);
758
+ const relationship = match.patterns[0].chain[1] as Relationship;
759
+ expect(relationship.types).toEqual(["KNOWS", "FOLLOWS"]);
760
+ expect(relationship.type).toBe("KNOWS");
761
+ });
762
+
763
+ test("Match with ORed relationship types with optional colons", () => {
764
+ const parser = new Parser();
765
+ const ast = parser.parse("MATCH (a:Person)-[:KNOWS|:FOLLOWS|:LIKES]->(b:Person) RETURN a, b");
766
+ const match = ast.firstChild() as Match;
767
+ const relationship = match.patterns[0].chain[1] as Relationship;
768
+ expect(relationship.types).toEqual(["KNOWS", "FOLLOWS", "LIKES"]);
769
+ });
770
+
771
+ test("Match with ORed relationship types and variable", () => {
772
+ const parser = new Parser();
773
+ const ast = parser.parse("MATCH (a:Person)-[r:KNOWS|FOLLOWS]->(b:Person) RETURN a, r, b");
774
+ const match = ast.firstChild() as Match;
775
+ const relationship = match.patterns[0].chain[1] as Relationship;
776
+ expect(relationship.identifier).toBe("r");
777
+ expect(relationship.types).toEqual(["KNOWS", "FOLLOWS"]);
778
+ });
779
+
780
+ test("Match with ORed relationship types and hops", () => {
781
+ const parser = new Parser();
782
+ const ast = parser.parse("MATCH (a:Person)-[:KNOWS|FOLLOWS*1..3]->(b:Person) RETURN a, b");
783
+ const match = ast.firstChild() as Match;
784
+ const relationship = match.patterns[0].chain[1] as Relationship;
785
+ expect(relationship.types).toEqual(["KNOWS", "FOLLOWS"]);
786
+ expect(relationship.hops!.min).toBe(1);
787
+ expect(relationship.hops!.max).toBe(3);
788
+ });
789
+
753
790
  test("Test not equal operator", () => {
754
791
  const parser = new Parser();
755
792
  const ast = parser.parse("RETURN 1 <> 2");