Skip to content

Commit 046b6ed

Browse files
authored
Merge pull request #31 from microsoft/language_fixes
Language fixes
2 parents 1dea767 + e38e66a commit 046b6ed

25 files changed

+825
-69
lines changed

flowquery-py/src/graph/database.py

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import Any, Dict, Optional, Union
5+
from typing import Any, AsyncIterator, Dict, List, Optional, Union
66

77
from ..parsing.ast_node import ASTNode
88
from .node import Node
@@ -48,35 +48,57 @@ def add_relationship(self, relationship: 'Relationship', statement: ASTNode) ->
4848
physical = PhysicalRelationship()
4949
physical.type = relationship.type
5050
physical.statement = statement
51+
if relationship.source is not None:
52+
physical.source = relationship.source
53+
if relationship.target is not None:
54+
physical.target = relationship.target
5155
Database._relationships[relationship.type] = physical
5256

5357
def get_relationship(self, relationship: 'Relationship') -> Optional['PhysicalRelationship']:
5458
"""Gets a relationship from the database."""
5559
return Database._relationships.get(relationship.type) if relationship.type else None
5660

57-
async def schema(self) -> list[dict[str, Any]]:
61+
def get_relationships(self, relationship: 'Relationship') -> list['PhysicalRelationship']:
62+
"""Gets multiple physical relationships for ORed types."""
63+
result = []
64+
for rel_type in relationship.types:
65+
physical = Database._relationships.get(rel_type)
66+
if physical:
67+
result.append(physical)
68+
return result
69+
70+
async def schema(self) -> List[Dict[str, Any]]:
5871
"""Returns the graph schema with node/relationship labels and sample data."""
59-
result: list[dict[str, Any]] = []
72+
return [item async for item in self._schema()]
6073

74+
async def _schema(self) -> AsyncIterator[Dict[str, Any]]:
75+
"""Async generator for graph schema with node/relationship labels and sample data."""
6176
for label, physical_node in Database._nodes.items():
6277
records = await physical_node.data()
63-
entry: dict[str, Any] = {"kind": "node", "label": label}
78+
entry: Dict[str, Any] = {"kind": "Node", "label": label}
6479
if records:
6580
sample = {k: v for k, v in records[0].items() if k != "id"}
66-
if sample:
81+
properties = list(sample.keys())
82+
if properties:
83+
entry["properties"] = properties
6784
entry["sample"] = sample
68-
result.append(entry)
85+
yield entry
6986

7087
for rel_type, physical_rel in Database._relationships.items():
7188
records = await physical_rel.data()
72-
entry_rel: dict[str, Any] = {"kind": "relationship", "type": rel_type}
89+
entry_rel: Dict[str, Any] = {
90+
"kind": "Relationship",
91+
"type": rel_type,
92+
"from_label": physical_rel.source.label if physical_rel.source else None,
93+
"to_label": physical_rel.target.label if physical_rel.target else None,
94+
}
7395
if records:
7496
sample = {k: v for k, v in records[0].items() if k not in ("left_id", "right_id")}
75-
if sample:
97+
properties = list(sample.keys())
98+
if properties:
99+
entry_rel["properties"] = properties
76100
entry_rel["sample"] = sample
77-
result.append(entry_rel)
78-
79-
return result
101+
yield entry_rel
80102

81103
async def get_data(self, element: Union['Node', 'Relationship']) -> Union['NodeData', 'RelationshipData']:
82104
"""Gets data for a node or relationship."""
@@ -87,6 +109,17 @@ async def get_data(self, element: Union['Node', 'Relationship']) -> Union['NodeD
87109
data = await node.data()
88110
return NodeData(data)
89111
elif isinstance(element, Relationship):
112+
if len(element.types) > 1:
113+
physicals = self.get_relationships(element)
114+
if not physicals:
115+
raise ValueError(f"No physical relationships found for types {', '.join(element.types)}")
116+
all_records = []
117+
for i, physical in enumerate(physicals):
118+
records = await physical.data()
119+
type_name = element.types[i]
120+
for record in records:
121+
all_records.append({**record, "_type": type_name})
122+
return RelationshipData(all_records)
90123
relationship = self.get_relationship(element)
91124
if relationship is None:
92125
raise ValueError(f"Physical relationship not found for type {element.type}")

flowquery-py/src/graph/relationship.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Relationship(ASTNode):
1919
def __init__(self) -> None:
2020
super().__init__()
2121
self._identifier: Optional[str] = None
22-
self._type: Optional[str] = None
22+
self._types: List[str] = []
2323
self._hops: Hops = Hops()
2424
self._source: Optional['Node'] = None
2525
self._target: Optional['Node'] = None
@@ -39,11 +39,19 @@ def identifier(self, value: str) -> None:
3939

4040
@property
4141
def type(self) -> Optional[str]:
42-
return self._type
42+
return self._types[0] if self._types else None
4343

4444
@type.setter
4545
def type(self, value: str) -> None:
46-
self._type = value
46+
self._types = [value]
47+
48+
@property
49+
def types(self) -> List[str]:
50+
return self._types
51+
52+
@types.setter
53+
def types(self, value: List[str]) -> None:
54+
self._types = value
4755

4856
@property
4957
def hops(self) -> Hops:

flowquery-py/src/graph/relationship_data.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ def find(self, id: str, hop: int = 0, direction: str = "right") -> bool:
2525
return self._find(id, hop, key)
2626

2727
def properties(self) -> Optional[Dict[str, Any]]:
28-
"""Get properties of current relationship, excluding left_id and right_id."""
28+
"""Get properties of current relationship, excluding left_id, right_id, and _type."""
2929
current = self.current()
3030
if current:
3131
props = dict(current)
3232
props.pop("left_id", None)
3333
props.pop("right_id", None)
34+
props.pop("_type", None)
3435
return props
3536
return None

flowquery-py/src/graph/relationship_match_collector.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,15 @@ def push(self, relationship: 'Relationship', traversal_id: str = "") -> Relation
2828
"""Push a new match onto the collector."""
2929
start_node_value = relationship.source.value() if relationship.source else None
3030
rel_data = relationship.get_data()
31+
current_record = rel_data.current() if rel_data else None
32+
default_type = relationship.type or ""
33+
if current_record and isinstance(current_record, dict):
34+
actual_type = current_record.get('_type', default_type)
35+
else:
36+
actual_type = default_type
3137
rel_props: Dict[str, Any] = (rel_data.properties() or {}) if rel_data else {}
3238
match: RelationshipMatchRecord = {
33-
"type": relationship.type or "",
39+
"type": actual_type,
3440
"startNode": start_node_value or {},
3541
"endNode": None,
3642
"properties": rel_props,

flowquery-py/src/graph/relationship_reference.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class RelationshipReference(Relationship):
1010
def __init__(self, relationship: Relationship, referred: ASTNode) -> None:
1111
super().__init__()
1212
self._referred = referred
13-
if relationship.type:
14-
self.type = relationship.type
13+
if relationship.types:
14+
self.types = relationship.types
1515

1616
@property
1717
def referred(self) -> ASTNode:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from .to_json import ToJson
4040
from .to_lower import ToLower
4141
from .to_string import ToString
42+
from .trim import Trim
4243
from .type_ import Type
4344
from .value_holder import ValueHolder
4445

@@ -78,6 +79,7 @@
7879
"ToJson",
7980
"ToLower",
8081
"ToString",
82+
"Trim",
8183
"Type",
8284
"Functions",
8385
"Schema",

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""PredicateSum function."""
22

3-
from typing import Any, Optional
3+
from typing import Any
44

55
from .function_metadata import FunctionDef
66
from .predicate_function import PredicateFunction
@@ -41,12 +41,9 @@ def value(self) -> Any:
4141
if array is None or not isinstance(array, list):
4242
raise ValueError("Invalid array for sum function")
4343

44-
_sum: Optional[Any] = None
44+
_sum: int = 0
4545
for item in array:
4646
self._value_holder.holder = item
4747
if self.where is None or self.where.value():
48-
if _sum is None:
49-
_sum = self._return.value()
50-
else:
51-
_sum += self._return.value()
48+
_sum += self._return.value()
5249
return _sum

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,27 @@
99
@FunctionDef({
1010
"description": (
1111
"Returns the graph schema listing all nodes and relationships "
12-
"with a sample of their data."
12+
"with their properties and a sample of their data."
1313
),
1414
"category": "async",
1515
"parameters": [],
1616
"output": {
17-
"description": "Schema entry with kind, label/type, and optional sample data",
17+
"description": "Schema entry with label/type, properties, and optional sample data",
1818
"type": "object",
1919
},
2020
"examples": [
21-
"CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample",
21+
"CALL schema() YIELD label, type, from_label, to_label, properties, sample "
22+
"RETURN label, type, from_label, to_label, properties, sample",
2223
],
2324
})
2425
class Schema(AsyncFunction):
2526
"""Returns the graph schema of the database.
2627
27-
Lists all nodes and relationships with their labels/types and a sample
28-
of their data (excluding id from nodes, left_id and right_id from relationships).
28+
Lists all nodes and relationships with their labels/types, properties,
29+
and a sample of their data (excluding id from nodes, left_id and right_id from relationships).
30+
31+
Nodes: {label, properties, sample}
32+
Relationships: {type, from_label, to_label, properties, sample}
2933
"""
3034

3135
async def generate(self) -> AsyncGenerator[Any, None]:
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Trim function."""
2+
3+
from typing import Any
4+
5+
from .function import Function
6+
from .function_metadata import FunctionDef
7+
8+
9+
@FunctionDef({
10+
"description": "Removes leading and trailing whitespace from a string",
11+
"category": "scalar",
12+
"parameters": [
13+
{"name": "text", "description": "String to trim", "type": "string"}
14+
],
15+
"output": {"description": "Trimmed string", "type": "string", "example": "hello"},
16+
"examples": [
17+
"WITH ' hello ' AS s RETURN trim(s)",
18+
"WITH '\\tfoo\\n' AS s RETURN trim(s)"
19+
]
20+
})
21+
class Trim(Function):
22+
"""Trim function.
23+
24+
Removes leading and trailing whitespace from a string.
25+
"""
26+
27+
def __init__(self) -> None:
28+
super().__init__("trim")
29+
self._expected_parameter_count = 1
30+
31+
def value(self) -> Any:
32+
val = self.get_children()[0].value()
33+
if not isinstance(val, str):
34+
raise ValueError("Invalid argument for trim function: expected a string")
35+
return val.strip()

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ def generate_results(
122122
self.mappers[mapper_index].overridden = child.value
123123
yield from self.generate_results(mapper_index + 1, child)
124124
else:
125+
if node.elements is None:
126+
node.elements = [reducer.element() for reducer in self.reducers]
125127
if node.elements:
126128
for i, element in enumerate(node.elements):
127129
self.reducers[i].overridden = element.value

0 commit comments

Comments
 (0)