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
@@ -636,6 +636,87 @@ class TestRunner:
636
636
  assert len(results) == 1
637
637
  assert results[0] == {"stringify": '{\n "a": 1,\n "b": 2\n}'}
638
638
 
639
+ @pytest.mark.asyncio
640
+ async def test_to_string_function_with_number(self):
641
+ """Test toString function with a number."""
642
+ runner = Runner("RETURN toString(42) as result")
643
+ await runner.run()
644
+ results = runner.results
645
+ assert len(results) == 1
646
+ assert results[0] == {"result": "42"}
647
+
648
+ @pytest.mark.asyncio
649
+ async def test_to_string_function_with_boolean(self):
650
+ """Test toString function with a boolean."""
651
+ runner = Runner("RETURN toString(true) as result")
652
+ await runner.run()
653
+ results = runner.results
654
+ assert len(results) == 1
655
+ assert results[0] == {"result": "true"}
656
+
657
+ @pytest.mark.asyncio
658
+ async def test_to_string_function_with_object(self):
659
+ """Test toString function with an object."""
660
+ runner = Runner("RETURN toString({a: 1}) as result")
661
+ await runner.run()
662
+ results = runner.results
663
+ assert len(results) == 1
664
+ assert results[0] == {"result": '{"a": 1}'}
665
+
666
+ @pytest.mark.asyncio
667
+ async def test_to_lower_function(self):
668
+ """Test toLower function."""
669
+ runner = Runner('RETURN toLower("Hello World") as result')
670
+ await runner.run()
671
+ results = runner.results
672
+ assert len(results) == 1
673
+ assert results[0] == {"result": "hello world"}
674
+
675
+ @pytest.mark.asyncio
676
+ async def test_to_lower_function_with_all_uppercase(self):
677
+ """Test toLower function with all uppercase."""
678
+ runner = Runner('RETURN toLower("FOO BAR") as result')
679
+ await runner.run()
680
+ results = runner.results
681
+ assert len(results) == 1
682
+ assert results[0] == {"result": "foo bar"}
683
+
684
+ @pytest.mark.asyncio
685
+ async def test_trim_function(self):
686
+ """Test trim function."""
687
+ runner = Runner('RETURN trim(" hello ") as result')
688
+ await runner.run()
689
+ results = runner.results
690
+ assert len(results) == 1
691
+ assert results[0] == {"result": "hello"}
692
+
693
+ @pytest.mark.asyncio
694
+ async def test_trim_function_with_tabs_and_newlines(self):
695
+ """Test trim function with tabs and newlines."""
696
+ runner = Runner('WITH "\tfoo\n" AS s RETURN trim(s) as result')
697
+ await runner.run()
698
+ results = runner.results
699
+ assert len(results) == 1
700
+ assert results[0] == {"result": "foo"}
701
+
702
+ @pytest.mark.asyncio
703
+ async def test_trim_function_with_no_whitespace(self):
704
+ """Test trim function with no whitespace."""
705
+ runner = Runner('RETURN trim("hello") as result')
706
+ await runner.run()
707
+ results = runner.results
708
+ assert len(results) == 1
709
+ assert results[0] == {"result": "hello"}
710
+
711
+ @pytest.mark.asyncio
712
+ async def test_trim_function_with_empty_string(self):
713
+ """Test trim function with empty string."""
714
+ runner = Runner('RETURN trim("") as result')
715
+ await runner.run()
716
+ results = runner.results
717
+ assert len(results) == 1
718
+ assert results[0] == {"result": ""}
719
+
639
720
  @pytest.mark.asyncio
640
721
  async def test_associative_array_with_key_which_is_keyword(self):
641
722
  """Test associative array with key which is keyword."""
@@ -2107,20 +2188,24 @@ class TestRunner:
2107
2188
  ).run()
2108
2189
 
2109
2190
  runner = Runner(
2110
- "CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
2191
+ "CALL schema() YIELD kind, label, type, from_label, to_label, properties, sample RETURN kind, label, type, from_label, to_label, properties, sample"
2111
2192
  )
2112
2193
  await runner.run()
2113
2194
  results = runner.results
2114
2195
 
2115
- animal = next((r for r in results if r.get("kind") == "node" and r.get("label") == "Animal"), None)
2196
+ animal = next((r for r in results if r.get("kind") == "Node" and r.get("label") == "Animal"), None)
2116
2197
  assert animal is not None
2198
+ assert animal["properties"] == ["species", "legs"]
2117
2199
  assert animal["sample"] is not None
2118
2200
  assert "id" not in animal["sample"]
2119
2201
  assert "species" in animal["sample"]
2120
2202
  assert "legs" in animal["sample"]
2121
2203
 
2122
- chases = next((r for r in results if r.get("kind") == "relationship" and r.get("type") == "CHASES"), None)
2204
+ chases = next((r for r in results if r.get("kind") == "Relationship" and r.get("type") == "CHASES"), None)
2123
2205
  assert chases is not None
2206
+ assert chases["from_label"] == "Animal"
2207
+ assert chases["to_label"] == "Animal"
2208
+ assert chases["properties"] == ["speed"]
2124
2209
  assert chases["sample"] is not None
2125
2210
  assert "left_id" not in chases["sample"]
2126
2211
  assert "right_id" not in chases["sample"]
@@ -2504,6 +2589,64 @@ class TestRunner:
2504
2589
  # Add operator tests
2505
2590
  # ============================================================
2506
2591
 
2592
+ @pytest.mark.asyncio
2593
+ async def test_collected_patterns_and_unwind(self):
2594
+ """Test collecting graph patterns and unwinding them."""
2595
+ await Runner("""
2596
+ CREATE VIRTUAL (:Person) AS {
2597
+ unwind [
2598
+ {id: 1, name: 'Person 1'},
2599
+ {id: 2, name: 'Person 2'},
2600
+ {id: 3, name: 'Person 3'},
2601
+ {id: 4, name: 'Person 4'}
2602
+ ] as record
2603
+ RETURN record.id as id, record.name as name
2604
+ }
2605
+ """).run()
2606
+ await Runner("""
2607
+ CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
2608
+ unwind [
2609
+ {left_id: 1, right_id: 2},
2610
+ {left_id: 2, right_id: 3},
2611
+ {left_id: 3, right_id: 4}
2612
+ ] as record
2613
+ RETURN record.left_id as left_id, record.right_id as right_id
2614
+ }
2615
+ """).run()
2616
+ runner = Runner("""
2617
+ MATCH p=(a:Person)-[:KNOWS*0..3]->(b:Person)
2618
+ WITH collect(p) AS patterns
2619
+ UNWIND patterns AS pattern
2620
+ RETURN pattern
2621
+ """)
2622
+ await runner.run()
2623
+ results = runner.results
2624
+ assert len(results) == 10
2625
+ # Index 0: Person 1 zero-hop - pattern = [node1] (single node)
2626
+ assert len(results[0]["pattern"]) == 1
2627
+ assert results[0]["pattern"][0]["id"] == 1
2628
+ # Index 1: Person 1 -> Person 2 (1-hop)
2629
+ assert len(results[1]["pattern"]) == 3
2630
+ # Index 2: Person 1 -> Person 2 -> Person 3 (2-hop)
2631
+ assert len(results[2]["pattern"]) == 5
2632
+ # Index 3: Person 1 -> Person 2 -> Person 3 -> Person 4 (3-hop)
2633
+ assert len(results[3]["pattern"]) == 7
2634
+ # Index 4: Person 2 zero-hop
2635
+ assert len(results[4]["pattern"]) == 1
2636
+ assert results[4]["pattern"][0]["id"] == 2
2637
+ # Index 5: Person 2 -> Person 3 (1-hop)
2638
+ assert len(results[5]["pattern"]) == 3
2639
+ # Index 6: Person 2 -> Person 3 -> Person 4 (2-hop)
2640
+ assert len(results[6]["pattern"]) == 5
2641
+ # Index 7: Person 3 zero-hop
2642
+ assert len(results[7]["pattern"]) == 1
2643
+ assert results[7]["pattern"][0]["id"] == 3
2644
+ # Index 8: Person 3 -> Person 4 (1-hop)
2645
+ assert len(results[8]["pattern"]) == 3
2646
+ # Index 9: Person 4 zero-hop
2647
+ assert len(results[9]["pattern"]) == 1
2648
+ assert results[9]["pattern"][0]["id"] == 4
2649
+
2507
2650
  @pytest.mark.asyncio
2508
2651
  async def test_add_two_integers(self):
2509
2652
  """Test add two integers."""
@@ -2809,4 +2952,151 @@ class TestRunner:
2809
2952
  await runner.run()
2810
2953
  results = runner.results
2811
2954
  assert len(results) == 1
2812
- assert results == [{"x": 1}]
2955
+ assert results == [{"x": 1}]
2956
+
2957
+ @pytest.mark.asyncio
2958
+ async def test_language_name_hits_query_with_virtual_graph(self):
2959
+ """Test full language-name-hits query with virtual graph.
2960
+
2961
+ Reproduces the original bug: collect(distinct ...) on MATCH results,
2962
+ then sum(lang IN langs | ...) in a WITH clause, was throwing
2963
+ "Invalid array for sum function" because collect() returned null
2964
+ instead of [] when no rows entered aggregation.
2965
+ """
2966
+ # Create Language nodes
2967
+ await Runner(
2968
+ """
2969
+ CREATE VIRTUAL (:Language) AS {
2970
+ UNWIND [
2971
+ {id: 1, name: 'Python'},
2972
+ {id: 2, name: 'JavaScript'},
2973
+ {id: 3, name: 'TypeScript'}
2974
+ ] AS record
2975
+ RETURN record.id AS id, record.name AS name
2976
+ }
2977
+ """
2978
+ ).run()
2979
+
2980
+ # Create Chat nodes with messages
2981
+ await Runner(
2982
+ """
2983
+ CREATE VIRTUAL (:Chat) AS {
2984
+ UNWIND [
2985
+ {id: 1, name: 'Dev Discussion', messages: [
2986
+ {From: 'Alice', SentDateTime: '2025-01-01T10:00:00', Content: 'I love Python and JavaScript'},
2987
+ {From: 'Bob', SentDateTime: '2025-01-01T10:05:00', Content: 'What languages do you prefer?'}
2988
+ ]},
2989
+ {id: 2, name: 'General', messages: [
2990
+ {From: 'Charlie', SentDateTime: '2025-01-02T09:00:00', Content: 'The weather is nice today'},
2991
+ {From: 'Alice', SentDateTime: '2025-01-02T09:05:00', Content: 'TypeScript is great for language tooling'}
2992
+ ]}
2993
+ ] AS record
2994
+ RETURN record.id AS id, record.name AS name, record.messages AS messages
2995
+ }
2996
+ """
2997
+ ).run()
2998
+
2999
+ # Create User nodes
3000
+ await Runner(
3001
+ """
3002
+ CREATE VIRTUAL (:User) AS {
3003
+ UNWIND [
3004
+ {id: 1, displayName: 'Alice'},
3005
+ {id: 2, displayName: 'Bob'},
3006
+ {id: 3, displayName: 'Charlie'}
3007
+ ] AS record
3008
+ RETURN record.id AS id, record.displayName AS displayName
3009
+ }
3010
+ """
3011
+ ).run()
3012
+
3013
+ # Create PARTICIPATES_IN relationships
3014
+ await Runner(
3015
+ """
3016
+ CREATE VIRTUAL (:User)-[:PARTICIPATES_IN]-(:Chat) AS {
3017
+ UNWIND [
3018
+ {left_id: 1, right_id: 1},
3019
+ {left_id: 2, right_id: 1},
3020
+ {left_id: 3, right_id: 2},
3021
+ {left_id: 1, right_id: 2}
3022
+ ] AS record
3023
+ RETURN record.left_id AS left_id, record.right_id AS right_id
3024
+ }
3025
+ """
3026
+ ).run()
3027
+
3028
+ # Run the original query (using 'sender' alias since 'from' is a reserved keyword)
3029
+ runner = Runner(
3030
+ """
3031
+ MATCH (l:Language)
3032
+ WITH collect(distinct l.name) AS langs
3033
+ MATCH (c:Chat)
3034
+ UNWIND c.messages AS msg
3035
+ WITH c, msg, langs,
3036
+ sum(lang IN langs | 1 where toLower(msg.Content) CONTAINS toLower(lang)) AS langNameHits
3037
+ WHERE toLower(msg.Content) CONTAINS "language"
3038
+ OR toLower(msg.Content) CONTAINS "languages"
3039
+ OR langNameHits > 0
3040
+ OPTIONAL MATCH (u:User)-[:PARTICIPATES_IN]->(c)
3041
+ RETURN
3042
+ c.name AS chat,
3043
+ collect(distinct u.displayName) AS participants,
3044
+ msg.From AS sender,
3045
+ msg.SentDateTime AS sentDateTime,
3046
+ msg.Content AS message
3047
+ """
3048
+ )
3049
+ await runner.run()
3050
+ results = runner.results
3051
+
3052
+ # Messages that mention a language name or the word "language(s)":
3053
+ # 1. "I love Python and JavaScript" - langNameHits=2
3054
+ # 2. "What languages do you prefer?" - contains "languages"
3055
+ # 3. "TypeScript is great for language tooling" - langNameHits=1, also "language"
3056
+ assert len(results) == 3
3057
+ assert results[0]["chat"] == "Dev Discussion"
3058
+ assert results[0]["message"] == "I love Python and JavaScript"
3059
+ assert results[0]["sender"] == "Alice"
3060
+ assert results[1]["chat"] == "Dev Discussion"
3061
+ assert results[1]["message"] == "What languages do you prefer?"
3062
+ assert results[1]["sender"] == "Bob"
3063
+ assert results[2]["chat"] == "General"
3064
+ assert results[2]["message"] == "TypeScript is great for language tooling"
3065
+ assert results[2]["sender"] == "Alice"
3066
+
3067
+ @pytest.mark.asyncio
3068
+ async def test_sum_with_empty_collected_array(self):
3069
+ """Reproduces the original bug: collect on empty input should yield []
3070
+ and sum over that empty array should return 0, not throw."""
3071
+ runner = Runner(
3072
+ """
3073
+ UNWIND [] AS lang
3074
+ WITH collect(distinct lang) AS langs
3075
+ UNWIND ['hello', 'world'] AS msg
3076
+ WITH msg, langs, sum(l IN langs | 1 where toLower(msg) CONTAINS toLower(l)) AS hits
3077
+ RETURN msg, hits
3078
+ """
3079
+ )
3080
+ await runner.run()
3081
+ results = runner.results
3082
+ assert len(results) == 2
3083
+ assert results[0] == {"msg": "hello", "hits": 0}
3084
+ assert results[1] == {"msg": "world", "hits": 0}
3085
+
3086
+ @pytest.mark.asyncio
3087
+ async def test_sum_where_all_elements_filtered_returns_0(self):
3088
+ """Test sum returns 0 when where clause filters everything."""
3089
+ runner = Runner("RETURN sum(n in [1, 2, 3] | n where n > 100) as sum")
3090
+ await runner.run()
3091
+ results = runner.results
3092
+ assert len(results) == 1
3093
+ assert results[0] == {"sum": 0}
3094
+
3095
+ @pytest.mark.asyncio
3096
+ async def test_sum_over_empty_array_returns_0(self):
3097
+ """Test sum over empty array returns 0."""
3098
+ runner = Runner("WITH [] AS arr RETURN sum(n in arr | n) as sum")
3099
+ await runner.run()
3100
+ results = runner.results
3101
+ assert len(results) == 1
3102
+ assert results[0] == {"sum": 0}