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,21 @@
|
|
|
1
1
|
"""Graph pattern representation for FlowQuery."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from typing import Any, Generator, List, Optional, Sequence, Union
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
from ..parsing.ast_node import ASTNode
|
|
8
|
+
from .database import Database
|
|
9
|
+
from .node import Node
|
|
10
|
+
from .node_data import NodeData
|
|
11
|
+
from .relationship import Relationship
|
|
12
|
+
from .relationship_data import RelationshipData
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class Pattern(ASTNode):
|
|
13
16
|
"""Represents a graph pattern for matching."""
|
|
14
17
|
|
|
15
|
-
def __init__(self):
|
|
18
|
+
def __init__(self) -> None:
|
|
16
19
|
super().__init__()
|
|
17
20
|
self._identifier: Optional[str] = None
|
|
18
21
|
self._chain: List[Union['Node', 'Relationship']] = []
|
|
@@ -30,17 +33,14 @@ class Pattern(ASTNode):
|
|
|
30
33
|
return self._chain
|
|
31
34
|
|
|
32
35
|
@property
|
|
33
|
-
def elements(self) ->
|
|
36
|
+
def elements(self) -> Sequence[ASTNode]:
|
|
34
37
|
return self._chain
|
|
35
38
|
|
|
36
39
|
def add_element(self, element: Union['Node', 'Relationship']) -> None:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (len(self._chain) > 0 and
|
|
41
|
-
type(self._chain[-1]) == type(element)):
|
|
40
|
+
if (len(self._chain) > 0 and
|
|
41
|
+
type(self._chain[-1]) is type(element)):
|
|
42
42
|
raise ValueError("Cannot add two consecutive elements of the same type to the graph pattern")
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
if len(self._chain) > 0:
|
|
45
45
|
last = self._chain[-1]
|
|
46
46
|
if isinstance(last, Node) and isinstance(element, Relationship):
|
|
@@ -49,13 +49,12 @@ class Pattern(ASTNode):
|
|
|
49
49
|
if isinstance(last, Relationship) and isinstance(element, Node):
|
|
50
50
|
last.target = element
|
|
51
51
|
element.incoming = last
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
self._chain.append(element)
|
|
54
54
|
self.add_child(element)
|
|
55
55
|
|
|
56
56
|
@property
|
|
57
57
|
def start_node(self) -> 'Node':
|
|
58
|
-
from .node import Node
|
|
59
58
|
if len(self._chain) == 0:
|
|
60
59
|
raise ValueError("Pattern is empty")
|
|
61
60
|
first = self._chain[0]
|
|
@@ -65,7 +64,6 @@ class Pattern(ASTNode):
|
|
|
65
64
|
|
|
66
65
|
@property
|
|
67
66
|
def end_node(self) -> 'Node':
|
|
68
|
-
from .node import Node
|
|
69
67
|
if len(self._chain) == 0:
|
|
70
68
|
raise ValueError("Pattern is empty")
|
|
71
69
|
last = self._chain[-1]
|
|
@@ -73,7 +71,7 @@ class Pattern(ASTNode):
|
|
|
73
71
|
return last
|
|
74
72
|
raise ValueError("Pattern does not end with a node")
|
|
75
73
|
|
|
76
|
-
def first_node(self) -> Optional['Node']:
|
|
74
|
+
def first_node(self) -> Optional[Union['Node', 'Relationship']]:
|
|
77
75
|
if len(self._chain) > 0:
|
|
78
76
|
return self._chain[0]
|
|
79
77
|
return None
|
|
@@ -82,38 +80,30 @@ class Pattern(ASTNode):
|
|
|
82
80
|
return list(self.values())
|
|
83
81
|
|
|
84
82
|
def values(self) -> Generator[Any, None, None]:
|
|
85
|
-
|
|
86
|
-
from .relationship import Relationship
|
|
87
|
-
|
|
88
|
-
for element in self._chain:
|
|
83
|
+
for i, element in enumerate(self._chain):
|
|
89
84
|
if isinstance(element, Node):
|
|
85
|
+
# Skip node if previous element was a zero-hop relationship (no matches)
|
|
86
|
+
prev = self._chain[i-1] if i > 0 else None
|
|
87
|
+
if isinstance(prev, Relationship) and len(prev.matches) == 0:
|
|
88
|
+
continue
|
|
90
89
|
yield element.value()
|
|
91
90
|
elif isinstance(element, Relationship):
|
|
92
|
-
|
|
93
|
-
for match in element.matches:
|
|
91
|
+
for j, match in enumerate(element.matches):
|
|
94
92
|
yield match
|
|
95
|
-
if
|
|
93
|
+
if j < len(element.matches) - 1:
|
|
96
94
|
yield match["endNode"]
|
|
97
|
-
i += 1
|
|
98
95
|
|
|
99
96
|
async def fetch_data(self) -> None:
|
|
100
97
|
"""Loads data from the database for all elements."""
|
|
101
|
-
from .database import Database
|
|
102
|
-
from .node import Node
|
|
103
|
-
from .relationship import Relationship
|
|
104
|
-
from .node_reference import NodeReference
|
|
105
|
-
from .relationship_reference import RelationshipReference
|
|
106
|
-
from .node_data import NodeData
|
|
107
|
-
from .relationship_data import RelationshipData
|
|
108
|
-
|
|
109
98
|
db = Database.get_instance()
|
|
110
99
|
for element in self._chain:
|
|
111
|
-
|
|
100
|
+
# Use type name comparison to avoid issues with module double-loading
|
|
101
|
+
if type(element).__name__ in ('NodeReference', 'RelationshipReference'):
|
|
112
102
|
continue
|
|
113
103
|
data = await db.get_data(element)
|
|
114
|
-
if isinstance(element, Node):
|
|
104
|
+
if isinstance(element, Node) and isinstance(data, NodeData):
|
|
115
105
|
element.set_data(data)
|
|
116
|
-
elif isinstance(element, Relationship):
|
|
106
|
+
elif isinstance(element, Relationship) and isinstance(data, RelationshipData):
|
|
117
107
|
element.set_data(data)
|
|
118
108
|
|
|
119
109
|
async def initialize(self) -> None:
|
|
@@ -121,5 +111,5 @@ class Pattern(ASTNode):
|
|
|
121
111
|
|
|
122
112
|
async def traverse(self) -> None:
|
|
123
113
|
first = self.first_node()
|
|
124
|
-
if first:
|
|
114
|
+
if first and isinstance(first, Node):
|
|
125
115
|
await first.next()
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
"""Pattern expression for FlowQuery."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Union
|
|
4
4
|
|
|
5
5
|
from ..parsing.ast_node import ASTNode
|
|
6
|
+
from .node import Node
|
|
6
7
|
from .node_reference import NodeReference
|
|
7
8
|
from .pattern import Pattern
|
|
9
|
+
from .relationship import Relationship
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class PatternExpression(Pattern):
|
|
11
13
|
"""Represents a pattern expression that can be evaluated.
|
|
12
|
-
|
|
14
|
+
|
|
13
15
|
PatternExpression is used in WHERE clauses to test whether a graph pattern
|
|
14
16
|
exists. It evaluates to True if the pattern is matched, False otherwise.
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
|
-
def __init__(self):
|
|
19
|
+
def __init__(self) -> None:
|
|
18
20
|
super().__init__()
|
|
19
21
|
self._fetched: bool = False
|
|
20
22
|
self._evaluation: bool = False
|
|
21
23
|
|
|
22
|
-
def add_element(self, element) -> None:
|
|
24
|
+
def add_element(self, element: Union[Node, Relationship]) -> None:
|
|
23
25
|
super().add_element(element)
|
|
24
26
|
|
|
25
27
|
def verify(self) -> None:
|
|
@@ -29,11 +31,11 @@ class PatternExpression(Pattern):
|
|
|
29
31
|
raise ValueError("PatternExpression must contain at least one NodeReference")
|
|
30
32
|
|
|
31
33
|
@property
|
|
32
|
-
def identifier(self):
|
|
34
|
+
def identifier(self) -> None:
|
|
33
35
|
return None
|
|
34
36
|
|
|
35
37
|
@identifier.setter
|
|
36
|
-
def identifier(self, value):
|
|
38
|
+
def identifier(self, value: str) -> None:
|
|
37
39
|
raise ValueError("Cannot set identifier on PatternExpression")
|
|
38
40
|
|
|
39
41
|
async def fetch_data(self) -> None:
|
|
@@ -45,18 +47,18 @@ class PatternExpression(Pattern):
|
|
|
45
47
|
|
|
46
48
|
async def evaluate(self) -> None:
|
|
47
49
|
"""Evaluates the pattern expression by traversing the graph.
|
|
48
|
-
|
|
50
|
+
|
|
49
51
|
Sets _evaluation to True if the pattern is matched, False otherwise.
|
|
50
52
|
"""
|
|
51
53
|
self._evaluation = False
|
|
52
|
-
|
|
53
|
-
async def set_evaluation_true():
|
|
54
|
+
|
|
55
|
+
async def set_evaluation_true() -> None:
|
|
54
56
|
self._evaluation = True
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
self.end_node.todo_next = set_evaluation_true
|
|
57
59
|
await self.start_node.next()
|
|
58
60
|
|
|
59
|
-
def value(self) ->
|
|
61
|
+
def value(self) -> Any:
|
|
60
62
|
"""Returns the result of the pattern evaluation."""
|
|
61
63
|
return self._evaluation
|
|
62
64
|
|
|
@@ -8,7 +8,7 @@ from .pattern import Pattern
|
|
|
8
8
|
class Patterns:
|
|
9
9
|
"""Manages a collection of graph patterns."""
|
|
10
10
|
|
|
11
|
-
def __init__(self, patterns: Optional[List[Pattern]] = None):
|
|
11
|
+
def __init__(self, patterns: Optional[List[Pattern]] = None) -> None:
|
|
12
12
|
self._patterns = patterns or []
|
|
13
13
|
self._to_do_next: Optional[Callable[[], Awaitable[None]]] = None
|
|
14
14
|
|
|
@@ -32,7 +32,7 @@ class Patterns:
|
|
|
32
32
|
await pattern.fetch_data() # Ensure data is loaded
|
|
33
33
|
if previous is not None:
|
|
34
34
|
# Chain the patterns together
|
|
35
|
-
async def next_pattern_start(p=pattern):
|
|
35
|
+
async def next_pattern_start(p: Pattern = pattern) -> None:
|
|
36
36
|
await p.start_node.next()
|
|
37
37
|
previous.end_node.todo_next = next_pattern_start
|
|
38
38
|
previous = pattern
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Physical node representation for FlowQuery."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
from ..parsing.ast_node import ASTNode
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
7
6
|
|
|
7
|
+
from ..parsing.ast_node import ASTNode
|
|
8
8
|
from .node import Node
|
|
9
9
|
|
|
10
10
|
|
|
@@ -34,6 +34,7 @@ class PhysicalNode(Node):
|
|
|
34
34
|
async def data(self) -> List[Dict[str, Any]]:
|
|
35
35
|
if self._statement is None:
|
|
36
36
|
raise ValueError("Statement is null")
|
|
37
|
+
# Import at runtime to avoid circular dependency
|
|
37
38
|
from ..compute.runner import Runner
|
|
38
39
|
runner = Runner(ast=self._statement)
|
|
39
40
|
await runner.run()
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
"""Physical relationship representation for FlowQuery."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
5
4
|
|
|
6
|
-
from
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
from ..parsing.ast_node import ASTNode
|
|
8
|
+
from .relationship import Relationship
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class PhysicalRelationship(Relationship):
|
|
13
12
|
"""Represents a physical relationship in the graph database."""
|
|
14
13
|
|
|
15
|
-
def __init__(self):
|
|
14
|
+
def __init__(self) -> None:
|
|
16
15
|
super().__init__()
|
|
17
16
|
self._statement: Optional[ASTNode] = None
|
|
18
17
|
|
|
@@ -30,6 +29,7 @@ class PhysicalRelationship(Relationship):
|
|
|
30
29
|
"""Execute the statement and return results."""
|
|
31
30
|
if self._statement is None:
|
|
32
31
|
raise ValueError("Statement is null")
|
|
32
|
+
# Import at runtime to avoid circular dependency
|
|
33
33
|
from ..compute.runner import Runner
|
|
34
34
|
runner = Runner(None, self._statement)
|
|
35
35
|
await runner.run()
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
"""Graph relationship representation for FlowQuery."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
4
6
|
|
|
5
7
|
from ..parsing.ast_node import ASTNode
|
|
6
8
|
from .hops import Hops
|
|
9
|
+
from .relationship_data import RelationshipData
|
|
7
10
|
from .relationship_match_collector import RelationshipMatchCollector, RelationshipMatchRecord
|
|
8
11
|
|
|
9
12
|
if TYPE_CHECKING:
|
|
10
13
|
from .node import Node
|
|
11
|
-
from .relationship_data import RelationshipData, RelationshipRecord
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class Relationship(ASTNode):
|
|
15
17
|
"""Represents a relationship in a graph pattern."""
|
|
16
18
|
|
|
17
|
-
def __init__(self):
|
|
19
|
+
def __init__(self) -> None:
|
|
18
20
|
super().__init__()
|
|
19
21
|
self._identifier: Optional[str] = None
|
|
20
22
|
self._type: Optional[str] = None
|
|
21
23
|
self._hops: Hops = Hops()
|
|
22
24
|
self._source: Optional['Node'] = None
|
|
23
25
|
self._target: Optional['Node'] = None
|
|
26
|
+
self._direction: str = "right"
|
|
24
27
|
self._data: Optional['RelationshipData'] = None
|
|
25
28
|
self._value: Optional[Union[RelationshipMatchRecord, List[RelationshipMatchRecord]]] = None
|
|
26
29
|
self._matches: RelationshipMatchCollector = RelationshipMatchCollector()
|
|
@@ -52,10 +55,26 @@ class Relationship(ASTNode):
|
|
|
52
55
|
|
|
53
56
|
@property
|
|
54
57
|
def properties(self) -> Dict[str, Any]:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
return self._properties
|
|
59
|
+
|
|
60
|
+
@properties.setter
|
|
61
|
+
def properties(self, value: Dict[str, Any]) -> None:
|
|
62
|
+
self._properties = value
|
|
63
|
+
|
|
64
|
+
def _matches_properties(self, hop: int = 0) -> bool:
|
|
65
|
+
"""Check if current record matches all constraint properties."""
|
|
66
|
+
if not self._properties:
|
|
67
|
+
return True
|
|
68
|
+
if self._data is None:
|
|
69
|
+
return True
|
|
70
|
+
for key, expression in self._properties.items():
|
|
71
|
+
record = self._data.current(hop)
|
|
72
|
+
if record is None:
|
|
73
|
+
raise ValueError("No current relationship data available")
|
|
74
|
+
if key not in record:
|
|
75
|
+
raise ValueError("Relationship does not have property")
|
|
76
|
+
return bool(record[key] == expression.value())
|
|
77
|
+
return True
|
|
59
78
|
|
|
60
79
|
@property
|
|
61
80
|
def source(self) -> Optional['Node']:
|
|
@@ -73,6 +92,14 @@ class Relationship(ASTNode):
|
|
|
73
92
|
def target(self, value: 'Node') -> None:
|
|
74
93
|
self._target = value
|
|
75
94
|
|
|
95
|
+
@property
|
|
96
|
+
def direction(self) -> str:
|
|
97
|
+
return self._direction
|
|
98
|
+
|
|
99
|
+
@direction.setter
|
|
100
|
+
def direction(self, value: str) -> None:
|
|
101
|
+
self._direction = value
|
|
102
|
+
|
|
76
103
|
# Keep start/end aliases for backward compatibility
|
|
77
104
|
@property
|
|
78
105
|
def start(self) -> Optional['Node']:
|
|
@@ -93,6 +120,9 @@ class Relationship(ASTNode):
|
|
|
93
120
|
def set_data(self, data: Optional['RelationshipData']) -> None:
|
|
94
121
|
self._data = data
|
|
95
122
|
|
|
123
|
+
def get_data(self) -> Optional['RelationshipData']:
|
|
124
|
+
return self._data
|
|
125
|
+
|
|
96
126
|
def set_value(self, relationship: 'Relationship') -> None:
|
|
97
127
|
"""Set value by pushing match to collector."""
|
|
98
128
|
self._matches.push(relationship)
|
|
@@ -113,23 +143,41 @@ class Relationship(ASTNode):
|
|
|
113
143
|
"""Find relationships starting from the given node ID."""
|
|
114
144
|
# Save original source node
|
|
115
145
|
original = self._source
|
|
146
|
+
is_left = self._direction == "left"
|
|
116
147
|
if hop > 0:
|
|
117
148
|
# For hops greater than 0, the source becomes the target of the previous hop
|
|
118
149
|
self._source = self._target
|
|
119
150
|
if hop == 0:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
if self._data:
|
|
152
|
+
self._data.reset()
|
|
153
|
+
|
|
154
|
+
# Handle zero-hop case: when min is 0 on a variable-length relationship,
|
|
155
|
+
# match source node as target (no traversal)
|
|
156
|
+
if self._hops and self._hops.multi() and self._hops.min == 0 and self._target:
|
|
157
|
+
# For zero-hop, target finds the same node as source (left_id)
|
|
158
|
+
# No relationship match is pushed since no edge is traversed
|
|
159
|
+
await self._target.find(left_id, hop)
|
|
160
|
+
|
|
161
|
+
def find_match(id_: str, h: int) -> bool:
|
|
162
|
+
if self._data is None:
|
|
163
|
+
return False
|
|
164
|
+
if is_left:
|
|
165
|
+
return self._data.find_reverse(id_, h)
|
|
166
|
+
return self._data.find(id_, h)
|
|
167
|
+
follow_id = 'left_id' if is_left else 'right_id'
|
|
168
|
+
while self._data and find_match(left_id, hop):
|
|
123
169
|
data = self._data.current(hop)
|
|
124
170
|
if data and self._hops and hop >= self._hops.min:
|
|
125
171
|
self.set_value(self)
|
|
126
|
-
if self.
|
|
127
|
-
|
|
172
|
+
if not self._matches_properties(hop):
|
|
173
|
+
continue
|
|
174
|
+
if self._target and follow_id in data:
|
|
175
|
+
await self._target.find(data[follow_id], hop)
|
|
128
176
|
if self._matches.is_circular():
|
|
129
177
|
raise ValueError("Circular relationship detected")
|
|
130
178
|
if self._hops and hop + 1 < self._hops.max:
|
|
131
|
-
await self.find(data[
|
|
179
|
+
await self.find(data[follow_id], hop + 1)
|
|
132
180
|
self._matches.pop()
|
|
133
|
-
|
|
181
|
+
|
|
134
182
|
# Restore original source node
|
|
135
183
|
self._source = original
|
|
@@ -12,15 +12,20 @@ class RelationshipRecord(TypedDict, total=False):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class RelationshipData(Data):
|
|
15
|
-
"""Relationship data class extending Data with left_id
|
|
15
|
+
"""Relationship data class extending Data with left_id and right_id indexing."""
|
|
16
16
|
|
|
17
17
|
def __init__(self, records: Optional[List[Dict[str, Any]]] = None):
|
|
18
18
|
super().__init__(records)
|
|
19
19
|
self._build_index("left_id")
|
|
20
|
+
self._build_index("right_id")
|
|
20
21
|
|
|
21
22
|
def find(self, left_id: str, hop: int = 0) -> bool:
|
|
22
23
|
"""Find a relationship by start node ID."""
|
|
23
|
-
return self._find(left_id, hop)
|
|
24
|
+
return self._find(left_id, hop, "left_id")
|
|
25
|
+
|
|
26
|
+
def find_reverse(self, right_id: str, hop: int = 0) -> bool:
|
|
27
|
+
"""Find a relationship by end node ID (reverse direction)."""
|
|
28
|
+
return self._find(right_id, hop, "right_id")
|
|
24
29
|
|
|
25
30
|
def properties(self) -> Optional[Dict[str, Any]]:
|
|
26
31
|
"""Get properties of current relationship, excluding left_id and right_id."""
|
|
@@ -1,43 +1,47 @@
|
|
|
1
1
|
"""Collector for relationship match records."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict, Union
|
|
4
6
|
|
|
5
7
|
if TYPE_CHECKING:
|
|
6
|
-
from .relationship import Relationship
|
|
7
8
|
from .node import Node
|
|
9
|
+
from .relationship import Relationship
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class RelationshipMatchRecord(TypedDict, total=False):
|
|
11
13
|
"""Represents a matched relationship record."""
|
|
12
14
|
type: str
|
|
13
|
-
startNode:
|
|
14
|
-
endNode:
|
|
15
|
+
startNode: Any
|
|
16
|
+
endNode: Any
|
|
15
17
|
properties: Dict[str, Any]
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class RelationshipMatchCollector:
|
|
19
21
|
"""Collects relationship matches during graph traversal."""
|
|
20
22
|
|
|
21
|
-
def __init__(self):
|
|
23
|
+
def __init__(self) -> None:
|
|
22
24
|
self._matches: List[RelationshipMatchRecord] = []
|
|
23
25
|
self._node_ids: List[str] = []
|
|
24
26
|
|
|
25
27
|
def push(self, relationship: 'Relationship') -> RelationshipMatchRecord:
|
|
26
28
|
"""Push a new match onto the collector."""
|
|
29
|
+
start_node_value = relationship.source.value() if relationship.source else None
|
|
30
|
+
rel_data = relationship.get_data()
|
|
31
|
+
rel_props: Dict[str, Any] = (rel_data.properties() or {}) if rel_data else {}
|
|
27
32
|
match: RelationshipMatchRecord = {
|
|
28
33
|
"type": relationship.type or "",
|
|
29
|
-
"startNode":
|
|
34
|
+
"startNode": start_node_value or {},
|
|
30
35
|
"endNode": None,
|
|
31
|
-
"properties":
|
|
36
|
+
"properties": rel_props,
|
|
32
37
|
}
|
|
33
38
|
self._matches.append(match)
|
|
34
|
-
start_node_value = match.get("startNode", {})
|
|
35
39
|
if isinstance(start_node_value, dict):
|
|
36
40
|
self._node_ids.append(start_node_value.get("id", ""))
|
|
37
41
|
return match
|
|
38
42
|
|
|
39
43
|
@property
|
|
40
|
-
def end_node(self) ->
|
|
44
|
+
def end_node(self) -> Any:
|
|
41
45
|
"""Get the end node of the last match."""
|
|
42
46
|
if self._matches:
|
|
43
47
|
return self._matches[-1].get("endNode")
|
|
@@ -47,7 +51,8 @@ class RelationshipMatchCollector:
|
|
|
47
51
|
def end_node(self, node: 'Node') -> None:
|
|
48
52
|
"""Set the end node of the last match."""
|
|
49
53
|
if self._matches:
|
|
50
|
-
|
|
54
|
+
node_value = node.value()
|
|
55
|
+
self._matches[-1]["endNode"] = node_value if node_value else None
|
|
51
56
|
|
|
52
57
|
def pop(self) -> Optional[RelationshipMatchRecord]:
|
|
53
58
|
"""Pop the last match from the collector."""
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
from typing import Any, Optional
|
|
2
2
|
|
|
3
|
-
from .relationship import Relationship
|
|
4
3
|
from ..parsing.ast_node import ASTNode
|
|
4
|
+
from .relationship import Relationship
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class RelationshipReference(Relationship):
|
|
8
8
|
"""Represents a reference to an existing relationship variable."""
|
|
9
9
|
|
|
10
|
-
def __init__(self, relationship: Relationship, referred: ASTNode):
|
|
10
|
+
def __init__(self, relationship: Relationship, referred: ASTNode) -> None:
|
|
11
11
|
super().__init__()
|
|
12
12
|
self._referred = referred
|
|
13
13
|
if relationship.type:
|
|
@@ -17,5 +17,5 @@ class RelationshipReference(Relationship):
|
|
|
17
17
|
def referred(self) -> ASTNode:
|
|
18
18
|
return self._referred
|
|
19
19
|
|
|
20
|
-
def value(self):
|
|
20
|
+
def value(self) -> Optional[Any]:
|
|
21
21
|
return self._referred.value() if self._referred else None
|
|
@@ -2,34 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from ..compute.runner import Runner
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class CommandLine:
|
|
11
10
|
"""Interactive command-line interface for FlowQuery.
|
|
12
|
-
|
|
11
|
+
|
|
13
12
|
Provides a REPL (Read-Eval-Print Loop) for executing FlowQuery statements
|
|
14
13
|
and displaying results.
|
|
15
|
-
|
|
14
|
+
|
|
16
15
|
Example:
|
|
17
16
|
cli = CommandLine()
|
|
18
17
|
cli.loop() # Starts interactive mode
|
|
19
|
-
|
|
18
|
+
|
|
20
19
|
# Or execute a single query:
|
|
21
20
|
cli.execute("load json from 'https://example.com/data' as d return d")
|
|
22
21
|
"""
|
|
23
22
|
|
|
24
23
|
def execute(self, query: str) -> None:
|
|
25
24
|
"""Execute a single FlowQuery statement and print results.
|
|
26
|
-
|
|
25
|
+
|
|
27
26
|
Args:
|
|
28
27
|
query: The FlowQuery statement to execute.
|
|
29
28
|
"""
|
|
30
29
|
# Remove the termination semicolon if present
|
|
31
30
|
query = query.strip().rstrip(";")
|
|
32
|
-
|
|
31
|
+
|
|
33
32
|
try:
|
|
34
33
|
runner = Runner(query)
|
|
35
34
|
asyncio.run(self._execute(runner))
|
|
@@ -38,13 +37,13 @@ class CommandLine:
|
|
|
38
37
|
|
|
39
38
|
def loop(self) -> None:
|
|
40
39
|
"""Starts the interactive command loop.
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
Prompts the user for FlowQuery statements, executes them, and displays results.
|
|
43
42
|
Type "exit" to quit the loop. End multi-line queries with ";".
|
|
44
43
|
"""
|
|
45
44
|
print('Welcome to FlowQuery! Type "exit" to quit.')
|
|
46
45
|
print('End queries with ";" to execute. Multi-line input supported.')
|
|
47
|
-
|
|
46
|
+
|
|
48
47
|
while True:
|
|
49
48
|
try:
|
|
50
49
|
lines = []
|
|
@@ -61,13 +60,13 @@ class CommandLine:
|
|
|
61
60
|
prompt = "... "
|
|
62
61
|
except EOFError:
|
|
63
62
|
break
|
|
64
|
-
|
|
63
|
+
|
|
65
64
|
if user_input.strip() == "":
|
|
66
65
|
continue
|
|
67
|
-
|
|
66
|
+
|
|
68
67
|
# Remove the termination semicolon before sending to the engine
|
|
69
68
|
user_input = user_input.strip().rstrip(";")
|
|
70
|
-
|
|
69
|
+
|
|
71
70
|
try:
|
|
72
71
|
runner = Runner(user_input)
|
|
73
72
|
asyncio.run(self._execute(runner))
|
|
@@ -83,7 +82,7 @@ class CommandLine:
|
|
|
83
82
|
|
|
84
83
|
def main() -> None:
|
|
85
84
|
"""Entry point for the flowquery CLI command.
|
|
86
|
-
|
|
85
|
+
|
|
87
86
|
Usage:
|
|
88
87
|
flowquery # Start interactive mode
|
|
89
88
|
flowquery -c "query" # Execute a single query
|
|
@@ -99,10 +98,10 @@ def main() -> None:
|
|
|
99
98
|
metavar="QUERY",
|
|
100
99
|
help="Execute a FlowQuery statement and exit"
|
|
101
100
|
)
|
|
102
|
-
|
|
101
|
+
|
|
103
102
|
args = parser.parse_args()
|
|
104
103
|
cli = CommandLine()
|
|
105
|
-
|
|
104
|
+
|
|
106
105
|
if args.command:
|
|
107
106
|
cli.execute(args.command)
|
|
108
107
|
else:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Parsing module for FlowQuery."""
|
|
2
2
|
|
|
3
|
-
from .ast_node import ASTNode
|
|
4
|
-
from .context import Context
|
|
5
3
|
from .alias import Alias
|
|
6
4
|
from .alias_option import AliasOption
|
|
5
|
+
from .ast_node import ASTNode
|
|
7
6
|
from .base_parser import BaseParser
|
|
7
|
+
from .context import Context
|
|
8
8
|
from .parser import Parser
|
|
9
9
|
|
|
10
10
|
__all__ = [
|