flowquery 1.0.38 → 1.0.40

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 +2 -0
  3. package/dist/graph/database.d.ts.map +1 -1
  4. package/dist/graph/database.js +12 -0
  5. package/dist/graph/database.js.map +1 -1
  6. package/dist/parsing/expressions/operator.js +4 -4
  7. package/dist/parsing/expressions/operator.js.map +1 -1
  8. package/dist/parsing/functions/function_factory.d.ts +1 -0
  9. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  10. package/dist/parsing/functions/function_factory.js +1 -0
  11. package/dist/parsing/functions/function_factory.js.map +1 -1
  12. package/dist/parsing/functions/substring.d.ts +9 -0
  13. package/dist/parsing/functions/substring.d.ts.map +1 -0
  14. package/dist/parsing/functions/substring.js +62 -0
  15. package/dist/parsing/functions/substring.js.map +1 -0
  16. package/dist/parsing/operations/aggregated_return.d.ts.map +1 -1
  17. package/dist/parsing/operations/aggregated_return.js +6 -2
  18. package/dist/parsing/operations/aggregated_return.js.map +1 -1
  19. package/dist/parsing/operations/delete_node.d.ts +11 -0
  20. package/dist/parsing/operations/delete_node.d.ts.map +1 -0
  21. package/dist/parsing/operations/delete_node.js +46 -0
  22. package/dist/parsing/operations/delete_node.js.map +1 -0
  23. package/dist/parsing/operations/delete_relationship.d.ts +11 -0
  24. package/dist/parsing/operations/delete_relationship.d.ts.map +1 -0
  25. package/dist/parsing/operations/delete_relationship.js +46 -0
  26. package/dist/parsing/operations/delete_relationship.js.map +1 -0
  27. package/dist/parsing/operations/limit.d.ts +1 -0
  28. package/dist/parsing/operations/limit.d.ts.map +1 -1
  29. package/dist/parsing/operations/limit.js +3 -0
  30. package/dist/parsing/operations/limit.js.map +1 -1
  31. package/dist/parsing/operations/order_by.d.ts +35 -0
  32. package/dist/parsing/operations/order_by.d.ts.map +1 -0
  33. package/dist/parsing/operations/order_by.js +87 -0
  34. package/dist/parsing/operations/order_by.js.map +1 -0
  35. package/dist/parsing/operations/return.d.ts +3 -0
  36. package/dist/parsing/operations/return.d.ts.map +1 -1
  37. package/dist/parsing/operations/return.js +16 -3
  38. package/dist/parsing/operations/return.js.map +1 -1
  39. package/dist/parsing/parser.d.ts +2 -0
  40. package/dist/parsing/parser.d.ts.map +1 -1
  41. package/dist/parsing/parser.js +116 -2
  42. package/dist/parsing/parser.js.map +1 -1
  43. package/dist/tokenization/token.d.ts +8 -0
  44. package/dist/tokenization/token.d.ts.map +1 -1
  45. package/dist/tokenization/token.js +24 -0
  46. package/dist/tokenization/token.js.map +1 -1
  47. package/docs/flowquery.min.js +1 -1
  48. package/flowquery-py/pyproject.toml +1 -1
  49. package/flowquery-py/src/graph/database.py +12 -0
  50. package/flowquery-py/src/parsing/expressions/operator.py +4 -4
  51. package/flowquery-py/src/parsing/functions/__init__.py +2 -0
  52. package/flowquery-py/src/parsing/functions/substring.py +74 -0
  53. package/flowquery-py/src/parsing/operations/__init__.py +7 -0
  54. package/flowquery-py/src/parsing/operations/aggregated_return.py +4 -1
  55. package/flowquery-py/src/parsing/operations/delete_node.py +29 -0
  56. package/flowquery-py/src/parsing/operations/delete_relationship.py +29 -0
  57. package/flowquery-py/src/parsing/operations/limit.py +4 -0
  58. package/flowquery-py/src/parsing/operations/order_by.py +72 -0
  59. package/flowquery-py/src/parsing/operations/return_op.py +20 -3
  60. package/flowquery-py/src/parsing/parser.py +98 -3
  61. package/flowquery-py/src/tokenization/token.py +28 -0
  62. package/flowquery-py/tests/compute/test_runner.py +329 -1
  63. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  64. package/package.json +1 -1
  65. package/src/graph/database.ts +12 -0
  66. package/src/parsing/expressions/operator.ts +4 -4
  67. package/src/parsing/functions/function_factory.ts +1 -0
  68. package/src/parsing/functions/substring.ts +65 -0
  69. package/src/parsing/operations/aggregated_return.ts +9 -5
  70. package/src/parsing/operations/delete_node.ts +33 -0
  71. package/src/parsing/operations/delete_relationship.ts +32 -0
  72. package/src/parsing/operations/limit.ts +3 -0
  73. package/src/parsing/operations/order_by.ts +75 -0
  74. package/src/parsing/operations/return.ts +17 -3
  75. package/src/parsing/parser.ts +115 -2
  76. package/src/tokenization/token.ts +32 -0
  77. package/tests/compute/runner.test.ts +291 -0
  78. package/tests/parsing/parser.test.ts +1 -1
@@ -3,6 +3,9 @@
3
3
  import pytest
4
4
  from typing import AsyncIterator
5
5
  from flowquery.compute.runner import Runner
6
+ from flowquery.graph.node import Node
7
+ from flowquery.graph.relationship import Relationship
8
+ from flowquery.graph.database import Database
6
9
  from flowquery.parsing.functions.async_function import AsyncFunction
7
10
  from flowquery.parsing.functions.function_metadata import FunctionDef
8
11
 
@@ -810,6 +813,42 @@ class TestRunner:
810
813
  assert len(results) == 1
811
814
  assert results[0] == {"result": ""}
812
815
 
816
+ @pytest.mark.asyncio
817
+ async def test_substring_function_with_start_and_length(self):
818
+ """Test substring function with start and length."""
819
+ runner = Runner('RETURN substring("hello", 1, 3) as result')
820
+ await runner.run()
821
+ results = runner.results
822
+ assert len(results) == 1
823
+ assert results[0] == {"result": "ell"}
824
+
825
+ @pytest.mark.asyncio
826
+ async def test_substring_function_with_start_only(self):
827
+ """Test substring function with start only."""
828
+ runner = Runner('RETURN substring("hello", 2) as result')
829
+ await runner.run()
830
+ results = runner.results
831
+ assert len(results) == 1
832
+ assert results[0] == {"result": "llo"}
833
+
834
+ @pytest.mark.asyncio
835
+ async def test_substring_function_with_zero_start(self):
836
+ """Test substring function with zero start."""
837
+ runner = Runner('RETURN substring("hello", 0, 5) as result')
838
+ await runner.run()
839
+ results = runner.results
840
+ assert len(results) == 1
841
+ assert results[0] == {"result": "hello"}
842
+
843
+ @pytest.mark.asyncio
844
+ async def test_substring_function_with_zero_length(self):
845
+ """Test substring function with zero length."""
846
+ runner = Runner('RETURN substring("hello", 1, 0) as result')
847
+ await runner.run()
848
+ results = runner.results
849
+ assert len(results) == 1
850
+ assert results[0] == {"result": ""}
851
+
813
852
  @pytest.mark.asyncio
814
853
  async def test_associative_array_with_key_which_is_keyword(self):
815
854
  """Test associative array with key which is keyword."""
@@ -2798,6 +2837,58 @@ class TestRunner:
2798
2837
  assert len(results) == 3
2799
2838
  assert [r["n"] for r in results] == [10, 15, 20]
2800
2839
 
2840
+ @pytest.mark.asyncio
2841
+ async def test_where_with_and_before_in(self):
2842
+ """Test WHERE with AND before IN (IN on right side of AND)."""
2843
+ runner = Runner("""
2844
+ unwind ['expert', 'intermediate', 'beginner'] as proficiency
2845
+ with proficiency where 1=1 and proficiency in ['expert']
2846
+ return proficiency
2847
+ """)
2848
+ await runner.run()
2849
+ results = runner.results
2850
+ assert len(results) == 1
2851
+ assert results[0] == {"proficiency": "expert"}
2852
+
2853
+ @pytest.mark.asyncio
2854
+ async def test_where_with_and_before_not_in(self):
2855
+ """Test WHERE with AND before NOT IN."""
2856
+ runner = Runner("""
2857
+ unwind ['expert', 'intermediate', 'beginner'] as proficiency
2858
+ with proficiency where 1=1 and proficiency not in ['expert']
2859
+ return proficiency
2860
+ """)
2861
+ await runner.run()
2862
+ results = runner.results
2863
+ assert len(results) == 2
2864
+ assert [r["proficiency"] for r in results] == ["intermediate", "beginner"]
2865
+
2866
+ @pytest.mark.asyncio
2867
+ async def test_where_with_or_before_in(self):
2868
+ """Test WHERE with OR before IN."""
2869
+ runner = Runner("""
2870
+ unwind range(1, 10) as n
2871
+ with n where 1=0 or n in [3, 7]
2872
+ return n
2873
+ """)
2874
+ await runner.run()
2875
+ results = runner.results
2876
+ assert len(results) == 2
2877
+ assert [r["n"] for r in results] == [3, 7]
2878
+
2879
+ @pytest.mark.asyncio
2880
+ async def test_in_as_return_expression_with_and_in_where(self):
2881
+ """Test IN as return expression with AND in WHERE."""
2882
+ runner = Runner("""
2883
+ unwind ['expert', 'intermediate', 'beginner'] as proficiency
2884
+ with proficiency where 1=1 and proficiency in ['expert']
2885
+ return proficiency, proficiency in ['expert'] as isExpert
2886
+ """)
2887
+ await runner.run()
2888
+ results = runner.results
2889
+ assert len(results) == 1
2890
+ assert results[0] == {"proficiency": "expert", "isExpert": 1}
2891
+
2801
2892
  @pytest.mark.asyncio
2802
2893
  async def test_where_with_contains(self):
2803
2894
  """Test WHERE with CONTAINS."""
@@ -4011,4 +4102,241 @@ class TestRunner:
4011
4102
  assert d["hours"] == 2
4012
4103
  assert d["minutes"] == 30
4013
4104
  assert d["totalSeconds"] == 9000
4014
- assert d["formatted"] == "PT2H30M"
4105
+ assert d["formatted"] == "PT2H30M"
4106
+
4107
+ # ORDER BY tests
4108
+
4109
+ @pytest.mark.asyncio
4110
+ async def test_order_by_ascending(self):
4111
+ """Test ORDER BY ascending (default)."""
4112
+ runner = Runner("unwind [3, 1, 2] as x return x order by x")
4113
+ await runner.run()
4114
+ results = runner.results
4115
+ assert len(results) == 3
4116
+ assert results[0] == {"x": 1}
4117
+ assert results[1] == {"x": 2}
4118
+ assert results[2] == {"x": 3}
4119
+
4120
+ @pytest.mark.asyncio
4121
+ async def test_order_by_descending(self):
4122
+ """Test ORDER BY descending."""
4123
+ runner = Runner("unwind [3, 1, 2] as x return x order by x desc")
4124
+ await runner.run()
4125
+ results = runner.results
4126
+ assert len(results) == 3
4127
+ assert results[0] == {"x": 3}
4128
+ assert results[1] == {"x": 2}
4129
+ assert results[2] == {"x": 1}
4130
+
4131
+ @pytest.mark.asyncio
4132
+ async def test_order_by_ascending_explicit(self):
4133
+ """Test ORDER BY with explicit ASC."""
4134
+ runner = Runner("unwind [3, 1, 2] as x return x order by x asc")
4135
+ await runner.run()
4136
+ results = runner.results
4137
+ assert len(results) == 3
4138
+ assert results[0] == {"x": 1}
4139
+ assert results[1] == {"x": 2}
4140
+ assert results[2] == {"x": 3}
4141
+
4142
+ @pytest.mark.asyncio
4143
+ async def test_order_by_with_multiple_fields(self):
4144
+ """Test ORDER BY with multiple sort fields."""
4145
+ runner = Runner(
4146
+ "unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: 25}, {name: 'Alice', age: 25}] as person "
4147
+ "return person.name as name, person.age as age "
4148
+ "order by name asc, age asc"
4149
+ )
4150
+ await runner.run()
4151
+ results = runner.results
4152
+ assert len(results) == 3
4153
+ assert results[0] == {"name": "Alice", "age": 25}
4154
+ assert results[1] == {"name": "Alice", "age": 30}
4155
+ assert results[2] == {"name": "Bob", "age": 25}
4156
+
4157
+ @pytest.mark.asyncio
4158
+ async def test_order_by_with_strings(self):
4159
+ """Test ORDER BY with string values."""
4160
+ runner = Runner(
4161
+ "unwind ['banana', 'apple', 'cherry'] as fruit return fruit order by fruit"
4162
+ )
4163
+ await runner.run()
4164
+ results = runner.results
4165
+ assert len(results) == 3
4166
+ assert results[0] == {"fruit": "apple"}
4167
+ assert results[1] == {"fruit": "banana"}
4168
+ assert results[2] == {"fruit": "cherry"}
4169
+
4170
+ @pytest.mark.asyncio
4171
+ async def test_order_by_with_aggregated_return(self):
4172
+ """Test ORDER BY with aggregated RETURN."""
4173
+ runner = Runner(
4174
+ "unwind [1, 1, 2, 2, 3, 3] as x "
4175
+ "return x, count(x) as cnt "
4176
+ "order by x desc"
4177
+ )
4178
+ await runner.run()
4179
+ results = runner.results
4180
+ assert len(results) == 3
4181
+ assert results[0] == {"x": 3, "cnt": 2}
4182
+ assert results[1] == {"x": 2, "cnt": 2}
4183
+ assert results[2] == {"x": 1, "cnt": 2}
4184
+
4185
+ @pytest.mark.asyncio
4186
+ async def test_order_by_with_limit(self):
4187
+ """Test ORDER BY combined with LIMIT."""
4188
+ runner = Runner(
4189
+ "unwind [3, 1, 4, 1, 5, 9, 2, 6] as x return x order by x limit 3"
4190
+ )
4191
+ await runner.run()
4192
+ results = runner.results
4193
+ assert len(results) == 3
4194
+ assert results[0] == {"x": 1}
4195
+ assert results[1] == {"x": 1}
4196
+ assert results[2] == {"x": 2}
4197
+
4198
+ @pytest.mark.asyncio
4199
+ async def test_order_by_with_where(self):
4200
+ """Test ORDER BY combined with WHERE."""
4201
+ runner = Runner(
4202
+ "unwind [3, 1, 4, 1, 5, 9, 2, 6] as x return x where x > 2 order by x desc"
4203
+ )
4204
+ await runner.run()
4205
+ results = runner.results
4206
+ assert len(results) == 5
4207
+ assert results[0] == {"x": 9}
4208
+ assert results[1] == {"x": 6}
4209
+ assert results[2] == {"x": 5}
4210
+ assert results[3] == {"x": 4}
4211
+ assert results[4] == {"x": 3}
4212
+
4213
+ @pytest.mark.asyncio
4214
+ async def test_delete_virtual_node_operation(self):
4215
+ """Test delete virtual node operation."""
4216
+ db = Database.get_instance()
4217
+ # Create a virtual node first
4218
+ create = Runner(
4219
+ """
4220
+ CREATE VIRTUAL (:PyDeleteTestPerson) AS {
4221
+ unwind [
4222
+ {id: 1, name: 'Person 1'},
4223
+ {id: 2, name: 'Person 2'}
4224
+ ] as record
4225
+ RETURN record.id as id, record.name as name
4226
+ }
4227
+ """
4228
+ )
4229
+ await create.run()
4230
+ assert db.get_node(Node(None, "PyDeleteTestPerson")) is not None
4231
+
4232
+ # Delete the virtual node
4233
+ del_runner = Runner("DELETE VIRTUAL (:PyDeleteTestPerson)")
4234
+ await del_runner.run()
4235
+ assert len(del_runner.results) == 0
4236
+ assert db.get_node(Node(None, "PyDeleteTestPerson")) is None
4237
+
4238
+ @pytest.mark.asyncio
4239
+ async def test_delete_virtual_node_then_match_throws(self):
4240
+ """Test that matching a deleted virtual node throws."""
4241
+ # Create a virtual node
4242
+ create = Runner(
4243
+ """
4244
+ CREATE VIRTUAL (:PyDeleteMatchPerson) AS {
4245
+ unwind [{id: 1, name: 'Alice'}] as record
4246
+ RETURN record.id as id, record.name as name
4247
+ }
4248
+ """
4249
+ )
4250
+ await create.run()
4251
+
4252
+ # Verify it can be matched
4253
+ match1 = Runner("MATCH (n:PyDeleteMatchPerson) RETURN n")
4254
+ await match1.run()
4255
+ assert len(match1.results) == 1
4256
+
4257
+ # Delete the virtual node
4258
+ del_runner = Runner("DELETE VIRTUAL (:PyDeleteMatchPerson)")
4259
+ await del_runner.run()
4260
+
4261
+ # Matching should now throw since the node is gone
4262
+ match2 = Runner("MATCH (n:PyDeleteMatchPerson) RETURN n")
4263
+ with pytest.raises(ValueError):
4264
+ await match2.run()
4265
+
4266
+ @pytest.mark.asyncio
4267
+ async def test_delete_virtual_relationship_operation(self):
4268
+ """Test delete virtual relationship operation."""
4269
+ db = Database.get_instance()
4270
+ # Create virtual nodes and relationship
4271
+ await Runner(
4272
+ """
4273
+ CREATE VIRTUAL (:PyDelRelUser) AS {
4274
+ unwind [
4275
+ {id: 1, name: 'Alice'},
4276
+ {id: 2, name: 'Bob'}
4277
+ ] as record
4278
+ RETURN record.id as id, record.name as name
4279
+ }
4280
+ """
4281
+ ).run()
4282
+
4283
+ await Runner(
4284
+ """
4285
+ CREATE VIRTUAL (:PyDelRelUser)-[:PY_DEL_KNOWS]-(:PyDelRelUser) AS {
4286
+ unwind [
4287
+ {left_id: 1, right_id: 2}
4288
+ ] as record
4289
+ RETURN record.left_id as left_id, record.right_id as right_id
4290
+ }
4291
+ """
4292
+ ).run()
4293
+
4294
+ # Verify relationship exists
4295
+ rel = Relationship()
4296
+ rel.type = "PY_DEL_KNOWS"
4297
+ assert db.get_relationship(rel) is not None
4298
+
4299
+ # Delete the virtual relationship
4300
+ del_runner = Runner("DELETE VIRTUAL (:PyDelRelUser)-[:PY_DEL_KNOWS]-(:PyDelRelUser)")
4301
+ await del_runner.run()
4302
+ assert len(del_runner.results) == 0
4303
+ assert db.get_relationship(rel) is None
4304
+
4305
+ @pytest.mark.asyncio
4306
+ async def test_delete_virtual_node_leaves_other_nodes_intact(self):
4307
+ """Test that deleting one virtual node leaves others intact."""
4308
+ db = Database.get_instance()
4309
+ # Create two virtual node types
4310
+ await Runner(
4311
+ """
4312
+ CREATE VIRTUAL (:PyKeepNode) AS {
4313
+ unwind [{id: 1, name: 'Keep'}] as record
4314
+ RETURN record.id as id, record.name as name
4315
+ }
4316
+ """
4317
+ ).run()
4318
+
4319
+ await Runner(
4320
+ """
4321
+ CREATE VIRTUAL (:PyRemoveNode) AS {
4322
+ unwind [{id: 2, name: 'Remove'}] as record
4323
+ RETURN record.id as id, record.name as name
4324
+ }
4325
+ """
4326
+ ).run()
4327
+
4328
+ assert db.get_node(Node(None, "PyKeepNode")) is not None
4329
+ assert db.get_node(Node(None, "PyRemoveNode")) is not None
4330
+
4331
+ # Delete only one
4332
+ await Runner("DELETE VIRTUAL (:PyRemoveNode)").run()
4333
+
4334
+ # The other should still exist
4335
+ assert db.get_node(Node(None, "PyKeepNode")) is not None
4336
+ assert db.get_node(Node(None, "PyRemoveNode")) is None
4337
+
4338
+ # The remaining node can still be matched
4339
+ match = Runner("MATCH (n:PyKeepNode) RETURN n")
4340
+ await match.run()
4341
+ assert len(match.results) == 1
4342
+ assert match.results[0]["n"]["name"] == "Keep"