flowquery 1.0.18 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.gitattributes +3 -0
  2. package/.github/workflows/python-publish.yml +56 -4
  3. package/.github/workflows/release.yml +26 -19
  4. package/.husky/pre-commit +26 -0
  5. package/README.md +37 -32
  6. package/dist/flowquery.min.js +1 -1
  7. package/dist/graph/data.d.ts +5 -4
  8. package/dist/graph/data.d.ts.map +1 -1
  9. package/dist/graph/data.js +38 -20
  10. package/dist/graph/data.js.map +1 -1
  11. package/dist/graph/node.d.ts +2 -0
  12. package/dist/graph/node.d.ts.map +1 -1
  13. package/dist/graph/node.js +23 -0
  14. package/dist/graph/node.js.map +1 -1
  15. package/dist/graph/node_data.js +1 -1
  16. package/dist/graph/node_data.js.map +1 -1
  17. package/dist/graph/pattern.d.ts.map +1 -1
  18. package/dist/graph/pattern.js +11 -4
  19. package/dist/graph/pattern.js.map +1 -1
  20. package/dist/graph/relationship.d.ts +6 -1
  21. package/dist/graph/relationship.d.ts.map +1 -1
  22. package/dist/graph/relationship.js +43 -5
  23. package/dist/graph/relationship.js.map +1 -1
  24. package/dist/graph/relationship_data.d.ts +2 -0
  25. package/dist/graph/relationship_data.d.ts.map +1 -1
  26. package/dist/graph/relationship_data.js +8 -1
  27. package/dist/graph/relationship_data.js.map +1 -1
  28. package/dist/graph/relationship_match_collector.js +2 -2
  29. package/dist/graph/relationship_match_collector.js.map +1 -1
  30. package/dist/graph/relationship_reference.d.ts.map +1 -1
  31. package/dist/graph/relationship_reference.js +2 -1
  32. package/dist/graph/relationship_reference.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/parsing/parser.d.ts +6 -0
  36. package/dist/parsing/parser.d.ts.map +1 -1
  37. package/dist/parsing/parser.js +139 -72
  38. package/dist/parsing/parser.js.map +1 -1
  39. package/docs/flowquery.min.js +1 -1
  40. package/flowquery-py/misc/data/test.json +10 -0
  41. package/flowquery-py/misc/data/users.json +242 -0
  42. package/flowquery-py/notebooks/TestFlowQuery.ipynb +440 -0
  43. package/flowquery-py/pyproject.toml +48 -2
  44. package/flowquery-py/src/__init__.py +7 -5
  45. package/flowquery-py/src/compute/runner.py +14 -10
  46. package/flowquery-py/src/extensibility.py +8 -8
  47. package/flowquery-py/src/graph/__init__.py +7 -7
  48. package/flowquery-py/src/graph/data.py +38 -20
  49. package/flowquery-py/src/graph/database.py +10 -20
  50. package/flowquery-py/src/graph/node.py +50 -19
  51. package/flowquery-py/src/graph/node_data.py +1 -1
  52. package/flowquery-py/src/graph/node_reference.py +10 -11
  53. package/flowquery-py/src/graph/pattern.py +27 -37
  54. package/flowquery-py/src/graph/pattern_expression.py +13 -11
  55. package/flowquery-py/src/graph/patterns.py +2 -2
  56. package/flowquery-py/src/graph/physical_node.py +4 -3
  57. package/flowquery-py/src/graph/physical_relationship.py +5 -5
  58. package/flowquery-py/src/graph/relationship.py +62 -14
  59. package/flowquery-py/src/graph/relationship_data.py +7 -2
  60. package/flowquery-py/src/graph/relationship_match_collector.py +15 -10
  61. package/flowquery-py/src/graph/relationship_reference.py +4 -4
  62. package/flowquery-py/src/io/command_line.py +13 -14
  63. package/flowquery-py/src/parsing/__init__.py +2 -2
  64. package/flowquery-py/src/parsing/alias_option.py +1 -1
  65. package/flowquery-py/src/parsing/ast_node.py +21 -20
  66. package/flowquery-py/src/parsing/base_parser.py +7 -7
  67. package/flowquery-py/src/parsing/components/__init__.py +3 -3
  68. package/flowquery-py/src/parsing/components/from_.py +3 -1
  69. package/flowquery-py/src/parsing/components/headers.py +2 -2
  70. package/flowquery-py/src/parsing/components/null.py +2 -2
  71. package/flowquery-py/src/parsing/context.py +7 -7
  72. package/flowquery-py/src/parsing/data_structures/associative_array.py +7 -7
  73. package/flowquery-py/src/parsing/data_structures/json_array.py +3 -3
  74. package/flowquery-py/src/parsing/data_structures/key_value_pair.py +4 -4
  75. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -2
  76. package/flowquery-py/src/parsing/data_structures/range_lookup.py +2 -2
  77. package/flowquery-py/src/parsing/expressions/__init__.py +16 -16
  78. package/flowquery-py/src/parsing/expressions/expression.py +16 -13
  79. package/flowquery-py/src/parsing/expressions/expression_map.py +9 -9
  80. package/flowquery-py/src/parsing/expressions/f_string.py +3 -3
  81. package/flowquery-py/src/parsing/expressions/identifier.py +4 -3
  82. package/flowquery-py/src/parsing/expressions/number.py +3 -3
  83. package/flowquery-py/src/parsing/expressions/operator.py +16 -16
  84. package/flowquery-py/src/parsing/expressions/reference.py +3 -3
  85. package/flowquery-py/src/parsing/expressions/string.py +2 -2
  86. package/flowquery-py/src/parsing/functions/__init__.py +17 -17
  87. package/flowquery-py/src/parsing/functions/aggregate_function.py +8 -8
  88. package/flowquery-py/src/parsing/functions/async_function.py +12 -9
  89. package/flowquery-py/src/parsing/functions/avg.py +4 -4
  90. package/flowquery-py/src/parsing/functions/collect.py +6 -6
  91. package/flowquery-py/src/parsing/functions/function.py +6 -6
  92. package/flowquery-py/src/parsing/functions/function_factory.py +31 -34
  93. package/flowquery-py/src/parsing/functions/function_metadata.py +10 -11
  94. package/flowquery-py/src/parsing/functions/functions.py +14 -6
  95. package/flowquery-py/src/parsing/functions/join.py +3 -3
  96. package/flowquery-py/src/parsing/functions/keys.py +3 -3
  97. package/flowquery-py/src/parsing/functions/predicate_function.py +8 -7
  98. package/flowquery-py/src/parsing/functions/predicate_sum.py +12 -7
  99. package/flowquery-py/src/parsing/functions/rand.py +2 -2
  100. package/flowquery-py/src/parsing/functions/range_.py +9 -4
  101. package/flowquery-py/src/parsing/functions/replace.py +2 -2
  102. package/flowquery-py/src/parsing/functions/round_.py +2 -2
  103. package/flowquery-py/src/parsing/functions/size.py +2 -2
  104. package/flowquery-py/src/parsing/functions/split.py +9 -4
  105. package/flowquery-py/src/parsing/functions/stringify.py +3 -3
  106. package/flowquery-py/src/parsing/functions/sum.py +4 -4
  107. package/flowquery-py/src/parsing/functions/to_json.py +2 -2
  108. package/flowquery-py/src/parsing/functions/type_.py +3 -3
  109. package/flowquery-py/src/parsing/functions/value_holder.py +1 -1
  110. package/flowquery-py/src/parsing/logic/__init__.py +2 -2
  111. package/flowquery-py/src/parsing/logic/case.py +0 -1
  112. package/flowquery-py/src/parsing/logic/when.py +3 -1
  113. package/flowquery-py/src/parsing/operations/__init__.py +10 -10
  114. package/flowquery-py/src/parsing/operations/aggregated_return.py +3 -5
  115. package/flowquery-py/src/parsing/operations/aggregated_with.py +4 -4
  116. package/flowquery-py/src/parsing/operations/call.py +6 -7
  117. package/flowquery-py/src/parsing/operations/create_node.py +5 -4
  118. package/flowquery-py/src/parsing/operations/create_relationship.py +5 -4
  119. package/flowquery-py/src/parsing/operations/group_by.py +18 -16
  120. package/flowquery-py/src/parsing/operations/load.py +21 -19
  121. package/flowquery-py/src/parsing/operations/match.py +8 -7
  122. package/flowquery-py/src/parsing/operations/operation.py +3 -3
  123. package/flowquery-py/src/parsing/operations/projection.py +6 -6
  124. package/flowquery-py/src/parsing/operations/return_op.py +9 -5
  125. package/flowquery-py/src/parsing/operations/unwind.py +3 -2
  126. package/flowquery-py/src/parsing/operations/where.py +9 -7
  127. package/flowquery-py/src/parsing/operations/with_op.py +2 -2
  128. package/flowquery-py/src/parsing/parser.py +178 -114
  129. package/flowquery-py/src/parsing/token_to_node.py +2 -2
  130. package/flowquery-py/src/tokenization/__init__.py +4 -4
  131. package/flowquery-py/src/tokenization/keyword.py +1 -1
  132. package/flowquery-py/src/tokenization/operator.py +1 -1
  133. package/flowquery-py/src/tokenization/string_walker.py +4 -4
  134. package/flowquery-py/src/tokenization/symbol.py +1 -1
  135. package/flowquery-py/src/tokenization/token.py +11 -11
  136. package/flowquery-py/src/tokenization/token_mapper.py +10 -9
  137. package/flowquery-py/src/tokenization/token_type.py +1 -1
  138. package/flowquery-py/src/tokenization/tokenizer.py +19 -19
  139. package/flowquery-py/src/tokenization/trie.py +18 -17
  140. package/flowquery-py/src/utils/__init__.py +1 -1
  141. package/flowquery-py/src/utils/object_utils.py +3 -3
  142. package/flowquery-py/src/utils/string_utils.py +12 -12
  143. package/flowquery-py/tests/compute/test_runner.py +214 -7
  144. package/flowquery-py/tests/parsing/test_parser.py +41 -0
  145. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  146. package/package.json +1 -1
  147. package/src/graph/data.ts +38 -20
  148. package/src/graph/node.ts +23 -0
  149. package/src/graph/node_data.ts +1 -1
  150. package/src/graph/pattern.ts +13 -4
  151. package/src/graph/relationship.ts +45 -5
  152. package/src/graph/relationship_data.ts +8 -1
  153. package/src/graph/relationship_match_collector.ts +1 -1
  154. package/src/graph/relationship_reference.ts +2 -1
  155. package/src/index.ts +5 -5
  156. package/src/parsing/parser.ts +139 -71
  157. package/tests/compute/runner.test.ts +249 -79
  158. package/tests/parsing/parser.test.ts +32 -0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowquery"
3
- version = "1.0.7"
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.predicate_function import PredicateFunction
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
- self._first: Operation = _ast.first_child()
41
- self._last: Operation = _ast.last_child()
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.predicate_function import PredicateFunction
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
- ParameterSchema,
33
+ FunctionMetadata,
36
34
  OutputSchema,
37
- FunctionCategory,
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 .relationship_data import RelationshipData
13
- from .relationship_reference import RelationshipReference
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, index: Dict[str, IndexEntry]):
42
- self._index: Dict[str, IndexEntry] = index
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 index(self) -> Dict[str, IndexEntry]:
47
- """Get the index dictionary."""
48
- return self._index
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.clear()
71
- for idx, record in enumerate(self._records):
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 self.layer(level).index:
74
- self.layer(level).index[record[key]] = IndexEntry()
75
- self.layer(level).index[record[key]].add(idx)
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
- cloned = {}
82
- for key, entry in first.index.items():
83
- cloned[key] = entry.clone()
84
- self._layers[level] = Layer(cloned)
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
- if key not in self.layer(level).index:
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 = self.layer(level).index[key]
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.layer(0).current = -1
104
- for entry in self.layer(0).index.values():
105
- entry.reset()
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 typing import Any, Dict, Optional, Union, TYPE_CHECKING
3
+ from __future__ import annotations
4
4
 
5
- from ..parsing.ast_node import ASTNode
5
+ from typing import Dict, Optional, Union
6
6
 
7
- if TYPE_CHECKING:
8
- from .node import Node
9
- from .relationship import Relationship
10
- from .node_data import NodeData
11
- from .relationship_data import RelationshipData
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 typing import Any, Callable, Dict, Optional, TYPE_CHECKING
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 set_value(self, value: 'NodeRecord') -> None:
58
- self._value = value
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.set_value(self._data.current())
87
- if self._outgoing:
88
- await self._outgoing.find(self._value['id'])
89
- await self.run_todo_next()
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.set_value(self._data.current(hop))
96
- if self._incoming:
97
- self._incoming.set_end_node(self)
98
- if self._outgoing:
99
- await self._outgoing.find(self._value['id'], hop)
100
- await self.run_todo_next()
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
- await self._todo_next()
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
- """Node reference for FlowQuery."""
1
+ from __future__ import annotations
2
2
 
3
- from typing import Optional, TYPE_CHECKING
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.set_value(self._reference.value())
37
- if self._outgoing and self._value:
38
- await self._outgoing.find(self._value['id'])
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()