flowquery 1.0.37 → 1.0.38
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.
- package/dist/flowquery.min.js +1 -1
- package/dist/parsing/operations/group_by.d.ts.map +1 -1
- package/dist/parsing/operations/group_by.js +3 -2
- package/dist/parsing/operations/group_by.js.map +1 -1
- package/dist/parsing/operations/limit.d.ts +2 -0
- package/dist/parsing/operations/limit.d.ts.map +1 -1
- package/dist/parsing/operations/limit.js +6 -0
- package/dist/parsing/operations/limit.js.map +1 -1
- package/dist/parsing/operations/return.d.ts +3 -0
- package/dist/parsing/operations/return.d.ts.map +1 -1
- package/dist/parsing/operations/return.js +10 -0
- package/dist/parsing/operations/return.js.map +1 -1
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +13 -10
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/parsing/operations/limit.py +7 -0
- package/flowquery-py/src/parsing/operations/return_op.py +14 -0
- package/flowquery-py/src/parsing/parser.py +13 -9
- package/flowquery-py/tests/compute/test_runner.py +79 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/parsing/operations/group_by.ts +4 -2
- package/src/parsing/operations/limit.ts +7 -1
- package/src/parsing/operations/return.ts +11 -0
- package/src/parsing/parser.ts +12 -10
- package/tests/compute/runner.test.ts +67 -0
|
@@ -11,6 +11,13 @@ class Limit(Operation):
|
|
|
11
11
|
self._count = 0
|
|
12
12
|
self._limit = limit
|
|
13
13
|
|
|
14
|
+
@property
|
|
15
|
+
def is_limit_reached(self) -> bool:
|
|
16
|
+
return self._count >= self._limit
|
|
17
|
+
|
|
18
|
+
def increment(self) -> None:
|
|
19
|
+
self._count += 1
|
|
20
|
+
|
|
14
21
|
async def run(self) -> None:
|
|
15
22
|
if self._count >= self._limit:
|
|
16
23
|
return
|
|
@@ -4,6 +4,7 @@ import copy
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
5
5
|
|
|
6
6
|
from ..ast_node import ASTNode
|
|
7
|
+
from .limit import Limit
|
|
7
8
|
from .projection import Projection
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
@@ -24,6 +25,7 @@ class Return(Projection):
|
|
|
24
25
|
super().__init__(expressions)
|
|
25
26
|
self._where: Optional['Where'] = None
|
|
26
27
|
self._results: List[Dict[str, Any]] = []
|
|
28
|
+
self._limit: Optional[Limit] = None
|
|
27
29
|
|
|
28
30
|
@property
|
|
29
31
|
def where(self) -> Any:
|
|
@@ -35,9 +37,19 @@ class Return(Projection):
|
|
|
35
37
|
def where(self, where: 'Where') -> None:
|
|
36
38
|
self._where = where
|
|
37
39
|
|
|
40
|
+
@property
|
|
41
|
+
def limit(self) -> Optional[Limit]:
|
|
42
|
+
return self._limit
|
|
43
|
+
|
|
44
|
+
@limit.setter
|
|
45
|
+
def limit(self, limit: Limit) -> None:
|
|
46
|
+
self._limit = limit
|
|
47
|
+
|
|
38
48
|
async def run(self) -> None:
|
|
39
49
|
if not self.where:
|
|
40
50
|
return
|
|
51
|
+
if self._limit is not None and self._limit.is_limit_reached:
|
|
52
|
+
return
|
|
41
53
|
record: Dict[str, Any] = {}
|
|
42
54
|
for expression, alias in self.expressions():
|
|
43
55
|
raw = expression.value()
|
|
@@ -45,6 +57,8 @@ class Return(Projection):
|
|
|
45
57
|
value = copy.deepcopy(raw) if isinstance(raw, (dict, list)) else raw
|
|
46
58
|
record[alias] = value
|
|
47
59
|
self._results.append(record)
|
|
60
|
+
if self._limit is not None:
|
|
61
|
+
self._limit.increment()
|
|
48
62
|
|
|
49
63
|
async def initialize(self) -> None:
|
|
50
64
|
self._results = []
|
|
@@ -116,6 +116,9 @@ class Parser(BaseParser):
|
|
|
116
116
|
if self.token.is_union():
|
|
117
117
|
break
|
|
118
118
|
|
|
119
|
+
if self.token.is_eof():
|
|
120
|
+
break
|
|
121
|
+
|
|
119
122
|
operation = self._parse_operation()
|
|
120
123
|
if operation is None and not is_sub_query:
|
|
121
124
|
raise ValueError("Expected one of WITH, UNWIND, RETURN, LOAD, OR CALL")
|
|
@@ -145,8 +148,11 @@ class Parser(BaseParser):
|
|
|
145
148
|
|
|
146
149
|
limit = self._parse_limit()
|
|
147
150
|
if limit is not None:
|
|
148
|
-
operation
|
|
149
|
-
|
|
151
|
+
if isinstance(operation, Return):
|
|
152
|
+
operation.limit = limit
|
|
153
|
+
else:
|
|
154
|
+
operation.add_sibling(limit)
|
|
155
|
+
operation = limit
|
|
150
156
|
|
|
151
157
|
previous = operation
|
|
152
158
|
|
|
@@ -539,13 +545,11 @@ class Parser(BaseParser):
|
|
|
539
545
|
node.properties = dict(self._parse_properties())
|
|
540
546
|
if identifier is not None and identifier in self._state.variables:
|
|
541
547
|
reference = self._state.variables.get(identifier)
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
reference = inner
|
|
548
|
-
if reference is None or (not isinstance(reference, Node) and not isinstance(reference, Unwind)):
|
|
548
|
+
if reference is None or (
|
|
549
|
+
not isinstance(reference, Node)
|
|
550
|
+
and not isinstance(reference, Unwind)
|
|
551
|
+
and not isinstance(reference, Expression)
|
|
552
|
+
):
|
|
549
553
|
raise ValueError(f"Undefined node reference: {identifier}")
|
|
550
554
|
node = NodeReference(node, reference)
|
|
551
555
|
elif identifier is not None:
|
|
@@ -925,6 +925,20 @@ class TestRunner:
|
|
|
925
925
|
results = runner.results
|
|
926
926
|
assert len(results) == 50
|
|
927
927
|
|
|
928
|
+
@pytest.mark.asyncio
|
|
929
|
+
async def test_limit_as_last_operation(self):
|
|
930
|
+
"""Test limit as the last operation after return."""
|
|
931
|
+
runner = Runner(
|
|
932
|
+
"""
|
|
933
|
+
unwind range(1, 10) as i
|
|
934
|
+
return i
|
|
935
|
+
limit 5
|
|
936
|
+
"""
|
|
937
|
+
)
|
|
938
|
+
await runner.run()
|
|
939
|
+
results = runner.results
|
|
940
|
+
assert len(results) == 5
|
|
941
|
+
|
|
928
942
|
@pytest.mark.asyncio
|
|
929
943
|
async def test_range_lookup(self):
|
|
930
944
|
"""Test range lookup."""
|
|
@@ -1457,6 +1471,71 @@ class TestRunner:
|
|
|
1457
1471
|
assert results[0] == {"name1": "Person 1", "name2": "Person 2", "name3": "Person 3"}
|
|
1458
1472
|
assert results[1] == {"name1": "Person 2", "name2": "Person 3", "name3": "Person 4"}
|
|
1459
1473
|
|
|
1474
|
+
@pytest.mark.asyncio
|
|
1475
|
+
async def test_match_with_aggregated_with_and_subsequent_match(self):
|
|
1476
|
+
"""Test match with aggregated WITH followed by another match using the same node reference."""
|
|
1477
|
+
await Runner(
|
|
1478
|
+
"""
|
|
1479
|
+
CREATE VIRTUAL (:AggUser) AS {
|
|
1480
|
+
unwind [
|
|
1481
|
+
{id: 1, name: 'Alice'},
|
|
1482
|
+
{id: 2, name: 'Bob'},
|
|
1483
|
+
{id: 3, name: 'Carol'}
|
|
1484
|
+
] as record
|
|
1485
|
+
RETURN record.id as id, record.name as name
|
|
1486
|
+
}
|
|
1487
|
+
"""
|
|
1488
|
+
).run()
|
|
1489
|
+
await Runner(
|
|
1490
|
+
"""
|
|
1491
|
+
CREATE VIRTUAL (:AggUser)-[:KNOWS]-(:AggUser) AS {
|
|
1492
|
+
unwind [
|
|
1493
|
+
{left_id: 1, right_id: 2},
|
|
1494
|
+
{left_id: 1, right_id: 3}
|
|
1495
|
+
] as record
|
|
1496
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1497
|
+
}
|
|
1498
|
+
"""
|
|
1499
|
+
).run()
|
|
1500
|
+
await Runner(
|
|
1501
|
+
"""
|
|
1502
|
+
CREATE VIRTUAL (:AggProject) AS {
|
|
1503
|
+
unwind [
|
|
1504
|
+
{id: 1, name: 'Project A'},
|
|
1505
|
+
{id: 2, name: 'Project B'}
|
|
1506
|
+
] as record
|
|
1507
|
+
RETURN record.id as id, record.name as name
|
|
1508
|
+
}
|
|
1509
|
+
"""
|
|
1510
|
+
).run()
|
|
1511
|
+
await Runner(
|
|
1512
|
+
"""
|
|
1513
|
+
CREATE VIRTUAL (:AggUser)-[:WORKS_ON]-(:AggProject) AS {
|
|
1514
|
+
unwind [
|
|
1515
|
+
{left_id: 1, right_id: 1},
|
|
1516
|
+
{left_id: 1, right_id: 2}
|
|
1517
|
+
] as record
|
|
1518
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1519
|
+
}
|
|
1520
|
+
"""
|
|
1521
|
+
).run()
|
|
1522
|
+
match = Runner(
|
|
1523
|
+
"""
|
|
1524
|
+
MATCH (u:AggUser)-[:KNOWS]->(s:AggUser)
|
|
1525
|
+
WITH u, count(s) as acquaintances
|
|
1526
|
+
MATCH (u)-[:WORKS_ON]->(p:AggProject)
|
|
1527
|
+
RETURN u.name as name, acquaintances, collect(p.name) as projects
|
|
1528
|
+
"""
|
|
1529
|
+
)
|
|
1530
|
+
await match.run()
|
|
1531
|
+
results = match.results
|
|
1532
|
+
assert len(results) == 1
|
|
1533
|
+
assert results[0] == {
|
|
1534
|
+
"name": "Alice",
|
|
1535
|
+
"acquaintances": 2,
|
|
1536
|
+
"projects": ["Project A", "Project B"],
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1460
1539
|
@pytest.mark.asyncio
|
|
1461
1540
|
async def test_match_and_return_full_node(self):
|
|
1462
1541
|
"""Test match and return full node."""
|