flowquery 1.0.35 → 1.0.37

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 (144) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/parsing/data_structures/lookup.d.ts.map +1 -1
  3. package/dist/parsing/data_structures/lookup.js +5 -1
  4. package/dist/parsing/data_structures/lookup.js.map +1 -1
  5. package/dist/parsing/functions/coalesce.d.ts +16 -0
  6. package/dist/parsing/functions/coalesce.d.ts.map +1 -0
  7. package/dist/parsing/functions/coalesce.js +60 -0
  8. package/dist/parsing/functions/coalesce.js.map +1 -0
  9. package/dist/parsing/functions/date.d.ts +20 -0
  10. package/dist/parsing/functions/date.d.ts.map +1 -0
  11. package/dist/parsing/functions/date.js +69 -0
  12. package/dist/parsing/functions/date.js.map +1 -0
  13. package/dist/parsing/functions/datetime.d.ts +20 -0
  14. package/dist/parsing/functions/datetime.d.ts.map +1 -0
  15. package/dist/parsing/functions/datetime.js +69 -0
  16. package/dist/parsing/functions/datetime.js.map +1 -0
  17. package/dist/parsing/functions/duration.d.ts +7 -0
  18. package/dist/parsing/functions/duration.d.ts.map +1 -0
  19. package/dist/parsing/functions/duration.js +145 -0
  20. package/dist/parsing/functions/duration.js.map +1 -0
  21. package/dist/parsing/functions/element_id.d.ts +7 -0
  22. package/dist/parsing/functions/element_id.d.ts.map +1 -0
  23. package/dist/parsing/functions/element_id.js +58 -0
  24. package/dist/parsing/functions/element_id.js.map +1 -0
  25. package/dist/parsing/functions/function_factory.d.ts +20 -0
  26. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  27. package/dist/parsing/functions/function_factory.js +20 -0
  28. package/dist/parsing/functions/function_factory.js.map +1 -1
  29. package/dist/parsing/functions/head.d.ts +7 -0
  30. package/dist/parsing/functions/head.d.ts.map +1 -0
  31. package/dist/parsing/functions/head.js +53 -0
  32. package/dist/parsing/functions/head.js.map +1 -0
  33. package/dist/parsing/functions/id.d.ts +7 -0
  34. package/dist/parsing/functions/id.d.ts.map +1 -0
  35. package/dist/parsing/functions/id.js +58 -0
  36. package/dist/parsing/functions/id.js.map +1 -0
  37. package/dist/parsing/functions/last.d.ts +7 -0
  38. package/dist/parsing/functions/last.d.ts.map +1 -0
  39. package/dist/parsing/functions/last.js +53 -0
  40. package/dist/parsing/functions/last.js.map +1 -0
  41. package/dist/parsing/functions/localdatetime.d.ts +19 -0
  42. package/dist/parsing/functions/localdatetime.d.ts.map +1 -0
  43. package/dist/parsing/functions/localdatetime.js +69 -0
  44. package/dist/parsing/functions/localdatetime.js.map +1 -0
  45. package/dist/parsing/functions/localtime.d.ts +18 -0
  46. package/dist/parsing/functions/localtime.d.ts.map +1 -0
  47. package/dist/parsing/functions/localtime.js +65 -0
  48. package/dist/parsing/functions/localtime.js.map +1 -0
  49. package/dist/parsing/functions/max.d.ts +14 -0
  50. package/dist/parsing/functions/max.d.ts.map +1 -0
  51. package/dist/parsing/functions/max.js +51 -0
  52. package/dist/parsing/functions/max.js.map +1 -0
  53. package/dist/parsing/functions/min.d.ts +14 -0
  54. package/dist/parsing/functions/min.d.ts.map +1 -0
  55. package/dist/parsing/functions/min.js +51 -0
  56. package/dist/parsing/functions/min.js.map +1 -0
  57. package/dist/parsing/functions/nodes.d.ts +7 -0
  58. package/dist/parsing/functions/nodes.d.ts.map +1 -0
  59. package/dist/parsing/functions/nodes.js +63 -0
  60. package/dist/parsing/functions/nodes.js.map +1 -0
  61. package/dist/parsing/functions/properties.d.ts +7 -0
  62. package/dist/parsing/functions/properties.d.ts.map +1 -0
  63. package/dist/parsing/functions/properties.js +74 -0
  64. package/dist/parsing/functions/properties.js.map +1 -0
  65. package/dist/parsing/functions/relationships.d.ts +7 -0
  66. package/dist/parsing/functions/relationships.d.ts.map +1 -0
  67. package/dist/parsing/functions/relationships.js +61 -0
  68. package/dist/parsing/functions/relationships.js.map +1 -0
  69. package/dist/parsing/functions/tail.d.ts +7 -0
  70. package/dist/parsing/functions/tail.d.ts.map +1 -0
  71. package/dist/parsing/functions/tail.js +50 -0
  72. package/dist/parsing/functions/tail.js.map +1 -0
  73. package/dist/parsing/functions/temporal_utils.d.ts +39 -0
  74. package/dist/parsing/functions/temporal_utils.d.ts.map +1 -0
  75. package/dist/parsing/functions/temporal_utils.js +168 -0
  76. package/dist/parsing/functions/temporal_utils.js.map +1 -0
  77. package/dist/parsing/functions/time.d.ts +18 -0
  78. package/dist/parsing/functions/time.d.ts.map +1 -0
  79. package/dist/parsing/functions/time.js +65 -0
  80. package/dist/parsing/functions/time.js.map +1 -0
  81. package/dist/parsing/functions/timestamp.d.ts +15 -0
  82. package/dist/parsing/functions/timestamp.d.ts.map +1 -0
  83. package/dist/parsing/functions/timestamp.js +48 -0
  84. package/dist/parsing/functions/timestamp.js.map +1 -0
  85. package/dist/parsing/functions/to_float.d.ts +7 -0
  86. package/dist/parsing/functions/to_float.d.ts.map +1 -0
  87. package/dist/parsing/functions/to_float.js +61 -0
  88. package/dist/parsing/functions/to_float.js.map +1 -0
  89. package/dist/parsing/functions/to_integer.d.ts +7 -0
  90. package/dist/parsing/functions/to_integer.d.ts.map +1 -0
  91. package/dist/parsing/functions/to_integer.js +61 -0
  92. package/dist/parsing/functions/to_integer.js.map +1 -0
  93. package/docs/flowquery.min.js +1 -1
  94. package/flowquery-py/pyproject.toml +1 -1
  95. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -0
  96. package/flowquery-py/src/parsing/functions/__init__.py +40 -2
  97. package/flowquery-py/src/parsing/functions/coalesce.py +43 -0
  98. package/flowquery-py/src/parsing/functions/date_.py +61 -0
  99. package/flowquery-py/src/parsing/functions/datetime_.py +62 -0
  100. package/flowquery-py/src/parsing/functions/duration.py +159 -0
  101. package/flowquery-py/src/parsing/functions/element_id.py +50 -0
  102. package/flowquery-py/src/parsing/functions/head.py +39 -0
  103. package/flowquery-py/src/parsing/functions/id_.py +49 -0
  104. package/flowquery-py/src/parsing/functions/last.py +39 -0
  105. package/flowquery-py/src/parsing/functions/localdatetime.py +60 -0
  106. package/flowquery-py/src/parsing/functions/localtime.py +57 -0
  107. package/flowquery-py/src/parsing/functions/max_.py +49 -0
  108. package/flowquery-py/src/parsing/functions/min_.py +49 -0
  109. package/flowquery-py/src/parsing/functions/nodes.py +48 -0
  110. package/flowquery-py/src/parsing/functions/properties.py +50 -0
  111. package/flowquery-py/src/parsing/functions/relationships.py +46 -0
  112. package/flowquery-py/src/parsing/functions/tail.py +37 -0
  113. package/flowquery-py/src/parsing/functions/temporal_utils.py +186 -0
  114. package/flowquery-py/src/parsing/functions/time_.py +57 -0
  115. package/flowquery-py/src/parsing/functions/timestamp.py +37 -0
  116. package/flowquery-py/src/parsing/functions/to_float.py +46 -0
  117. package/flowquery-py/src/parsing/functions/to_integer.py +46 -0
  118. package/flowquery-py/tests/compute/test_runner.py +834 -1
  119. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  120. package/package.json +1 -1
  121. package/src/parsing/data_structures/lookup.ts +8 -4
  122. package/src/parsing/functions/coalesce.ts +49 -0
  123. package/src/parsing/functions/date.ts +63 -0
  124. package/src/parsing/functions/datetime.ts +63 -0
  125. package/src/parsing/functions/duration.ts +143 -0
  126. package/src/parsing/functions/element_id.ts +51 -0
  127. package/src/parsing/functions/function_factory.ts +20 -0
  128. package/src/parsing/functions/head.ts +42 -0
  129. package/src/parsing/functions/id.ts +51 -0
  130. package/src/parsing/functions/last.ts +42 -0
  131. package/src/parsing/functions/localdatetime.ts +63 -0
  132. package/src/parsing/functions/localtime.ts +58 -0
  133. package/src/parsing/functions/max.ts +37 -0
  134. package/src/parsing/functions/min.ts +37 -0
  135. package/src/parsing/functions/nodes.ts +54 -0
  136. package/src/parsing/functions/properties.ts +56 -0
  137. package/src/parsing/functions/relationships.ts +52 -0
  138. package/src/parsing/functions/tail.ts +39 -0
  139. package/src/parsing/functions/temporal_utils.ts +180 -0
  140. package/src/parsing/functions/time.ts +58 -0
  141. package/src/parsing/functions/timestamp.ts +37 -0
  142. package/src/parsing/functions/to_float.ts +50 -0
  143. package/src/parsing/functions/to_integer.ts +50 -0
  144. package/tests/compute/runner.test.ts +726 -0
@@ -0,0 +1,57 @@
1
+ """Local time 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_time_object, parse_temporal_arg
9
+
10
+
11
+ @FunctionDef({
12
+ "description": (
13
+ "Returns a local time value (no timezone). With no arguments returns the current local time. "
14
+ "Accepts an ISO 8601 time string or a map of components."
15
+ ),
16
+ "category": "scalar",
17
+ "parameters": [
18
+ {
19
+ "name": "input",
20
+ "description": "Optional. An ISO 8601 time string (HH:MM:SS) or a map of components.",
21
+ "type": "string",
22
+ "required": False,
23
+ },
24
+ ],
25
+ "output": {
26
+ "description": "A time object with properties: hour, minute, second, millisecond, formatted",
27
+ "type": "object",
28
+ },
29
+ "examples": [
30
+ "RETURN localtime() AS now",
31
+ "RETURN localtime('14:30:00') AS t",
32
+ "WITH localtime() AS t RETURN t.hour, t.minute",
33
+ ],
34
+ })
35
+ class LocalTime(Function):
36
+ """Local time function.
37
+
38
+ Returns a local time value (no timezone offset).
39
+ When called with no arguments, returns the current local time.
40
+ When called with a string argument, parses it.
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ super().__init__("localtime")
45
+ self._expected_parameter_count = None
46
+
47
+ def value(self) -> Any:
48
+ children = self.get_children()
49
+ if len(children) > 1:
50
+ raise ValueError("localtime() accepts at most one argument")
51
+
52
+ if len(children) == 1:
53
+ d = parse_temporal_arg(children[0].value(), "localtime")
54
+ else:
55
+ d = datetime.now()
56
+
57
+ return build_time_object(d, utc=False)
@@ -0,0 +1,49 @@
1
+ """Max aggregate function."""
2
+
3
+ from typing import Any
4
+
5
+ from .aggregate_function import AggregateFunction
6
+ from .function_metadata import FunctionDef
7
+ from .reducer_element import ReducerElement
8
+
9
+
10
+ class MaxReducerElement(ReducerElement):
11
+ """Reducer element for Max aggregate function."""
12
+
13
+ def __init__(self) -> None:
14
+ self._value: Any = None
15
+
16
+ @property
17
+ def value(self) -> Any:
18
+ return self._value
19
+
20
+ @value.setter
21
+ def value(self, val: Any) -> None:
22
+ if self._value is None or val > self._value:
23
+ self._value = val
24
+
25
+
26
+ @FunctionDef({
27
+ "description": "Returns the maximum value across grouped rows",
28
+ "category": "aggregate",
29
+ "parameters": [
30
+ {"name": "value", "description": "Value to compare", "type": "number"}
31
+ ],
32
+ "output": {"description": "Maximum value", "type": "number", "example": 10},
33
+ "examples": ["WITH [3, 1, 2] AS nums UNWIND nums AS n RETURN max(n)"]
34
+ })
35
+ class Max(AggregateFunction):
36
+ """Max aggregate function.
37
+
38
+ Returns the maximum value across grouped rows.
39
+ """
40
+
41
+ def __init__(self) -> None:
42
+ super().__init__("max")
43
+ self._expected_parameter_count = 1
44
+
45
+ def reduce(self, element: MaxReducerElement) -> None:
46
+ element.value = self.first_child().value()
47
+
48
+ def element(self) -> MaxReducerElement:
49
+ return MaxReducerElement()
@@ -0,0 +1,49 @@
1
+ """Min aggregate function."""
2
+
3
+ from typing import Any
4
+
5
+ from .aggregate_function import AggregateFunction
6
+ from .function_metadata import FunctionDef
7
+ from .reducer_element import ReducerElement
8
+
9
+
10
+ class MinReducerElement(ReducerElement):
11
+ """Reducer element for Min aggregate function."""
12
+
13
+ def __init__(self) -> None:
14
+ self._value: Any = None
15
+
16
+ @property
17
+ def value(self) -> Any:
18
+ return self._value
19
+
20
+ @value.setter
21
+ def value(self, val: Any) -> None:
22
+ if self._value is None or val < self._value:
23
+ self._value = val
24
+
25
+
26
+ @FunctionDef({
27
+ "description": "Returns the minimum value across grouped rows",
28
+ "category": "aggregate",
29
+ "parameters": [
30
+ {"name": "value", "description": "Value to compare", "type": "number"}
31
+ ],
32
+ "output": {"description": "Minimum value", "type": "number", "example": 1},
33
+ "examples": ["WITH [3, 1, 2] AS nums UNWIND nums AS n RETURN min(n)"]
34
+ })
35
+ class Min(AggregateFunction):
36
+ """Min aggregate function.
37
+
38
+ Returns the minimum value across grouped rows.
39
+ """
40
+
41
+ def __init__(self) -> None:
42
+ super().__init__("min")
43
+ self._expected_parameter_count = 1
44
+
45
+ def reduce(self, element: MinReducerElement) -> None:
46
+ element.value = self.first_child().value()
47
+
48
+ def element(self) -> MinReducerElement:
49
+ return MinReducerElement()
@@ -0,0 +1,48 @@
1
+ """Nodes function."""
2
+
3
+ from typing import Any, List
4
+
5
+ from .function import Function
6
+ from .function_metadata import FunctionDef
7
+
8
+
9
+ @FunctionDef({
10
+ "description": "Returns all nodes in a path as an array",
11
+ "category": "scalar",
12
+ "parameters": [
13
+ {"name": "path", "description": "A path value returned from a graph pattern match", "type": "array"}
14
+ ],
15
+ "output": {
16
+ "description": "Array of node records", "type": "array",
17
+ "example": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
18
+ },
19
+ "examples": ["MATCH p=(:Person)-[:KNOWS]-(:Person) RETURN nodes(p)"]
20
+ })
21
+ class Nodes(Function):
22
+ """Nodes function.
23
+
24
+ Returns all nodes in a path as an array.
25
+ """
26
+
27
+ def __init__(self) -> None:
28
+ super().__init__("nodes")
29
+ self._expected_parameter_count = 1
30
+
31
+ def value(self) -> Any:
32
+ path = self.get_children()[0].value()
33
+ if path is None:
34
+ return []
35
+ if not isinstance(path, list):
36
+ raise ValueError("nodes() expects a path (array)")
37
+ # A path is an array of alternating node and relationship objects:
38
+ # [node, rel, node, rel, node, ...]
39
+ # Nodes are plain dicts (have 'id' but not all of 'type'/'startNode'/'endNode'/'properties')
40
+ # Relationships are RelationshipMatchRecords (have 'type', 'startNode', 'endNode', 'properties')
41
+ result: List[Any] = []
42
+ for element in path:
43
+ if element is None or not isinstance(element, dict):
44
+ continue
45
+ # A RelationshipMatchRecord has type, startNode, endNode, properties
46
+ if not all(k in element for k in ("type", "startNode", "endNode", "properties")):
47
+ result.append(element)
48
+ return result
@@ -0,0 +1,50 @@
1
+ """Properties 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 a map containing all the properties of a node, relationship, or map. "
12
+ "For nodes and relationships, internal identifiers are excluded."
13
+ ),
14
+ "category": "scalar",
15
+ "parameters": [
16
+ {"name": "entity", "description": "A node, relationship, or map to extract properties from", "type": "object"}
17
+ ],
18
+ "output": {"description": "Map of properties", "type": "object", "example": {"name": "Alice", "age": 30}},
19
+ "examples": [
20
+ "MATCH (n:Person) RETURN properties(n)",
21
+ "WITH { name: 'Alice', age: 30 } AS obj RETURN properties(obj)"
22
+ ]
23
+ })
24
+ class Properties(Function):
25
+ """Properties function.
26
+
27
+ Returns a map containing all the properties of a node, relationship, or map.
28
+ """
29
+
30
+ def __init__(self) -> None:
31
+ super().__init__("properties")
32
+ self._expected_parameter_count = 1
33
+
34
+ def value(self) -> Any:
35
+ obj = self.get_children()[0].value()
36
+ if obj is None:
37
+ return None
38
+ if not isinstance(obj, dict):
39
+ raise ValueError("properties() expects a node, relationship, or map")
40
+
41
+ # If it's a RelationshipMatchRecord (has type, startNode, endNode, properties)
42
+ if all(k in obj for k in ("type", "startNode", "endNode", "properties")):
43
+ return obj["properties"]
44
+
45
+ # If it's a node record (has id field), exclude id
46
+ if "id" in obj:
47
+ return {k: v for k, v in obj.items() if k != "id"}
48
+
49
+ # Otherwise, treat as a plain map and return a copy
50
+ return dict(obj)
@@ -0,0 +1,46 @@
1
+ """Relationships function."""
2
+
3
+ from typing import Any, List
4
+
5
+ from .function import Function
6
+ from .function_metadata import FunctionDef
7
+
8
+
9
+ @FunctionDef({
10
+ "description": "Returns all relationships in a path as an array",
11
+ "category": "scalar",
12
+ "parameters": [
13
+ {"name": "path", "description": "A path value returned from a graph pattern match", "type": "array"}
14
+ ],
15
+ "output": {
16
+ "description": "Array of relationship records", "type": "array",
17
+ "example": [{"type": "KNOWS", "properties": {"since": "2020"}}]
18
+ },
19
+ "examples": ["MATCH p=(:Person)-[:KNOWS]-(:Person) RETURN relationships(p)"]
20
+ })
21
+ class Relationships(Function):
22
+ """Relationships function.
23
+
24
+ Returns all relationships in a path as an array.
25
+ """
26
+
27
+ def __init__(self) -> None:
28
+ super().__init__("relationships")
29
+ self._expected_parameter_count = 1
30
+
31
+ def value(self) -> Any:
32
+ path = self.get_children()[0].value()
33
+ if path is None:
34
+ return []
35
+ if not isinstance(path, list):
36
+ raise ValueError("relationships() expects a path (array)")
37
+ # A path is an array of alternating node and relationship objects:
38
+ # [node, rel, node, rel, node, ...]
39
+ # Relationships are RelationshipMatchRecords (have 'type', 'startNode', 'endNode', 'properties')
40
+ result: List[Any] = []
41
+ for element in path:
42
+ if element is None or not isinstance(element, dict):
43
+ continue
44
+ if all(k in element for k in ("type", "startNode", "endNode", "properties")):
45
+ result.append(element)
46
+ return result
@@ -0,0 +1,37 @@
1
+ """Tail 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 all elements of a list except the first",
11
+ "category": "scalar",
12
+ "parameters": [
13
+ {"name": "list", "description": "The list to get all but the first element from", "type": "array"}
14
+ ],
15
+ "output": {"description": "All elements except the first", "type": "array", "example": [2, 3]},
16
+ "examples": [
17
+ "RETURN tail([1, 2, 3])",
18
+ "WITH ['a', 'b', 'c'] AS items RETURN tail(items)"
19
+ ]
20
+ })
21
+ class Tail(Function):
22
+ """Tail function.
23
+
24
+ Returns all elements of a list except the first.
25
+ """
26
+
27
+ def __init__(self) -> None:
28
+ super().__init__("tail")
29
+ self._expected_parameter_count = 1
30
+
31
+ def value(self) -> Any:
32
+ val = self.get_children()[0].value()
33
+ if val is None:
34
+ return None
35
+ if not isinstance(val, list):
36
+ raise ValueError("tail() expects a list")
37
+ return val[1:]
@@ -0,0 +1,186 @@
1
+ """Shared utility functions for temporal (date/time) operations.
2
+
3
+ These helpers are used by the datetime_, date_, time_, localdatetime,
4
+ localtime, and timestamp functions.
5
+ """
6
+
7
+ from datetime import date, datetime, timezone
8
+ from typing import Any, Dict
9
+
10
+
11
+ def iso_day_of_week(d: date) -> int:
12
+ """Computes the ISO day of the week (1 = Monday, 7 = Sunday)."""
13
+ return d.isoweekday()
14
+
15
+
16
+ def day_of_year(d: date) -> int:
17
+ """Computes the day of the year (1-based)."""
18
+ return d.timetuple().tm_yday
19
+
20
+
21
+ def quarter(month: int) -> int:
22
+ """Computes the quarter (1-4) from a month (1-12)."""
23
+ return (month - 1) // 3 + 1
24
+
25
+
26
+ def parse_temporal_arg(arg: Any, fn_name: str) -> datetime:
27
+ """Parses a temporal argument (string, number, or map) into a datetime object.
28
+
29
+ Args:
30
+ arg: The argument to parse (string, number, or dict with components)
31
+ fn_name: The calling function name for error messages
32
+
33
+ Returns:
34
+ A datetime object
35
+ """
36
+ if isinstance(arg, str):
37
+ try:
38
+ return datetime.fromisoformat(arg.replace("Z", "+00:00"))
39
+ except ValueError:
40
+ raise ValueError(f"{fn_name}(): Invalid temporal string: '{arg}'")
41
+
42
+ if isinstance(arg, (int, float)):
43
+ # Treat as epoch milliseconds
44
+ return datetime.fromtimestamp(arg / 1000, tz=timezone.utc)
45
+
46
+ if isinstance(arg, dict):
47
+ # Map-style construction: {year, month, day, hour, minute, second, millisecond}
48
+ now = datetime.now()
49
+ year = arg.get("year", now.year)
50
+ month = arg.get("month", 1)
51
+ day = arg.get("day", 1)
52
+ hour = arg.get("hour", 0)
53
+ minute = arg.get("minute", 0)
54
+ second = arg.get("second", 0)
55
+ millisecond = arg.get("millisecond", 0)
56
+ return datetime(year, month, day, hour, minute, second, millisecond * 1000)
57
+
58
+ raise ValueError(
59
+ f"{fn_name}(): Expected a string, number (epoch millis), or map argument, "
60
+ f"got {type(arg).__name__}"
61
+ )
62
+
63
+
64
+ def build_datetime_object(d: datetime, utc: bool) -> Dict[str, Any]:
65
+ """Builds a datetime result object with full temporal properties.
66
+
67
+ Args:
68
+ d: The datetime object
69
+ utc: If True, use UTC values; if False, use local values
70
+
71
+ Returns:
72
+ A dict with year, month, day, hour, minute, second, millisecond,
73
+ epochMillis, epochSeconds, dayOfWeek, dayOfYear, quarter, formatted
74
+ """
75
+ if utc:
76
+ if d.tzinfo is None:
77
+ d = d.replace(tzinfo=timezone.utc)
78
+ d_utc = d.astimezone(timezone.utc)
79
+ year = d_utc.year
80
+ month = d_utc.month
81
+ day = d_utc.day
82
+ hour = d_utc.hour
83
+ minute = d_utc.minute
84
+ second = d_utc.second
85
+ millisecond = d_utc.microsecond // 1000
86
+ formatted = d_utc.strftime("%Y-%m-%dT%H:%M:%S.") + f"{millisecond:03d}Z"
87
+ else:
88
+ if d.tzinfo is not None:
89
+ d = d.astimezone(tz=None).replace(tzinfo=None)
90
+ year = d.year
91
+ month = d.month
92
+ day = d.day
93
+ hour = d.hour
94
+ minute = d.minute
95
+ second = d.second
96
+ millisecond = d.microsecond // 1000
97
+ formatted = d.strftime("%Y-%m-%dT%H:%M:%S.") + f"{millisecond:03d}"
98
+
99
+ date_part = date(year, month, day)
100
+ epoch_millis = int(d.timestamp() * 1000) if d.tzinfo else int(
101
+ datetime(year, month, day, hour, minute, second, millisecond * 1000).timestamp() * 1000
102
+ )
103
+
104
+ return {
105
+ "year": year,
106
+ "month": month,
107
+ "day": day,
108
+ "hour": hour,
109
+ "minute": minute,
110
+ "second": second,
111
+ "millisecond": millisecond,
112
+ "epochMillis": epoch_millis,
113
+ "epochSeconds": epoch_millis // 1000,
114
+ "dayOfWeek": iso_day_of_week(date_part),
115
+ "dayOfYear": day_of_year(date_part),
116
+ "quarter": quarter(month),
117
+ "formatted": formatted,
118
+ }
119
+
120
+
121
+ def build_date_object(d: datetime) -> Dict[str, Any]:
122
+ """Builds a date result object (no time component).
123
+
124
+ Args:
125
+ d: The datetime object
126
+
127
+ Returns:
128
+ A dict with year, month, day, epochMillis, dayOfWeek, dayOfYear, quarter, formatted
129
+ """
130
+ year = d.year
131
+ month = d.month
132
+ day_val = d.day
133
+
134
+ date_only = datetime(year, month, day_val)
135
+ epoch_millis = int(date_only.timestamp() * 1000)
136
+
137
+ date_part = date(year, month, day_val)
138
+
139
+ return {
140
+ "year": year,
141
+ "month": month,
142
+ "day": day_val,
143
+ "epochMillis": epoch_millis,
144
+ "dayOfWeek": iso_day_of_week(date_part),
145
+ "dayOfYear": day_of_year(date_part),
146
+ "quarter": quarter(month),
147
+ "formatted": f"{year}-{month:02d}-{day_val:02d}",
148
+ }
149
+
150
+
151
+ def build_time_object(d: datetime, utc: bool) -> Dict[str, Any]:
152
+ """Builds a time result object (no date component).
153
+
154
+ Args:
155
+ d: The datetime object
156
+ utc: If True, use UTC values; if False, use local values
157
+
158
+ Returns:
159
+ A dict with hour, minute, second, millisecond, formatted
160
+ """
161
+ if utc:
162
+ if d.tzinfo is None:
163
+ d = d.replace(tzinfo=timezone.utc)
164
+ d_utc = d.astimezone(timezone.utc)
165
+ hour = d_utc.hour
166
+ minute = d_utc.minute
167
+ second = d_utc.second
168
+ millisecond = d_utc.microsecond // 1000
169
+ else:
170
+ if d.tzinfo is not None:
171
+ d = d.astimezone(tz=None).replace(tzinfo=None)
172
+ hour = d.hour
173
+ minute = d.minute
174
+ second = d.second
175
+ millisecond = d.microsecond // 1000
176
+
177
+ time_part = f"{hour:02d}:{minute:02d}:{second:02d}.{millisecond:03d}"
178
+ formatted = f"{time_part}Z" if utc else time_part
179
+
180
+ return {
181
+ "hour": hour,
182
+ "minute": minute,
183
+ "second": second,
184
+ "millisecond": millisecond,
185
+ "formatted": formatted,
186
+ }
@@ -0,0 +1,57 @@
1
+ """Time 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_time_object, parse_temporal_arg
9
+
10
+
11
+ @FunctionDef({
12
+ "description": (
13
+ "Returns a time value. With no arguments returns the current UTC time. "
14
+ "Accepts an ISO 8601 time string or a map of components (hour, minute, second, millisecond)."
15
+ ),
16
+ "category": "scalar",
17
+ "parameters": [
18
+ {
19
+ "name": "input",
20
+ "description": "Optional. An ISO 8601 time string (HH:MM:SS) or a map of components.",
21
+ "type": "string",
22
+ "required": False,
23
+ },
24
+ ],
25
+ "output": {
26
+ "description": "A time object with properties: hour, minute, second, millisecond, formatted",
27
+ "type": "object",
28
+ },
29
+ "examples": [
30
+ "RETURN time() AS now",
31
+ "RETURN time('12:30:00') AS t",
32
+ "WITH time() AS t RETURN t.hour, t.minute",
33
+ ],
34
+ })
35
+ class Time(Function):
36
+ """Time function.
37
+
38
+ Returns a time value (with timezone offset awareness).
39
+ When called with no arguments, returns the current UTC time.
40
+ When called with a string argument, parses it.
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ super().__init__("time")
45
+ self._expected_parameter_count = None
46
+
47
+ def value(self) -> Any:
48
+ children = self.get_children()
49
+ if len(children) > 1:
50
+ raise ValueError("time() accepts at most one argument")
51
+
52
+ if len(children) == 1:
53
+ d = parse_temporal_arg(children[0].value(), "time")
54
+ else:
55
+ d = datetime.now(timezone.utc)
56
+
57
+ return build_time_object(d, utc=True)
@@ -0,0 +1,37 @@
1
+ """Timestamp function."""
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from .function import Function
7
+ from .function_metadata import FunctionDef
8
+
9
+
10
+ @FunctionDef({
11
+ "description": (
12
+ "Returns the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z)."
13
+ ),
14
+ "category": "scalar",
15
+ "parameters": [],
16
+ "output": {
17
+ "description": "Milliseconds since Unix epoch",
18
+ "type": "number",
19
+ "example": 1718450000000,
20
+ },
21
+ "examples": [
22
+ "RETURN timestamp() AS ts",
23
+ "WITH timestamp() AS before, timestamp() AS after RETURN after - before",
24
+ ],
25
+ })
26
+ class Timestamp(Function):
27
+ """Timestamp function.
28
+
29
+ Returns the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
30
+ """
31
+
32
+ def __init__(self) -> None:
33
+ super().__init__("timestamp")
34
+ self._expected_parameter_count = 0
35
+
36
+ def value(self) -> Any:
37
+ return int(time.time() * 1000)
@@ -0,0 +1,46 @@
1
+ """ToFloat 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": "Converts a value to a floating point number",
11
+ "category": "scalar",
12
+ "parameters": [
13
+ {"name": "value", "description": "The value to convert to a float", "type": "any"}
14
+ ],
15
+ "output": {"description": "The floating point representation of the value", "type": "number", "example": 3.14},
16
+ "examples": [
17
+ 'RETURN toFloat("3.14")',
18
+ "RETURN toFloat(42)",
19
+ "RETURN toFloat(true)"
20
+ ]
21
+ })
22
+ class ToFloat(Function):
23
+ """ToFloat function.
24
+
25
+ Converts a value to a floating point number.
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ super().__init__("tofloat")
30
+ self._expected_parameter_count = 1
31
+
32
+ def value(self) -> Any:
33
+ val = self.get_children()[0].value()
34
+ if val is None:
35
+ return None
36
+ if isinstance(val, bool):
37
+ return 1.0 if val else 0.0
38
+ if isinstance(val, (int, float)):
39
+ return float(val)
40
+ if isinstance(val, str):
41
+ trimmed = val.strip()
42
+ try:
43
+ return float(trimmed)
44
+ except (ValueError, OverflowError):
45
+ raise ValueError(f'Cannot convert string "{val}" to float')
46
+ raise ValueError("toFloat() expects a number, string, or boolean")