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
@@ -1,18 +1,26 @@
1
1
  """Main parser for FlowQuery statements."""
2
2
 
3
- from typing import Dict, Iterator, List, Optional
3
+ import sys
4
+ from typing import Dict, Iterator, List, Optional, Tuple, cast
4
5
 
6
+ from ..graph.hops import Hops
7
+ from ..graph.node import Node
8
+ from ..graph.node_reference import NodeReference
9
+ from ..graph.pattern import Pattern
10
+ from ..graph.pattern_expression import PatternExpression
11
+ from ..graph.relationship import Relationship
12
+ from ..graph.relationship_reference import RelationshipReference
5
13
  from ..tokenization.token import Token
6
14
  from ..utils.object_utils import ObjectUtils
7
15
  from .alias import Alias
8
16
  from .alias_option import AliasOption
9
17
  from .ast_node import ASTNode
10
18
  from .base_parser import BaseParser
11
- from .context import Context
12
19
  from .components.from_ import From
13
20
  from .components.headers import Headers
14
21
  from .components.null import Null
15
22
  from .components.post import Post
23
+ from .context import Context
16
24
  from .data_structures.associative_array import AssociativeArray
17
25
  from .data_structures.json_array import JSONArray
18
26
  from .data_structures.key_value_pair import KeyValuePair
@@ -30,12 +38,14 @@ from .functions.function import Function
30
38
  from .functions.function_factory import FunctionFactory
31
39
  from .functions.predicate_function import PredicateFunction
32
40
  from .logic.case import Case
33
- from .logic.when import When
34
- from .logic.then import Then
35
41
  from .logic.else_ import Else
42
+ from .logic.then import Then
43
+ from .logic.when import When
36
44
  from .operations.aggregated_return import AggregatedReturn
37
45
  from .operations.aggregated_with import AggregatedWith
38
46
  from .operations.call import Call
47
+ from .operations.create_node import CreateNode
48
+ from .operations.create_relationship import CreateRelationship
39
49
  from .operations.limit import Limit
40
50
  from .operations.load import Load
41
51
  from .operations.match import Match
@@ -44,22 +54,15 @@ from .operations.return_op import Return
44
54
  from .operations.unwind import Unwind
45
55
  from .operations.where import Where
46
56
  from .operations.with_op import With
47
- from ..graph.node import Node
48
- from ..graph.node_reference import NodeReference
49
- from ..graph.pattern import Pattern
50
- from ..graph.pattern_expression import PatternExpression
51
- from ..graph.relationship import Relationship
52
- from .operations.create_node import CreateNode
53
- from .operations.create_relationship import CreateRelationship
54
57
 
55
58
 
56
59
  class Parser(BaseParser):
57
60
  """Main parser for FlowQuery statements.
58
-
61
+
59
62
  Parses FlowQuery declarative query language statements into an Abstract Syntax Tree (AST).
60
63
  Supports operations like WITH, UNWIND, RETURN, LOAD, WHERE, and LIMIT, along with
61
64
  expressions, functions, data structures, and logical constructs.
62
-
65
+
63
66
  Example:
64
67
  parser = Parser()
65
68
  ast = parser.parse("unwind [1, 2, 3, 4, 5] as num return num")
@@ -73,13 +76,13 @@ class Parser(BaseParser):
73
76
 
74
77
  def parse(self, statement: str) -> ASTNode:
75
78
  """Parses a FlowQuery statement into an Abstract Syntax Tree.
76
-
79
+
77
80
  Args:
78
81
  statement: The FlowQuery statement to parse
79
-
82
+
80
83
  Returns:
81
84
  The root AST node containing the parsed structure
82
-
85
+
83
86
  Raises:
84
87
  ValueError: If the statement is malformed or contains syntax errors
85
88
  """
@@ -90,32 +93,32 @@ class Parser(BaseParser):
90
93
  root = ASTNode()
91
94
  previous: Optional[Operation] = None
92
95
  operation: Optional[Operation] = None
93
-
96
+
94
97
  while not self.token.is_eof():
95
98
  if root.child_count() > 0:
96
99
  self._expect_and_skip_whitespace_and_comments()
97
100
  else:
98
101
  self._skip_whitespace_and_comments()
99
-
102
+
100
103
  operation = self._parse_operation()
101
104
  if operation is None and not is_sub_query:
102
105
  raise ValueError("Expected one of WITH, UNWIND, RETURN, LOAD, OR CALL")
103
106
  elif operation is None and is_sub_query:
104
107
  return root
105
-
108
+
106
109
  if self._returns > 1:
107
110
  raise ValueError("Only one RETURN statement is allowed")
108
-
111
+
109
112
  if isinstance(previous, Call) and not previous.has_yield:
110
113
  raise ValueError(
111
114
  "CALL operations must have a YIELD clause unless they are the last operation"
112
115
  )
113
-
116
+
114
117
  if previous is not None:
115
118
  previous.add_sibling(operation)
116
119
  else:
117
120
  root.add_child(operation)
118
-
121
+
119
122
  where = self._parse_where()
120
123
  if where is not None:
121
124
  if isinstance(operation, Return):
@@ -123,17 +126,17 @@ class Parser(BaseParser):
123
126
  else:
124
127
  operation.add_sibling(where)
125
128
  operation = where
126
-
129
+
127
130
  limit = self._parse_limit()
128
131
  if limit is not None:
129
132
  operation.add_sibling(limit)
130
133
  operation = limit
131
-
134
+
132
135
  previous = operation
133
-
136
+
134
137
  if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship)):
135
138
  raise ValueError("Last statement must be a RETURN, WHERE, CALL, or CREATE statement")
136
-
139
+
137
140
  return root
138
141
 
139
142
  def _parse_operation(self) -> Optional[Operation]:
@@ -156,7 +159,7 @@ class Parser(BaseParser):
156
159
  if len(expressions) == 0:
157
160
  raise ValueError("Expected expression")
158
161
  if any(expr.has_reducers() for expr in expressions):
159
- return AggregatedWith(expressions)
162
+ return AggregatedWith(expressions) # type: ignore[return-value]
160
163
  return With(expressions)
161
164
 
162
165
  def _parse_unwind(self) -> Optional[Unwind]:
@@ -228,7 +231,7 @@ class Parser(BaseParser):
228
231
  self._expect_and_skip_whitespace_and_comments()
229
232
  from_node = From()
230
233
  load.add_child(from_node)
231
-
234
+
232
235
  # Check if source is async function
233
236
  async_func = self._parse_async_function()
234
237
  if async_func is not None:
@@ -238,7 +241,7 @@ class Parser(BaseParser):
238
241
  if expression is None:
239
242
  raise ValueError("Expected expression or async function")
240
243
  from_node.add_child(expression)
241
-
244
+
242
245
  self._expect_and_skip_whitespace_and_comments()
243
246
  if self.token.is_headers():
244
247
  headers = Headers()
@@ -250,7 +253,7 @@ class Parser(BaseParser):
250
253
  headers.add_child(header)
251
254
  load.add_child(headers)
252
255
  self._expect_and_skip_whitespace_and_comments()
253
-
256
+
254
257
  if self.token.is_post():
255
258
  post = Post()
256
259
  self.set_next_token()
@@ -261,7 +264,7 @@ class Parser(BaseParser):
261
264
  post.add_child(payload)
262
265
  load.add_child(post)
263
266
  self._expect_and_skip_whitespace_and_comments()
264
-
267
+
265
268
  alias = self._parse_alias()
266
269
  if alias is not None:
267
270
  load.add_child(alias)
@@ -288,7 +291,7 @@ class Parser(BaseParser):
288
291
  expressions = list(self._parse_expressions(AliasOption.OPTIONAL))
289
292
  if len(expressions) == 0:
290
293
  raise ValueError("Expected at least one expression")
291
- call.yielded = expressions
294
+ call.yielded = expressions # type: ignore[assignment]
292
295
  return call
293
296
 
294
297
  def _parse_match(self) -> Optional[Match]:
@@ -311,11 +314,11 @@ class Parser(BaseParser):
311
314
  raise ValueError("Expected VIRTUAL")
312
315
  self.set_next_token()
313
316
  self._expect_and_skip_whitespace_and_comments()
314
-
317
+
315
318
  node = self._parse_node()
316
319
  if node is None:
317
320
  raise ValueError("Expected node definition")
318
-
321
+
319
322
  relationship: Optional[Relationship] = None
320
323
  if self.token.is_subtract() and self.peek() and self.peek().is_opening_bracket():
321
324
  self.set_next_token() # skip -
@@ -341,17 +344,17 @@ class Parser(BaseParser):
341
344
  raise ValueError("Expected target node definition")
342
345
  relationship = Relationship()
343
346
  relationship.type = rel_type
344
-
347
+
345
348
  self._expect_and_skip_whitespace_and_comments()
346
349
  if not self.token.is_as():
347
350
  raise ValueError("Expected AS")
348
351
  self.set_next_token()
349
352
  self._expect_and_skip_whitespace_and_comments()
350
-
353
+
351
354
  query = self._parse_sub_query()
352
355
  if query is None:
353
356
  raise ValueError("Expected sub-query")
354
-
357
+
355
358
  if relationship is not None:
356
359
  return CreateRelationship(relationship, query)
357
360
  else:
@@ -416,7 +419,7 @@ class Parser(BaseParser):
416
419
 
417
420
  def _parse_pattern_expression(self) -> Optional[PatternExpression]:
418
421
  """Parse a pattern expression for WHERE clauses.
419
-
422
+
420
423
  PatternExpression is used to test if a graph pattern exists.
421
424
  It must start with a NodeReference (referencing an existing variable).
422
425
  """
@@ -459,17 +462,17 @@ class Parser(BaseParser):
459
462
  raise ValueError("Expected node label identifier")
460
463
  if self.token.is_colon() and peek is not None and peek.is_identifier():
461
464
  self.set_next_token()
462
- label = self.token.value
465
+ label = cast(str, self.token.value) # Guaranteed by is_identifier check
463
466
  self.set_next_token()
464
467
  self._skip_whitespace_and_comments()
465
468
  node = Node()
466
469
  node.label = label
470
+ node.properties = dict(self._parse_properties())
467
471
  if label is not None and identifier is not None:
468
472
  node.identifier = identifier
469
473
  self._variables[identifier] = node
470
474
  elif identifier is not None:
471
475
  reference = self._variables.get(identifier)
472
- from ..graph.node_reference import NodeReference
473
476
  if reference is None or not isinstance(reference, Node):
474
477
  raise ValueError(f"Undefined node reference: {identifier}")
475
478
  node = NodeReference(node, reference)
@@ -479,7 +482,9 @@ class Parser(BaseParser):
479
482
  return node
480
483
 
481
484
  def _parse_relationship(self) -> Optional[Relationship]:
485
+ direction = "right"
482
486
  if self.token.is_less_than() and self.peek() is not None and self.peek().is_subtract():
487
+ direction = "left"
483
488
  self.set_next_token()
484
489
  self.set_next_token()
485
490
  elif self.token.is_subtract():
@@ -501,6 +506,7 @@ class Parser(BaseParser):
501
506
  rel_type: str = self.token.value or ""
502
507
  self.set_next_token()
503
508
  hops = self._parse_relationship_hops()
509
+ properties: Dict[str, Expression] = dict(self._parse_properties())
504
510
  if not self.token.is_closing_bracket():
505
511
  raise ValueError("Expected closing bracket for relationship definition")
506
512
  self.set_next_token()
@@ -510,12 +516,13 @@ class Parser(BaseParser):
510
516
  if self.token.is_greater_than():
511
517
  self.set_next_token()
512
518
  relationship = Relationship()
519
+ relationship.direction = direction
520
+ relationship.properties = properties
513
521
  if rel_type is not None and variable is not None:
514
522
  relationship.identifier = variable
515
523
  self._variables[variable] = relationship
516
524
  elif variable is not None:
517
525
  reference = self._variables.get(variable)
518
- from ..graph.relationship_reference import RelationshipReference
519
526
  if reference is None or not isinstance(reference, Relationship):
520
527
  raise ValueError(f"Undefined relationship reference: {variable}")
521
528
  relationship = RelationshipReference(relationship, reference)
@@ -524,9 +531,40 @@ class Parser(BaseParser):
524
531
  relationship.type = rel_type
525
532
  return relationship
526
533
 
527
- def _parse_relationship_hops(self):
528
- import sys
529
- from ..graph.hops import Hops
534
+ def _parse_properties(self) -> Iterator[Tuple[str, Expression]]:
535
+ parts: int = 0
536
+ while True:
537
+ self._skip_whitespace_and_comments()
538
+ if not self.token.is_opening_brace() and parts == 0:
539
+ return
540
+ elif not self.token.is_opening_brace() and parts > 0:
541
+ raise ValueError("Expected opening brace")
542
+ self.set_next_token()
543
+ self._skip_whitespace_and_comments()
544
+ if not self.token.is_identifier():
545
+ raise ValueError("Expected identifier")
546
+ key: str = self.token.value or ""
547
+ self.set_next_token()
548
+ self._skip_whitespace_and_comments()
549
+ if not self.token.is_colon():
550
+ raise ValueError("Expected colon")
551
+ self.set_next_token()
552
+ self._skip_whitespace_and_comments()
553
+ expression = self._parse_expression()
554
+ if expression is None:
555
+ raise ValueError("Expected expression")
556
+ self._skip_whitespace_and_comments()
557
+ if not self.token.is_closing_brace():
558
+ raise ValueError("Expected closing brace")
559
+ self.set_next_token()
560
+ yield (key, expression)
561
+ self._skip_whitespace_and_comments()
562
+ if not self.token.is_comma():
563
+ break
564
+ self.set_next_token()
565
+ parts += 1
566
+
567
+ def _parse_relationship_hops(self) -> Optional[Hops]:
530
568
  if not self.token.is_multiply():
531
569
  return None
532
570
  hops = Hops()
@@ -540,9 +578,10 @@ class Parser(BaseParser):
540
578
  raise ValueError("Expected '..' for relationship hops")
541
579
  self.set_next_token()
542
580
  if not self.token.is_number():
543
- raise ValueError("Expected number for relationship hops")
544
- hops.max = int(self.token.value or "0")
545
- self.set_next_token()
581
+ hops.max = sys.maxsize
582
+ else:
583
+ hops.max = int(self.token.value or "0")
584
+ self.set_next_token()
546
585
  else:
547
586
  # Just * without numbers means unbounded
548
587
  hops.min = 0
@@ -571,10 +610,11 @@ class Parser(BaseParser):
571
610
  alias = self._parse_alias()
572
611
  if isinstance(expression.first_child(), Reference) and alias is None:
573
612
  reference = expression.first_child()
613
+ assert isinstance(reference, Reference) # For type narrowing
574
614
  expression.set_alias(reference.identifier)
575
615
  self._variables[reference.identifier] = expression
576
- elif (alias_option == AliasOption.REQUIRED and
577
- alias is None and
616
+ elif (alias_option == AliasOption.REQUIRED and
617
+ alias is None and
578
618
  not isinstance(expression.first_child(), Reference)):
579
619
  raise ValueError("Alias required")
580
620
  elif alias_option == AliasOption.NOT_ALLOWED and alias is not None:
@@ -590,64 +630,88 @@ class Parser(BaseParser):
590
630
  break
591
631
  self.set_next_token()
592
632
 
633
+ def _parse_operand(self, expression: Expression) -> bool:
634
+ """Parse a single operand (without operators). Returns True if an operand was parsed."""
635
+ self._skip_whitespace_and_comments()
636
+ if self.token.is_identifier() and (self.peek() is None or not self.peek().is_left_parenthesis()):
637
+ identifier = self.token.value or ""
638
+ reference = Reference(identifier, self._variables.get(identifier))
639
+ self.set_next_token()
640
+ lookup = self._parse_lookup(reference)
641
+ expression.add_node(lookup)
642
+ return True
643
+ elif self.token.is_identifier() and self.peek() is not None and self.peek().is_left_parenthesis():
644
+ func = self._parse_predicate_function() or self._parse_function()
645
+ if func is not None:
646
+ lookup = self._parse_lookup(func)
647
+ expression.add_node(lookup)
648
+ return True
649
+ elif (
650
+ self.token.is_left_parenthesis()
651
+ and self.peek() is not None
652
+ and (
653
+ self.peek().is_identifier()
654
+ or self.peek().is_colon()
655
+ or self.peek().is_right_parenthesis()
656
+ )
657
+ ):
658
+ # Possible graph pattern expression
659
+ pattern = self._parse_pattern_expression()
660
+ if pattern is not None:
661
+ expression.add_node(pattern)
662
+ return True
663
+ elif self.token.is_operand():
664
+ expression.add_node(self.token.node)
665
+ self.set_next_token()
666
+ return True
667
+ elif self.token.is_f_string():
668
+ f_string = self._parse_f_string()
669
+ if f_string is None:
670
+ raise ValueError("Expected f-string")
671
+ expression.add_node(f_string)
672
+ return True
673
+ elif self.token.is_left_parenthesis():
674
+ self.set_next_token()
675
+ sub = self._parse_expression()
676
+ if sub is None:
677
+ raise ValueError("Expected expression")
678
+ if not self.token.is_right_parenthesis():
679
+ raise ValueError("Expected right parenthesis")
680
+ self.set_next_token()
681
+ lookup = self._parse_lookup(sub)
682
+ expression.add_node(lookup)
683
+ return True
684
+ elif self.token.is_opening_brace() or self.token.is_opening_bracket():
685
+ json = self._parse_json()
686
+ if json is None:
687
+ raise ValueError("Expected JSON object")
688
+ lookup = self._parse_lookup(json)
689
+ expression.add_node(lookup)
690
+ return True
691
+ elif self.token.is_case():
692
+ case = self._parse_case()
693
+ if case is None:
694
+ raise ValueError("Expected CASE statement")
695
+ expression.add_node(case)
696
+ return True
697
+ elif self.token.is_not():
698
+ not_node = Not()
699
+ self.set_next_token()
700
+ # NOT should only bind to the next operand, not the entire expression
701
+ # Create a temporary expression to parse just one operand
702
+ temp_expr = Expression()
703
+ if not self._parse_operand(temp_expr):
704
+ raise ValueError("Expected expression after NOT")
705
+ temp_expr.finish()
706
+ not_node.add_child(temp_expr)
707
+ expression.add_node(not_node)
708
+ return True
709
+ return False
710
+
593
711
  def _parse_expression(self) -> Optional[Expression]:
594
712
  expression = Expression()
595
713
  while True:
596
- self._skip_whitespace_and_comments()
597
- if self.token.is_identifier() and (self.peek() is None or not self.peek().is_left_parenthesis()):
598
- identifier = self.token.value or ""
599
- reference = Reference(identifier, self._variables.get(identifier))
600
- self.set_next_token()
601
- lookup = self._parse_lookup(reference)
602
- expression.add_node(lookup)
603
- elif self.token.is_identifier() and self.peek() is not None and self.peek().is_left_parenthesis():
604
- func = self._parse_predicate_function() or self._parse_function()
605
- if func is not None:
606
- lookup = self._parse_lookup(func)
607
- expression.add_node(lookup)
608
- elif self.token.is_left_parenthesis() and self.peek() is not None and (self.peek().is_identifier() or self.peek().is_colon() or self.peek().is_right_parenthesis()):
609
- # Possible graph pattern expression
610
- pattern = self._parse_pattern_expression()
611
- if pattern is not None:
612
- expression.add_node(pattern)
613
- elif self.token.is_operand():
614
- expression.add_node(self.token.node)
615
- self.set_next_token()
616
- elif self.token.is_f_string():
617
- f_string = self._parse_f_string()
618
- if f_string is None:
619
- raise ValueError("Expected f-string")
620
- expression.add_node(f_string)
621
- elif self.token.is_left_parenthesis():
622
- self.set_next_token()
623
- sub = self._parse_expression()
624
- if sub is None:
625
- raise ValueError("Expected expression")
626
- if not self.token.is_right_parenthesis():
627
- raise ValueError("Expected right parenthesis")
628
- self.set_next_token()
629
- lookup = self._parse_lookup(sub)
630
- expression.add_node(lookup)
631
- elif self.token.is_opening_brace() or self.token.is_opening_bracket():
632
- json = self._parse_json()
633
- if json is None:
634
- raise ValueError("Expected JSON object")
635
- lookup = self._parse_lookup(json)
636
- expression.add_node(lookup)
637
- elif self.token.is_case():
638
- case = self._parse_case()
639
- if case is None:
640
- raise ValueError("Expected CASE statement")
641
- expression.add_node(case)
642
- elif self.token.is_not():
643
- not_node = Not()
644
- self.set_next_token()
645
- sub = self._parse_expression()
646
- if sub is None:
647
- raise ValueError("Expected expression")
648
- not_node.add_child(sub)
649
- expression.add_node(not_node)
650
- else:
714
+ if not self._parse_operand(expression):
651
715
  if expression.nodes_added():
652
716
  raise ValueError("Expected operand or left parenthesis")
653
717
  else:
@@ -658,7 +722,7 @@ class Parser(BaseParser):
658
722
  else:
659
723
  break
660
724
  self.set_next_token()
661
-
725
+
662
726
  if expression.nodes_added():
663
727
  expression.finish()
664
728
  return expression
@@ -666,7 +730,7 @@ class Parser(BaseParser):
666
730
 
667
731
  def _parse_lookup(self, node: ASTNode) -> ASTNode:
668
732
  variable = node
669
- lookup = None
733
+ lookup: Lookup | RangeLookup | None = None
670
734
  while True:
671
735
  if self.token.is_dot():
672
736
  self.set_next_token()
@@ -853,30 +917,30 @@ class Parser(BaseParser):
853
917
  name = self.token.value or ""
854
918
  if not self.peek() or not self.peek().is_left_parenthesis():
855
919
  return None
856
-
920
+
857
921
  try:
858
922
  func = FunctionFactory.create(name)
859
923
  except ValueError:
860
924
  raise ValueError(f"Unknown function: {name}")
861
-
925
+
862
926
  # Check for nested aggregate functions
863
927
  if isinstance(func, AggregateFunction) and self._context.contains_type(AggregateFunction):
864
928
  raise ValueError("Aggregate functions cannot be nested")
865
-
929
+
866
930
  self._context.push(func)
867
931
  self.set_next_token() # skip function name
868
932
  self.set_next_token() # skip left parenthesis
869
933
  self._skip_whitespace_and_comments()
870
-
934
+
871
935
  # Check for DISTINCT keyword
872
936
  if self.token.is_distinct():
873
937
  func.distinct = True
874
938
  self.set_next_token()
875
939
  self._expect_and_skip_whitespace_and_comments()
876
-
940
+
877
941
  params = list(self._parse_function_parameters())
878
942
  func.parameters = params
879
-
943
+
880
944
  if not self.token.is_right_parenthesis():
881
945
  raise ValueError("Expected right parenthesis")
882
946
  self.set_next_token()
@@ -893,11 +957,11 @@ class Parser(BaseParser):
893
957
  if not self.token.is_left_parenthesis():
894
958
  raise ValueError("Expected left parenthesis")
895
959
  self.set_next_token()
896
-
960
+
897
961
  func = FunctionFactory.create_async(name)
898
962
  params = list(self._parse_function_parameters())
899
963
  func.parameters = params
900
-
964
+
901
965
  if not self.token.is_right_parenthesis():
902
966
  raise ValueError("Expected right parenthesis")
903
967
  self.set_next_token()
@@ -9,7 +9,6 @@ from .components.text import Text
9
9
  from .expressions.boolean import Boolean
10
10
  from .expressions.identifier import Identifier
11
11
  from .expressions.number import Number
12
- from .expressions.string import String
13
12
  from .expressions.operator import (
14
13
  Add,
15
14
  And,
@@ -28,6 +27,7 @@ from .expressions.operator import (
28
27
  Power,
29
28
  Subtract,
30
29
  )
30
+ from .expressions.string import String
31
31
  from .logic.else_ import Else
32
32
  from .logic.end import End
33
33
  from .logic.then import Then
@@ -103,7 +103,7 @@ class TokenToNode:
103
103
  elif token.is_null():
104
104
  return Null()
105
105
  elif token.is_boolean():
106
- return Boolean(token.value)
106
+ return Boolean(token.value or "")
107
107
  else:
108
108
  raise ValueError("Unknown token")
109
109
  return ASTNode()
@@ -1,13 +1,13 @@
1
1
  """Tokenization module for FlowQuery."""
2
2
 
3
- from .tokenizer import Tokenizer
4
- from .token import Token
5
- from .token_type import TokenType
6
3
  from .keyword import Keyword
7
4
  from .operator import Operator
5
+ from .string_walker import StringWalker
8
6
  from .symbol import Symbol
7
+ from .token import Token
9
8
  from .token_mapper import TokenMapper
10
- from .string_walker import StringWalker
9
+ from .token_type import TokenType
10
+ from .tokenizer import Tokenizer
11
11
  from .trie import Trie
12
12
 
13
13
  __all__ = [
@@ -5,7 +5,7 @@ from enum import Enum
5
5
 
6
6
  class Keyword(Enum):
7
7
  """Enumeration of all keywords in FlowQuery."""
8
-
8
+
9
9
  RETURN = "RETURN"
10
10
  MATCH = "MATCH"
11
11
  WHERE = "WHERE"
@@ -5,7 +5,7 @@ from enum import Enum
5
5
 
6
6
  class Operator(Enum):
7
7
  """Enumeration of all operators in FlowQuery."""
8
-
8
+
9
9
  # Arithmetic
10
10
  ADD = "+"
11
11
  SUBTRACT = "-"
@@ -5,10 +5,10 @@ from ..utils.string_utils import StringUtils
5
5
 
6
6
  class StringWalker:
7
7
  """Utility class for walking through a string character by character during tokenization.
8
-
8
+
9
9
  Provides methods to check for specific character patterns, move through the string,
10
10
  and extract substrings. Used by the Tokenizer to process input text.
11
-
11
+
12
12
  Example:
13
13
  walker = StringWalker("WITH x as variable")
14
14
  while not walker.is_at_end:
@@ -17,7 +17,7 @@ class StringWalker:
17
17
 
18
18
  def __init__(self, text: str):
19
19
  """Creates a new StringWalker for the given text.
20
-
20
+
21
21
  Args:
22
22
  text: The input text to walk through
23
23
  """
@@ -89,7 +89,7 @@ class StringWalker:
89
89
  return self.current_char == '\\' and self.next_char == char
90
90
 
91
91
  def escaped_brace(self) -> bool:
92
- return ((self.current_char == '{' and self.next_char == '{') or
92
+ return ((self.current_char == '{' and self.next_char == '{') or
93
93
  (self.current_char == '}' and self.next_char == '}'))
94
94
 
95
95
  def opening_brace(self) -> bool:
@@ -5,7 +5,7 @@ from enum import Enum
5
5
 
6
6
  class Symbol(Enum):
7
7
  """Enumeration of all symbols in FlowQuery."""
8
-
8
+
9
9
  LEFT_PARENTHESIS = "("
10
10
  RIGHT_PARENTHESIS = ")"
11
11
  COMMA = ","