flowquery 1.0.38 → 1.0.40
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/dist/flowquery.min.js +1 -1
- package/dist/graph/database.d.ts +2 -0
- package/dist/graph/database.d.ts.map +1 -1
- package/dist/graph/database.js +12 -0
- package/dist/graph/database.js.map +1 -1
- package/dist/parsing/expressions/operator.js +4 -4
- package/dist/parsing/expressions/operator.js.map +1 -1
- package/dist/parsing/functions/function_factory.d.ts +1 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +1 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/substring.d.ts +9 -0
- package/dist/parsing/functions/substring.d.ts.map +1 -0
- package/dist/parsing/functions/substring.js +62 -0
- package/dist/parsing/functions/substring.js.map +1 -0
- package/dist/parsing/operations/aggregated_return.d.ts.map +1 -1
- package/dist/parsing/operations/aggregated_return.js +6 -2
- package/dist/parsing/operations/aggregated_return.js.map +1 -1
- package/dist/parsing/operations/delete_node.d.ts +11 -0
- package/dist/parsing/operations/delete_node.d.ts.map +1 -0
- package/dist/parsing/operations/delete_node.js +46 -0
- package/dist/parsing/operations/delete_node.js.map +1 -0
- package/dist/parsing/operations/delete_relationship.d.ts +11 -0
- package/dist/parsing/operations/delete_relationship.d.ts.map +1 -0
- package/dist/parsing/operations/delete_relationship.js +46 -0
- package/dist/parsing/operations/delete_relationship.js.map +1 -0
- package/dist/parsing/operations/limit.d.ts +1 -0
- package/dist/parsing/operations/limit.d.ts.map +1 -1
- package/dist/parsing/operations/limit.js +3 -0
- package/dist/parsing/operations/limit.js.map +1 -1
- package/dist/parsing/operations/order_by.d.ts +35 -0
- package/dist/parsing/operations/order_by.d.ts.map +1 -0
- package/dist/parsing/operations/order_by.js +87 -0
- package/dist/parsing/operations/order_by.js.map +1 -0
- package/dist/parsing/operations/return.d.ts +3 -0
- package/dist/parsing/operations/return.d.ts.map +1 -1
- package/dist/parsing/operations/return.js +16 -3
- package/dist/parsing/operations/return.js.map +1 -1
- package/dist/parsing/parser.d.ts +2 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +116 -2
- package/dist/parsing/parser.js.map +1 -1
- package/dist/tokenization/token.d.ts +8 -0
- package/dist/tokenization/token.d.ts.map +1 -1
- package/dist/tokenization/token.js +24 -0
- package/dist/tokenization/token.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/database.py +12 -0
- package/flowquery-py/src/parsing/expressions/operator.py +4 -4
- package/flowquery-py/src/parsing/functions/__init__.py +2 -0
- package/flowquery-py/src/parsing/functions/substring.py +74 -0
- package/flowquery-py/src/parsing/operations/__init__.py +7 -0
- package/flowquery-py/src/parsing/operations/aggregated_return.py +4 -1
- package/flowquery-py/src/parsing/operations/delete_node.py +29 -0
- package/flowquery-py/src/parsing/operations/delete_relationship.py +29 -0
- package/flowquery-py/src/parsing/operations/limit.py +4 -0
- package/flowquery-py/src/parsing/operations/order_by.py +72 -0
- package/flowquery-py/src/parsing/operations/return_op.py +20 -3
- package/flowquery-py/src/parsing/parser.py +98 -3
- package/flowquery-py/src/tokenization/token.py +28 -0
- package/flowquery-py/tests/compute/test_runner.py +329 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/database.ts +12 -0
- package/src/parsing/expressions/operator.ts +4 -4
- package/src/parsing/functions/function_factory.ts +1 -0
- package/src/parsing/functions/substring.ts +65 -0
- package/src/parsing/operations/aggregated_return.ts +9 -5
- package/src/parsing/operations/delete_node.ts +33 -0
- package/src/parsing/operations/delete_relationship.ts +32 -0
- package/src/parsing/operations/limit.ts +3 -0
- package/src/parsing/operations/order_by.ts +75 -0
- package/src/parsing/operations/return.ts +17 -3
- package/src/parsing/parser.ts +115 -2
- package/src/tokenization/token.ts +32 -0
- package/tests/compute/runner.test.ts +291 -0
- package/tests/parsing/parser.test.ts +1 -1
|
@@ -37,6 +37,12 @@ class Database:
|
|
|
37
37
|
physical.statement = statement
|
|
38
38
|
Database._nodes[node.label] = physical
|
|
39
39
|
|
|
40
|
+
def remove_node(self, node: 'Node') -> None:
|
|
41
|
+
"""Removes a node from the database."""
|
|
42
|
+
if node.label is None:
|
|
43
|
+
raise ValueError("Node label is null")
|
|
44
|
+
Database._nodes.pop(node.label, None)
|
|
45
|
+
|
|
40
46
|
def get_node(self, node: 'Node') -> Optional['PhysicalNode']:
|
|
41
47
|
"""Gets a node from the database."""
|
|
42
48
|
return Database._nodes.get(node.label) if node.label else None
|
|
@@ -54,6 +60,12 @@ class Database:
|
|
|
54
60
|
physical.target = relationship.target
|
|
55
61
|
Database._relationships[relationship.type] = physical
|
|
56
62
|
|
|
63
|
+
def remove_relationship(self, relationship: 'Relationship') -> None:
|
|
64
|
+
"""Removes a relationship from the database."""
|
|
65
|
+
if relationship.type is None:
|
|
66
|
+
raise ValueError("Relationship type is null")
|
|
67
|
+
Database._relationships.pop(relationship.type, None)
|
|
68
|
+
|
|
57
69
|
def get_relationship(self, relationship: 'Relationship') -> Optional['PhysicalRelationship']:
|
|
58
70
|
"""Gets a relationship from the database."""
|
|
59
71
|
return Database._relationships.get(relationship.type) if relationship.type else None
|
|
@@ -163,7 +163,7 @@ class Not(Operator):
|
|
|
163
163
|
|
|
164
164
|
class Is(Operator):
|
|
165
165
|
def __init__(self) -> None:
|
|
166
|
-
super().__init__(
|
|
166
|
+
super().__init__(0, True)
|
|
167
167
|
|
|
168
168
|
def value(self) -> int:
|
|
169
169
|
return 1 if self.lhs.value() == self.rhs.value() else 0
|
|
@@ -171,7 +171,7 @@ class Is(Operator):
|
|
|
171
171
|
|
|
172
172
|
class IsNot(Operator):
|
|
173
173
|
def __init__(self) -> None:
|
|
174
|
-
super().__init__(
|
|
174
|
+
super().__init__(0, True)
|
|
175
175
|
|
|
176
176
|
def value(self) -> int:
|
|
177
177
|
return 1 if self.lhs.value() != self.rhs.value() else 0
|
|
@@ -179,7 +179,7 @@ class IsNot(Operator):
|
|
|
179
179
|
|
|
180
180
|
class In(Operator):
|
|
181
181
|
def __init__(self) -> None:
|
|
182
|
-
super().__init__(
|
|
182
|
+
super().__init__(0, True)
|
|
183
183
|
|
|
184
184
|
def value(self) -> int:
|
|
185
185
|
lst = self.rhs.value()
|
|
@@ -190,7 +190,7 @@ class In(Operator):
|
|
|
190
190
|
|
|
191
191
|
class NotIn(Operator):
|
|
192
192
|
def __init__(self) -> None:
|
|
193
|
-
super().__init__(
|
|
193
|
+
super().__init__(0, True)
|
|
194
194
|
|
|
195
195
|
def value(self) -> int:
|
|
196
196
|
lst = self.rhs.value()
|
|
@@ -48,6 +48,7 @@ from .size import Size
|
|
|
48
48
|
from .split import Split
|
|
49
49
|
from .string_distance import StringDistance
|
|
50
50
|
from .stringify import Stringify
|
|
51
|
+
from .substring import Substring
|
|
51
52
|
from .sum import Sum
|
|
52
53
|
from .tail import Tail
|
|
53
54
|
from .time_ import Time
|
|
@@ -107,6 +108,7 @@ __all__ = [
|
|
|
107
108
|
"Split",
|
|
108
109
|
"StringDistance",
|
|
109
110
|
"Stringify",
|
|
111
|
+
"Substring",
|
|
110
112
|
"Tail",
|
|
111
113
|
"Time",
|
|
112
114
|
"Timestamp",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Substring function."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, List
|
|
4
|
+
|
|
5
|
+
from ..ast_node import ASTNode
|
|
6
|
+
from .function import Function
|
|
7
|
+
from .function_metadata import FunctionDef
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@FunctionDef({
|
|
11
|
+
"description": "Returns a substring of a string, starting at a 0-based index with an optional length",
|
|
12
|
+
"category": "scalar",
|
|
13
|
+
"parameters": [
|
|
14
|
+
{"name": "original", "description": "The original string", "type": "string"},
|
|
15
|
+
{"name": "start", "description": "The 0-based start index", "type": "integer"},
|
|
16
|
+
{
|
|
17
|
+
"name": "length",
|
|
18
|
+
"description": "The length of the substring (optional)",
|
|
19
|
+
"type": "integer",
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"output": {"description": "The substring", "type": "string", "example": "llo"},
|
|
23
|
+
"examples": [
|
|
24
|
+
"RETURN substring('hello', 1, 3)",
|
|
25
|
+
"RETURN substring('hello', 2)"
|
|
26
|
+
]
|
|
27
|
+
})
|
|
28
|
+
class Substring(Function):
|
|
29
|
+
"""Substring function.
|
|
30
|
+
|
|
31
|
+
Returns a substring of a string, starting at a 0-based index with an optional length.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
super().__init__("substring")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def parameters(self) -> List[ASTNode]:
|
|
39
|
+
return self.get_children()
|
|
40
|
+
|
|
41
|
+
@parameters.setter
|
|
42
|
+
def parameters(self, nodes: List[ASTNode]) -> None:
|
|
43
|
+
if len(nodes) < 2 or len(nodes) > 3:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Function substring expected 2 or 3 parameters, but got {len(nodes)}"
|
|
46
|
+
)
|
|
47
|
+
for node in nodes:
|
|
48
|
+
self.add_child(node)
|
|
49
|
+
|
|
50
|
+
def value(self) -> Any:
|
|
51
|
+
children = self.get_children()
|
|
52
|
+
original = children[0].value()
|
|
53
|
+
start = children[1].value()
|
|
54
|
+
|
|
55
|
+
if not isinstance(original, str):
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"Invalid argument for substring function: expected a string as the first argument"
|
|
58
|
+
)
|
|
59
|
+
if not isinstance(start, (int, float)) or (isinstance(start, float) and not start.is_integer()):
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"Invalid argument for substring function: expected an integer as the second argument"
|
|
62
|
+
)
|
|
63
|
+
start = int(start)
|
|
64
|
+
|
|
65
|
+
if len(children) == 3:
|
|
66
|
+
length = children[2].value()
|
|
67
|
+
if not isinstance(length, (int, float)) or (isinstance(length, float) and not length.is_integer()):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"Invalid argument for substring function: expected an integer as the third argument"
|
|
70
|
+
)
|
|
71
|
+
length = int(length)
|
|
72
|
+
return original[start:start + length]
|
|
73
|
+
|
|
74
|
+
return original[start:]
|
|
@@ -5,11 +5,14 @@ from .aggregated_with import AggregatedWith
|
|
|
5
5
|
from .call import Call
|
|
6
6
|
from .create_node import CreateNode
|
|
7
7
|
from .create_relationship import CreateRelationship
|
|
8
|
+
from .delete_node import DeleteNode
|
|
9
|
+
from .delete_relationship import DeleteRelationship
|
|
8
10
|
from .group_by import GroupBy
|
|
9
11
|
from .limit import Limit
|
|
10
12
|
from .load import Load
|
|
11
13
|
from .match import Match
|
|
12
14
|
from .operation import Operation
|
|
15
|
+
from .order_by import OrderBy, SortField
|
|
13
16
|
from .projection import Projection
|
|
14
17
|
from .return_op import Return
|
|
15
18
|
from .union import Union
|
|
@@ -34,6 +37,10 @@ __all__ = [
|
|
|
34
37
|
"Match",
|
|
35
38
|
"CreateNode",
|
|
36
39
|
"CreateRelationship",
|
|
40
|
+
"DeleteNode",
|
|
41
|
+
"DeleteRelationship",
|
|
37
42
|
"Union",
|
|
38
43
|
"UnionAll",
|
|
44
|
+
"OrderBy",
|
|
45
|
+
"SortField",
|
|
39
46
|
]
|
|
@@ -19,4 +19,7 @@ class AggregatedReturn(Return):
|
|
|
19
19
|
def results(self) -> List[Dict[str, Any]]:
|
|
20
20
|
if self._where is not None:
|
|
21
21
|
self._group_by.where = self._where
|
|
22
|
-
|
|
22
|
+
results = list(self._group_by.generate_results())
|
|
23
|
+
if self._order_by is not None:
|
|
24
|
+
results = self._order_by.sort(results)
|
|
25
|
+
return results
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Represents a DELETE operation for deleting virtual nodes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from ...graph.database import Database
|
|
6
|
+
from ...graph.node import Node
|
|
7
|
+
from .operation import Operation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeleteNode(Operation):
|
|
11
|
+
"""Represents a DELETE operation for deleting virtual nodes."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, node: Node) -> None:
|
|
14
|
+
super().__init__()
|
|
15
|
+
self._node = node
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def node(self) -> Node:
|
|
19
|
+
return self._node
|
|
20
|
+
|
|
21
|
+
async def run(self) -> None:
|
|
22
|
+
if self._node is None:
|
|
23
|
+
raise ValueError("Node is null")
|
|
24
|
+
db = Database.get_instance()
|
|
25
|
+
db.remove_node(self._node)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def results(self) -> List[Dict[str, Any]]:
|
|
29
|
+
return []
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Represents a DELETE operation for deleting virtual relationships."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from ...graph.database import Database
|
|
6
|
+
from ...graph.relationship import Relationship
|
|
7
|
+
from .operation import Operation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeleteRelationship(Operation):
|
|
11
|
+
"""Represents a DELETE operation for deleting virtual relationships."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, relationship: Relationship) -> None:
|
|
14
|
+
super().__init__()
|
|
15
|
+
self._relationship = relationship
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def relationship(self) -> Relationship:
|
|
19
|
+
return self._relationship
|
|
20
|
+
|
|
21
|
+
async def run(self) -> None:
|
|
22
|
+
if self._relationship is None:
|
|
23
|
+
raise ValueError("Relationship is null")
|
|
24
|
+
db = Database.get_instance()
|
|
25
|
+
db.remove_relationship(self._relationship)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def results(self) -> List[Dict[str, Any]]:
|
|
29
|
+
return []
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Represents an ORDER BY operation that sorts results."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from .operation import Operation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SortField:
|
|
9
|
+
"""A single sort specification: field name and direction."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, field: str, direction: str = "asc"):
|
|
12
|
+
self.field = field
|
|
13
|
+
self.direction = direction
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OrderBy(Operation):
|
|
17
|
+
"""Represents an ORDER BY operation that sorts results.
|
|
18
|
+
|
|
19
|
+
Can be attached to a RETURN operation (sorting its results),
|
|
20
|
+
or used as a standalone accumulating operation after a non-aggregate WITH.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
RETURN x ORDER BY x DESC
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, fields: List[SortField]):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self._fields = fields
|
|
29
|
+
self._results: List[Dict[str, Any]] = []
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def fields(self) -> List[SortField]:
|
|
33
|
+
return self._fields
|
|
34
|
+
|
|
35
|
+
def sort(self, records: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
36
|
+
"""Sorts an array of records according to the sort fields."""
|
|
37
|
+
import functools
|
|
38
|
+
|
|
39
|
+
def compare(a: Dict[str, Any], b: Dict[str, Any]) -> int:
|
|
40
|
+
for sf in self._fields:
|
|
41
|
+
a_val = a.get(sf.field)
|
|
42
|
+
b_val = b.get(sf.field)
|
|
43
|
+
cmp = 0
|
|
44
|
+
if a_val is None and b_val is None:
|
|
45
|
+
cmp = 0
|
|
46
|
+
elif a_val is None:
|
|
47
|
+
cmp = -1
|
|
48
|
+
elif b_val is None:
|
|
49
|
+
cmp = 1
|
|
50
|
+
elif a_val < b_val:
|
|
51
|
+
cmp = -1
|
|
52
|
+
elif a_val > b_val:
|
|
53
|
+
cmp = 1
|
|
54
|
+
if cmp != 0:
|
|
55
|
+
return -cmp if sf.direction == "desc" else cmp
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
return sorted(records, key=functools.cmp_to_key(compare))
|
|
59
|
+
|
|
60
|
+
async def run(self) -> None:
|
|
61
|
+
"""When used as a standalone operation, passes through to next."""
|
|
62
|
+
if self.next:
|
|
63
|
+
await self.next.run()
|
|
64
|
+
|
|
65
|
+
async def initialize(self) -> None:
|
|
66
|
+
self._results = []
|
|
67
|
+
if self.next:
|
|
68
|
+
await self.next.initialize()
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def results(self) -> List[Dict[str, Any]]:
|
|
72
|
+
return self._results
|
|
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
|
5
5
|
|
|
6
6
|
from ..ast_node import ASTNode
|
|
7
7
|
from .limit import Limit
|
|
8
|
+
from .order_by import OrderBy
|
|
8
9
|
from .projection import Projection
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -26,6 +27,7 @@ class Return(Projection):
|
|
|
26
27
|
self._where: Optional['Where'] = None
|
|
27
28
|
self._results: List[Dict[str, Any]] = []
|
|
28
29
|
self._limit: Optional[Limit] = None
|
|
30
|
+
self._order_by: Optional[OrderBy] = None
|
|
29
31
|
|
|
30
32
|
@property
|
|
31
33
|
def where(self) -> Any:
|
|
@@ -45,10 +47,20 @@ class Return(Projection):
|
|
|
45
47
|
def limit(self, limit: Limit) -> None:
|
|
46
48
|
self._limit = limit
|
|
47
49
|
|
|
50
|
+
@property
|
|
51
|
+
def order_by(self) -> Optional[OrderBy]:
|
|
52
|
+
return self._order_by
|
|
53
|
+
|
|
54
|
+
@order_by.setter
|
|
55
|
+
def order_by(self, order_by: OrderBy) -> None:
|
|
56
|
+
self._order_by = order_by
|
|
57
|
+
|
|
48
58
|
async def run(self) -> None:
|
|
49
59
|
if not self.where:
|
|
50
60
|
return
|
|
51
|
-
|
|
61
|
+
# When ORDER BY is present, skip limit during accumulation;
|
|
62
|
+
# limit will be applied after sorting in results property
|
|
63
|
+
if self._order_by is None and self._limit is not None and self._limit.is_limit_reached:
|
|
52
64
|
return
|
|
53
65
|
record: Dict[str, Any] = {}
|
|
54
66
|
for expression, alias in self.expressions():
|
|
@@ -57,7 +69,7 @@ class Return(Projection):
|
|
|
57
69
|
value = copy.deepcopy(raw) if isinstance(raw, (dict, list)) else raw
|
|
58
70
|
record[alias] = value
|
|
59
71
|
self._results.append(record)
|
|
60
|
-
if self._limit is not None:
|
|
72
|
+
if self._order_by is None and self._limit is not None:
|
|
61
73
|
self._limit.increment()
|
|
62
74
|
|
|
63
75
|
async def initialize(self) -> None:
|
|
@@ -65,4 +77,9 @@ class Return(Projection):
|
|
|
65
77
|
|
|
66
78
|
@property
|
|
67
79
|
def results(self) -> List[Dict[str, Any]]:
|
|
68
|
-
|
|
80
|
+
result = self._results
|
|
81
|
+
if self._order_by is not None:
|
|
82
|
+
result = self._order_by.sort(result)
|
|
83
|
+
if self._order_by is not None and self._limit is not None:
|
|
84
|
+
result = result[:self._limit.limit_value]
|
|
85
|
+
return result
|
|
@@ -57,10 +57,13 @@ from .operations.aggregated_with import AggregatedWith
|
|
|
57
57
|
from .operations.call import Call
|
|
58
58
|
from .operations.create_node import CreateNode
|
|
59
59
|
from .operations.create_relationship import CreateRelationship
|
|
60
|
+
from .operations.delete_node import DeleteNode
|
|
61
|
+
from .operations.delete_relationship import DeleteRelationship
|
|
60
62
|
from .operations.limit import Limit
|
|
61
63
|
from .operations.load import Load
|
|
62
64
|
from .operations.match import Match
|
|
63
65
|
from .operations.operation import Operation
|
|
66
|
+
from .operations.order_by import OrderBy, SortField
|
|
64
67
|
from .operations.return_op import Return
|
|
65
68
|
from .operations.union import Union
|
|
66
69
|
from .operations.union_all import UnionAll
|
|
@@ -146,6 +149,14 @@ class Parser(BaseParser):
|
|
|
146
149
|
operation.add_sibling(where)
|
|
147
150
|
operation = where
|
|
148
151
|
|
|
152
|
+
order_by = self._parse_order_by()
|
|
153
|
+
if order_by is not None:
|
|
154
|
+
if isinstance(operation, Return):
|
|
155
|
+
operation.order_by = order_by
|
|
156
|
+
else:
|
|
157
|
+
operation.add_sibling(order_by)
|
|
158
|
+
operation = order_by
|
|
159
|
+
|
|
149
160
|
limit = self._parse_limit()
|
|
150
161
|
if limit is not None:
|
|
151
162
|
if isinstance(operation, Return):
|
|
@@ -176,8 +187,8 @@ class Parser(BaseParser):
|
|
|
176
187
|
new_root.add_child(union)
|
|
177
188
|
return new_root
|
|
178
189
|
|
|
179
|
-
if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship)):
|
|
180
|
-
raise ValueError("Last statement must be a RETURN, WHERE, CALL, or
|
|
190
|
+
if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship, DeleteNode, DeleteRelationship)):
|
|
191
|
+
raise ValueError("Last statement must be a RETURN, WHERE, CALL, CREATE, or DELETE statement")
|
|
181
192
|
|
|
182
193
|
return root
|
|
183
194
|
|
|
@@ -189,7 +200,8 @@ class Parser(BaseParser):
|
|
|
189
200
|
self._parse_load() or
|
|
190
201
|
self._parse_call() or
|
|
191
202
|
self._parse_match() or
|
|
192
|
-
self._parse_create()
|
|
203
|
+
self._parse_create() or
|
|
204
|
+
self._parse_delete()
|
|
193
205
|
)
|
|
194
206
|
|
|
195
207
|
def _parse_with(self) -> Optional[With]:
|
|
@@ -422,6 +434,54 @@ class Parser(BaseParser):
|
|
|
422
434
|
else:
|
|
423
435
|
return CreateNode(node, query)
|
|
424
436
|
|
|
437
|
+
def _parse_delete(self) -> Optional[Operation]:
|
|
438
|
+
"""Parse DELETE VIRTUAL statement for nodes and relationships."""
|
|
439
|
+
if not self.token.is_delete():
|
|
440
|
+
return None
|
|
441
|
+
self.set_next_token()
|
|
442
|
+
self._expect_and_skip_whitespace_and_comments()
|
|
443
|
+
if not self.token.is_virtual():
|
|
444
|
+
raise ValueError("Expected VIRTUAL")
|
|
445
|
+
self.set_next_token()
|
|
446
|
+
self._expect_and_skip_whitespace_and_comments()
|
|
447
|
+
|
|
448
|
+
node = self._parse_node()
|
|
449
|
+
if node is None:
|
|
450
|
+
raise ValueError("Expected node definition")
|
|
451
|
+
|
|
452
|
+
relationship: Optional[Relationship] = None
|
|
453
|
+
if self.token.is_subtract() and self.peek() and self.peek().is_opening_bracket():
|
|
454
|
+
self.set_next_token() # skip -
|
|
455
|
+
self.set_next_token() # skip [
|
|
456
|
+
if not self.token.is_colon():
|
|
457
|
+
raise ValueError("Expected ':' for relationship type")
|
|
458
|
+
self.set_next_token()
|
|
459
|
+
if not self.token.is_identifier_or_keyword():
|
|
460
|
+
raise ValueError("Expected relationship type identifier")
|
|
461
|
+
rel_type = self.token.value or ""
|
|
462
|
+
self.set_next_token()
|
|
463
|
+
if not self.token.is_closing_bracket():
|
|
464
|
+
raise ValueError("Expected closing bracket for relationship definition")
|
|
465
|
+
self.set_next_token()
|
|
466
|
+
if not self.token.is_subtract():
|
|
467
|
+
raise ValueError("Expected '-' for relationship definition")
|
|
468
|
+
self.set_next_token()
|
|
469
|
+
# Skip optional direction indicator '>'
|
|
470
|
+
if self.token.is_greater_than():
|
|
471
|
+
self.set_next_token()
|
|
472
|
+
target = self._parse_node()
|
|
473
|
+
if target is None:
|
|
474
|
+
raise ValueError("Expected target node definition")
|
|
475
|
+
relationship = Relationship()
|
|
476
|
+
relationship.type = rel_type
|
|
477
|
+
relationship.source = node
|
|
478
|
+
relationship.target = target
|
|
479
|
+
|
|
480
|
+
if relationship is not None:
|
|
481
|
+
return DeleteRelationship(relationship)
|
|
482
|
+
else:
|
|
483
|
+
return DeleteNode(node)
|
|
484
|
+
|
|
425
485
|
def _parse_union(self) -> Optional[Union]:
|
|
426
486
|
"""Parse a UNION or UNION ALL keyword."""
|
|
427
487
|
if not self.token.is_union():
|
|
@@ -694,6 +754,41 @@ class Parser(BaseParser):
|
|
|
694
754
|
self.set_next_token()
|
|
695
755
|
return limit
|
|
696
756
|
|
|
757
|
+
def _parse_order_by(self) -> Optional[OrderBy]:
|
|
758
|
+
self._skip_whitespace_and_comments()
|
|
759
|
+
if not self.token.is_order():
|
|
760
|
+
return None
|
|
761
|
+
self._expect_previous_token_to_be_whitespace_or_comment()
|
|
762
|
+
self.set_next_token()
|
|
763
|
+
self._expect_and_skip_whitespace_and_comments()
|
|
764
|
+
if not self.token.is_by():
|
|
765
|
+
raise ValueError("Expected BY after ORDER")
|
|
766
|
+
self.set_next_token()
|
|
767
|
+
self._expect_and_skip_whitespace_and_comments()
|
|
768
|
+
fields: list[SortField] = []
|
|
769
|
+
while True:
|
|
770
|
+
if not self.token.is_identifier_or_keyword():
|
|
771
|
+
raise ValueError("Expected field name in ORDER BY")
|
|
772
|
+
field = self.token.value
|
|
773
|
+
self.set_next_token()
|
|
774
|
+
self._skip_whitespace_and_comments()
|
|
775
|
+
direction = "asc"
|
|
776
|
+
if self.token.is_asc():
|
|
777
|
+
direction = "asc"
|
|
778
|
+
self.set_next_token()
|
|
779
|
+
self._skip_whitespace_and_comments()
|
|
780
|
+
elif self.token.is_desc():
|
|
781
|
+
direction = "desc"
|
|
782
|
+
self.set_next_token()
|
|
783
|
+
self._skip_whitespace_and_comments()
|
|
784
|
+
fields.append(SortField(field, direction))
|
|
785
|
+
if self.token.is_comma():
|
|
786
|
+
self.set_next_token()
|
|
787
|
+
self._skip_whitespace_and_comments()
|
|
788
|
+
else:
|
|
789
|
+
break
|
|
790
|
+
return OrderBy(fields)
|
|
791
|
+
|
|
697
792
|
def _parse_expressions(
|
|
698
793
|
self, alias_option: AliasOption = AliasOption.NOT_ALLOWED
|
|
699
794
|
) -> Iterator[Expression]:
|
|
@@ -630,6 +630,34 @@ class Token:
|
|
|
630
630
|
def is_all(self) -> bool:
|
|
631
631
|
return self._type == TokenType.KEYWORD and self._value == Keyword.ALL.value
|
|
632
632
|
|
|
633
|
+
@staticmethod
|
|
634
|
+
def ORDER() -> Token:
|
|
635
|
+
return Token(TokenType.KEYWORD, Keyword.ORDER.value)
|
|
636
|
+
|
|
637
|
+
def is_order(self) -> bool:
|
|
638
|
+
return self._type == TokenType.KEYWORD and self._value == Keyword.ORDER.value
|
|
639
|
+
|
|
640
|
+
@staticmethod
|
|
641
|
+
def BY() -> Token:
|
|
642
|
+
return Token(TokenType.KEYWORD, Keyword.BY.value)
|
|
643
|
+
|
|
644
|
+
def is_by(self) -> bool:
|
|
645
|
+
return self._type == TokenType.KEYWORD and self._value == Keyword.BY.value
|
|
646
|
+
|
|
647
|
+
@staticmethod
|
|
648
|
+
def ASC() -> Token:
|
|
649
|
+
return Token(TokenType.KEYWORD, Keyword.ASC.value)
|
|
650
|
+
|
|
651
|
+
def is_asc(self) -> bool:
|
|
652
|
+
return self._type == TokenType.KEYWORD and self._value == Keyword.ASC.value
|
|
653
|
+
|
|
654
|
+
@staticmethod
|
|
655
|
+
def DESC() -> Token:
|
|
656
|
+
return Token(TokenType.KEYWORD, Keyword.DESC.value)
|
|
657
|
+
|
|
658
|
+
def is_desc(self) -> bool:
|
|
659
|
+
return self._type == TokenType.KEYWORD and self._value == Keyword.DESC.value
|
|
660
|
+
|
|
633
661
|
# End of file token
|
|
634
662
|
|
|
635
663
|
@staticmethod
|