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,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flowquery"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.11"
|
|
4
4
|
description = "A declarative query language for data processing pipelines"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -38,6 +38,11 @@ Issues = "https://github.com/microsoft/FlowQuery/issues"
|
|
|
38
38
|
dev = [
|
|
39
39
|
"pytest>=7.0.0",
|
|
40
40
|
"pytest-asyncio>=0.21.0",
|
|
41
|
+
"jupyter>=1.0.0",
|
|
42
|
+
"ipykernel>=6.0.0",
|
|
43
|
+
"nbstripout>=0.6.0",
|
|
44
|
+
"mypy>=1.0.0",
|
|
45
|
+
"ruff>=0.1.0",
|
|
41
46
|
]
|
|
42
47
|
|
|
43
48
|
[build-system]
|
|
@@ -72,4 +77,45 @@ python_functions = ["test_*"]
|
|
|
72
77
|
addopts = "-v --tb=short"
|
|
73
78
|
|
|
74
79
|
[tool.pytest-asyncio]
|
|
75
|
-
mode = "auto"
|
|
80
|
+
mode = "auto"
|
|
81
|
+
|
|
82
|
+
[tool.mypy]
|
|
83
|
+
python_version = "3.10"
|
|
84
|
+
strict = true
|
|
85
|
+
ignore_missing_imports = true
|
|
86
|
+
exclude = [
|
|
87
|
+
"tests/",
|
|
88
|
+
"__pycache__",
|
|
89
|
+
".git",
|
|
90
|
+
"build",
|
|
91
|
+
"dist",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[[tool.mypy.overrides]]
|
|
95
|
+
module = "src.parsing.parser"
|
|
96
|
+
warn_return_any = false
|
|
97
|
+
disable_error_code = ["union-attr", "arg-type"]
|
|
98
|
+
|
|
99
|
+
[tool.ruff]
|
|
100
|
+
target-version = "py310"
|
|
101
|
+
line-length = 120
|
|
102
|
+
exclude = [
|
|
103
|
+
".git",
|
|
104
|
+
"__pycache__",
|
|
105
|
+
"build",
|
|
106
|
+
"dist",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
[tool.ruff.lint]
|
|
110
|
+
select = [
|
|
111
|
+
"F", # Pyflakes (includes F401 unused imports)
|
|
112
|
+
"E", # pycodestyle errors
|
|
113
|
+
"W", # pycodestyle warnings
|
|
114
|
+
"I", # isort
|
|
115
|
+
]
|
|
116
|
+
ignore = [
|
|
117
|
+
"E501", # line too long (handled by formatter)
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
[tool.ruff.lint.isort]
|
|
121
|
+
known-first-party = ["flowquery", "src"]
|
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
FlowQuery - A declarative query language for data processing pipelines.
|
|
3
3
|
|
|
4
4
|
This is the Python implementation of FlowQuery.
|
|
5
|
+
|
|
6
|
+
This module provides the core components for defining, parsing, and executing FlowQuery queries.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
from .compute.runner import Runner
|
|
8
10
|
from .io.command_line import CommandLine
|
|
9
|
-
from .parsing.parser import Parser
|
|
10
|
-
from .parsing.functions.function import Function
|
|
11
11
|
from .parsing.functions.aggregate_function import AggregateFunction
|
|
12
12
|
from .parsing.functions.async_function import AsyncFunction
|
|
13
|
-
from .parsing.functions.
|
|
14
|
-
from .parsing.functions.reducer_element import ReducerElement
|
|
13
|
+
from .parsing.functions.function import Function
|
|
15
14
|
from .parsing.functions.function_metadata import (
|
|
15
|
+
FunctionCategory,
|
|
16
16
|
FunctionDef,
|
|
17
17
|
FunctionMetadata,
|
|
18
|
-
FunctionCategory,
|
|
19
18
|
)
|
|
19
|
+
from .parsing.functions.predicate_function import PredicateFunction
|
|
20
|
+
from .parsing.functions.reducer_element import ReducerElement
|
|
21
|
+
from .parsing.parser import Parser
|
|
20
22
|
|
|
21
23
|
__all__ = [
|
|
22
24
|
"Runner",
|
|
@@ -9,10 +9,10 @@ from ..parsing.parser import Parser
|
|
|
9
9
|
|
|
10
10
|
class Runner:
|
|
11
11
|
"""Executes a FlowQuery statement and retrieves the results.
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
The Runner class parses a FlowQuery statement into an AST and executes it,
|
|
14
14
|
managing the execution flow from the first operation to the final return statement.
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
Example:
|
|
17
17
|
runner = Runner("WITH 1 as x RETURN x")
|
|
18
18
|
await runner.run()
|
|
@@ -25,24 +25,28 @@ class Runner:
|
|
|
25
25
|
ast: Optional[ASTNode] = None
|
|
26
26
|
):
|
|
27
27
|
"""Creates a new Runner instance and parses the FlowQuery statement.
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
Args:
|
|
30
30
|
statement: The FlowQuery statement to execute
|
|
31
31
|
ast: An already-parsed AST (optional)
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
Raises:
|
|
34
34
|
ValueError: If neither statement nor AST is provided
|
|
35
35
|
"""
|
|
36
36
|
if (statement is None or statement == "") and ast is None:
|
|
37
37
|
raise ValueError("Either statement or AST must be provided")
|
|
38
|
-
|
|
39
|
-
_ast = ast if ast is not None else Parser().parse(statement)
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
|
|
39
|
+
_ast = ast if ast is not None else Parser().parse(statement or "")
|
|
40
|
+
first = _ast.first_child()
|
|
41
|
+
last = _ast.last_child()
|
|
42
|
+
if not isinstance(first, Operation) or not isinstance(last, Operation):
|
|
43
|
+
raise ValueError("AST must contain Operations")
|
|
44
|
+
self._first: Operation = first
|
|
45
|
+
self._last: Operation = last
|
|
42
46
|
|
|
43
47
|
async def run(self) -> None:
|
|
44
48
|
"""Executes the parsed FlowQuery statement.
|
|
45
|
-
|
|
49
|
+
|
|
46
50
|
Raises:
|
|
47
51
|
Exception: If an error occurs during execution
|
|
48
52
|
"""
|
|
@@ -53,7 +57,7 @@ class Runner:
|
|
|
53
57
|
@property
|
|
54
58
|
def results(self) -> List[Dict[str, Any]]:
|
|
55
59
|
"""Gets the results from the executed statement.
|
|
56
|
-
|
|
60
|
+
|
|
57
61
|
Returns:
|
|
58
62
|
The results from the last operation (typically a RETURN statement)
|
|
59
63
|
"""
|
|
@@ -4,7 +4,7 @@ This module provides all the exports needed to create custom FlowQuery functions
|
|
|
4
4
|
|
|
5
5
|
Example:
|
|
6
6
|
from flowquery.extensibility import Function, FunctionDef
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
@FunctionDef({
|
|
9
9
|
'description': "Converts a string to uppercase",
|
|
10
10
|
'category': "string",
|
|
@@ -15,27 +15,27 @@ Example:
|
|
|
15
15
|
def __init__(self):
|
|
16
16
|
super().__init__("uppercase")
|
|
17
17
|
self._expected_parameter_count = 1
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
def value(self) -> str:
|
|
20
20
|
return str(self.get_children()[0].value()).upper()
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
# Base function classes for creating custom functions
|
|
24
|
-
from .parsing.functions.function import Function
|
|
25
24
|
from .parsing.functions.aggregate_function import AggregateFunction
|
|
26
25
|
from .parsing.functions.async_function import AsyncFunction
|
|
27
|
-
from .parsing.functions.
|
|
28
|
-
from .parsing.functions.reducer_element import ReducerElement
|
|
26
|
+
from .parsing.functions.function import Function
|
|
29
27
|
|
|
30
28
|
# Decorator and metadata types for function registration
|
|
31
29
|
from .parsing.functions.function_metadata import (
|
|
30
|
+
FunctionCategory,
|
|
32
31
|
FunctionDef,
|
|
33
|
-
FunctionMetadata,
|
|
34
32
|
FunctionDefOptions,
|
|
35
|
-
|
|
33
|
+
FunctionMetadata,
|
|
36
34
|
OutputSchema,
|
|
37
|
-
|
|
35
|
+
ParameterSchema,
|
|
38
36
|
)
|
|
37
|
+
from .parsing.functions.predicate_function import PredicateFunction
|
|
38
|
+
from .parsing.functions.reducer_element import ReducerElement
|
|
39
39
|
|
|
40
40
|
__all__ = [
|
|
41
41
|
"Function",
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
"""Graph module for FlowQuery."""
|
|
2
2
|
|
|
3
|
-
from .node import Node
|
|
4
|
-
from .relationship import Relationship
|
|
5
|
-
from .pattern import Pattern
|
|
6
|
-
from .patterns import Patterns
|
|
7
|
-
from .pattern_expression import PatternExpression
|
|
8
3
|
from .database import Database
|
|
9
4
|
from .hops import Hops
|
|
5
|
+
from .node import Node
|
|
10
6
|
from .node_data import NodeData
|
|
11
7
|
from .node_reference import NodeReference
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
8
|
+
from .pattern import Pattern
|
|
9
|
+
from .pattern_expression import PatternExpression
|
|
10
|
+
from .patterns import Patterns
|
|
14
11
|
from .physical_node import PhysicalNode
|
|
15
12
|
from .physical_relationship import PhysicalRelationship
|
|
13
|
+
from .relationship import Relationship
|
|
14
|
+
from .relationship_data import RelationshipData
|
|
15
|
+
from .relationship_reference import RelationshipReference
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"Node",
|
|
@@ -38,14 +38,20 @@ class IndexEntry:
|
|
|
38
38
|
class Layer:
|
|
39
39
|
"""Layer for managing index state at a specific level."""
|
|
40
40
|
|
|
41
|
-
def __init__(self,
|
|
42
|
-
self.
|
|
41
|
+
def __init__(self, indexes: Dict[str, Dict[str, IndexEntry]]):
|
|
42
|
+
self._indexes: Dict[str, Dict[str, IndexEntry]] = indexes
|
|
43
43
|
self._current: int = -1
|
|
44
44
|
|
|
45
|
+
def index(self, name: str) -> Dict[str, IndexEntry]:
|
|
46
|
+
"""Get or create an index by name."""
|
|
47
|
+
if name not in self._indexes:
|
|
48
|
+
self._indexes[name] = {}
|
|
49
|
+
return self._indexes[name]
|
|
50
|
+
|
|
45
51
|
@property
|
|
46
|
-
def
|
|
47
|
-
"""Get
|
|
48
|
-
return self.
|
|
52
|
+
def indexes(self) -> Dict[str, Dict[str, IndexEntry]]:
|
|
53
|
+
"""Get all indexes."""
|
|
54
|
+
return self._indexes
|
|
49
55
|
|
|
50
56
|
@property
|
|
51
57
|
def current(self) -> int:
|
|
@@ -67,30 +73,40 @@ class Data:
|
|
|
67
73
|
|
|
68
74
|
def _build_index(self, key: str, level: int = 0) -> None:
|
|
69
75
|
"""Build an index for the given key at the specified level."""
|
|
70
|
-
self.layer(level).index
|
|
71
|
-
|
|
76
|
+
idx = self.layer(level).index(key)
|
|
77
|
+
idx.clear()
|
|
78
|
+
for i, record in enumerate(self._records):
|
|
72
79
|
if key in record:
|
|
73
|
-
if record[key] not in
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
if record[key] not in idx:
|
|
81
|
+
idx[record[key]] = IndexEntry()
|
|
82
|
+
idx[record[key]].add(i)
|
|
76
83
|
|
|
77
84
|
def layer(self, level: int = 0) -> Layer:
|
|
78
85
|
"""Get or create a layer at the specified level."""
|
|
79
86
|
if level not in self._layers:
|
|
80
87
|
first = self._layers[0]
|
|
81
|
-
|
|
82
|
-
for
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
cloned_indexes = {}
|
|
89
|
+
for name, index_map in first.indexes.items():
|
|
90
|
+
cloned_map = {}
|
|
91
|
+
for key, entry in index_map.items():
|
|
92
|
+
cloned_map[key] = entry.clone()
|
|
93
|
+
cloned_indexes[name] = cloned_map
|
|
94
|
+
self._layers[level] = Layer(cloned_indexes)
|
|
85
95
|
return self._layers[level]
|
|
86
96
|
|
|
87
|
-
def _find(self, key: str, level: int = 0) -> bool:
|
|
97
|
+
def _find(self, key: str, level: int = 0, index_name: Optional[str] = None) -> bool:
|
|
88
98
|
"""Find the next record with the given key value."""
|
|
89
|
-
|
|
99
|
+
idx: Optional[Dict[str, IndexEntry]] = None
|
|
100
|
+
if index_name:
|
|
101
|
+
idx = self.layer(level).index(index_name)
|
|
102
|
+
else:
|
|
103
|
+
indexes = self.layer(level).indexes
|
|
104
|
+
idx = next(iter(indexes.values())) if indexes else None
|
|
105
|
+
if not idx or key not in idx:
|
|
90
106
|
self.layer(level).current = len(self._records) # Move to end
|
|
91
107
|
return False
|
|
92
108
|
else:
|
|
93
|
-
entry =
|
|
109
|
+
entry = idx[key]
|
|
94
110
|
more = entry.next()
|
|
95
111
|
if not more:
|
|
96
112
|
self.layer(level).current = len(self._records) # Move to end
|
|
@@ -100,9 +116,11 @@ class Data:
|
|
|
100
116
|
|
|
101
117
|
def reset(self) -> None:
|
|
102
118
|
"""Reset iteration to the beginning."""
|
|
103
|
-
self.
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
for layer in self._layers.values():
|
|
120
|
+
layer.current = -1
|
|
121
|
+
for index_map in layer.indexes.values():
|
|
122
|
+
for entry in index_map.values():
|
|
123
|
+
entry.reset()
|
|
106
124
|
|
|
107
125
|
def next(self, level: int = 0) -> bool:
|
|
108
126
|
"""Move to the next record. Returns True if successful."""
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"""Graph database for FlowQuery."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from typing import Dict, Optional, Union
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
from ..parsing.ast_node import ASTNode
|
|
8
|
+
from .node import Node
|
|
9
|
+
from .node_data import NodeData
|
|
10
|
+
from .physical_node import PhysicalNode
|
|
11
|
+
from .physical_relationship import PhysicalRelationship
|
|
12
|
+
from .relationship import Relationship
|
|
13
|
+
from .relationship_data import RelationshipData
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class Database:
|
|
@@ -18,7 +20,7 @@ class Database:
|
|
|
18
20
|
_nodes: Dict[str, 'PhysicalNode'] = {}
|
|
19
21
|
_relationships: Dict[str, 'PhysicalRelationship'] = {}
|
|
20
22
|
|
|
21
|
-
def __init__(self):
|
|
23
|
+
def __init__(self) -> None:
|
|
22
24
|
pass
|
|
23
25
|
|
|
24
26
|
@classmethod
|
|
@@ -29,7 +31,6 @@ class Database:
|
|
|
29
31
|
|
|
30
32
|
def add_node(self, node: 'Node', statement: ASTNode) -> None:
|
|
31
33
|
"""Adds a node to the database."""
|
|
32
|
-
from .physical_node import PhysicalNode
|
|
33
34
|
if node.label is None:
|
|
34
35
|
raise ValueError("Node label is null")
|
|
35
36
|
physical = PhysicalNode(None, node.label)
|
|
@@ -42,7 +43,6 @@ class Database:
|
|
|
42
43
|
|
|
43
44
|
def add_relationship(self, relationship: 'Relationship', statement: ASTNode) -> None:
|
|
44
45
|
"""Adds a relationship to the database."""
|
|
45
|
-
from .physical_relationship import PhysicalRelationship
|
|
46
46
|
if relationship.type is None:
|
|
47
47
|
raise ValueError("Relationship type is null")
|
|
48
48
|
physical = PhysicalRelationship()
|
|
@@ -56,11 +56,6 @@ class Database:
|
|
|
56
56
|
|
|
57
57
|
async def get_data(self, element: Union['Node', 'Relationship']) -> Union['NodeData', 'RelationshipData']:
|
|
58
58
|
"""Gets data for a node or relationship."""
|
|
59
|
-
from .node import Node
|
|
60
|
-
from .relationship import Relationship
|
|
61
|
-
from .node_data import NodeData
|
|
62
|
-
from .relationship_data import RelationshipData
|
|
63
|
-
|
|
64
59
|
if isinstance(element, Node):
|
|
65
60
|
node = self.get_node(element)
|
|
66
61
|
if node is None:
|
|
@@ -75,8 +70,3 @@ class Database:
|
|
|
75
70
|
return RelationshipData(data)
|
|
76
71
|
else:
|
|
77
72
|
raise ValueError("Element is neither Node nor Relationship")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# Import for type hints
|
|
81
|
-
from .physical_node import PhysicalNode
|
|
82
|
-
from .physical_relationship import PhysicalRelationship
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""Graph node representation for FlowQuery."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Union
|
|
4
6
|
|
|
5
7
|
from ..parsing.ast_node import ASTNode
|
|
6
8
|
from ..parsing.expressions.expression import Expression
|
|
9
|
+
from .node_data import NodeData, NodeRecord
|
|
7
10
|
|
|
8
11
|
if TYPE_CHECKING:
|
|
9
12
|
from .relationship import Relationship
|
|
10
|
-
from .node_data import NodeData, NodeRecord
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class Node(ASTNode):
|
|
@@ -26,7 +28,7 @@ class Node(ASTNode):
|
|
|
26
28
|
self._incoming: Optional['Relationship'] = None
|
|
27
29
|
self._outgoing: Optional['Relationship'] = None
|
|
28
30
|
self._data: Optional['NodeData'] = None
|
|
29
|
-
self._todo_next: Optional[Callable[[], None]] = None
|
|
31
|
+
self._todo_next: Optional[Callable[[], Union[None, Awaitable[None]]]] = None
|
|
30
32
|
|
|
31
33
|
@property
|
|
32
34
|
def identifier(self) -> Optional[str]:
|
|
@@ -41,21 +43,40 @@ class Node(ASTNode):
|
|
|
41
43
|
return self._label
|
|
42
44
|
|
|
43
45
|
@label.setter
|
|
44
|
-
def label(self, value: str) -> None:
|
|
46
|
+
def label(self, value: Optional[str]) -> None:
|
|
45
47
|
self._label = value
|
|
46
48
|
|
|
47
49
|
@property
|
|
48
50
|
def properties(self) -> Dict[str, Expression]:
|
|
49
51
|
return self._properties
|
|
50
52
|
|
|
53
|
+
@properties.setter
|
|
54
|
+
def properties(self, value: Dict[str, Expression]) -> None:
|
|
55
|
+
self._properties = value
|
|
56
|
+
|
|
51
57
|
def set_property(self, key: str, value: Expression) -> None:
|
|
52
58
|
self._properties[key] = value
|
|
53
59
|
|
|
54
60
|
def get_property(self, key: str) -> Optional[Expression]:
|
|
55
61
|
return self._properties.get(key)
|
|
56
62
|
|
|
57
|
-
def
|
|
58
|
-
|
|
63
|
+
def _matches_properties(self, hop: int = 0) -> bool:
|
|
64
|
+
"""Check if current record matches all constraint properties."""
|
|
65
|
+
if not self._properties:
|
|
66
|
+
return True
|
|
67
|
+
if self._data is None:
|
|
68
|
+
return True
|
|
69
|
+
for key, expression in self._properties.items():
|
|
70
|
+
record = self._data.current(hop)
|
|
71
|
+
if record is None:
|
|
72
|
+
raise ValueError("No current node data available")
|
|
73
|
+
if key not in record:
|
|
74
|
+
raise ValueError("Node does not have property")
|
|
75
|
+
return bool(record[key] == expression.value())
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def set_value(self, value: Dict[str, Any]) -> None:
|
|
79
|
+
self._value = value # type: ignore[assignment]
|
|
59
80
|
|
|
60
81
|
def value(self) -> Optional['NodeRecord']:
|
|
61
82
|
return self._value
|
|
@@ -83,30 +104,40 @@ class Node(ASTNode):
|
|
|
83
104
|
if self._data:
|
|
84
105
|
self._data.reset()
|
|
85
106
|
while self._data.next():
|
|
86
|
-
self.
|
|
87
|
-
if
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
current = self._data.current()
|
|
108
|
+
if current is not None:
|
|
109
|
+
self.set_value(current)
|
|
110
|
+
if not self._matches_properties():
|
|
111
|
+
continue
|
|
112
|
+
if self._outgoing and self._value:
|
|
113
|
+
await self._outgoing.find(self._value['id'])
|
|
114
|
+
await self.run_todo_next()
|
|
90
115
|
|
|
91
116
|
async def find(self, id_: str, hop: int = 0) -> None:
|
|
92
117
|
if self._data:
|
|
93
118
|
self._data.reset()
|
|
94
119
|
while self._data.find(id_, hop):
|
|
95
|
-
self.
|
|
96
|
-
if
|
|
97
|
-
self.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
current = self._data.current(hop)
|
|
121
|
+
if current is not None:
|
|
122
|
+
self.set_value(current)
|
|
123
|
+
if not self._matches_properties(hop):
|
|
124
|
+
continue
|
|
125
|
+
if self._incoming:
|
|
126
|
+
self._incoming.set_end_node(self)
|
|
127
|
+
if self._outgoing and self._value:
|
|
128
|
+
await self._outgoing.find(self._value['id'], hop)
|
|
129
|
+
await self.run_todo_next()
|
|
101
130
|
|
|
102
131
|
@property
|
|
103
|
-
def todo_next(self) -> Optional[Callable[[], None]]:
|
|
132
|
+
def todo_next(self) -> Optional[Callable[[], Union[None, Awaitable[None]]]]:
|
|
104
133
|
return self._todo_next
|
|
105
134
|
|
|
106
135
|
@todo_next.setter
|
|
107
|
-
def todo_next(self, func: Optional[Callable[[], None]]) -> None:
|
|
136
|
+
def todo_next(self, func: Optional[Callable[[], Union[None, Awaitable[None]]]]) -> None:
|
|
108
137
|
self._todo_next = func
|
|
109
138
|
|
|
110
139
|
async def run_todo_next(self) -> None:
|
|
111
140
|
if self._todo_next:
|
|
112
|
-
|
|
141
|
+
result = self._todo_next()
|
|
142
|
+
if result is not None:
|
|
143
|
+
await result
|
|
@@ -19,7 +19,7 @@ class NodeData(Data):
|
|
|
19
19
|
|
|
20
20
|
def find(self, id_: str, hop: int = 0) -> bool:
|
|
21
21
|
"""Find a record by ID."""
|
|
22
|
-
return self._find(id_, hop)
|
|
22
|
+
return self._find(id_, hop, "id")
|
|
23
23
|
|
|
24
24
|
def current(self, hop: int = 0) -> Optional[Dict[str, Any]]:
|
|
25
25
|
"""Get the current record."""
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any, Optional
|
|
4
4
|
|
|
5
5
|
from .node import Node
|
|
6
6
|
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from ..parsing.ast_node import ASTNode
|
|
9
|
-
|
|
10
7
|
|
|
11
8
|
class NodeReference(Node):
|
|
12
9
|
"""Represents a reference to an existing node variable."""
|
|
13
10
|
|
|
14
|
-
def __init__(self, base: Node, reference: Node):
|
|
11
|
+
def __init__(self, base: Node, reference: Node) -> None:
|
|
15
12
|
super().__init__(base.identifier, base.label)
|
|
16
13
|
self._reference: Node = reference
|
|
17
14
|
# Copy properties from base
|
|
@@ -28,14 +25,16 @@ class NodeReference(Node):
|
|
|
28
25
|
def referred(self) -> Node:
|
|
29
26
|
return self._reference
|
|
30
27
|
|
|
31
|
-
def value(self):
|
|
28
|
+
def value(self) -> Optional[Any]:
|
|
32
29
|
return self._reference.value() if self._reference else None
|
|
33
30
|
|
|
34
31
|
async def next(self) -> None:
|
|
35
32
|
"""Process next using the referenced node's value."""
|
|
36
|
-
self.
|
|
37
|
-
if
|
|
38
|
-
|
|
33
|
+
ref_value = self._reference.value()
|
|
34
|
+
if ref_value is not None:
|
|
35
|
+
self.set_value(dict(ref_value))
|
|
36
|
+
if self._outgoing and self._value:
|
|
37
|
+
await self._outgoing.find(self._value['id'])
|
|
39
38
|
await self.run_todo_next()
|
|
40
39
|
|
|
41
40
|
async def find(self, id_: str, hop: int = 0) -> None:
|
|
@@ -43,7 +42,7 @@ class NodeReference(Node):
|
|
|
43
42
|
referenced = self._reference.value()
|
|
44
43
|
if referenced is None or id_ != referenced.get('id'):
|
|
45
44
|
return
|
|
46
|
-
self.set_value(referenced)
|
|
45
|
+
self.set_value(dict(referenced))
|
|
47
46
|
if self._outgoing and self._value:
|
|
48
47
|
await self._outgoing.find(self._value['id'], hop)
|
|
49
48
|
await self.run_todo_next()
|