flowquery 1.0.36 → 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/functions/coalesce.d.ts +0 -1
- package/dist/parsing/functions/coalesce.d.ts.map +1 -1
- package/dist/parsing/functions/coalesce.js +0 -1
- package/dist/parsing/functions/coalesce.js.map +1 -1
- package/dist/parsing/functions/date.d.ts +0 -2
- package/dist/parsing/functions/date.d.ts.map +1 -1
- package/dist/parsing/functions/date.js +0 -2
- package/dist/parsing/functions/date.js.map +1 -1
- package/dist/parsing/functions/datetime.d.ts +0 -2
- package/dist/parsing/functions/datetime.d.ts.map +1 -1
- package/dist/parsing/functions/datetime.js +0 -2
- package/dist/parsing/functions/datetime.js.map +1 -1
- package/dist/parsing/functions/localdatetime.d.ts +0 -2
- package/dist/parsing/functions/localdatetime.d.ts.map +1 -1
- package/dist/parsing/functions/localdatetime.js +0 -2
- package/dist/parsing/functions/localdatetime.js.map +1 -1
- package/dist/parsing/functions/localtime.d.ts +0 -2
- package/dist/parsing/functions/localtime.d.ts.map +1 -1
- package/dist/parsing/functions/localtime.js +0 -2
- package/dist/parsing/functions/localtime.js.map +1 -1
- package/dist/parsing/functions/temporal_utils.js +1 -1
- package/dist/parsing/functions/time.d.ts +0 -2
- package/dist/parsing/functions/time.d.ts.map +1 -1
- package/dist/parsing/functions/time.js +0 -2
- package/dist/parsing/functions/time.js.map +1 -1
- package/dist/parsing/functions/timestamp.d.ts +0 -2
- package/dist/parsing/functions/timestamp.d.ts.map +1 -1
- package/dist/parsing/functions/timestamp.js +1 -4
- package/dist/parsing/functions/timestamp.js.map +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/functions/coalesce.py +1 -2
- package/flowquery-py/src/parsing/functions/date_.py +0 -2
- package/flowquery-py/src/parsing/functions/datetime_.py +0 -2
- package/flowquery-py/src/parsing/functions/localdatetime.py +0 -2
- package/flowquery-py/src/parsing/functions/localtime.py +0 -2
- package/flowquery-py/src/parsing/functions/temporal_utils.py +1 -1
- package/flowquery-py/src/parsing/functions/time_.py +0 -2
- package/flowquery-py/src/parsing/functions/timestamp.py +1 -3
- 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 +81 -2
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/parsing/functions/coalesce.ts +0 -1
- package/src/parsing/functions/date.ts +0 -2
- package/src/parsing/functions/datetime.ts +0 -2
- package/src/parsing/functions/localdatetime.ts +0 -2
- package/src/parsing/functions/localtime.ts +0 -2
- package/src/parsing/functions/temporal_utils.ts +1 -1
- package/src/parsing/functions/time.ts +0 -2
- package/src/parsing/functions/timestamp.ts +1 -5
- 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 +69 -2
|
@@ -22,7 +22,6 @@ class Coalesce(Function):
|
|
|
22
22
|
"""Coalesce function.
|
|
23
23
|
|
|
24
24
|
Returns the first non-null value from a list of expressions.
|
|
25
|
-
Equivalent to Neo4j's coalesce() function.
|
|
26
25
|
"""
|
|
27
26
|
|
|
28
27
|
def __init__(self) -> None:
|
|
@@ -37,7 +36,7 @@ class Coalesce(Function):
|
|
|
37
36
|
try:
|
|
38
37
|
val = child.value()
|
|
39
38
|
except (KeyError, AttributeError):
|
|
40
|
-
# Treat missing properties/keys as null
|
|
39
|
+
# Treat missing properties/keys as null
|
|
41
40
|
val = None
|
|
42
41
|
if val is not None:
|
|
43
42
|
return val
|
|
@@ -42,8 +42,6 @@ class DateFunction(Function):
|
|
|
42
42
|
Returns a date value (no time component).
|
|
43
43
|
When called with no arguments, returns the current date.
|
|
44
44
|
When called with a string argument, parses it as an ISO 8601 date.
|
|
45
|
-
|
|
46
|
-
Equivalent to Neo4j's date() function.
|
|
47
45
|
"""
|
|
48
46
|
|
|
49
47
|
def __init__(self) -> None:
|
|
@@ -43,8 +43,6 @@ class Datetime(Function):
|
|
|
43
43
|
When called with no arguments, returns the current UTC datetime.
|
|
44
44
|
When called with a string argument, parses it as an ISO 8601 datetime.
|
|
45
45
|
When called with a map argument, constructs a datetime from components.
|
|
46
|
-
|
|
47
|
-
Equivalent to Neo4j's datetime() function.
|
|
48
46
|
"""
|
|
49
47
|
|
|
50
48
|
def __init__(self) -> None:
|
|
@@ -41,8 +41,6 @@ class LocalDatetime(Function):
|
|
|
41
41
|
Returns a local datetime value (date + time, no timezone offset).
|
|
42
42
|
When called with no arguments, returns the current local datetime.
|
|
43
43
|
When called with a string argument, parses it as an ISO 8601 datetime.
|
|
44
|
-
|
|
45
|
-
Equivalent to Neo4j's localdatetime() function.
|
|
46
44
|
"""
|
|
47
45
|
|
|
48
46
|
def __init__(self) -> None:
|
|
@@ -38,8 +38,6 @@ class LocalTime(Function):
|
|
|
38
38
|
Returns a local time value (no timezone offset).
|
|
39
39
|
When called with no arguments, returns the current local time.
|
|
40
40
|
When called with a string argument, parses it.
|
|
41
|
-
|
|
42
|
-
Equivalent to Neo4j's localtime() function.
|
|
43
41
|
"""
|
|
44
42
|
|
|
45
43
|
def __init__(self) -> None:
|
|
@@ -9,7 +9,7 @@ from typing import Any, Dict
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def iso_day_of_week(d: date) -> int:
|
|
12
|
-
"""Computes the ISO day of the week (1 = Monday, 7 = Sunday)
|
|
12
|
+
"""Computes the ISO day of the week (1 = Monday, 7 = Sunday)."""
|
|
13
13
|
return d.isoweekday()
|
|
14
14
|
|
|
15
15
|
|
|
@@ -38,8 +38,6 @@ class Time(Function):
|
|
|
38
38
|
Returns a time value (with timezone offset awareness).
|
|
39
39
|
When called with no arguments, returns the current UTC time.
|
|
40
40
|
When called with a string argument, parses it.
|
|
41
|
-
|
|
42
|
-
Equivalent to Neo4j's time() function.
|
|
43
41
|
"""
|
|
44
42
|
|
|
45
43
|
def __init__(self) -> None:
|
|
@@ -9,8 +9,7 @@ from .function_metadata import FunctionDef
|
|
|
9
9
|
|
|
10
10
|
@FunctionDef({
|
|
11
11
|
"description": (
|
|
12
|
-
"Returns the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
|
|
13
|
-
"Equivalent to Neo4j's timestamp() function."
|
|
12
|
+
"Returns the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z)."
|
|
14
13
|
),
|
|
15
14
|
"category": "scalar",
|
|
16
15
|
"parameters": [],
|
|
@@ -28,7 +27,6 @@ class Timestamp(Function):
|
|
|
28
27
|
"""Timestamp function.
|
|
29
28
|
|
|
30
29
|
Returns the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
|
|
31
|
-
Equivalent to Neo4j's timestamp() function.
|
|
32
30
|
"""
|
|
33
31
|
|
|
34
32
|
def __init__(self) -> None:
|
|
@@ -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."""
|
|
@@ -2232,7 +2311,7 @@ class TestRunner:
|
|
|
2232
2311
|
}
|
|
2233
2312
|
"""
|
|
2234
2313
|
).run()
|
|
2235
|
-
# When accessing b.name and b is null (no match), should return null
|
|
2314
|
+
# When accessing b.name and b is null (no match), should return null
|
|
2236
2315
|
match = Runner(
|
|
2237
2316
|
"""
|
|
2238
2317
|
MATCH (a:OptPropPerson)
|
|
@@ -3427,7 +3506,7 @@ class TestRunner:
|
|
|
3427
3506
|
assert results[0] == {"result": "Alice"}
|
|
3428
3507
|
|
|
3429
3508
|
# ============================================================
|
|
3430
|
-
# Temporal / Time Functions
|
|
3509
|
+
# Temporal / Time Functions
|
|
3431
3510
|
# ============================================================
|
|
3432
3511
|
|
|
3433
3512
|
@pytest.mark.asyncio
|