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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowquery"
3
- version = "1.0.28"
3
+ version = "1.0.30"
4
4
  description = "A declarative query language for data processing pipelines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -37,6 +37,12 @@ class Database:
37
37
  physical.statement = statement
38
38
  Database._nodes[node.label] = physical
39
39
 
40
+ def remove_node(self, node: 'Node') -> None:
41
+ """Removes a node from the database."""
42
+ if node.label is None:
43
+ raise ValueError("Node label is null")
44
+ Database._nodes.pop(node.label, None)
45
+
40
46
  def get_node(self, node: 'Node') -> Optional['PhysicalNode']:
41
47
  """Gets a node from the database."""
42
48
  return Database._nodes.get(node.label) if node.label else None
@@ -54,6 +60,12 @@ class Database:
54
60
  physical.target = relationship.target
55
61
  Database._relationships[relationship.type] = physical
56
62
 
63
+ def remove_relationship(self, relationship: 'Relationship') -> None:
64
+ """Removes a relationship from the database."""
65
+ if relationship.type is None:
66
+ raise ValueError("Relationship type is null")
67
+ Database._relationships.pop(relationship.type, None)
68
+
57
69
  def get_relationship(self, relationship: 'Relationship') -> Optional['PhysicalRelationship']:
58
70
  """Gets a relationship from the database."""
59
71
  return Database._relationships.get(relationship.type) if relationship.type else None
@@ -163,7 +163,7 @@ class Not(Operator):
163
163
 
164
164
  class Is(Operator):
165
165
  def __init__(self) -> None:
166
- super().__init__(-1, True)
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__(-1, True)
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__(-1, True)
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__(-1, True)
193
+ super().__init__(0, True)
194
194
 
195
195
  def value(self) -> int:
196
196
  lst = self.rhs.value()
@@ -48,6 +48,7 @@ from .size import Size
48
48
  from .split import Split
49
49
  from .string_distance import StringDistance
50
50
  from .stringify import Stringify
51
+ from .substring import Substring
51
52
  from .sum import Sum
52
53
  from .tail import Tail
53
54
  from .time_ import Time
@@ -107,6 +108,7 @@ __all__ = [
107
108
  "Split",
108
109
  "StringDistance",
109
110
  "Stringify",
111
+ "Substring",
110
112
  "Tail",
111
113
  "Time",
112
114
  "Timestamp",
@@ -0,0 +1,74 @@
1
+ """Substring function."""
2
+
3
+ from typing import Any, List
4
+
5
+ from ..ast_node import ASTNode
6
+ from .function import Function
7
+ from .function_metadata import FunctionDef
8
+
9
+
10
+ @FunctionDef({
11
+ "description": "Returns a substring of a string, starting at a 0-based index with an optional length",
12
+ "category": "scalar",
13
+ "parameters": [
14
+ {"name": "original", "description": "The original string", "type": "string"},
15
+ {"name": "start", "description": "The 0-based start index", "type": "integer"},
16
+ {
17
+ "name": "length",
18
+ "description": "The length of the substring (optional)",
19
+ "type": "integer",
20
+ }
21
+ ],
22
+ "output": {"description": "The substring", "type": "string", "example": "llo"},
23
+ "examples": [
24
+ "RETURN substring('hello', 1, 3)",
25
+ "RETURN substring('hello', 2)"
26
+ ]
27
+ })
28
+ class Substring(Function):
29
+ """Substring function.
30
+
31
+ Returns a substring of a string, starting at a 0-based index with an optional length.
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ super().__init__("substring")
36
+
37
+ @property
38
+ def parameters(self) -> List[ASTNode]:
39
+ return self.get_children()
40
+
41
+ @parameters.setter
42
+ def parameters(self, nodes: List[ASTNode]) -> None:
43
+ if len(nodes) < 2 or len(nodes) > 3:
44
+ raise ValueError(
45
+ f"Function substring expected 2 or 3 parameters, but got {len(nodes)}"
46
+ )
47
+ for node in nodes:
48
+ self.add_child(node)
49
+
50
+ def value(self) -> Any:
51
+ children = self.get_children()
52
+ original = children[0].value()
53
+ start = children[1].value()
54
+
55
+ if not isinstance(original, str):
56
+ raise ValueError(
57
+ "Invalid argument for substring function: expected a string as the first argument"
58
+ )
59
+ if not isinstance(start, (int, float)) or (isinstance(start, float) and not start.is_integer()):
60
+ raise ValueError(
61
+ "Invalid argument for substring function: expected an integer as the second argument"
62
+ )
63
+ start = int(start)
64
+
65
+ if len(children) == 3:
66
+ length = children[2].value()
67
+ if not isinstance(length, (int, float)) or (isinstance(length, float) and not length.is_integer()):
68
+ raise ValueError(
69
+ "Invalid argument for substring function: expected an integer as the third argument"
70
+ )
71
+ length = int(length)
72
+ return original[start:start + length]
73
+
74
+ return original[start:]
@@ -5,11 +5,14 @@ from .aggregated_with import AggregatedWith
5
5
  from .call import Call
6
6
  from .create_node import CreateNode
7
7
  from .create_relationship import CreateRelationship
8
+ from .delete_node import DeleteNode
9
+ from .delete_relationship import DeleteRelationship
8
10
  from .group_by import GroupBy
9
11
  from .limit import Limit
10
12
  from .load import Load
11
13
  from .match import Match
12
14
  from .operation import Operation
15
+ from .order_by import OrderBy, SortField
13
16
  from .projection import Projection
14
17
  from .return_op import Return
15
18
  from .union import Union
@@ -34,6 +37,10 @@ __all__ = [
34
37
  "Match",
35
38
  "CreateNode",
36
39
  "CreateRelationship",
40
+ "DeleteNode",
41
+ "DeleteRelationship",
37
42
  "Union",
38
43
  "UnionAll",
44
+ "OrderBy",
45
+ "SortField",
39
46
  ]
@@ -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
- return list(self._group_by.generate_results())
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,29 @@
1
+ """Represents a DELETE operation for deleting virtual nodes."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from ...graph.database import Database
6
+ from ...graph.node import Node
7
+ from .operation import Operation
8
+
9
+
10
+ class DeleteNode(Operation):
11
+ """Represents a DELETE operation for deleting virtual nodes."""
12
+
13
+ def __init__(self, node: Node) -> None:
14
+ super().__init__()
15
+ self._node = node
16
+
17
+ @property
18
+ def node(self) -> Node:
19
+ return self._node
20
+
21
+ async def run(self) -> None:
22
+ if self._node is None:
23
+ raise ValueError("Node is null")
24
+ db = Database.get_instance()
25
+ db.remove_node(self._node)
26
+
27
+ @property
28
+ def results(self) -> List[Dict[str, Any]]:
29
+ return []
@@ -0,0 +1,29 @@
1
+ """Represents a DELETE operation for deleting virtual relationships."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from ...graph.database import Database
6
+ from ...graph.relationship import Relationship
7
+ from .operation import Operation
8
+
9
+
10
+ class DeleteRelationship(Operation):
11
+ """Represents a DELETE operation for deleting virtual relationships."""
12
+
13
+ def __init__(self, relationship: Relationship) -> None:
14
+ super().__init__()
15
+ self._relationship = relationship
16
+
17
+ @property
18
+ def relationship(self) -> Relationship:
19
+ return self._relationship
20
+
21
+ async def run(self) -> None:
22
+ if self._relationship is None:
23
+ raise ValueError("Relationship is null")
24
+ db = Database.get_instance()
25
+ db.remove_relationship(self._relationship)
26
+
27
+ @property
28
+ def results(self) -> List[Dict[str, Any]]:
29
+ return []
@@ -15,6 +15,10 @@ class Limit(Operation):
15
15
  def is_limit_reached(self) -> bool:
16
16
  return self._count >= self._limit
17
17
 
18
+ @property
19
+ def limit_value(self) -> int:
20
+ return self._limit
21
+
18
22
  def increment(self) -> None:
19
23
  self._count += 1
20
24
 
@@ -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
- if self._limit is not None and self._limit.is_limit_reached:
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
- return self._results
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
@@ -57,10 +57,13 @@ from .operations.aggregated_with import AggregatedWith
57
57
  from .operations.call import Call
58
58
  from .operations.create_node import CreateNode
59
59
  from .operations.create_relationship import CreateRelationship
60
+ from .operations.delete_node import DeleteNode
61
+ from .operations.delete_relationship import DeleteRelationship
60
62
  from .operations.limit import Limit
61
63
  from .operations.load import Load
62
64
  from .operations.match import Match
63
65
  from .operations.operation import Operation
66
+ from .operations.order_by import OrderBy, SortField
64
67
  from .operations.return_op import Return
65
68
  from .operations.union import Union
66
69
  from .operations.union_all import UnionAll
@@ -146,6 +149,14 @@ class Parser(BaseParser):
146
149
  operation.add_sibling(where)
147
150
  operation = where
148
151
 
152
+ order_by = self._parse_order_by()
153
+ if order_by is not None:
154
+ if isinstance(operation, Return):
155
+ operation.order_by = order_by
156
+ else:
157
+ operation.add_sibling(order_by)
158
+ operation = order_by
159
+
149
160
  limit = self._parse_limit()
150
161
  if limit is not None:
151
162
  if isinstance(operation, Return):
@@ -176,8 +187,8 @@ class Parser(BaseParser):
176
187
  new_root.add_child(union)
177
188
  return new_root
178
189
 
179
- if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship)):
180
- raise ValueError("Last statement must be a RETURN, WHERE, CALL, or CREATE statement")
190
+ if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship, DeleteNode, DeleteRelationship)):
191
+ raise ValueError("Last statement must be a RETURN, WHERE, CALL, CREATE, or DELETE statement")
181
192
 
182
193
  return root
183
194
 
@@ -189,7 +200,8 @@ class Parser(BaseParser):
189
200
  self._parse_load() or
190
201
  self._parse_call() or
191
202
  self._parse_match() or
192
- self._parse_create()
203
+ self._parse_create() or
204
+ self._parse_delete()
193
205
  )
194
206
 
195
207
  def _parse_with(self) -> Optional[With]:
@@ -422,6 +434,54 @@ class Parser(BaseParser):
422
434
  else:
423
435
  return CreateNode(node, query)
424
436
 
437
+ def _parse_delete(self) -> Optional[Operation]:
438
+ """Parse DELETE VIRTUAL statement for nodes and relationships."""
439
+ if not self.token.is_delete():
440
+ return None
441
+ self.set_next_token()
442
+ self._expect_and_skip_whitespace_and_comments()
443
+ if not self.token.is_virtual():
444
+ raise ValueError("Expected VIRTUAL")
445
+ self.set_next_token()
446
+ self._expect_and_skip_whitespace_and_comments()
447
+
448
+ node = self._parse_node()
449
+ if node is None:
450
+ raise ValueError("Expected node definition")
451
+
452
+ relationship: Optional[Relationship] = None
453
+ if self.token.is_subtract() and self.peek() and self.peek().is_opening_bracket():
454
+ self.set_next_token() # skip -
455
+ self.set_next_token() # skip [
456
+ if not self.token.is_colon():
457
+ raise ValueError("Expected ':' for relationship type")
458
+ self.set_next_token()
459
+ if not self.token.is_identifier_or_keyword():
460
+ raise ValueError("Expected relationship type identifier")
461
+ rel_type = self.token.value or ""
462
+ self.set_next_token()
463
+ if not self.token.is_closing_bracket():
464
+ raise ValueError("Expected closing bracket for relationship definition")
465
+ self.set_next_token()
466
+ if not self.token.is_subtract():
467
+ raise ValueError("Expected '-' for relationship definition")
468
+ self.set_next_token()
469
+ # Skip optional direction indicator '>'
470
+ if self.token.is_greater_than():
471
+ self.set_next_token()
472
+ target = self._parse_node()
473
+ if target is None:
474
+ raise ValueError("Expected target node definition")
475
+ relationship = Relationship()
476
+ relationship.type = rel_type
477
+ relationship.source = node
478
+ relationship.target = target
479
+
480
+ if relationship is not None:
481
+ return DeleteRelationship(relationship)
482
+ else:
483
+ return DeleteNode(node)
484
+
425
485
  def _parse_union(self) -> Optional[Union]:
426
486
  """Parse a UNION or UNION ALL keyword."""
427
487
  if not self.token.is_union():
@@ -694,6 +754,41 @@ class Parser(BaseParser):
694
754
  self.set_next_token()
695
755
  return limit
696
756
 
757
+ def _parse_order_by(self) -> Optional[OrderBy]:
758
+ self._skip_whitespace_and_comments()
759
+ if not self.token.is_order():
760
+ return None
761
+ self._expect_previous_token_to_be_whitespace_or_comment()
762
+ self.set_next_token()
763
+ self._expect_and_skip_whitespace_and_comments()
764
+ if not self.token.is_by():
765
+ raise ValueError("Expected BY after ORDER")
766
+ self.set_next_token()
767
+ self._expect_and_skip_whitespace_and_comments()
768
+ fields: list[SortField] = []
769
+ while True:
770
+ if not self.token.is_identifier_or_keyword():
771
+ raise ValueError("Expected field name in ORDER BY")
772
+ field = self.token.value
773
+ self.set_next_token()
774
+ self._skip_whitespace_and_comments()
775
+ direction = "asc"
776
+ if self.token.is_asc():
777
+ direction = "asc"
778
+ self.set_next_token()
779
+ self._skip_whitespace_and_comments()
780
+ elif self.token.is_desc():
781
+ direction = "desc"
782
+ self.set_next_token()
783
+ self._skip_whitespace_and_comments()
784
+ fields.append(SortField(field, direction))
785
+ if self.token.is_comma():
786
+ self.set_next_token()
787
+ self._skip_whitespace_and_comments()
788
+ else:
789
+ break
790
+ return OrderBy(fields)
791
+
697
792
  def _parse_expressions(
698
793
  self, alias_option: AliasOption = AliasOption.NOT_ALLOWED
699
794
  ) -> 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