flowquery 1.0.38 → 1.0.39
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/expressions/operator.js +4 -4
- package/dist/parsing/expressions/operator.js.map +1 -1
- package/dist/parsing/operations/aggregated_return.d.ts.map +1 -1
- package/dist/parsing/operations/aggregated_return.js +6 -2
- package/dist/parsing/operations/aggregated_return.js.map +1 -1
- package/dist/parsing/operations/limit.d.ts +1 -0
- package/dist/parsing/operations/limit.d.ts.map +1 -1
- package/dist/parsing/operations/limit.js +3 -0
- package/dist/parsing/operations/limit.js.map +1 -1
- package/dist/parsing/operations/order_by.d.ts +35 -0
- package/dist/parsing/operations/order_by.d.ts.map +1 -0
- package/dist/parsing/operations/order_by.js +87 -0
- package/dist/parsing/operations/order_by.js.map +1 -0
- 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 +16 -3
- package/dist/parsing/operations/return.js.map +1 -1
- package/dist/parsing/parser.d.ts +1 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +54 -0
- package/dist/parsing/parser.js.map +1 -1
- package/dist/tokenization/token.d.ts +8 -0
- package/dist/tokenization/token.d.ts.map +1 -1
- package/dist/tokenization/token.js +24 -0
- package/dist/tokenization/token.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/parsing/expressions/operator.py +4 -4
- package/flowquery-py/src/parsing/operations/__init__.py +3 -0
- package/flowquery-py/src/parsing/operations/aggregated_return.py +4 -1
- package/flowquery-py/src/parsing/operations/limit.py +4 -0
- package/flowquery-py/src/parsing/operations/order_by.py +72 -0
- package/flowquery-py/src/parsing/operations/return_op.py +20 -3
- package/flowquery-py/src/parsing/parser.py +44 -0
- package/flowquery-py/src/tokenization/token.py +28 -0
- package/flowquery-py/tests/compute/test_runner.py +159 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/parsing/expressions/operator.ts +4 -4
- package/src/parsing/operations/aggregated_return.ts +9 -5
- package/src/parsing/operations/limit.ts +3 -0
- package/src/parsing/operations/order_by.ts +75 -0
- package/src/parsing/operations/return.ts +17 -3
- package/src/parsing/parser.ts +52 -0
- package/src/tokenization/token.ts +32 -0
- package/tests/compute/runner.test.ts +144 -0
|
@@ -163,7 +163,7 @@ class Not(Operator):
|
|
|
163
163
|
|
|
164
164
|
class Is(Operator):
|
|
165
165
|
def __init__(self) -> None:
|
|
166
|
-
super().__init__(
|
|
166
|
+
super().__init__(0, True)
|
|
167
167
|
|
|
168
168
|
def value(self) -> int:
|
|
169
169
|
return 1 if self.lhs.value() == self.rhs.value() else 0
|
|
@@ -171,7 +171,7 @@ class Is(Operator):
|
|
|
171
171
|
|
|
172
172
|
class IsNot(Operator):
|
|
173
173
|
def __init__(self) -> None:
|
|
174
|
-
super().__init__(
|
|
174
|
+
super().__init__(0, True)
|
|
175
175
|
|
|
176
176
|
def value(self) -> int:
|
|
177
177
|
return 1 if self.lhs.value() != self.rhs.value() else 0
|
|
@@ -179,7 +179,7 @@ class IsNot(Operator):
|
|
|
179
179
|
|
|
180
180
|
class In(Operator):
|
|
181
181
|
def __init__(self) -> None:
|
|
182
|
-
super().__init__(
|
|
182
|
+
super().__init__(0, True)
|
|
183
183
|
|
|
184
184
|
def value(self) -> int:
|
|
185
185
|
lst = self.rhs.value()
|
|
@@ -190,7 +190,7 @@ class In(Operator):
|
|
|
190
190
|
|
|
191
191
|
class NotIn(Operator):
|
|
192
192
|
def __init__(self) -> None:
|
|
193
|
-
super().__init__(
|
|
193
|
+
super().__init__(0, True)
|
|
194
194
|
|
|
195
195
|
def value(self) -> int:
|
|
196
196
|
lst = self.rhs.value()
|
|
@@ -10,6 +10,7 @@ from .limit import Limit
|
|
|
10
10
|
from .load import Load
|
|
11
11
|
from .match import Match
|
|
12
12
|
from .operation import Operation
|
|
13
|
+
from .order_by import OrderBy, SortField
|
|
13
14
|
from .projection import Projection
|
|
14
15
|
from .return_op import Return
|
|
15
16
|
from .union import Union
|
|
@@ -36,4 +37,6 @@ __all__ = [
|
|
|
36
37
|
"CreateRelationship",
|
|
37
38
|
"Union",
|
|
38
39
|
"UnionAll",
|
|
40
|
+
"OrderBy",
|
|
41
|
+
"SortField",
|
|
39
42
|
]
|
|
@@ -19,4 +19,7 @@ class AggregatedReturn(Return):
|
|
|
19
19
|
def results(self) -> List[Dict[str, Any]]:
|
|
20
20
|
if self._where is not None:
|
|
21
21
|
self._group_by.where = self._where
|
|
22
|
-
|
|
22
|
+
results = list(self._group_by.generate_results())
|
|
23
|
+
if self._order_by is not None:
|
|
24
|
+
results = self._order_by.sort(results)
|
|
25
|
+
return results
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Represents an ORDER BY operation that sorts results."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from .operation import Operation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SortField:
|
|
9
|
+
"""A single sort specification: field name and direction."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, field: str, direction: str = "asc"):
|
|
12
|
+
self.field = field
|
|
13
|
+
self.direction = direction
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OrderBy(Operation):
|
|
17
|
+
"""Represents an ORDER BY operation that sorts results.
|
|
18
|
+
|
|
19
|
+
Can be attached to a RETURN operation (sorting its results),
|
|
20
|
+
or used as a standalone accumulating operation after a non-aggregate WITH.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
RETURN x ORDER BY x DESC
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, fields: List[SortField]):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self._fields = fields
|
|
29
|
+
self._results: List[Dict[str, Any]] = []
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def fields(self) -> List[SortField]:
|
|
33
|
+
return self._fields
|
|
34
|
+
|
|
35
|
+
def sort(self, records: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
36
|
+
"""Sorts an array of records according to the sort fields."""
|
|
37
|
+
import functools
|
|
38
|
+
|
|
39
|
+
def compare(a: Dict[str, Any], b: Dict[str, Any]) -> int:
|
|
40
|
+
for sf in self._fields:
|
|
41
|
+
a_val = a.get(sf.field)
|
|
42
|
+
b_val = b.get(sf.field)
|
|
43
|
+
cmp = 0
|
|
44
|
+
if a_val is None and b_val is None:
|
|
45
|
+
cmp = 0
|
|
46
|
+
elif a_val is None:
|
|
47
|
+
cmp = -1
|
|
48
|
+
elif b_val is None:
|
|
49
|
+
cmp = 1
|
|
50
|
+
elif a_val < b_val:
|
|
51
|
+
cmp = -1
|
|
52
|
+
elif a_val > b_val:
|
|
53
|
+
cmp = 1
|
|
54
|
+
if cmp != 0:
|
|
55
|
+
return -cmp if sf.direction == "desc" else cmp
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
return sorted(records, key=functools.cmp_to_key(compare))
|
|
59
|
+
|
|
60
|
+
async def run(self) -> None:
|
|
61
|
+
"""When used as a standalone operation, passes through to next."""
|
|
62
|
+
if self.next:
|
|
63
|
+
await self.next.run()
|
|
64
|
+
|
|
65
|
+
async def initialize(self) -> None:
|
|
66
|
+
self._results = []
|
|
67
|
+
if self.next:
|
|
68
|
+
await self.next.initialize()
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def results(self) -> List[Dict[str, Any]]:
|
|
72
|
+
return self._results
|
|
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
|
5
5
|
|
|
6
6
|
from ..ast_node import ASTNode
|
|
7
7
|
from .limit import Limit
|
|
8
|
+
from .order_by import OrderBy
|
|
8
9
|
from .projection import Projection
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -26,6 +27,7 @@ class Return(Projection):
|
|
|
26
27
|
self._where: Optional['Where'] = None
|
|
27
28
|
self._results: List[Dict[str, Any]] = []
|
|
28
29
|
self._limit: Optional[Limit] = None
|
|
30
|
+
self._order_by: Optional[OrderBy] = None
|
|
29
31
|
|
|
30
32
|
@property
|
|
31
33
|
def where(self) -> Any:
|
|
@@ -45,10 +47,20 @@ class Return(Projection):
|
|
|
45
47
|
def limit(self, limit: Limit) -> None:
|
|
46
48
|
self._limit = limit
|
|
47
49
|
|
|
50
|
+
@property
|
|
51
|
+
def order_by(self) -> Optional[OrderBy]:
|
|
52
|
+
return self._order_by
|
|
53
|
+
|
|
54
|
+
@order_by.setter
|
|
55
|
+
def order_by(self, order_by: OrderBy) -> None:
|
|
56
|
+
self._order_by = order_by
|
|
57
|
+
|
|
48
58
|
async def run(self) -> None:
|
|
49
59
|
if not self.where:
|
|
50
60
|
return
|
|
51
|
-
|
|
61
|
+
# When ORDER BY is present, skip limit during accumulation;
|
|
62
|
+
# limit will be applied after sorting in results property
|
|
63
|
+
if self._order_by is None and self._limit is not None and self._limit.is_limit_reached:
|
|
52
64
|
return
|
|
53
65
|
record: Dict[str, Any] = {}
|
|
54
66
|
for expression, alias in self.expressions():
|
|
@@ -57,7 +69,7 @@ class Return(Projection):
|
|
|
57
69
|
value = copy.deepcopy(raw) if isinstance(raw, (dict, list)) else raw
|
|
58
70
|
record[alias] = value
|
|
59
71
|
self._results.append(record)
|
|
60
|
-
if self._limit is not None:
|
|
72
|
+
if self._order_by is None and self._limit is not None:
|
|
61
73
|
self._limit.increment()
|
|
62
74
|
|
|
63
75
|
async def initialize(self) -> None:
|
|
@@ -65,4 +77,9 @@ class Return(Projection):
|
|
|
65
77
|
|
|
66
78
|
@property
|
|
67
79
|
def results(self) -> List[Dict[str, Any]]:
|
|
68
|
-
|
|
80
|
+
result = self._results
|
|
81
|
+
if self._order_by is not None:
|
|
82
|
+
result = self._order_by.sort(result)
|
|
83
|
+
if self._order_by is not None and self._limit is not None:
|
|
84
|
+
result = result[:self._limit.limit_value]
|
|
85
|
+
return result
|
|
@@ -61,6 +61,7 @@ from .operations.limit import Limit
|
|
|
61
61
|
from .operations.load import Load
|
|
62
62
|
from .operations.match import Match
|
|
63
63
|
from .operations.operation import Operation
|
|
64
|
+
from .operations.order_by import OrderBy, SortField
|
|
64
65
|
from .operations.return_op import Return
|
|
65
66
|
from .operations.union import Union
|
|
66
67
|
from .operations.union_all import UnionAll
|
|
@@ -146,6 +147,14 @@ class Parser(BaseParser):
|
|
|
146
147
|
operation.add_sibling(where)
|
|
147
148
|
operation = where
|
|
148
149
|
|
|
150
|
+
order_by = self._parse_order_by()
|
|
151
|
+
if order_by is not None:
|
|
152
|
+
if isinstance(operation, Return):
|
|
153
|
+
operation.order_by = order_by
|
|
154
|
+
else:
|
|
155
|
+
operation.add_sibling(order_by)
|
|
156
|
+
operation = order_by
|
|
157
|
+
|
|
149
158
|
limit = self._parse_limit()
|
|
150
159
|
if limit is not None:
|
|
151
160
|
if isinstance(operation, Return):
|
|
@@ -694,6 +703,41 @@ class Parser(BaseParser):
|
|
|
694
703
|
self.set_next_token()
|
|
695
704
|
return limit
|
|
696
705
|
|
|
706
|
+
def _parse_order_by(self) -> Optional[OrderBy]:
|
|
707
|
+
self._skip_whitespace_and_comments()
|
|
708
|
+
if not self.token.is_order():
|
|
709
|
+
return None
|
|
710
|
+
self._expect_previous_token_to_be_whitespace_or_comment()
|
|
711
|
+
self.set_next_token()
|
|
712
|
+
self._expect_and_skip_whitespace_and_comments()
|
|
713
|
+
if not self.token.is_by():
|
|
714
|
+
raise ValueError("Expected BY after ORDER")
|
|
715
|
+
self.set_next_token()
|
|
716
|
+
self._expect_and_skip_whitespace_and_comments()
|
|
717
|
+
fields: list[SortField] = []
|
|
718
|
+
while True:
|
|
719
|
+
if not self.token.is_identifier_or_keyword():
|
|
720
|
+
raise ValueError("Expected field name in ORDER BY")
|
|
721
|
+
field = self.token.value
|
|
722
|
+
self.set_next_token()
|
|
723
|
+
self._skip_whitespace_and_comments()
|
|
724
|
+
direction = "asc"
|
|
725
|
+
if self.token.is_asc():
|
|
726
|
+
direction = "asc"
|
|
727
|
+
self.set_next_token()
|
|
728
|
+
self._skip_whitespace_and_comments()
|
|
729
|
+
elif self.token.is_desc():
|
|
730
|
+
direction = "desc"
|
|
731
|
+
self.set_next_token()
|
|
732
|
+
self._skip_whitespace_and_comments()
|
|
733
|
+
fields.append(SortField(field, direction))
|
|
734
|
+
if self.token.is_comma():
|
|
735
|
+
self.set_next_token()
|
|
736
|
+
self._skip_whitespace_and_comments()
|
|
737
|
+
else:
|
|
738
|
+
break
|
|
739
|
+
return OrderBy(fields)
|
|
740
|
+
|
|
697
741
|
def _parse_expressions(
|
|
698
742
|
self, alias_option: AliasOption = AliasOption.NOT_ALLOWED
|
|
699
743
|
) -> Iterator[Expression]:
|
|
@@ -630,6 +630,34 @@ class Token:
|
|
|
630
630
|
def is_all(self) -> bool:
|
|
631
631
|
return self._type == TokenType.KEYWORD and self._value == Keyword.ALL.value
|
|
632
632
|
|
|
633
|
+
@staticmethod
|
|
634
|
+
def ORDER() -> Token:
|
|
635
|
+
return Token(TokenType.KEYWORD, Keyword.ORDER.value)
|
|
636
|
+
|
|
637
|
+
def is_order(self) -> bool:
|
|
638
|
+
return self._type == TokenType.KEYWORD and self._value == Keyword.ORDER.value
|
|
639
|
+
|
|
640
|
+
@staticmethod
|
|
641
|
+
def BY() -> Token:
|
|
642
|
+
return Token(TokenType.KEYWORD, Keyword.BY.value)
|
|
643
|
+
|
|
644
|
+
def is_by(self) -> bool:
|
|
645
|
+
return self._type == TokenType.KEYWORD and self._value == Keyword.BY.value
|
|
646
|
+
|
|
647
|
+
@staticmethod
|
|
648
|
+
def ASC() -> Token:
|
|
649
|
+
return Token(TokenType.KEYWORD, Keyword.ASC.value)
|
|
650
|
+
|
|
651
|
+
def is_asc(self) -> bool:
|
|
652
|
+
return self._type == TokenType.KEYWORD and self._value == Keyword.ASC.value
|
|
653
|
+
|
|
654
|
+
@staticmethod
|
|
655
|
+
def DESC() -> Token:
|
|
656
|
+
return Token(TokenType.KEYWORD, Keyword.DESC.value)
|
|
657
|
+
|
|
658
|
+
def is_desc(self) -> bool:
|
|
659
|
+
return self._type == TokenType.KEYWORD and self._value == Keyword.DESC.value
|
|
660
|
+
|
|
633
661
|
# End of file token
|
|
634
662
|
|
|
635
663
|
@staticmethod
|
|
@@ -2798,6 +2798,58 @@ class TestRunner:
|
|
|
2798
2798
|
assert len(results) == 3
|
|
2799
2799
|
assert [r["n"] for r in results] == [10, 15, 20]
|
|
2800
2800
|
|
|
2801
|
+
@pytest.mark.asyncio
|
|
2802
|
+
async def test_where_with_and_before_in(self):
|
|
2803
|
+
"""Test WHERE with AND before IN (IN on right side of AND)."""
|
|
2804
|
+
runner = Runner("""
|
|
2805
|
+
unwind ['expert', 'intermediate', 'beginner'] as proficiency
|
|
2806
|
+
with proficiency where 1=1 and proficiency in ['expert']
|
|
2807
|
+
return proficiency
|
|
2808
|
+
""")
|
|
2809
|
+
await runner.run()
|
|
2810
|
+
results = runner.results
|
|
2811
|
+
assert len(results) == 1
|
|
2812
|
+
assert results[0] == {"proficiency": "expert"}
|
|
2813
|
+
|
|
2814
|
+
@pytest.mark.asyncio
|
|
2815
|
+
async def test_where_with_and_before_not_in(self):
|
|
2816
|
+
"""Test WHERE with AND before NOT IN."""
|
|
2817
|
+
runner = Runner("""
|
|
2818
|
+
unwind ['expert', 'intermediate', 'beginner'] as proficiency
|
|
2819
|
+
with proficiency where 1=1 and proficiency not in ['expert']
|
|
2820
|
+
return proficiency
|
|
2821
|
+
""")
|
|
2822
|
+
await runner.run()
|
|
2823
|
+
results = runner.results
|
|
2824
|
+
assert len(results) == 2
|
|
2825
|
+
assert [r["proficiency"] for r in results] == ["intermediate", "beginner"]
|
|
2826
|
+
|
|
2827
|
+
@pytest.mark.asyncio
|
|
2828
|
+
async def test_where_with_or_before_in(self):
|
|
2829
|
+
"""Test WHERE with OR before IN."""
|
|
2830
|
+
runner = Runner("""
|
|
2831
|
+
unwind range(1, 10) as n
|
|
2832
|
+
with n where 1=0 or n in [3, 7]
|
|
2833
|
+
return n
|
|
2834
|
+
""")
|
|
2835
|
+
await runner.run()
|
|
2836
|
+
results = runner.results
|
|
2837
|
+
assert len(results) == 2
|
|
2838
|
+
assert [r["n"] for r in results] == [3, 7]
|
|
2839
|
+
|
|
2840
|
+
@pytest.mark.asyncio
|
|
2841
|
+
async def test_in_as_return_expression_with_and_in_where(self):
|
|
2842
|
+
"""Test IN as return expression with AND in WHERE."""
|
|
2843
|
+
runner = Runner("""
|
|
2844
|
+
unwind ['expert', 'intermediate', 'beginner'] as proficiency
|
|
2845
|
+
with proficiency where 1=1 and proficiency in ['expert']
|
|
2846
|
+
return proficiency, proficiency in ['expert'] as isExpert
|
|
2847
|
+
""")
|
|
2848
|
+
await runner.run()
|
|
2849
|
+
results = runner.results
|
|
2850
|
+
assert len(results) == 1
|
|
2851
|
+
assert results[0] == {"proficiency": "expert", "isExpert": 1}
|
|
2852
|
+
|
|
2801
2853
|
@pytest.mark.asyncio
|
|
2802
2854
|
async def test_where_with_contains(self):
|
|
2803
2855
|
"""Test WHERE with CONTAINS."""
|
|
@@ -4011,4 +4063,110 @@ class TestRunner:
|
|
|
4011
4063
|
assert d["hours"] == 2
|
|
4012
4064
|
assert d["minutes"] == 30
|
|
4013
4065
|
assert d["totalSeconds"] == 9000
|
|
4014
|
-
assert d["formatted"] == "PT2H30M"
|
|
4066
|
+
assert d["formatted"] == "PT2H30M"
|
|
4067
|
+
|
|
4068
|
+
# ORDER BY tests
|
|
4069
|
+
|
|
4070
|
+
@pytest.mark.asyncio
|
|
4071
|
+
async def test_order_by_ascending(self):
|
|
4072
|
+
"""Test ORDER BY ascending (default)."""
|
|
4073
|
+
runner = Runner("unwind [3, 1, 2] as x return x order by x")
|
|
4074
|
+
await runner.run()
|
|
4075
|
+
results = runner.results
|
|
4076
|
+
assert len(results) == 3
|
|
4077
|
+
assert results[0] == {"x": 1}
|
|
4078
|
+
assert results[1] == {"x": 2}
|
|
4079
|
+
assert results[2] == {"x": 3}
|
|
4080
|
+
|
|
4081
|
+
@pytest.mark.asyncio
|
|
4082
|
+
async def test_order_by_descending(self):
|
|
4083
|
+
"""Test ORDER BY descending."""
|
|
4084
|
+
runner = Runner("unwind [3, 1, 2] as x return x order by x desc")
|
|
4085
|
+
await runner.run()
|
|
4086
|
+
results = runner.results
|
|
4087
|
+
assert len(results) == 3
|
|
4088
|
+
assert results[0] == {"x": 3}
|
|
4089
|
+
assert results[1] == {"x": 2}
|
|
4090
|
+
assert results[2] == {"x": 1}
|
|
4091
|
+
|
|
4092
|
+
@pytest.mark.asyncio
|
|
4093
|
+
async def test_order_by_ascending_explicit(self):
|
|
4094
|
+
"""Test ORDER BY with explicit ASC."""
|
|
4095
|
+
runner = Runner("unwind [3, 1, 2] as x return x order by x asc")
|
|
4096
|
+
await runner.run()
|
|
4097
|
+
results = runner.results
|
|
4098
|
+
assert len(results) == 3
|
|
4099
|
+
assert results[0] == {"x": 1}
|
|
4100
|
+
assert results[1] == {"x": 2}
|
|
4101
|
+
assert results[2] == {"x": 3}
|
|
4102
|
+
|
|
4103
|
+
@pytest.mark.asyncio
|
|
4104
|
+
async def test_order_by_with_multiple_fields(self):
|
|
4105
|
+
"""Test ORDER BY with multiple sort fields."""
|
|
4106
|
+
runner = Runner(
|
|
4107
|
+
"unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: 25}, {name: 'Alice', age: 25}] as person "
|
|
4108
|
+
"return person.name as name, person.age as age "
|
|
4109
|
+
"order by name asc, age asc"
|
|
4110
|
+
)
|
|
4111
|
+
await runner.run()
|
|
4112
|
+
results = runner.results
|
|
4113
|
+
assert len(results) == 3
|
|
4114
|
+
assert results[0] == {"name": "Alice", "age": 25}
|
|
4115
|
+
assert results[1] == {"name": "Alice", "age": 30}
|
|
4116
|
+
assert results[2] == {"name": "Bob", "age": 25}
|
|
4117
|
+
|
|
4118
|
+
@pytest.mark.asyncio
|
|
4119
|
+
async def test_order_by_with_strings(self):
|
|
4120
|
+
"""Test ORDER BY with string values."""
|
|
4121
|
+
runner = Runner(
|
|
4122
|
+
"unwind ['banana', 'apple', 'cherry'] as fruit return fruit order by fruit"
|
|
4123
|
+
)
|
|
4124
|
+
await runner.run()
|
|
4125
|
+
results = runner.results
|
|
4126
|
+
assert len(results) == 3
|
|
4127
|
+
assert results[0] == {"fruit": "apple"}
|
|
4128
|
+
assert results[1] == {"fruit": "banana"}
|
|
4129
|
+
assert results[2] == {"fruit": "cherry"}
|
|
4130
|
+
|
|
4131
|
+
@pytest.mark.asyncio
|
|
4132
|
+
async def test_order_by_with_aggregated_return(self):
|
|
4133
|
+
"""Test ORDER BY with aggregated RETURN."""
|
|
4134
|
+
runner = Runner(
|
|
4135
|
+
"unwind [1, 1, 2, 2, 3, 3] as x "
|
|
4136
|
+
"return x, count(x) as cnt "
|
|
4137
|
+
"order by x desc"
|
|
4138
|
+
)
|
|
4139
|
+
await runner.run()
|
|
4140
|
+
results = runner.results
|
|
4141
|
+
assert len(results) == 3
|
|
4142
|
+
assert results[0] == {"x": 3, "cnt": 2}
|
|
4143
|
+
assert results[1] == {"x": 2, "cnt": 2}
|
|
4144
|
+
assert results[2] == {"x": 1, "cnt": 2}
|
|
4145
|
+
|
|
4146
|
+
@pytest.mark.asyncio
|
|
4147
|
+
async def test_order_by_with_limit(self):
|
|
4148
|
+
"""Test ORDER BY combined with LIMIT."""
|
|
4149
|
+
runner = Runner(
|
|
4150
|
+
"unwind [3, 1, 4, 1, 5, 9, 2, 6] as x return x order by x limit 3"
|
|
4151
|
+
)
|
|
4152
|
+
await runner.run()
|
|
4153
|
+
results = runner.results
|
|
4154
|
+
assert len(results) == 3
|
|
4155
|
+
assert results[0] == {"x": 1}
|
|
4156
|
+
assert results[1] == {"x": 1}
|
|
4157
|
+
assert results[2] == {"x": 2}
|
|
4158
|
+
|
|
4159
|
+
@pytest.mark.asyncio
|
|
4160
|
+
async def test_order_by_with_where(self):
|
|
4161
|
+
"""Test ORDER BY combined with WHERE."""
|
|
4162
|
+
runner = Runner(
|
|
4163
|
+
"unwind [3, 1, 4, 1, 5, 9, 2, 6] as x return x where x > 2 order by x desc"
|
|
4164
|
+
)
|
|
4165
|
+
await runner.run()
|
|
4166
|
+
results = runner.results
|
|
4167
|
+
assert len(results) == 5
|
|
4168
|
+
assert results[0] == {"x": 9}
|
|
4169
|
+
assert results[1] == {"x": 6}
|
|
4170
|
+
assert results[2] == {"x": 5}
|
|
4171
|
+
assert results[3] == {"x": 4}
|
|
4172
|
+
assert results[4] == {"x": 3}
|