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.
- package/.gitattributes +3 -0
- package/.github/workflows/python-publish.yml +56 -4
- package/.github/workflows/release.yml +26 -19
- package/.husky/pre-commit +26 -0
- package/README.md +37 -32
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/data.d.ts +5 -4
- package/dist/graph/data.d.ts.map +1 -1
- package/dist/graph/data.js +38 -20
- package/dist/graph/data.js.map +1 -1
- package/dist/graph/node.d.ts +2 -0
- package/dist/graph/node.d.ts.map +1 -1
- package/dist/graph/node.js +23 -0
- package/dist/graph/node.js.map +1 -1
- package/dist/graph/node_data.js +1 -1
- package/dist/graph/node_data.js.map +1 -1
- package/dist/graph/pattern.d.ts.map +1 -1
- package/dist/graph/pattern.js +11 -4
- package/dist/graph/pattern.js.map +1 -1
- package/dist/graph/relationship.d.ts +6 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +43 -5
- package/dist/graph/relationship.js.map +1 -1
- package/dist/graph/relationship_data.d.ts +2 -0
- package/dist/graph/relationship_data.d.ts.map +1 -1
- package/dist/graph/relationship_data.js +8 -1
- package/dist/graph/relationship_data.js.map +1 -1
- package/dist/graph/relationship_match_collector.js +2 -2
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/dist/graph/relationship_reference.d.ts.map +1 -1
- package/dist/graph/relationship_reference.js +2 -1
- package/dist/graph/relationship_reference.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/parsing/parser.d.ts +6 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +139 -72
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/misc/data/test.json +10 -0
- package/flowquery-py/misc/data/users.json +242 -0
- package/flowquery-py/notebooks/TestFlowQuery.ipynb +440 -0
- package/flowquery-py/pyproject.toml +48 -2
- package/flowquery-py/src/__init__.py +7 -5
- package/flowquery-py/src/compute/runner.py +14 -10
- package/flowquery-py/src/extensibility.py +8 -8
- package/flowquery-py/src/graph/__init__.py +7 -7
- package/flowquery-py/src/graph/data.py +38 -20
- package/flowquery-py/src/graph/database.py +10 -20
- package/flowquery-py/src/graph/node.py +50 -19
- package/flowquery-py/src/graph/node_data.py +1 -1
- package/flowquery-py/src/graph/node_reference.py +10 -11
- package/flowquery-py/src/graph/pattern.py +27 -37
- package/flowquery-py/src/graph/pattern_expression.py +13 -11
- package/flowquery-py/src/graph/patterns.py +2 -2
- package/flowquery-py/src/graph/physical_node.py +4 -3
- package/flowquery-py/src/graph/physical_relationship.py +5 -5
- package/flowquery-py/src/graph/relationship.py +62 -14
- package/flowquery-py/src/graph/relationship_data.py +7 -2
- package/flowquery-py/src/graph/relationship_match_collector.py +15 -10
- package/flowquery-py/src/graph/relationship_reference.py +4 -4
- package/flowquery-py/src/io/command_line.py +13 -14
- package/flowquery-py/src/parsing/__init__.py +2 -2
- package/flowquery-py/src/parsing/alias_option.py +1 -1
- package/flowquery-py/src/parsing/ast_node.py +21 -20
- package/flowquery-py/src/parsing/base_parser.py +7 -7
- package/flowquery-py/src/parsing/components/__init__.py +3 -3
- package/flowquery-py/src/parsing/components/from_.py +3 -1
- package/flowquery-py/src/parsing/components/headers.py +2 -2
- package/flowquery-py/src/parsing/components/null.py +2 -2
- package/flowquery-py/src/parsing/context.py +7 -7
- package/flowquery-py/src/parsing/data_structures/associative_array.py +7 -7
- package/flowquery-py/src/parsing/data_structures/json_array.py +3 -3
- package/flowquery-py/src/parsing/data_structures/key_value_pair.py +4 -4
- package/flowquery-py/src/parsing/data_structures/lookup.py +2 -2
- package/flowquery-py/src/parsing/data_structures/range_lookup.py +2 -2
- package/flowquery-py/src/parsing/expressions/__init__.py +16 -16
- package/flowquery-py/src/parsing/expressions/expression.py +16 -13
- package/flowquery-py/src/parsing/expressions/expression_map.py +9 -9
- package/flowquery-py/src/parsing/expressions/f_string.py +3 -3
- package/flowquery-py/src/parsing/expressions/identifier.py +4 -3
- package/flowquery-py/src/parsing/expressions/number.py +3 -3
- package/flowquery-py/src/parsing/expressions/operator.py +16 -16
- package/flowquery-py/src/parsing/expressions/reference.py +3 -3
- package/flowquery-py/src/parsing/expressions/string.py +2 -2
- package/flowquery-py/src/parsing/functions/__init__.py +17 -17
- package/flowquery-py/src/parsing/functions/aggregate_function.py +8 -8
- package/flowquery-py/src/parsing/functions/async_function.py +12 -9
- package/flowquery-py/src/parsing/functions/avg.py +4 -4
- package/flowquery-py/src/parsing/functions/collect.py +6 -6
- package/flowquery-py/src/parsing/functions/function.py +6 -6
- package/flowquery-py/src/parsing/functions/function_factory.py +31 -34
- package/flowquery-py/src/parsing/functions/function_metadata.py +10 -11
- package/flowquery-py/src/parsing/functions/functions.py +14 -6
- package/flowquery-py/src/parsing/functions/join.py +3 -3
- package/flowquery-py/src/parsing/functions/keys.py +3 -3
- package/flowquery-py/src/parsing/functions/predicate_function.py +8 -7
- package/flowquery-py/src/parsing/functions/predicate_sum.py +12 -7
- package/flowquery-py/src/parsing/functions/rand.py +2 -2
- package/flowquery-py/src/parsing/functions/range_.py +9 -4
- package/flowquery-py/src/parsing/functions/replace.py +2 -2
- package/flowquery-py/src/parsing/functions/round_.py +2 -2
- package/flowquery-py/src/parsing/functions/size.py +2 -2
- package/flowquery-py/src/parsing/functions/split.py +9 -4
- package/flowquery-py/src/parsing/functions/stringify.py +3 -3
- package/flowquery-py/src/parsing/functions/sum.py +4 -4
- package/flowquery-py/src/parsing/functions/to_json.py +2 -2
- package/flowquery-py/src/parsing/functions/type_.py +3 -3
- package/flowquery-py/src/parsing/functions/value_holder.py +1 -1
- package/flowquery-py/src/parsing/logic/__init__.py +2 -2
- package/flowquery-py/src/parsing/logic/case.py +0 -1
- package/flowquery-py/src/parsing/logic/when.py +3 -1
- package/flowquery-py/src/parsing/operations/__init__.py +10 -10
- package/flowquery-py/src/parsing/operations/aggregated_return.py +3 -5
- package/flowquery-py/src/parsing/operations/aggregated_with.py +4 -4
- package/flowquery-py/src/parsing/operations/call.py +6 -7
- package/flowquery-py/src/parsing/operations/create_node.py +5 -4
- package/flowquery-py/src/parsing/operations/create_relationship.py +5 -4
- package/flowquery-py/src/parsing/operations/group_by.py +18 -16
- package/flowquery-py/src/parsing/operations/load.py +21 -19
- package/flowquery-py/src/parsing/operations/match.py +8 -7
- package/flowquery-py/src/parsing/operations/operation.py +3 -3
- package/flowquery-py/src/parsing/operations/projection.py +6 -6
- package/flowquery-py/src/parsing/operations/return_op.py +9 -5
- package/flowquery-py/src/parsing/operations/unwind.py +3 -2
- package/flowquery-py/src/parsing/operations/where.py +9 -7
- package/flowquery-py/src/parsing/operations/with_op.py +2 -2
- package/flowquery-py/src/parsing/parser.py +178 -114
- package/flowquery-py/src/parsing/token_to_node.py +2 -2
- package/flowquery-py/src/tokenization/__init__.py +4 -4
- package/flowquery-py/src/tokenization/keyword.py +1 -1
- package/flowquery-py/src/tokenization/operator.py +1 -1
- package/flowquery-py/src/tokenization/string_walker.py +4 -4
- package/flowquery-py/src/tokenization/symbol.py +1 -1
- package/flowquery-py/src/tokenization/token.py +11 -11
- package/flowquery-py/src/tokenization/token_mapper.py +10 -9
- package/flowquery-py/src/tokenization/token_type.py +1 -1
- package/flowquery-py/src/tokenization/tokenizer.py +19 -19
- package/flowquery-py/src/tokenization/trie.py +18 -17
- package/flowquery-py/src/utils/__init__.py +1 -1
- package/flowquery-py/src/utils/object_utils.py +3 -3
- package/flowquery-py/src/utils/string_utils.py +12 -12
- package/flowquery-py/tests/compute/test_runner.py +214 -7
- package/flowquery-py/tests/parsing/test_parser.py +41 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/data.ts +38 -20
- package/src/graph/node.ts +23 -0
- package/src/graph/node_data.ts +1 -1
- package/src/graph/pattern.ts +13 -4
- package/src/graph/relationship.ts +45 -5
- package/src/graph/relationship_data.ts +8 -1
- package/src/graph/relationship_match_collector.ts +1 -1
- package/src/graph/relationship_reference.ts +2 -1
- package/src/index.ts +5 -5
- package/src/parsing/parser.ts +139 -71
- package/tests/compute/runner.test.ts +249 -79
- package/tests/parsing/parser.test.ts +32 -0
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
"""Main parser for FlowQuery statements."""
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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.
|
|
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 .
|
|
9
|
+
from .token_type import TokenType
|
|
10
|
+
from .tokenizer import Tokenizer
|
|
11
11
|
from .trie import Trie
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
@@ -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:
|