flowquery 1.0.34 → 1.0.36
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 +1 -0
- package/dist/graph/database.d.ts.map +1 -1
- package/dist/graph/database.js +43 -6
- package/dist/graph/database.js.map +1 -1
- package/dist/graph/relationship.d.ts +3 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +12 -4
- package/dist/graph/relationship.js.map +1 -1
- package/dist/graph/relationship_data.js +1 -1
- package/dist/graph/relationship_data.js.map +1 -1
- package/dist/graph/relationship_match_collector.d.ts.map +1 -1
- package/dist/graph/relationship_match_collector.js +6 -3
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/dist/graph/relationship_reference.js +1 -1
- package/dist/graph/relationship_reference.js.map +1 -1
- package/dist/parsing/data_structures/lookup.d.ts.map +1 -1
- package/dist/parsing/data_structures/lookup.js +5 -1
- package/dist/parsing/data_structures/lookup.js.map +1 -1
- package/dist/parsing/functions/coalesce.d.ts +17 -0
- package/dist/parsing/functions/coalesce.d.ts.map +1 -0
- package/dist/parsing/functions/coalesce.js +61 -0
- package/dist/parsing/functions/coalesce.js.map +1 -0
- package/dist/parsing/functions/date.d.ts +22 -0
- package/dist/parsing/functions/date.d.ts.map +1 -0
- package/dist/parsing/functions/date.js +71 -0
- package/dist/parsing/functions/date.js.map +1 -0
- package/dist/parsing/functions/datetime.d.ts +22 -0
- package/dist/parsing/functions/datetime.d.ts.map +1 -0
- package/dist/parsing/functions/datetime.js +71 -0
- package/dist/parsing/functions/datetime.js.map +1 -0
- package/dist/parsing/functions/duration.d.ts +7 -0
- package/dist/parsing/functions/duration.d.ts.map +1 -0
- package/dist/parsing/functions/duration.js +145 -0
- package/dist/parsing/functions/duration.js.map +1 -0
- package/dist/parsing/functions/element_id.d.ts +7 -0
- package/dist/parsing/functions/element_id.d.ts.map +1 -0
- package/dist/parsing/functions/element_id.js +58 -0
- package/dist/parsing/functions/element_id.js.map +1 -0
- package/dist/parsing/functions/function_factory.d.ts +21 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +21 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/head.d.ts +7 -0
- package/dist/parsing/functions/head.d.ts.map +1 -0
- package/dist/parsing/functions/head.js +53 -0
- package/dist/parsing/functions/head.js.map +1 -0
- package/dist/parsing/functions/id.d.ts +7 -0
- package/dist/parsing/functions/id.d.ts.map +1 -0
- package/dist/parsing/functions/id.js +58 -0
- package/dist/parsing/functions/id.js.map +1 -0
- package/dist/parsing/functions/last.d.ts +7 -0
- package/dist/parsing/functions/last.d.ts.map +1 -0
- package/dist/parsing/functions/last.js +53 -0
- package/dist/parsing/functions/last.js.map +1 -0
- package/dist/parsing/functions/localdatetime.d.ts +21 -0
- package/dist/parsing/functions/localdatetime.d.ts.map +1 -0
- package/dist/parsing/functions/localdatetime.js +71 -0
- package/dist/parsing/functions/localdatetime.js.map +1 -0
- package/dist/parsing/functions/localtime.d.ts +20 -0
- package/dist/parsing/functions/localtime.d.ts.map +1 -0
- package/dist/parsing/functions/localtime.js +67 -0
- package/dist/parsing/functions/localtime.js.map +1 -0
- package/dist/parsing/functions/max.d.ts +14 -0
- package/dist/parsing/functions/max.d.ts.map +1 -0
- package/dist/parsing/functions/max.js +51 -0
- package/dist/parsing/functions/max.js.map +1 -0
- package/dist/parsing/functions/min.d.ts +14 -0
- package/dist/parsing/functions/min.d.ts.map +1 -0
- package/dist/parsing/functions/min.js +51 -0
- package/dist/parsing/functions/min.js.map +1 -0
- package/dist/parsing/functions/nodes.d.ts +7 -0
- package/dist/parsing/functions/nodes.d.ts.map +1 -0
- package/dist/parsing/functions/nodes.js +63 -0
- package/dist/parsing/functions/nodes.js.map +1 -0
- package/dist/parsing/functions/predicate_sum.d.ts.map +1 -1
- package/dist/parsing/functions/predicate_sum.js +13 -10
- package/dist/parsing/functions/predicate_sum.js.map +1 -1
- package/dist/parsing/functions/properties.d.ts +7 -0
- package/dist/parsing/functions/properties.d.ts.map +1 -0
- package/dist/parsing/functions/properties.js +74 -0
- package/dist/parsing/functions/properties.js.map +1 -0
- package/dist/parsing/functions/relationships.d.ts +7 -0
- package/dist/parsing/functions/relationships.d.ts.map +1 -0
- package/dist/parsing/functions/relationships.js +61 -0
- package/dist/parsing/functions/relationships.js.map +1 -0
- package/dist/parsing/functions/schema.d.ts +5 -2
- package/dist/parsing/functions/schema.d.ts.map +1 -1
- package/dist/parsing/functions/schema.js +7 -4
- package/dist/parsing/functions/schema.js.map +1 -1
- package/dist/parsing/functions/tail.d.ts +7 -0
- package/dist/parsing/functions/tail.d.ts.map +1 -0
- package/dist/parsing/functions/tail.js +50 -0
- package/dist/parsing/functions/tail.js.map +1 -0
- package/dist/parsing/functions/temporal_utils.d.ts +39 -0
- package/dist/parsing/functions/temporal_utils.d.ts.map +1 -0
- package/dist/parsing/functions/temporal_utils.js +168 -0
- package/dist/parsing/functions/temporal_utils.js.map +1 -0
- package/dist/parsing/functions/time.d.ts +20 -0
- package/dist/parsing/functions/time.d.ts.map +1 -0
- package/dist/parsing/functions/time.js +67 -0
- package/dist/parsing/functions/time.js.map +1 -0
- package/dist/parsing/functions/timestamp.d.ts +17 -0
- package/dist/parsing/functions/timestamp.d.ts.map +1 -0
- package/dist/parsing/functions/timestamp.js +51 -0
- package/dist/parsing/functions/timestamp.js.map +1 -0
- package/dist/parsing/functions/to_float.d.ts +7 -0
- package/dist/parsing/functions/to_float.d.ts.map +1 -0
- package/dist/parsing/functions/to_float.js +61 -0
- package/dist/parsing/functions/to_float.js.map +1 -0
- package/dist/parsing/functions/to_integer.d.ts +7 -0
- package/dist/parsing/functions/to_integer.d.ts.map +1 -0
- package/dist/parsing/functions/to_integer.js +61 -0
- package/dist/parsing/functions/to_integer.js.map +1 -0
- package/dist/parsing/functions/trim.d.ts +7 -0
- package/dist/parsing/functions/trim.d.ts.map +1 -0
- package/dist/parsing/functions/trim.js +37 -0
- package/dist/parsing/functions/trim.js.map +1 -0
- package/dist/parsing/operations/group_by.d.ts.map +1 -1
- package/dist/parsing/operations/group_by.js +4 -2
- package/dist/parsing/operations/group_by.js.map +1 -1
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +15 -2
- package/dist/parsing/parser.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 +44 -11
- package/flowquery-py/src/graph/relationship.py +11 -3
- package/flowquery-py/src/graph/relationship_data.py +2 -1
- package/flowquery-py/src/graph/relationship_match_collector.py +7 -1
- package/flowquery-py/src/graph/relationship_reference.py +2 -2
- package/flowquery-py/src/parsing/data_structures/lookup.py +2 -0
- package/flowquery-py/src/parsing/functions/__init__.py +42 -2
- package/flowquery-py/src/parsing/functions/coalesce.py +44 -0
- package/flowquery-py/src/parsing/functions/date_.py +63 -0
- package/flowquery-py/src/parsing/functions/datetime_.py +64 -0
- package/flowquery-py/src/parsing/functions/duration.py +159 -0
- package/flowquery-py/src/parsing/functions/element_id.py +50 -0
- package/flowquery-py/src/parsing/functions/head.py +39 -0
- package/flowquery-py/src/parsing/functions/id_.py +49 -0
- package/flowquery-py/src/parsing/functions/last.py +39 -0
- package/flowquery-py/src/parsing/functions/localdatetime.py +62 -0
- package/flowquery-py/src/parsing/functions/localtime.py +59 -0
- package/flowquery-py/src/parsing/functions/max_.py +49 -0
- package/flowquery-py/src/parsing/functions/min_.py +49 -0
- package/flowquery-py/src/parsing/functions/nodes.py +48 -0
- package/flowquery-py/src/parsing/functions/predicate_sum.py +3 -6
- package/flowquery-py/src/parsing/functions/properties.py +50 -0
- package/flowquery-py/src/parsing/functions/relationships.py +46 -0
- package/flowquery-py/src/parsing/functions/schema.py +9 -5
- package/flowquery-py/src/parsing/functions/tail.py +37 -0
- package/flowquery-py/src/parsing/functions/temporal_utils.py +186 -0
- package/flowquery-py/src/parsing/functions/time_.py +59 -0
- package/flowquery-py/src/parsing/functions/timestamp.py +39 -0
- package/flowquery-py/src/parsing/functions/to_float.py +46 -0
- package/flowquery-py/src/parsing/functions/to_integer.py +46 -0
- package/flowquery-py/src/parsing/functions/trim.py +35 -0
- package/flowquery-py/src/parsing/operations/group_by.py +2 -0
- package/flowquery-py/src/parsing/parser.py +12 -2
- package/flowquery-py/tests/compute/test_runner.py +1082 -4
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/database.ts +42 -4
- package/src/graph/relationship.ts +12 -4
- package/src/graph/relationship_data.ts +1 -1
- package/src/graph/relationship_match_collector.ts +6 -2
- package/src/graph/relationship_reference.ts +1 -1
- package/src/parsing/data_structures/lookup.ts +8 -4
- package/src/parsing/functions/coalesce.ts +50 -0
- package/src/parsing/functions/date.ts +65 -0
- package/src/parsing/functions/datetime.ts +65 -0
- package/src/parsing/functions/duration.ts +143 -0
- package/src/parsing/functions/element_id.ts +51 -0
- package/src/parsing/functions/function_factory.ts +21 -0
- package/src/parsing/functions/head.ts +42 -0
- package/src/parsing/functions/id.ts +51 -0
- package/src/parsing/functions/last.ts +42 -0
- package/src/parsing/functions/localdatetime.ts +65 -0
- package/src/parsing/functions/localtime.ts +60 -0
- package/src/parsing/functions/max.ts +37 -0
- package/src/parsing/functions/min.ts +37 -0
- package/src/parsing/functions/nodes.ts +54 -0
- package/src/parsing/functions/predicate_sum.ts +17 -12
- package/src/parsing/functions/properties.ts +56 -0
- package/src/parsing/functions/relationships.ts +52 -0
- package/src/parsing/functions/schema.ts +7 -4
- package/src/parsing/functions/tail.ts +39 -0
- package/src/parsing/functions/temporal_utils.ts +180 -0
- package/src/parsing/functions/time.ts +60 -0
- package/src/parsing/functions/timestamp.ts +41 -0
- package/src/parsing/functions/to_float.ts +50 -0
- package/src/parsing/functions/to_integer.ts +50 -0
- package/src/parsing/functions/trim.ts +25 -0
- package/src/parsing/operations/group_by.ts +4 -1
- package/src/parsing/parser.ts +15 -2
- package/tests/compute/runner.test.ts +1005 -3
- package/tests/parsing/parser.test.ts +37 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any, Dict, Optional, Union
|
|
5
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, Union
|
|
6
6
|
|
|
7
7
|
from ..parsing.ast_node import ASTNode
|
|
8
8
|
from .node import Node
|
|
@@ -48,35 +48,57 @@ class Database:
|
|
|
48
48
|
physical = PhysicalRelationship()
|
|
49
49
|
physical.type = relationship.type
|
|
50
50
|
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
|
|
51
55
|
Database._relationships[relationship.type] = physical
|
|
52
56
|
|
|
53
57
|
def get_relationship(self, relationship: 'Relationship') -> Optional['PhysicalRelationship']:
|
|
54
58
|
"""Gets a relationship from the database."""
|
|
55
59
|
return Database._relationships.get(relationship.type) if relationship.type else None
|
|
56
60
|
|
|
57
|
-
|
|
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]]:
|
|
58
71
|
"""Returns the graph schema with node/relationship labels and sample data."""
|
|
59
|
-
|
|
72
|
+
return [item async for item in self._schema()]
|
|
60
73
|
|
|
74
|
+
async def _schema(self) -> AsyncIterator[Dict[str, Any]]:
|
|
75
|
+
"""Async generator for graph schema with node/relationship labels and sample data."""
|
|
61
76
|
for label, physical_node in Database._nodes.items():
|
|
62
77
|
records = await physical_node.data()
|
|
63
|
-
entry:
|
|
78
|
+
entry: Dict[str, Any] = {"kind": "Node", "label": label}
|
|
64
79
|
if records:
|
|
65
80
|
sample = {k: v for k, v in records[0].items() if k != "id"}
|
|
66
|
-
|
|
81
|
+
properties = list(sample.keys())
|
|
82
|
+
if properties:
|
|
83
|
+
entry["properties"] = properties
|
|
67
84
|
entry["sample"] = sample
|
|
68
|
-
|
|
85
|
+
yield entry
|
|
69
86
|
|
|
70
87
|
for rel_type, physical_rel in Database._relationships.items():
|
|
71
88
|
records = await physical_rel.data()
|
|
72
|
-
entry_rel:
|
|
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
|
+
}
|
|
73
95
|
if records:
|
|
74
96
|
sample = {k: v for k, v in records[0].items() if k not in ("left_id", "right_id")}
|
|
75
|
-
|
|
97
|
+
properties = list(sample.keys())
|
|
98
|
+
if properties:
|
|
99
|
+
entry_rel["properties"] = properties
|
|
76
100
|
entry_rel["sample"] = sample
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return result
|
|
101
|
+
yield entry_rel
|
|
80
102
|
|
|
81
103
|
async def get_data(self, element: Union['Node', 'Relationship']) -> Union['NodeData', 'RelationshipData']:
|
|
82
104
|
"""Gets data for a node or relationship."""
|
|
@@ -87,6 +109,17 @@ class Database:
|
|
|
87
109
|
data = await node.data()
|
|
88
110
|
return NodeData(data)
|
|
89
111
|
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)
|
|
90
123
|
relationship = self.get_relationship(element)
|
|
91
124
|
if relationship is None:
|
|
92
125
|
raise ValueError(f"Physical relationship not found for type {element.type}")
|
|
@@ -19,7 +19,7 @@ class Relationship(ASTNode):
|
|
|
19
19
|
def __init__(self) -> None:
|
|
20
20
|
super().__init__()
|
|
21
21
|
self._identifier: Optional[str] = None
|
|
22
|
-
self.
|
|
22
|
+
self._types: List[str] = []
|
|
23
23
|
self._hops: Hops = Hops()
|
|
24
24
|
self._source: Optional['Node'] = None
|
|
25
25
|
self._target: Optional['Node'] = None
|
|
@@ -39,11 +39,19 @@ class Relationship(ASTNode):
|
|
|
39
39
|
|
|
40
40
|
@property
|
|
41
41
|
def type(self) -> Optional[str]:
|
|
42
|
-
return self.
|
|
42
|
+
return self._types[0] if self._types else None
|
|
43
43
|
|
|
44
44
|
@type.setter
|
|
45
45
|
def type(self, value: str) -> None:
|
|
46
|
-
self.
|
|
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
|
|
47
55
|
|
|
48
56
|
@property
|
|
49
57
|
def hops(self) -> Hops:
|
|
@@ -25,11 +25,12 @@ class RelationshipData(Data):
|
|
|
25
25
|
return self._find(id, hop, key)
|
|
26
26
|
|
|
27
27
|
def properties(self) -> Optional[Dict[str, Any]]:
|
|
28
|
-
"""Get properties of current relationship, excluding left_id and
|
|
28
|
+
"""Get properties of current relationship, excluding left_id, right_id, and _type."""
|
|
29
29
|
current = self.current()
|
|
30
30
|
if current:
|
|
31
31
|
props = dict(current)
|
|
32
32
|
props.pop("left_id", None)
|
|
33
33
|
props.pop("right_id", None)
|
|
34
|
+
props.pop("_type", None)
|
|
34
35
|
return props
|
|
35
36
|
return None
|
|
@@ -28,9 +28,15 @@ class RelationshipMatchCollector:
|
|
|
28
28
|
"""Push a new match onto the collector."""
|
|
29
29
|
start_node_value = relationship.source.value() if relationship.source else None
|
|
30
30
|
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
|
|
31
37
|
rel_props: Dict[str, Any] = (rel_data.properties() or {}) if rel_data else {}
|
|
32
38
|
match: RelationshipMatchRecord = {
|
|
33
|
-
"type":
|
|
39
|
+
"type": actual_type,
|
|
34
40
|
"startNode": start_node_value or {},
|
|
35
41
|
"endNode": None,
|
|
36
42
|
"properties": rel_props,
|
|
@@ -10,8 +10,8 @@ class RelationshipReference(Relationship):
|
|
|
10
10
|
def __init__(self, relationship: Relationship, referred: ASTNode) -> None:
|
|
11
11
|
super().__init__()
|
|
12
12
|
self._referred = referred
|
|
13
|
-
if relationship.
|
|
14
|
-
self.
|
|
13
|
+
if relationship.types:
|
|
14
|
+
self.types = relationship.types
|
|
15
15
|
|
|
16
16
|
@property
|
|
17
17
|
def referred(self) -> ASTNode:
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
from .aggregate_function import AggregateFunction
|
|
4
4
|
from .async_function import AsyncFunction
|
|
5
5
|
from .avg import Avg
|
|
6
|
+
from .coalesce import Coalesce
|
|
6
7
|
from .collect import Collect
|
|
7
8
|
from .count import Count
|
|
9
|
+
from .date_ import DateFunction
|
|
10
|
+
from .datetime_ import Datetime
|
|
11
|
+
from .duration import Duration
|
|
12
|
+
from .element_id import ElementId
|
|
8
13
|
from .function import Function
|
|
9
14
|
from .function_factory import FunctionFactory
|
|
10
15
|
from .function_metadata import (
|
|
@@ -19,13 +24,23 @@ from .function_metadata import (
|
|
|
19
24
|
get_registered_function_metadata,
|
|
20
25
|
)
|
|
21
26
|
from .functions import Functions
|
|
27
|
+
from .head import Head
|
|
28
|
+
from .id_ import Id
|
|
22
29
|
from .join import Join
|
|
23
30
|
from .keys import Keys
|
|
31
|
+
from .last import Last
|
|
32
|
+
from .localdatetime import LocalDatetime
|
|
33
|
+
from .localtime import LocalTime
|
|
34
|
+
from .max_ import Max
|
|
35
|
+
from .min_ import Min
|
|
36
|
+
from .nodes import Nodes
|
|
24
37
|
from .predicate_function import PredicateFunction
|
|
25
38
|
from .predicate_sum import PredicateSum
|
|
39
|
+
from .properties import Properties
|
|
26
40
|
from .rand import Rand
|
|
27
41
|
from .range_ import Range
|
|
28
42
|
from .reducer_element import ReducerElement
|
|
43
|
+
from .relationships import Relationships
|
|
29
44
|
from .replace import Replace
|
|
30
45
|
from .round_ import Round
|
|
31
46
|
from .schema import Schema
|
|
@@ -33,12 +48,16 @@ from .size import Size
|
|
|
33
48
|
from .split import Split
|
|
34
49
|
from .string_distance import StringDistance
|
|
35
50
|
from .stringify import Stringify
|
|
36
|
-
|
|
37
|
-
# Built-in functions
|
|
38
51
|
from .sum import Sum
|
|
52
|
+
from .tail import Tail
|
|
53
|
+
from .time_ import Time
|
|
54
|
+
from .timestamp import Timestamp
|
|
55
|
+
from .to_float import ToFloat
|
|
56
|
+
from .to_integer import ToInteger
|
|
39
57
|
from .to_json import ToJson
|
|
40
58
|
from .to_lower import ToLower
|
|
41
59
|
from .to_string import ToString
|
|
60
|
+
from .trim import Trim
|
|
42
61
|
from .type_ import Type
|
|
43
62
|
from .value_holder import ValueHolder
|
|
44
63
|
|
|
@@ -63,10 +82,23 @@ __all__ = [
|
|
|
63
82
|
# Built-in functions
|
|
64
83
|
"Sum",
|
|
65
84
|
"Avg",
|
|
85
|
+
"DateFunction",
|
|
86
|
+
"Datetime",
|
|
87
|
+
"Coalesce",
|
|
66
88
|
"Collect",
|
|
67
89
|
"Count",
|
|
90
|
+
"Duration",
|
|
91
|
+
"ElementId",
|
|
92
|
+
"Head",
|
|
93
|
+
"Id",
|
|
68
94
|
"Join",
|
|
95
|
+
"Last",
|
|
69
96
|
"Keys",
|
|
97
|
+
"Max",
|
|
98
|
+
"Min",
|
|
99
|
+
"Nodes",
|
|
100
|
+
"Properties",
|
|
101
|
+
"Relationships",
|
|
70
102
|
"Rand",
|
|
71
103
|
"Range",
|
|
72
104
|
"Replace",
|
|
@@ -75,10 +107,18 @@ __all__ = [
|
|
|
75
107
|
"Split",
|
|
76
108
|
"StringDistance",
|
|
77
109
|
"Stringify",
|
|
110
|
+
"Tail",
|
|
111
|
+
"Time",
|
|
112
|
+
"Timestamp",
|
|
113
|
+
"ToFloat",
|
|
114
|
+
"ToInteger",
|
|
78
115
|
"ToJson",
|
|
79
116
|
"ToLower",
|
|
80
117
|
"ToString",
|
|
118
|
+
"Trim",
|
|
81
119
|
"Type",
|
|
120
|
+
"LocalDatetime",
|
|
121
|
+
"LocalTime",
|
|
82
122
|
"Functions",
|
|
83
123
|
"Schema",
|
|
84
124
|
"PredicateSum",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Coalesce 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": "Returns the first non-null value from a list of expressions",
|
|
11
|
+
"category": "scalar",
|
|
12
|
+
"parameters": [
|
|
13
|
+
{"name": "expressions", "description": "Two or more expressions to evaluate", "type": "any"}
|
|
14
|
+
],
|
|
15
|
+
"output": {"description": "The first non-null value, or null if all values are null", "type": "any"},
|
|
16
|
+
"examples": [
|
|
17
|
+
"RETURN coalesce(null, 'hello', 'world')",
|
|
18
|
+
"MATCH (n) RETURN coalesce(n.nickname, n.name) AS displayName"
|
|
19
|
+
]
|
|
20
|
+
})
|
|
21
|
+
class Coalesce(Function):
|
|
22
|
+
"""Coalesce function.
|
|
23
|
+
|
|
24
|
+
Returns the first non-null value from a list of expressions.
|
|
25
|
+
Equivalent to Neo4j's coalesce() function.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
super().__init__("coalesce")
|
|
30
|
+
self._expected_parameter_count = None # variable number of parameters
|
|
31
|
+
|
|
32
|
+
def value(self) -> Any:
|
|
33
|
+
children = self.get_children()
|
|
34
|
+
if len(children) == 0:
|
|
35
|
+
raise ValueError("coalesce() requires at least one argument")
|
|
36
|
+
for child in children:
|
|
37
|
+
try:
|
|
38
|
+
val = child.value()
|
|
39
|
+
except (KeyError, AttributeError):
|
|
40
|
+
# Treat missing properties/keys as null, matching Neo4j behavior
|
|
41
|
+
val = None
|
|
42
|
+
if val is not None:
|
|
43
|
+
return val
|
|
44
|
+
return None
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Date function."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .function import Function
|
|
7
|
+
from .function_metadata import FunctionDef
|
|
8
|
+
from .temporal_utils import build_date_object, parse_temporal_arg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@FunctionDef({
|
|
12
|
+
"description": (
|
|
13
|
+
"Returns a date value. With no arguments returns the current date. "
|
|
14
|
+
"Accepts an ISO 8601 date string or a map of components (year, month, day)."
|
|
15
|
+
),
|
|
16
|
+
"category": "scalar",
|
|
17
|
+
"parameters": [
|
|
18
|
+
{
|
|
19
|
+
"name": "input",
|
|
20
|
+
"description": "Optional. An ISO 8601 date string (YYYY-MM-DD) or a map of components.",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"required": False,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
"output": {
|
|
26
|
+
"description": (
|
|
27
|
+
"A date object with properties: year, month, day, "
|
|
28
|
+
"epochMillis, dayOfWeek, dayOfYear, quarter, formatted"
|
|
29
|
+
),
|
|
30
|
+
"type": "object",
|
|
31
|
+
},
|
|
32
|
+
"examples": [
|
|
33
|
+
"RETURN date() AS today",
|
|
34
|
+
"RETURN date('2025-06-15') AS d",
|
|
35
|
+
"RETURN date({year: 2025, month: 6, day: 15}) AS d",
|
|
36
|
+
"WITH date() AS d RETURN d.year, d.month, d.dayOfWeek",
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
class DateFunction(Function):
|
|
40
|
+
"""Date function.
|
|
41
|
+
|
|
42
|
+
Returns a date value (no time component).
|
|
43
|
+
When called with no arguments, returns the current date.
|
|
44
|
+
When called with a string argument, parses it as an ISO 8601 date.
|
|
45
|
+
|
|
46
|
+
Equivalent to Neo4j's date() function.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
super().__init__("date")
|
|
51
|
+
self._expected_parameter_count = None
|
|
52
|
+
|
|
53
|
+
def value(self) -> Any:
|
|
54
|
+
children = self.get_children()
|
|
55
|
+
if len(children) > 1:
|
|
56
|
+
raise ValueError("date() accepts at most one argument")
|
|
57
|
+
|
|
58
|
+
if len(children) == 1:
|
|
59
|
+
d = parse_temporal_arg(children[0].value(), "date")
|
|
60
|
+
else:
|
|
61
|
+
d = datetime.now()
|
|
62
|
+
|
|
63
|
+
return build_date_object(d)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Datetime function."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .function import Function
|
|
7
|
+
from .function_metadata import FunctionDef
|
|
8
|
+
from .temporal_utils import build_datetime_object, parse_temporal_arg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@FunctionDef({
|
|
12
|
+
"description": (
|
|
13
|
+
"Returns a datetime value. With no arguments returns the current UTC datetime. "
|
|
14
|
+
"Accepts an ISO 8601 string or a map of components (year, month, day, hour, minute, second, millisecond)."
|
|
15
|
+
),
|
|
16
|
+
"category": "scalar",
|
|
17
|
+
"parameters": [
|
|
18
|
+
{
|
|
19
|
+
"name": "input",
|
|
20
|
+
"description": "Optional. An ISO 8601 datetime string or a map of components.",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"required": False,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
"output": {
|
|
26
|
+
"description": (
|
|
27
|
+
"A datetime object with properties: year, month, day, hour, minute, second, millisecond, "
|
|
28
|
+
"epochMillis, epochSeconds, dayOfWeek, dayOfYear, quarter, formatted"
|
|
29
|
+
),
|
|
30
|
+
"type": "object",
|
|
31
|
+
},
|
|
32
|
+
"examples": [
|
|
33
|
+
"RETURN datetime() AS now",
|
|
34
|
+
"RETURN datetime('2025-06-15T12:30:00Z') AS dt",
|
|
35
|
+
"RETURN datetime({year: 2025, month: 6, day: 15, hour: 12}) AS dt",
|
|
36
|
+
"WITH datetime() AS dt RETURN dt.year, dt.month, dt.day",
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
class Datetime(Function):
|
|
40
|
+
"""Datetime function.
|
|
41
|
+
|
|
42
|
+
Returns a datetime value (date + time + timezone offset).
|
|
43
|
+
When called with no arguments, returns the current UTC datetime.
|
|
44
|
+
When called with a string argument, parses it as an ISO 8601 datetime.
|
|
45
|
+
When called with a map argument, constructs a datetime from components.
|
|
46
|
+
|
|
47
|
+
Equivalent to Neo4j's datetime() function.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
super().__init__("datetime")
|
|
52
|
+
self._expected_parameter_count = None
|
|
53
|
+
|
|
54
|
+
def value(self) -> Any:
|
|
55
|
+
children = self.get_children()
|
|
56
|
+
if len(children) > 1:
|
|
57
|
+
raise ValueError("datetime() accepts at most one argument")
|
|
58
|
+
|
|
59
|
+
if len(children) == 1:
|
|
60
|
+
d = parse_temporal_arg(children[0].value(), "datetime")
|
|
61
|
+
else:
|
|
62
|
+
d = datetime.now(timezone.utc)
|
|
63
|
+
|
|
64
|
+
return build_datetime_object(d, utc=True)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Duration function."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
from .function import Function
|
|
7
|
+
from .function_metadata import FunctionDef
|
|
8
|
+
|
|
9
|
+
ISO_DURATION_REGEX = re.compile(
|
|
10
|
+
r"^P(?:(\d+(?:\.\d+)?)Y)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)W)?"
|
|
11
|
+
r"(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?"
|
|
12
|
+
r"(?:(\d+(?:\.\d+)?)S)?)?$"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_duration_string(s: str) -> Dict[str, float]:
|
|
17
|
+
"""Parse an ISO 8601 duration string into components."""
|
|
18
|
+
match = ISO_DURATION_REGEX.match(s)
|
|
19
|
+
if not match:
|
|
20
|
+
raise ValueError(f"duration(): Invalid ISO 8601 duration string: '{s}'")
|
|
21
|
+
return {
|
|
22
|
+
"years": float(match.group(1)) if match.group(1) else 0,
|
|
23
|
+
"months": float(match.group(2)) if match.group(2) else 0,
|
|
24
|
+
"weeks": float(match.group(3)) if match.group(3) else 0,
|
|
25
|
+
"days": float(match.group(4)) if match.group(4) else 0,
|
|
26
|
+
"hours": float(match.group(5)) if match.group(5) else 0,
|
|
27
|
+
"minutes": float(match.group(6)) if match.group(6) else 0,
|
|
28
|
+
"seconds": float(match.group(7)) if match.group(7) else 0,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_duration_object(components: Dict[str, Any]) -> Dict[str, Any]:
|
|
33
|
+
"""Build a duration result object from components."""
|
|
34
|
+
years = components.get("years", 0) or 0
|
|
35
|
+
months = components.get("months", 0) or 0
|
|
36
|
+
weeks = components.get("weeks", 0) or 0
|
|
37
|
+
days = components.get("days", 0) or 0
|
|
38
|
+
hours = components.get("hours", 0) or 0
|
|
39
|
+
minutes = components.get("minutes", 0) or 0
|
|
40
|
+
raw_seconds = components.get("seconds", 0) or 0
|
|
41
|
+
seconds = int(raw_seconds)
|
|
42
|
+
fractional_seconds = raw_seconds - seconds
|
|
43
|
+
|
|
44
|
+
if "milliseconds" in components and components["milliseconds"]:
|
|
45
|
+
milliseconds = int(components["milliseconds"])
|
|
46
|
+
else:
|
|
47
|
+
milliseconds = round(fractional_seconds * 1000)
|
|
48
|
+
|
|
49
|
+
if "nanoseconds" in components and components["nanoseconds"]:
|
|
50
|
+
nanoseconds = int(components["nanoseconds"])
|
|
51
|
+
else:
|
|
52
|
+
nanoseconds = round(fractional_seconds * 1_000_000_000) % 1_000_000
|
|
53
|
+
|
|
54
|
+
# Total days including weeks
|
|
55
|
+
total_days = int(days + weeks * 7)
|
|
56
|
+
|
|
57
|
+
# Total seconds for the time portion
|
|
58
|
+
total_seconds = int(hours * 3600 + minutes * 60 + seconds)
|
|
59
|
+
|
|
60
|
+
# Total months
|
|
61
|
+
total_months = int(years * 12 + months)
|
|
62
|
+
|
|
63
|
+
# Build ISO 8601 formatted string
|
|
64
|
+
formatted = "P"
|
|
65
|
+
if years:
|
|
66
|
+
formatted += f"{int(years)}Y"
|
|
67
|
+
if months:
|
|
68
|
+
formatted += f"{int(months)}M"
|
|
69
|
+
if weeks:
|
|
70
|
+
formatted += f"{int(weeks)}W"
|
|
71
|
+
raw_days = int(total_days - weeks * 7)
|
|
72
|
+
if raw_days:
|
|
73
|
+
formatted += f"{raw_days}D"
|
|
74
|
+
has_time = hours or minutes or seconds or milliseconds
|
|
75
|
+
if has_time:
|
|
76
|
+
formatted += "T"
|
|
77
|
+
if hours:
|
|
78
|
+
formatted += f"{int(hours)}H"
|
|
79
|
+
if minutes:
|
|
80
|
+
formatted += f"{int(minutes)}M"
|
|
81
|
+
if seconds or milliseconds:
|
|
82
|
+
if milliseconds:
|
|
83
|
+
formatted += f"{seconds}.{milliseconds:03d}S"
|
|
84
|
+
else:
|
|
85
|
+
formatted += f"{seconds}S"
|
|
86
|
+
if formatted == "P":
|
|
87
|
+
formatted = "PT0S"
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"years": int(years),
|
|
91
|
+
"months": int(months),
|
|
92
|
+
"weeks": int(weeks),
|
|
93
|
+
"days": total_days,
|
|
94
|
+
"hours": int(hours),
|
|
95
|
+
"minutes": int(minutes),
|
|
96
|
+
"seconds": seconds,
|
|
97
|
+
"milliseconds": milliseconds,
|
|
98
|
+
"nanoseconds": nanoseconds,
|
|
99
|
+
"totalMonths": total_months,
|
|
100
|
+
"totalDays": total_days,
|
|
101
|
+
"totalSeconds": total_seconds,
|
|
102
|
+
"formatted": formatted,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@FunctionDef({
|
|
107
|
+
"description": (
|
|
108
|
+
"Creates a duration value representing a span of time. "
|
|
109
|
+
"Accepts an ISO 8601 duration string (e.g., 'P1Y2M3DT4H5M6S') or a map of components "
|
|
110
|
+
"(years, months, weeks, days, hours, minutes, seconds, milliseconds, nanoseconds)."
|
|
111
|
+
),
|
|
112
|
+
"category": "scalar",
|
|
113
|
+
"parameters": [
|
|
114
|
+
{
|
|
115
|
+
"name": "input",
|
|
116
|
+
"description": (
|
|
117
|
+
"An ISO 8601 duration string or a map of components "
|
|
118
|
+
"(years, months, weeks, days, hours, minutes, seconds, milliseconds, nanoseconds)"
|
|
119
|
+
),
|
|
120
|
+
"type": "any",
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"output": {
|
|
124
|
+
"description": (
|
|
125
|
+
"A duration object with properties: years, months, weeks, days, hours, minutes, seconds, "
|
|
126
|
+
"milliseconds, nanoseconds, totalMonths, totalDays, totalSeconds, formatted"
|
|
127
|
+
),
|
|
128
|
+
"type": "object",
|
|
129
|
+
},
|
|
130
|
+
"examples": [
|
|
131
|
+
"RETURN duration('P1Y2M3D') AS d",
|
|
132
|
+
"RETURN duration('PT2H30M') AS d",
|
|
133
|
+
"RETURN duration({days: 14, hours: 16}) AS d",
|
|
134
|
+
"RETURN duration({months: 5, days: 1, hours: 12}) AS d",
|
|
135
|
+
],
|
|
136
|
+
})
|
|
137
|
+
class Duration(Function):
|
|
138
|
+
"""Duration function.
|
|
139
|
+
|
|
140
|
+
Creates a duration value representing a span of time.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self) -> None:
|
|
144
|
+
super().__init__("duration")
|
|
145
|
+
self._expected_parameter_count = 1
|
|
146
|
+
|
|
147
|
+
def value(self) -> Any:
|
|
148
|
+
arg = self.get_children()[0].value()
|
|
149
|
+
if arg is None:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
if isinstance(arg, str):
|
|
153
|
+
components = _parse_duration_string(arg)
|
|
154
|
+
return _build_duration_object(components)
|
|
155
|
+
|
|
156
|
+
if isinstance(arg, dict):
|
|
157
|
+
return _build_duration_object(arg)
|
|
158
|
+
|
|
159
|
+
raise ValueError("duration() expects a string or map argument")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""ElementId 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": (
|
|
11
|
+
"Returns the element id of a node or relationship as a string. "
|
|
12
|
+
"For nodes, returns the string representation of the id property. "
|
|
13
|
+
"For relationships, returns the type."
|
|
14
|
+
),
|
|
15
|
+
"category": "scalar",
|
|
16
|
+
"parameters": [
|
|
17
|
+
{"name": "entity", "description": "A node or relationship to get the element id from", "type": "object"}
|
|
18
|
+
],
|
|
19
|
+
"output": {"description": "The element id of the entity as a string", "type": "string", "example": "\"1\""},
|
|
20
|
+
"examples": [
|
|
21
|
+
"MATCH (n:Person) RETURN elementId(n)",
|
|
22
|
+
"MATCH (a)-[r]->(b) RETURN elementId(r)"
|
|
23
|
+
]
|
|
24
|
+
})
|
|
25
|
+
class ElementId(Function):
|
|
26
|
+
"""ElementId function.
|
|
27
|
+
|
|
28
|
+
Returns the element id of a node or relationship as a string.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
super().__init__("elementid")
|
|
33
|
+
self._expected_parameter_count = 1
|
|
34
|
+
|
|
35
|
+
def value(self) -> Any:
|
|
36
|
+
obj = self.get_children()[0].value()
|
|
37
|
+
if obj is None:
|
|
38
|
+
return None
|
|
39
|
+
if not isinstance(obj, dict):
|
|
40
|
+
raise ValueError("elementId() expects a node or relationship")
|
|
41
|
+
|
|
42
|
+
# If it's a RelationshipMatchRecord (has type, startNode, endNode, properties)
|
|
43
|
+
if all(k in obj for k in ("type", "startNode", "endNode", "properties")):
|
|
44
|
+
return str(obj["type"])
|
|
45
|
+
|
|
46
|
+
# If it's a node record (has id field)
|
|
47
|
+
if "id" in obj:
|
|
48
|
+
return str(obj["id"])
|
|
49
|
+
|
|
50
|
+
raise ValueError("elementId() expects a node or relationship")
|