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.
Files changed (197) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/graph/database.d.ts +1 -0
  3. package/dist/graph/database.d.ts.map +1 -1
  4. package/dist/graph/database.js +43 -6
  5. package/dist/graph/database.js.map +1 -1
  6. package/dist/graph/relationship.d.ts +3 -1
  7. package/dist/graph/relationship.d.ts.map +1 -1
  8. package/dist/graph/relationship.js +12 -4
  9. package/dist/graph/relationship.js.map +1 -1
  10. package/dist/graph/relationship_data.js +1 -1
  11. package/dist/graph/relationship_data.js.map +1 -1
  12. package/dist/graph/relationship_match_collector.d.ts.map +1 -1
  13. package/dist/graph/relationship_match_collector.js +6 -3
  14. package/dist/graph/relationship_match_collector.js.map +1 -1
  15. package/dist/graph/relationship_reference.js +1 -1
  16. package/dist/graph/relationship_reference.js.map +1 -1
  17. package/dist/parsing/data_structures/lookup.d.ts.map +1 -1
  18. package/dist/parsing/data_structures/lookup.js +5 -1
  19. package/dist/parsing/data_structures/lookup.js.map +1 -1
  20. package/dist/parsing/functions/coalesce.d.ts +17 -0
  21. package/dist/parsing/functions/coalesce.d.ts.map +1 -0
  22. package/dist/parsing/functions/coalesce.js +61 -0
  23. package/dist/parsing/functions/coalesce.js.map +1 -0
  24. package/dist/parsing/functions/date.d.ts +22 -0
  25. package/dist/parsing/functions/date.d.ts.map +1 -0
  26. package/dist/parsing/functions/date.js +71 -0
  27. package/dist/parsing/functions/date.js.map +1 -0
  28. package/dist/parsing/functions/datetime.d.ts +22 -0
  29. package/dist/parsing/functions/datetime.d.ts.map +1 -0
  30. package/dist/parsing/functions/datetime.js +71 -0
  31. package/dist/parsing/functions/datetime.js.map +1 -0
  32. package/dist/parsing/functions/duration.d.ts +7 -0
  33. package/dist/parsing/functions/duration.d.ts.map +1 -0
  34. package/dist/parsing/functions/duration.js +145 -0
  35. package/dist/parsing/functions/duration.js.map +1 -0
  36. package/dist/parsing/functions/element_id.d.ts +7 -0
  37. package/dist/parsing/functions/element_id.d.ts.map +1 -0
  38. package/dist/parsing/functions/element_id.js +58 -0
  39. package/dist/parsing/functions/element_id.js.map +1 -0
  40. package/dist/parsing/functions/function_factory.d.ts +21 -0
  41. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  42. package/dist/parsing/functions/function_factory.js +21 -0
  43. package/dist/parsing/functions/function_factory.js.map +1 -1
  44. package/dist/parsing/functions/head.d.ts +7 -0
  45. package/dist/parsing/functions/head.d.ts.map +1 -0
  46. package/dist/parsing/functions/head.js +53 -0
  47. package/dist/parsing/functions/head.js.map +1 -0
  48. package/dist/parsing/functions/id.d.ts +7 -0
  49. package/dist/parsing/functions/id.d.ts.map +1 -0
  50. package/dist/parsing/functions/id.js +58 -0
  51. package/dist/parsing/functions/id.js.map +1 -0
  52. package/dist/parsing/functions/last.d.ts +7 -0
  53. package/dist/parsing/functions/last.d.ts.map +1 -0
  54. package/dist/parsing/functions/last.js +53 -0
  55. package/dist/parsing/functions/last.js.map +1 -0
  56. package/dist/parsing/functions/localdatetime.d.ts +21 -0
  57. package/dist/parsing/functions/localdatetime.d.ts.map +1 -0
  58. package/dist/parsing/functions/localdatetime.js +71 -0
  59. package/dist/parsing/functions/localdatetime.js.map +1 -0
  60. package/dist/parsing/functions/localtime.d.ts +20 -0
  61. package/dist/parsing/functions/localtime.d.ts.map +1 -0
  62. package/dist/parsing/functions/localtime.js +67 -0
  63. package/dist/parsing/functions/localtime.js.map +1 -0
  64. package/dist/parsing/functions/max.d.ts +14 -0
  65. package/dist/parsing/functions/max.d.ts.map +1 -0
  66. package/dist/parsing/functions/max.js +51 -0
  67. package/dist/parsing/functions/max.js.map +1 -0
  68. package/dist/parsing/functions/min.d.ts +14 -0
  69. package/dist/parsing/functions/min.d.ts.map +1 -0
  70. package/dist/parsing/functions/min.js +51 -0
  71. package/dist/parsing/functions/min.js.map +1 -0
  72. package/dist/parsing/functions/nodes.d.ts +7 -0
  73. package/dist/parsing/functions/nodes.d.ts.map +1 -0
  74. package/dist/parsing/functions/nodes.js +63 -0
  75. package/dist/parsing/functions/nodes.js.map +1 -0
  76. package/dist/parsing/functions/predicate_sum.d.ts.map +1 -1
  77. package/dist/parsing/functions/predicate_sum.js +13 -10
  78. package/dist/parsing/functions/predicate_sum.js.map +1 -1
  79. package/dist/parsing/functions/properties.d.ts +7 -0
  80. package/dist/parsing/functions/properties.d.ts.map +1 -0
  81. package/dist/parsing/functions/properties.js +74 -0
  82. package/dist/parsing/functions/properties.js.map +1 -0
  83. package/dist/parsing/functions/relationships.d.ts +7 -0
  84. package/dist/parsing/functions/relationships.d.ts.map +1 -0
  85. package/dist/parsing/functions/relationships.js +61 -0
  86. package/dist/parsing/functions/relationships.js.map +1 -0
  87. package/dist/parsing/functions/schema.d.ts +5 -2
  88. package/dist/parsing/functions/schema.d.ts.map +1 -1
  89. package/dist/parsing/functions/schema.js +7 -4
  90. package/dist/parsing/functions/schema.js.map +1 -1
  91. package/dist/parsing/functions/tail.d.ts +7 -0
  92. package/dist/parsing/functions/tail.d.ts.map +1 -0
  93. package/dist/parsing/functions/tail.js +50 -0
  94. package/dist/parsing/functions/tail.js.map +1 -0
  95. package/dist/parsing/functions/temporal_utils.d.ts +39 -0
  96. package/dist/parsing/functions/temporal_utils.d.ts.map +1 -0
  97. package/dist/parsing/functions/temporal_utils.js +168 -0
  98. package/dist/parsing/functions/temporal_utils.js.map +1 -0
  99. package/dist/parsing/functions/time.d.ts +20 -0
  100. package/dist/parsing/functions/time.d.ts.map +1 -0
  101. package/dist/parsing/functions/time.js +67 -0
  102. package/dist/parsing/functions/time.js.map +1 -0
  103. package/dist/parsing/functions/timestamp.d.ts +17 -0
  104. package/dist/parsing/functions/timestamp.d.ts.map +1 -0
  105. package/dist/parsing/functions/timestamp.js +51 -0
  106. package/dist/parsing/functions/timestamp.js.map +1 -0
  107. package/dist/parsing/functions/to_float.d.ts +7 -0
  108. package/dist/parsing/functions/to_float.d.ts.map +1 -0
  109. package/dist/parsing/functions/to_float.js +61 -0
  110. package/dist/parsing/functions/to_float.js.map +1 -0
  111. package/dist/parsing/functions/to_integer.d.ts +7 -0
  112. package/dist/parsing/functions/to_integer.d.ts.map +1 -0
  113. package/dist/parsing/functions/to_integer.js +61 -0
  114. package/dist/parsing/functions/to_integer.js.map +1 -0
  115. package/dist/parsing/functions/trim.d.ts +7 -0
  116. package/dist/parsing/functions/trim.d.ts.map +1 -0
  117. package/dist/parsing/functions/trim.js +37 -0
  118. package/dist/parsing/functions/trim.js.map +1 -0
  119. package/dist/parsing/operations/group_by.d.ts.map +1 -1
  120. package/dist/parsing/operations/group_by.js +4 -2
  121. package/dist/parsing/operations/group_by.js.map +1 -1
  122. package/dist/parsing/parser.d.ts.map +1 -1
  123. package/dist/parsing/parser.js +15 -2
  124. package/dist/parsing/parser.js.map +1 -1
  125. package/docs/flowquery.min.js +1 -1
  126. package/flowquery-py/pyproject.toml +1 -1
  127. package/flowquery-py/src/graph/database.py +44 -11
  128. package/flowquery-py/src/graph/relationship.py +11 -3
  129. package/flowquery-py/src/graph/relationship_data.py +2 -1
  130. package/flowquery-py/src/graph/relationship_match_collector.py +7 -1
  131. package/flowquery-py/src/graph/relationship_reference.py +2 -2
  132. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -0
  133. package/flowquery-py/src/parsing/functions/__init__.py +42 -2
  134. package/flowquery-py/src/parsing/functions/coalesce.py +44 -0
  135. package/flowquery-py/src/parsing/functions/date_.py +63 -0
  136. package/flowquery-py/src/parsing/functions/datetime_.py +64 -0
  137. package/flowquery-py/src/parsing/functions/duration.py +159 -0
  138. package/flowquery-py/src/parsing/functions/element_id.py +50 -0
  139. package/flowquery-py/src/parsing/functions/head.py +39 -0
  140. package/flowquery-py/src/parsing/functions/id_.py +49 -0
  141. package/flowquery-py/src/parsing/functions/last.py +39 -0
  142. package/flowquery-py/src/parsing/functions/localdatetime.py +62 -0
  143. package/flowquery-py/src/parsing/functions/localtime.py +59 -0
  144. package/flowquery-py/src/parsing/functions/max_.py +49 -0
  145. package/flowquery-py/src/parsing/functions/min_.py +49 -0
  146. package/flowquery-py/src/parsing/functions/nodes.py +48 -0
  147. package/flowquery-py/src/parsing/functions/predicate_sum.py +3 -6
  148. package/flowquery-py/src/parsing/functions/properties.py +50 -0
  149. package/flowquery-py/src/parsing/functions/relationships.py +46 -0
  150. package/flowquery-py/src/parsing/functions/schema.py +9 -5
  151. package/flowquery-py/src/parsing/functions/tail.py +37 -0
  152. package/flowquery-py/src/parsing/functions/temporal_utils.py +186 -0
  153. package/flowquery-py/src/parsing/functions/time_.py +59 -0
  154. package/flowquery-py/src/parsing/functions/timestamp.py +39 -0
  155. package/flowquery-py/src/parsing/functions/to_float.py +46 -0
  156. package/flowquery-py/src/parsing/functions/to_integer.py +46 -0
  157. package/flowquery-py/src/parsing/functions/trim.py +35 -0
  158. package/flowquery-py/src/parsing/operations/group_by.py +2 -0
  159. package/flowquery-py/src/parsing/parser.py +12 -2
  160. package/flowquery-py/tests/compute/test_runner.py +1082 -4
  161. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  162. package/package.json +1 -1
  163. package/src/graph/database.ts +42 -4
  164. package/src/graph/relationship.ts +12 -4
  165. package/src/graph/relationship_data.ts +1 -1
  166. package/src/graph/relationship_match_collector.ts +6 -2
  167. package/src/graph/relationship_reference.ts +1 -1
  168. package/src/parsing/data_structures/lookup.ts +8 -4
  169. package/src/parsing/functions/coalesce.ts +50 -0
  170. package/src/parsing/functions/date.ts +65 -0
  171. package/src/parsing/functions/datetime.ts +65 -0
  172. package/src/parsing/functions/duration.ts +143 -0
  173. package/src/parsing/functions/element_id.ts +51 -0
  174. package/src/parsing/functions/function_factory.ts +21 -0
  175. package/src/parsing/functions/head.ts +42 -0
  176. package/src/parsing/functions/id.ts +51 -0
  177. package/src/parsing/functions/last.ts +42 -0
  178. package/src/parsing/functions/localdatetime.ts +65 -0
  179. package/src/parsing/functions/localtime.ts +60 -0
  180. package/src/parsing/functions/max.ts +37 -0
  181. package/src/parsing/functions/min.ts +37 -0
  182. package/src/parsing/functions/nodes.ts +54 -0
  183. package/src/parsing/functions/predicate_sum.ts +17 -12
  184. package/src/parsing/functions/properties.ts +56 -0
  185. package/src/parsing/functions/relationships.ts +52 -0
  186. package/src/parsing/functions/schema.ts +7 -4
  187. package/src/parsing/functions/tail.ts +39 -0
  188. package/src/parsing/functions/temporal_utils.ts +180 -0
  189. package/src/parsing/functions/time.ts +60 -0
  190. package/src/parsing/functions/timestamp.ts +41 -0
  191. package/src/parsing/functions/to_float.ts +50 -0
  192. package/src/parsing/functions/to_integer.ts +50 -0
  193. package/src/parsing/functions/trim.ts +25 -0
  194. package/src/parsing/operations/group_by.ts +4 -1
  195. package/src/parsing/parser.ts +15 -2
  196. package/tests/compute/runner.test.ts +1005 -3
  197. package/tests/parsing/parser.test.ts +37 -0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowquery"
3
- version = "1.0.24"
3
+ version = "1.0.26"
4
4
  description = "A declarative query language for data processing pipelines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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
- 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]]:
58
71
  """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()]
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: dict[str, Any] = {"kind": "node", "label": label}
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
- if sample:
81
+ properties = list(sample.keys())
82
+ if properties:
83
+ entry["properties"] = properties
67
84
  entry["sample"] = sample
68
- result.append(entry)
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: 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
+ }
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
- if sample:
97
+ properties = list(sample.keys())
98
+ if properties:
99
+ entry_rel["properties"] = properties
76
100
  entry_rel["sample"] = sample
77
- result.append(entry_rel)
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._type: Optional[str] = None
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._type
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._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
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 right_id."""
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": relationship.type or "",
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.type:
14
- self.type = relationship.type
13
+ if relationship.types:
14
+ self.types = relationship.types
15
15
 
16
16
  @property
17
17
  def referred(self) -> ASTNode:
@@ -38,6 +38,8 @@ class Lookup(ASTNode):
38
38
 
39
39
  def value(self) -> Any:
40
40
  obj = self.variable.value()
41
+ if obj is None:
42
+ return None
41
43
  key = self.index.value()
42
44
  # Try dict-like access first, then fall back to attribute access for objects
43
45
  try:
@@ -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")