Skip to content

Commit 579cc5e

Browse files
authored
Merge pull request #36 from microsoft/improvements
Improvements
2 parents 4191f0a + 53c2ed1 commit 579cc5e

File tree

16 files changed

+729
-7
lines changed

16 files changed

+729
-7
lines changed

flowquery-py/src/graph/database.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def add_node(self, node: 'Node', statement: ASTNode) -> None:
3737
physical.statement = statement
3838
Database._nodes[node.label] = physical
3939

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+
4046
def get_node(self, node: 'Node') -> Optional['PhysicalNode']:
4147
"""Gets a node from the database."""
4248
return Database._nodes.get(node.label) if node.label else None
@@ -54,6 +60,12 @@ def add_relationship(self, relationship: 'Relationship', statement: ASTNode) ->
5460
physical.target = relationship.target
5561
Database._relationships[relationship.type] = physical
5662

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+
5769
def get_relationship(self, relationship: 'Relationship') -> Optional['PhysicalRelationship']:
5870
"""Gets a relationship from the database."""
5971
return Database._relationships.get(relationship.type) if relationship.type else None

flowquery-py/src/parsing/functions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from .split import Split
4949
from .string_distance import StringDistance
5050
from .stringify import Stringify
51+
from .substring import Substring
5152
from .sum import Sum
5253
from .tail import Tail
5354
from .time_ import Time
@@ -107,6 +108,7 @@
107108
"Split",
108109
"StringDistance",
109110
"Stringify",
111+
"Substring",
110112
"Tail",
111113
"Time",
112114
"Timestamp",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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:]

flowquery-py/src/parsing/operations/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from .call import Call
66
from .create_node import CreateNode
77
from .create_relationship import CreateRelationship
8+
from .delete_node import DeleteNode
9+
from .delete_relationship import DeleteRelationship
810
from .group_by import GroupBy
911
from .limit import Limit
1012
from .load import Load
@@ -35,6 +37,8 @@
3537
"Match",
3638
"CreateNode",
3739
"CreateRelationship",
40+
"DeleteNode",
41+
"DeleteRelationship",
3842
"Union",
3943
"UnionAll",
4044
"OrderBy",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 []
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 []

flowquery-py/src/parsing/parser.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
from .operations.call import Call
5858
from .operations.create_node import CreateNode
5959
from .operations.create_relationship import CreateRelationship
60+
from .operations.delete_node import DeleteNode
61+
from .operations.delete_relationship import DeleteRelationship
6062
from .operations.limit import Limit
6163
from .operations.load import Load
6264
from .operations.match import Match
@@ -185,8 +187,8 @@ def _parse_tokenized(self, is_sub_query: bool = False) -> ASTNode:
185187
new_root.add_child(union)
186188
return new_root
187189

188-
if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship)):
189-
raise ValueError("Last statement must be a RETURN, WHERE, CALL, or CREATE statement")
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")
190192

191193
return root
192194

@@ -198,7 +200,8 @@ def _parse_operation(self) -> Optional[Operation]:
198200
self._parse_load() or
199201
self._parse_call() or
200202
self._parse_match() or
201-
self._parse_create()
203+
self._parse_create() or
204+
self._parse_delete()
202205
)
203206

204207
def _parse_with(self) -> Optional[With]:
@@ -431,6 +434,54 @@ def _parse_create(self) -> Optional[Operation]:
431434
else:
432435
return CreateNode(node, query)
433436

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+
434485
def _parse_union(self) -> Optional[Union]:
435486
"""Parse a UNION or UNION ALL keyword."""
436487
if not self.token.is_union():

0 commit comments

Comments
 (0)