Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions flowquery-py/src/graph/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def add_node(self, node: 'Node', statement: ASTNode) -> None:
physical.statement = statement
Database._nodes[node.label] = physical

def remove_node(self, node: 'Node') -> None:
"""Removes a node from the database."""
if node.label is None:
raise ValueError("Node label is null")
Database._nodes.pop(node.label, None)

def get_node(self, node: 'Node') -> Optional['PhysicalNode']:
"""Gets a node from the database."""
return Database._nodes.get(node.label) if node.label else None
Expand All @@ -54,6 +60,12 @@ def add_relationship(self, relationship: 'Relationship', statement: ASTNode) ->
physical.target = relationship.target
Database._relationships[relationship.type] = physical

def remove_relationship(self, relationship: 'Relationship') -> None:
"""Removes a relationship from the database."""
if relationship.type is None:
raise ValueError("Relationship type is null")
Database._relationships.pop(relationship.type, None)

def get_relationship(self, relationship: 'Relationship') -> Optional['PhysicalRelationship']:
"""Gets a relationship from the database."""
return Database._relationships.get(relationship.type) if relationship.type else None
Expand Down
2 changes: 2 additions & 0 deletions flowquery-py/src/parsing/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from .split import Split
from .string_distance import StringDistance
from .stringify import Stringify
from .substring import Substring
from .sum import Sum
from .tail import Tail
from .time_ import Time
Expand Down Expand Up @@ -107,6 +108,7 @@
"Split",
"StringDistance",
"Stringify",
"Substring",
"Tail",
"Time",
"Timestamp",
Expand Down
74 changes: 74 additions & 0 deletions flowquery-py/src/parsing/functions/substring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Substring function."""

from typing import Any, List

from ..ast_node import ASTNode
from .function import Function
from .function_metadata import FunctionDef


@FunctionDef({
"description": "Returns a substring of a string, starting at a 0-based index with an optional length",
"category": "scalar",
"parameters": [
{"name": "original", "description": "The original string", "type": "string"},
{"name": "start", "description": "The 0-based start index", "type": "integer"},
{
"name": "length",
"description": "The length of the substring (optional)",
"type": "integer",
}
],
"output": {"description": "The substring", "type": "string", "example": "llo"},
"examples": [
"RETURN substring('hello', 1, 3)",
"RETURN substring('hello', 2)"
]
})
class Substring(Function):
"""Substring function.

Returns a substring of a string, starting at a 0-based index with an optional length.
"""

def __init__(self) -> None:
super().__init__("substring")

@property
def parameters(self) -> List[ASTNode]:
return self.get_children()

@parameters.setter
def parameters(self, nodes: List[ASTNode]) -> None:
if len(nodes) < 2 or len(nodes) > 3:
raise ValueError(
f"Function substring expected 2 or 3 parameters, but got {len(nodes)}"
)
for node in nodes:
self.add_child(node)

def value(self) -> Any:
children = self.get_children()
original = children[0].value()
start = children[1].value()

if not isinstance(original, str):
raise ValueError(
"Invalid argument for substring function: expected a string as the first argument"
)
if not isinstance(start, (int, float)) or (isinstance(start, float) and not start.is_integer()):
raise ValueError(
"Invalid argument for substring function: expected an integer as the second argument"
)
start = int(start)

if len(children) == 3:
length = children[2].value()
if not isinstance(length, (int, float)) or (isinstance(length, float) and not length.is_integer()):
raise ValueError(
"Invalid argument for substring function: expected an integer as the third argument"
)
length = int(length)
return original[start:start + length]

return original[start:]
4 changes: 4 additions & 0 deletions flowquery-py/src/parsing/operations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from .call import Call
from .create_node import CreateNode
from .create_relationship import CreateRelationship
from .delete_node import DeleteNode
from .delete_relationship import DeleteRelationship
from .group_by import GroupBy
from .limit import Limit
from .load import Load
Expand Down Expand Up @@ -35,6 +37,8 @@
"Match",
"CreateNode",
"CreateRelationship",
"DeleteNode",
"DeleteRelationship",
"Union",
"UnionAll",
"OrderBy",
Expand Down
29 changes: 29 additions & 0 deletions flowquery-py/src/parsing/operations/delete_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Represents a DELETE operation for deleting virtual nodes."""

from typing import Any, Dict, List

from ...graph.database import Database
from ...graph.node import Node
from .operation import Operation


class DeleteNode(Operation):
"""Represents a DELETE operation for deleting virtual nodes."""

def __init__(self, node: Node) -> None:
super().__init__()
self._node = node

@property
def node(self) -> Node:
return self._node

async def run(self) -> None:
if self._node is None:
raise ValueError("Node is null")
db = Database.get_instance()
db.remove_node(self._node)

@property
def results(self) -> List[Dict[str, Any]]:
return []
29 changes: 29 additions & 0 deletions flowquery-py/src/parsing/operations/delete_relationship.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Represents a DELETE operation for deleting virtual relationships."""

from typing import Any, Dict, List

from ...graph.database import Database
from ...graph.relationship import Relationship
from .operation import Operation


class DeleteRelationship(Operation):
"""Represents a DELETE operation for deleting virtual relationships."""

def __init__(self, relationship: Relationship) -> None:
super().__init__()
self._relationship = relationship

@property
def relationship(self) -> Relationship:
return self._relationship

async def run(self) -> None:
if self._relationship is None:
raise ValueError("Relationship is null")
db = Database.get_instance()
db.remove_relationship(self._relationship)

@property
def results(self) -> List[Dict[str, Any]]:
return []
57 changes: 54 additions & 3 deletions flowquery-py/src/parsing/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
from .operations.call import Call
from .operations.create_node import CreateNode
from .operations.create_relationship import CreateRelationship
from .operations.delete_node import DeleteNode
from .operations.delete_relationship import DeleteRelationship
from .operations.limit import Limit
from .operations.load import Load
from .operations.match import Match
Expand Down Expand Up @@ -185,8 +187,8 @@ def _parse_tokenized(self, is_sub_query: bool = False) -> ASTNode:
new_root.add_child(union)
return new_root

if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship)):
raise ValueError("Last statement must be a RETURN, WHERE, CALL, or CREATE statement")
if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship, DeleteNode, DeleteRelationship)):
raise ValueError("Last statement must be a RETURN, WHERE, CALL, CREATE, or DELETE statement")

return root

Expand All @@ -198,7 +200,8 @@ def _parse_operation(self) -> Optional[Operation]:
self._parse_load() or
self._parse_call() or
self._parse_match() or
self._parse_create()
self._parse_create() or
self._parse_delete()
)

def _parse_with(self) -> Optional[With]:
Expand Down Expand Up @@ -431,6 +434,54 @@ def _parse_create(self) -> Optional[Operation]:
else:
return CreateNode(node, query)

def _parse_delete(self) -> Optional[Operation]:
"""Parse DELETE VIRTUAL statement for nodes and relationships."""
if not self.token.is_delete():
return None
self.set_next_token()
self._expect_and_skip_whitespace_and_comments()
if not self.token.is_virtual():
raise ValueError("Expected VIRTUAL")
self.set_next_token()
self._expect_and_skip_whitespace_and_comments()

node = self._parse_node()
if node is None:
raise ValueError("Expected node definition")

relationship: Optional[Relationship] = None
if self.token.is_subtract() and self.peek() and self.peek().is_opening_bracket():
self.set_next_token() # skip -
self.set_next_token() # skip [
if not self.token.is_colon():
raise ValueError("Expected ':' for relationship type")
self.set_next_token()
if not self.token.is_identifier_or_keyword():
raise ValueError("Expected relationship type identifier")
rel_type = self.token.value or ""
self.set_next_token()
if not self.token.is_closing_bracket():
raise ValueError("Expected closing bracket for relationship definition")
self.set_next_token()
if not self.token.is_subtract():
raise ValueError("Expected '-' for relationship definition")
self.set_next_token()
# Skip optional direction indicator '>'
if self.token.is_greater_than():
self.set_next_token()
target = self._parse_node()
if target is None:
raise ValueError("Expected target node definition")
relationship = Relationship()
relationship.type = rel_type
relationship.source = node
relationship.target = target

if relationship is not None:
return DeleteRelationship(relationship)
else:
return DeleteNode(node)

def _parse_union(self) -> Optional[Union]:
"""Parse a UNION or UNION ALL keyword."""
if not self.token.is_union():
Expand Down
Loading