flowquery 1.0.18 → 1.0.21

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 (158) hide show
  1. package/.gitattributes +3 -0
  2. package/.github/workflows/python-publish.yml +56 -4
  3. package/.github/workflows/release.yml +26 -19
  4. package/.husky/pre-commit +26 -0
  5. package/README.md +37 -32
  6. package/dist/flowquery.min.js +1 -1
  7. package/dist/graph/data.d.ts +5 -4
  8. package/dist/graph/data.d.ts.map +1 -1
  9. package/dist/graph/data.js +38 -20
  10. package/dist/graph/data.js.map +1 -1
  11. package/dist/graph/node.d.ts +2 -0
  12. package/dist/graph/node.d.ts.map +1 -1
  13. package/dist/graph/node.js +23 -0
  14. package/dist/graph/node.js.map +1 -1
  15. package/dist/graph/node_data.js +1 -1
  16. package/dist/graph/node_data.js.map +1 -1
  17. package/dist/graph/pattern.d.ts.map +1 -1
  18. package/dist/graph/pattern.js +11 -4
  19. package/dist/graph/pattern.js.map +1 -1
  20. package/dist/graph/relationship.d.ts +6 -1
  21. package/dist/graph/relationship.d.ts.map +1 -1
  22. package/dist/graph/relationship.js +43 -5
  23. package/dist/graph/relationship.js.map +1 -1
  24. package/dist/graph/relationship_data.d.ts +2 -0
  25. package/dist/graph/relationship_data.d.ts.map +1 -1
  26. package/dist/graph/relationship_data.js +8 -1
  27. package/dist/graph/relationship_data.js.map +1 -1
  28. package/dist/graph/relationship_match_collector.js +2 -2
  29. package/dist/graph/relationship_match_collector.js.map +1 -1
  30. package/dist/graph/relationship_reference.d.ts.map +1 -1
  31. package/dist/graph/relationship_reference.js +2 -1
  32. package/dist/graph/relationship_reference.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/parsing/parser.d.ts +6 -0
  36. package/dist/parsing/parser.d.ts.map +1 -1
  37. package/dist/parsing/parser.js +139 -72
  38. package/dist/parsing/parser.js.map +1 -1
  39. package/docs/flowquery.min.js +1 -1
  40. package/flowquery-py/misc/data/test.json +10 -0
  41. package/flowquery-py/misc/data/users.json +242 -0
  42. package/flowquery-py/notebooks/TestFlowQuery.ipynb +440 -0
  43. package/flowquery-py/pyproject.toml +48 -2
  44. package/flowquery-py/src/__init__.py +7 -5
  45. package/flowquery-py/src/compute/runner.py +14 -10
  46. package/flowquery-py/src/extensibility.py +8 -8
  47. package/flowquery-py/src/graph/__init__.py +7 -7
  48. package/flowquery-py/src/graph/data.py +38 -20
  49. package/flowquery-py/src/graph/database.py +10 -20
  50. package/flowquery-py/src/graph/node.py +50 -19
  51. package/flowquery-py/src/graph/node_data.py +1 -1
  52. package/flowquery-py/src/graph/node_reference.py +10 -11
  53. package/flowquery-py/src/graph/pattern.py +27 -37
  54. package/flowquery-py/src/graph/pattern_expression.py +13 -11
  55. package/flowquery-py/src/graph/patterns.py +2 -2
  56. package/flowquery-py/src/graph/physical_node.py +4 -3
  57. package/flowquery-py/src/graph/physical_relationship.py +5 -5
  58. package/flowquery-py/src/graph/relationship.py +62 -14
  59. package/flowquery-py/src/graph/relationship_data.py +7 -2
  60. package/flowquery-py/src/graph/relationship_match_collector.py +15 -10
  61. package/flowquery-py/src/graph/relationship_reference.py +4 -4
  62. package/flowquery-py/src/io/command_line.py +13 -14
  63. package/flowquery-py/src/parsing/__init__.py +2 -2
  64. package/flowquery-py/src/parsing/alias_option.py +1 -1
  65. package/flowquery-py/src/parsing/ast_node.py +21 -20
  66. package/flowquery-py/src/parsing/base_parser.py +7 -7
  67. package/flowquery-py/src/parsing/components/__init__.py +3 -3
  68. package/flowquery-py/src/parsing/components/from_.py +3 -1
  69. package/flowquery-py/src/parsing/components/headers.py +2 -2
  70. package/flowquery-py/src/parsing/components/null.py +2 -2
  71. package/flowquery-py/src/parsing/context.py +7 -7
  72. package/flowquery-py/src/parsing/data_structures/associative_array.py +7 -7
  73. package/flowquery-py/src/parsing/data_structures/json_array.py +3 -3
  74. package/flowquery-py/src/parsing/data_structures/key_value_pair.py +4 -4
  75. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -2
  76. package/flowquery-py/src/parsing/data_structures/range_lookup.py +2 -2
  77. package/flowquery-py/src/parsing/expressions/__init__.py +16 -16
  78. package/flowquery-py/src/parsing/expressions/expression.py +16 -13
  79. package/flowquery-py/src/parsing/expressions/expression_map.py +9 -9
  80. package/flowquery-py/src/parsing/expressions/f_string.py +3 -3
  81. package/flowquery-py/src/parsing/expressions/identifier.py +4 -3
  82. package/flowquery-py/src/parsing/expressions/number.py +3 -3
  83. package/flowquery-py/src/parsing/expressions/operator.py +16 -16
  84. package/flowquery-py/src/parsing/expressions/reference.py +3 -3
  85. package/flowquery-py/src/parsing/expressions/string.py +2 -2
  86. package/flowquery-py/src/parsing/functions/__init__.py +17 -17
  87. package/flowquery-py/src/parsing/functions/aggregate_function.py +8 -8
  88. package/flowquery-py/src/parsing/functions/async_function.py +12 -9
  89. package/flowquery-py/src/parsing/functions/avg.py +4 -4
  90. package/flowquery-py/src/parsing/functions/collect.py +6 -6
  91. package/flowquery-py/src/parsing/functions/function.py +6 -6
  92. package/flowquery-py/src/parsing/functions/function_factory.py +31 -34
  93. package/flowquery-py/src/parsing/functions/function_metadata.py +10 -11
  94. package/flowquery-py/src/parsing/functions/functions.py +14 -6
  95. package/flowquery-py/src/parsing/functions/join.py +3 -3
  96. package/flowquery-py/src/parsing/functions/keys.py +3 -3
  97. package/flowquery-py/src/parsing/functions/predicate_function.py +8 -7
  98. package/flowquery-py/src/parsing/functions/predicate_sum.py +12 -7
  99. package/flowquery-py/src/parsing/functions/rand.py +2 -2
  100. package/flowquery-py/src/parsing/functions/range_.py +9 -4
  101. package/flowquery-py/src/parsing/functions/replace.py +2 -2
  102. package/flowquery-py/src/parsing/functions/round_.py +2 -2
  103. package/flowquery-py/src/parsing/functions/size.py +2 -2
  104. package/flowquery-py/src/parsing/functions/split.py +9 -4
  105. package/flowquery-py/src/parsing/functions/stringify.py +3 -3
  106. package/flowquery-py/src/parsing/functions/sum.py +4 -4
  107. package/flowquery-py/src/parsing/functions/to_json.py +2 -2
  108. package/flowquery-py/src/parsing/functions/type_.py +3 -3
  109. package/flowquery-py/src/parsing/functions/value_holder.py +1 -1
  110. package/flowquery-py/src/parsing/logic/__init__.py +2 -2
  111. package/flowquery-py/src/parsing/logic/case.py +0 -1
  112. package/flowquery-py/src/parsing/logic/when.py +3 -1
  113. package/flowquery-py/src/parsing/operations/__init__.py +10 -10
  114. package/flowquery-py/src/parsing/operations/aggregated_return.py +3 -5
  115. package/flowquery-py/src/parsing/operations/aggregated_with.py +4 -4
  116. package/flowquery-py/src/parsing/operations/call.py +6 -7
  117. package/flowquery-py/src/parsing/operations/create_node.py +5 -4
  118. package/flowquery-py/src/parsing/operations/create_relationship.py +5 -4
  119. package/flowquery-py/src/parsing/operations/group_by.py +18 -16
  120. package/flowquery-py/src/parsing/operations/load.py +21 -19
  121. package/flowquery-py/src/parsing/operations/match.py +8 -7
  122. package/flowquery-py/src/parsing/operations/operation.py +3 -3
  123. package/flowquery-py/src/parsing/operations/projection.py +6 -6
  124. package/flowquery-py/src/parsing/operations/return_op.py +9 -5
  125. package/flowquery-py/src/parsing/operations/unwind.py +3 -2
  126. package/flowquery-py/src/parsing/operations/where.py +9 -7
  127. package/flowquery-py/src/parsing/operations/with_op.py +2 -2
  128. package/flowquery-py/src/parsing/parser.py +178 -114
  129. package/flowquery-py/src/parsing/token_to_node.py +2 -2
  130. package/flowquery-py/src/tokenization/__init__.py +4 -4
  131. package/flowquery-py/src/tokenization/keyword.py +1 -1
  132. package/flowquery-py/src/tokenization/operator.py +1 -1
  133. package/flowquery-py/src/tokenization/string_walker.py +4 -4
  134. package/flowquery-py/src/tokenization/symbol.py +1 -1
  135. package/flowquery-py/src/tokenization/token.py +11 -11
  136. package/flowquery-py/src/tokenization/token_mapper.py +10 -9
  137. package/flowquery-py/src/tokenization/token_type.py +1 -1
  138. package/flowquery-py/src/tokenization/tokenizer.py +19 -19
  139. package/flowquery-py/src/tokenization/trie.py +18 -17
  140. package/flowquery-py/src/utils/__init__.py +1 -1
  141. package/flowquery-py/src/utils/object_utils.py +3 -3
  142. package/flowquery-py/src/utils/string_utils.py +12 -12
  143. package/flowquery-py/tests/compute/test_runner.py +214 -7
  144. package/flowquery-py/tests/parsing/test_parser.py +41 -0
  145. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  146. package/package.json +1 -1
  147. package/src/graph/data.ts +38 -20
  148. package/src/graph/node.ts +23 -0
  149. package/src/graph/node_data.ts +1 -1
  150. package/src/graph/pattern.ts +13 -4
  151. package/src/graph/relationship.ts +45 -5
  152. package/src/graph/relationship_data.ts +8 -1
  153. package/src/graph/relationship_match_collector.ts +1 -1
  154. package/src/graph/relationship_reference.ts +2 -1
  155. package/src/index.ts +5 -5
  156. package/src/parsing/parser.ts +139 -71
  157. package/tests/compute/runner.test.ts +249 -79
  158. package/tests/parsing/parser.test.ts +32 -0
@@ -871,10 +871,9 @@ class TestRunner:
871
871
  )
872
872
  await match.run()
873
873
  results = match.results
874
- assert len(results) == 3
875
- assert results[0] == {"name1": "Person 1", "name2": "Person 2"}
876
- assert results[1] == {"name1": "Person 1", "name2": "Person 3"}
877
- assert results[2] == {"name1": "Person 2", "name2": "Person 3"}
874
+ # With * meaning 0+ hops, each person also matches itself (zero-hop)
875
+ # Person 1→1, 1→2, 1→3, Person 2→2, 2→3, Person 3→3 + bidirectional = 7
876
+ assert len(results) == 7
878
877
 
879
878
  @pytest.mark.asyncio
880
879
  async def test_match_with_double_graph_pattern(self):
@@ -1175,7 +1174,8 @@ class TestRunner:
1175
1174
  )
1176
1175
  await match.run()
1177
1176
  results = match.results
1178
- assert len(results) == 6
1177
+ # With *0..3: Person 1 has 4 matches (0,1,2,3 hops), Person 2 has 3, Person 3 has 2, Person 4 has 1 = 10 total
1178
+ assert len(results) == 10
1179
1179
 
1180
1180
  @pytest.mark.asyncio
1181
1181
  async def test_return_match_pattern_with_variable_length_relationships(self):
@@ -1213,7 +1213,8 @@ class TestRunner:
1213
1213
  )
1214
1214
  await match.run()
1215
1215
  results = match.results
1216
- assert len(results) == 6
1216
+ # With *0..3: Person 1 has 4 matches (0,1,2,3 hops), Person 2 has 3, Person 3 has 2, Person 4 has 1 = 10 total
1217
+ assert len(results) == 10
1217
1218
 
1218
1219
  @pytest.mark.asyncio
1219
1220
  async def test_statement_with_graph_pattern_in_where_clause(self):
@@ -1332,4 +1333,210 @@ class TestRunner:
1332
1333
  )
1333
1334
  await match.run()
1334
1335
  results = match.results
1335
- assert len(results) == 2
1336
+ # With * meaning 0+ hops, Employee 1 (CEO) also matches itself (zero-hop)
1337
+ # Employee 1→1 (zero-hop), 2→1, 3→2→1, 4→2→1 = 4 results
1338
+ assert len(results) == 4
1339
+
1340
+ @pytest.mark.asyncio
1341
+ async def test_match_with_leftward_relationship_direction(self):
1342
+ """Test match with leftward relationship direction."""
1343
+ await Runner(
1344
+ """
1345
+ CREATE VIRTUAL (:DirPerson) AS {
1346
+ unwind [
1347
+ {id: 1, name: 'Person 1'},
1348
+ {id: 2, name: 'Person 2'},
1349
+ {id: 3, name: 'Person 3'}
1350
+ ] as record
1351
+ RETURN record.id as id, record.name as name
1352
+ }
1353
+ """
1354
+ ).run()
1355
+ await Runner(
1356
+ """
1357
+ CREATE VIRTUAL (:DirPerson)-[:REPORTS_TO]-(:DirPerson) AS {
1358
+ unwind [
1359
+ {left_id: 2, right_id: 1},
1360
+ {left_id: 3, right_id: 1}
1361
+ ] as record
1362
+ RETURN record.left_id as left_id, record.right_id as right_id
1363
+ }
1364
+ """
1365
+ ).run()
1366
+ # Rightward: left_id -> right_id (2->1, 3->1)
1367
+ right_match = Runner(
1368
+ """
1369
+ MATCH (a:DirPerson)-[:REPORTS_TO]->(b:DirPerson)
1370
+ RETURN a.name AS employee, b.name AS manager
1371
+ """
1372
+ )
1373
+ await right_match.run()
1374
+ right_results = right_match.results
1375
+ assert len(right_results) == 2
1376
+ assert right_results[0] == {"employee": "Person 2", "manager": "Person 1"}
1377
+ assert right_results[1] == {"employee": "Person 3", "manager": "Person 1"}
1378
+
1379
+ # Leftward: right_id -> left_id (1->2, 1->3) - reverse traversal
1380
+ left_match = Runner(
1381
+ """
1382
+ MATCH (m:DirPerson)<-[:REPORTS_TO]-(e:DirPerson)
1383
+ RETURN m.name AS manager, e.name AS employee
1384
+ """
1385
+ )
1386
+ await left_match.run()
1387
+ left_results = left_match.results
1388
+ assert len(left_results) == 2
1389
+ assert left_results[0] == {"manager": "Person 1", "employee": "Person 2"}
1390
+ assert left_results[1] == {"manager": "Person 1", "employee": "Person 3"}
1391
+
1392
+ @pytest.mark.asyncio
1393
+ async def test_match_with_leftward_direction_swapped_data(self):
1394
+ """Test match with leftward direction produces same results as rightward with swapped data."""
1395
+ await Runner(
1396
+ """
1397
+ CREATE VIRTUAL (:DirCity) AS {
1398
+ unwind [
1399
+ {id: 1, name: 'New York'},
1400
+ {id: 2, name: 'Boston'},
1401
+ {id: 3, name: 'Chicago'}
1402
+ ] as record
1403
+ RETURN record.id as id, record.name as name
1404
+ }
1405
+ """
1406
+ ).run()
1407
+ await Runner(
1408
+ """
1409
+ CREATE VIRTUAL (:DirCity)-[:ROUTE]-(:DirCity) AS {
1410
+ unwind [
1411
+ {left_id: 1, right_id: 2},
1412
+ {left_id: 1, right_id: 3}
1413
+ ] as record
1414
+ RETURN record.left_id as left_id, record.right_id as right_id
1415
+ }
1416
+ """
1417
+ ).run()
1418
+ # Leftward from destination: find where right_id matches, follow left_id
1419
+ match = Runner(
1420
+ """
1421
+ MATCH (dest:DirCity)<-[:ROUTE]-(origin:DirCity)
1422
+ RETURN dest.name AS destination, origin.name AS origin
1423
+ """
1424
+ )
1425
+ await match.run()
1426
+ results = match.results
1427
+ assert len(results) == 2
1428
+ assert results[0] == {"destination": "Boston", "origin": "New York"}
1429
+ assert results[1] == {"destination": "Chicago", "origin": "New York"}
1430
+
1431
+ @pytest.mark.asyncio
1432
+ async def test_match_with_leftward_variable_length(self):
1433
+ """Test match with leftward variable-length relationships."""
1434
+ await Runner(
1435
+ """
1436
+ CREATE VIRTUAL (:DirVarPerson) AS {
1437
+ unwind [
1438
+ {id: 1, name: 'Person 1'},
1439
+ {id: 2, name: 'Person 2'},
1440
+ {id: 3, name: 'Person 3'}
1441
+ ] as record
1442
+ RETURN record.id as id, record.name as name
1443
+ }
1444
+ """
1445
+ ).run()
1446
+ await Runner(
1447
+ """
1448
+ CREATE VIRTUAL (:DirVarPerson)-[:MANAGES]-(:DirVarPerson) AS {
1449
+ unwind [
1450
+ {left_id: 1, right_id: 2},
1451
+ {left_id: 2, right_id: 3}
1452
+ ] as record
1453
+ RETURN record.left_id as left_id, record.right_id as right_id
1454
+ }
1455
+ """
1456
+ ).run()
1457
+ # Leftward variable-length: traverse from right_id to left_id
1458
+ match = Runner(
1459
+ """
1460
+ MATCH (a:DirVarPerson)<-[:MANAGES*]-(b:DirVarPerson)
1461
+ RETURN a.name AS name1, b.name AS name2
1462
+ """
1463
+ )
1464
+ await match.run()
1465
+ results = match.results
1466
+ # Leftward indexes on right_id. find(id) looks up right_id=id, follows left_id.
1467
+ # Person 1: zero-hop only (no right_id=1)
1468
+ # Person 2: zero-hop, then left_id=1 (1 hop)
1469
+ # Person 3: zero-hop, then left_id=2 (1 hop), then left_id=1 (2 hops)
1470
+ assert len(results) == 6
1471
+ assert results[0] == {"name1": "Person 1", "name2": "Person 1"}
1472
+ assert results[1] == {"name1": "Person 2", "name2": "Person 2"}
1473
+ assert results[2] == {"name1": "Person 2", "name2": "Person 1"}
1474
+ assert results[3] == {"name1": "Person 3", "name2": "Person 3"}
1475
+ assert results[4] == {"name1": "Person 3", "name2": "Person 2"}
1476
+ assert results[5] == {"name1": "Person 3", "name2": "Person 1"}
1477
+
1478
+ @pytest.mark.asyncio
1479
+ async def test_match_with_leftward_double_graph_pattern(self):
1480
+ """Test match with leftward double graph pattern."""
1481
+ await Runner(
1482
+ """
1483
+ CREATE VIRTUAL (:DirDoublePerson) AS {
1484
+ unwind [
1485
+ {id: 1, name: 'Person 1'},
1486
+ {id: 2, name: 'Person 2'},
1487
+ {id: 3, name: 'Person 3'},
1488
+ {id: 4, name: 'Person 4'}
1489
+ ] as record
1490
+ RETURN record.id as id, record.name as name
1491
+ }
1492
+ """
1493
+ ).run()
1494
+ await Runner(
1495
+ """
1496
+ CREATE VIRTUAL (:DirDoublePerson)-[:KNOWS]-(:DirDoublePerson) AS {
1497
+ unwind [
1498
+ {left_id: 1, right_id: 2},
1499
+ {left_id: 2, right_id: 3},
1500
+ {left_id: 3, right_id: 4}
1501
+ ] as record
1502
+ RETURN record.left_id as left_id, record.right_id as right_id
1503
+ }
1504
+ """
1505
+ ).run()
1506
+ # Leftward chain: (c)<-[:KNOWS]-(b)<-[:KNOWS]-(a)
1507
+ match = Runner(
1508
+ """
1509
+ MATCH (c:DirDoublePerson)<-[:KNOWS]-(b:DirDoublePerson)<-[:KNOWS]-(a:DirDoublePerson)
1510
+ RETURN a.name AS name1, b.name AS name2, c.name AS name3
1511
+ """
1512
+ )
1513
+ await match.run()
1514
+ results = match.results
1515
+ assert len(results) == 2
1516
+ assert results[0] == {"name1": "Person 1", "name2": "Person 2", "name3": "Person 3"}
1517
+ assert results[1] == {"name1": "Person 2", "name2": "Person 3", "name3": "Person 4"}
1518
+
1519
+ async def test_match_with_constraints(self):
1520
+ await Runner(
1521
+ """
1522
+ CREATE VIRTUAL (:ConstraintEmployee) AS {
1523
+ unwind [
1524
+ {id: 1, name: 'Employee 1'},
1525
+ {id: 2, name: 'Employee 2'},
1526
+ {id: 3, name: 'Employee 3'},
1527
+ {id: 4, name: 'Employee 4'}
1528
+ ] as record
1529
+ RETURN record.id as id, record.name as name
1530
+ }
1531
+ """
1532
+ ).run()
1533
+ match = Runner(
1534
+ """
1535
+ match (e:ConstraintEmployee{name:'Employee 1'})
1536
+ return e.name as name
1537
+ """
1538
+ )
1539
+ await match.run()
1540
+ results = match.results
1541
+ assert len(results) == 1
1542
+ assert results[0]["name"] == "Employee 1"
@@ -5,6 +5,9 @@ from typing import AsyncIterator
5
5
  from flowquery.parsing.parser import Parser
6
6
  from flowquery.parsing.functions.async_function import AsyncFunction
7
7
  from flowquery.parsing.functions.function_metadata import FunctionDef
8
+ from flowquery.parsing.operations.match import Match
9
+ from flowquery.graph.node import Node
10
+ from flowquery.graph.relationship import Relationship
8
11
 
9
12
 
10
13
  # Test async function for CALL operation parsing test
@@ -678,3 +681,41 @@ class TestParser:
678
681
  parser = Parser()
679
682
  with pytest.raises(Exception, match="PatternExpression must contain at least one NodeReference"):
680
683
  parser.parse("MATCH (a:Person) WHERE (:Person)-[:KNOWS]->(:Person) RETURN a")
684
+
685
+ def test_node_with_properties(self):
686
+ """Test node with properties."""
687
+ parser = Parser()
688
+ ast = parser.parse("MATCH (a:Person{value: 'hello'}) return a")
689
+ expected = (
690
+ "ASTNode\n"
691
+ "- Match\n"
692
+ "- Return\n"
693
+ "-- Expression (a)\n"
694
+ "--- Reference (a)"
695
+ )
696
+ assert ast.print() == expected
697
+ match_op = ast.first_child()
698
+ assert isinstance(match_op, Match)
699
+ node = match_op.patterns[0].chain[0]
700
+ assert isinstance(node, Node)
701
+ assert node.properties.get("value") is not None
702
+ assert node.properties["value"].value() == "hello"
703
+
704
+ def test_relationship_with_properties(self):
705
+ """Test relationship with properties."""
706
+ parser = Parser()
707
+ ast = parser.parse("MATCH (:Person)-[r:LIKES{since: 2022}]->(:Food) return a")
708
+ expected = (
709
+ "ASTNode\n"
710
+ "- Match\n"
711
+ "- Return\n"
712
+ "-- Expression (a)\n"
713
+ "--- Reference (a)"
714
+ )
715
+ assert ast.print() == expected
716
+ match_op = ast.first_child()
717
+ assert isinstance(match_op, Match)
718
+ relationship = match_op.patterns[0].chain[1]
719
+ assert isinstance(relationship, Relationship)
720
+ assert relationship.properties.get("since") is not None
721
+ assert relationship.properties["since"].value() == 2022