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.
- package/dist/flowquery.min.js +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 +16 -0
- package/dist/parsing/functions/coalesce.d.ts.map +1 -0
- package/dist/parsing/functions/coalesce.js +60 -0
- package/dist/parsing/functions/coalesce.js.map +1 -0
- package/dist/parsing/functions/date.d.ts +20 -0
- package/dist/parsing/functions/date.d.ts.map +1 -0
- package/dist/parsing/functions/date.js +69 -0
- package/dist/parsing/functions/date.js.map +1 -0
- package/dist/parsing/functions/datetime.d.ts +20 -0
- package/dist/parsing/functions/datetime.d.ts.map +1 -0
- package/dist/parsing/functions/datetime.js +69 -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 +20 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +20 -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 +19 -0
- package/dist/parsing/functions/localdatetime.d.ts.map +1 -0
- package/dist/parsing/functions/localdatetime.js +69 -0
- package/dist/parsing/functions/localdatetime.js.map +1 -0
- package/dist/parsing/functions/localtime.d.ts +18 -0
- package/dist/parsing/functions/localtime.d.ts.map +1 -0
- package/dist/parsing/functions/localtime.js +65 -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/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/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 +18 -0
- package/dist/parsing/functions/time.d.ts.map +1 -0
- package/dist/parsing/functions/time.js +65 -0
- package/dist/parsing/functions/time.js.map +1 -0
- package/dist/parsing/functions/timestamp.d.ts +15 -0
- package/dist/parsing/functions/timestamp.d.ts.map +1 -0
- package/dist/parsing/functions/timestamp.js +48 -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/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/parsing/data_structures/lookup.py +2 -0
- package/flowquery-py/src/parsing/functions/__init__.py +40 -2
- package/flowquery-py/src/parsing/functions/coalesce.py +43 -0
- package/flowquery-py/src/parsing/functions/date_.py +61 -0
- package/flowquery-py/src/parsing/functions/datetime_.py +62 -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 +60 -0
- package/flowquery-py/src/parsing/functions/localtime.py +57 -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/properties.py +50 -0
- package/flowquery-py/src/parsing/functions/relationships.py +46 -0
- 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 +57 -0
- package/flowquery-py/src/parsing/functions/timestamp.py +37 -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/tests/compute/test_runner.py +834 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/parsing/data_structures/lookup.ts +8 -4
- package/src/parsing/functions/coalesce.ts +49 -0
- package/src/parsing/functions/date.ts +63 -0
- package/src/parsing/functions/datetime.ts +63 -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 +20 -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 +63 -0
- package/src/parsing/functions/localtime.ts +58 -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/properties.ts +56 -0
- package/src/parsing/functions/relationships.ts +52 -0
- package/src/parsing/functions/tail.ts +39 -0
- package/src/parsing/functions/temporal_utils.ts +180 -0
- package/src/parsing/functions/time.ts +58 -0
- package/src/parsing/functions/timestamp.ts +37 -0
- package/src/parsing/functions/to_float.ts +50 -0
- package/src/parsing/functions/to_integer.ts +50 -0
- package/tests/compute/runner.test.ts +726 -0
|
@@ -249,6 +249,99 @@ class TestRunner:
|
|
|
249
249
|
assert len(results) == 1
|
|
250
250
|
assert results[0] == {"avg": 1}
|
|
251
251
|
|
|
252
|
+
@pytest.mark.asyncio
|
|
253
|
+
async def test_min(self):
|
|
254
|
+
"""Test min aggregate function."""
|
|
255
|
+
runner = Runner("unwind [3, 1, 4, 1, 5, 9] as n return min(n) as minimum")
|
|
256
|
+
await runner.run()
|
|
257
|
+
results = runner.results
|
|
258
|
+
assert len(results) == 1
|
|
259
|
+
assert results[0] == {"minimum": 1}
|
|
260
|
+
|
|
261
|
+
@pytest.mark.asyncio
|
|
262
|
+
async def test_max(self):
|
|
263
|
+
"""Test max aggregate function."""
|
|
264
|
+
runner = Runner("unwind [3, 1, 4, 1, 5, 9] as n return max(n) as maximum")
|
|
265
|
+
await runner.run()
|
|
266
|
+
results = runner.results
|
|
267
|
+
assert len(results) == 1
|
|
268
|
+
assert results[0] == {"maximum": 9}
|
|
269
|
+
|
|
270
|
+
@pytest.mark.asyncio
|
|
271
|
+
async def test_min_with_grouped_values(self):
|
|
272
|
+
"""Test min with grouped values."""
|
|
273
|
+
runner = Runner(
|
|
274
|
+
"unwind [1, 1, 2, 2] as i unwind [10, 20, 30, 40] as j return i, min(j) as minimum"
|
|
275
|
+
)
|
|
276
|
+
await runner.run()
|
|
277
|
+
results = runner.results
|
|
278
|
+
assert len(results) == 2
|
|
279
|
+
assert results[0] == {"i": 1, "minimum": 10}
|
|
280
|
+
assert results[1] == {"i": 2, "minimum": 10}
|
|
281
|
+
|
|
282
|
+
@pytest.mark.asyncio
|
|
283
|
+
async def test_max_with_grouped_values(self):
|
|
284
|
+
"""Test max with grouped values."""
|
|
285
|
+
runner = Runner(
|
|
286
|
+
"unwind [1, 1, 2, 2] as i unwind [10, 20, 30, 40] as j return i, max(j) as maximum"
|
|
287
|
+
)
|
|
288
|
+
await runner.run()
|
|
289
|
+
results = runner.results
|
|
290
|
+
assert len(results) == 2
|
|
291
|
+
assert results[0] == {"i": 1, "maximum": 40}
|
|
292
|
+
assert results[1] == {"i": 2, "maximum": 40}
|
|
293
|
+
|
|
294
|
+
@pytest.mark.asyncio
|
|
295
|
+
async def test_min_with_null(self):
|
|
296
|
+
"""Test min with null."""
|
|
297
|
+
runner = Runner("return min(null) as minimum")
|
|
298
|
+
await runner.run()
|
|
299
|
+
results = runner.results
|
|
300
|
+
assert len(results) == 1
|
|
301
|
+
assert results[0] == {"minimum": None}
|
|
302
|
+
|
|
303
|
+
@pytest.mark.asyncio
|
|
304
|
+
async def test_max_with_null(self):
|
|
305
|
+
"""Test max with null."""
|
|
306
|
+
runner = Runner("return max(null) as maximum")
|
|
307
|
+
await runner.run()
|
|
308
|
+
results = runner.results
|
|
309
|
+
assert len(results) == 1
|
|
310
|
+
assert results[0] == {"maximum": None}
|
|
311
|
+
|
|
312
|
+
@pytest.mark.asyncio
|
|
313
|
+
async def test_min_with_strings(self):
|
|
314
|
+
"""Test min with string values."""
|
|
315
|
+
runner = Runner(
|
|
316
|
+
'unwind ["cherry", "apple", "banana"] as s return min(s) as minimum'
|
|
317
|
+
)
|
|
318
|
+
await runner.run()
|
|
319
|
+
results = runner.results
|
|
320
|
+
assert len(results) == 1
|
|
321
|
+
assert results[0] == {"minimum": "apple"}
|
|
322
|
+
|
|
323
|
+
@pytest.mark.asyncio
|
|
324
|
+
async def test_max_with_strings(self):
|
|
325
|
+
"""Test max with string values."""
|
|
326
|
+
runner = Runner(
|
|
327
|
+
'unwind ["cherry", "apple", "banana"] as s return max(s) as maximum'
|
|
328
|
+
)
|
|
329
|
+
await runner.run()
|
|
330
|
+
results = runner.results
|
|
331
|
+
assert len(results) == 1
|
|
332
|
+
assert results[0] == {"maximum": "cherry"}
|
|
333
|
+
|
|
334
|
+
@pytest.mark.asyncio
|
|
335
|
+
async def test_min_and_max_together(self):
|
|
336
|
+
"""Test min and max together."""
|
|
337
|
+
runner = Runner(
|
|
338
|
+
"unwind [3, 1, 4, 1, 5, 9] as n return min(n) as minimum, max(n) as maximum"
|
|
339
|
+
)
|
|
340
|
+
await runner.run()
|
|
341
|
+
results = runner.results
|
|
342
|
+
assert len(results) == 1
|
|
343
|
+
assert results[0] == {"minimum": 1, "maximum": 9}
|
|
344
|
+
|
|
252
345
|
@pytest.mark.asyncio
|
|
253
346
|
async def test_with_and_return(self):
|
|
254
347
|
"""Test with and return."""
|
|
@@ -901,6 +994,144 @@ class TestRunner:
|
|
|
901
994
|
assert len(results) == 1
|
|
902
995
|
assert results[0] == {"keys": ["name", "age"]}
|
|
903
996
|
|
|
997
|
+
@pytest.mark.asyncio
|
|
998
|
+
async def test_properties_function_with_map(self):
|
|
999
|
+
"""Test properties function with a plain map."""
|
|
1000
|
+
runner = Runner('RETURN properties({name: "Alice", age: 30}) as props')
|
|
1001
|
+
await runner.run()
|
|
1002
|
+
results = runner.results
|
|
1003
|
+
assert len(results) == 1
|
|
1004
|
+
assert results[0] == {"props": {"name": "Alice", "age": 30}}
|
|
1005
|
+
|
|
1006
|
+
@pytest.mark.asyncio
|
|
1007
|
+
async def test_properties_function_with_node(self):
|
|
1008
|
+
"""Test properties function with a graph node."""
|
|
1009
|
+
await Runner(
|
|
1010
|
+
"""
|
|
1011
|
+
CREATE VIRTUAL (:Animal) AS {
|
|
1012
|
+
UNWIND [
|
|
1013
|
+
{id: 1, name: 'Dog', legs: 4},
|
|
1014
|
+
{id: 2, name: 'Cat', legs: 4}
|
|
1015
|
+
] AS record
|
|
1016
|
+
RETURN record.id AS id, record.name AS name, record.legs AS legs
|
|
1017
|
+
}
|
|
1018
|
+
"""
|
|
1019
|
+
).run()
|
|
1020
|
+
match = Runner(
|
|
1021
|
+
"""
|
|
1022
|
+
MATCH (a:Animal)
|
|
1023
|
+
RETURN properties(a) AS props
|
|
1024
|
+
"""
|
|
1025
|
+
)
|
|
1026
|
+
await match.run()
|
|
1027
|
+
results = match.results
|
|
1028
|
+
assert len(results) == 2
|
|
1029
|
+
assert results[0] == {"props": {"name": "Dog", "legs": 4}}
|
|
1030
|
+
assert results[1] == {"props": {"name": "Cat", "legs": 4}}
|
|
1031
|
+
|
|
1032
|
+
@pytest.mark.asyncio
|
|
1033
|
+
async def test_properties_function_with_null(self):
|
|
1034
|
+
"""Test properties function with null."""
|
|
1035
|
+
runner = Runner("RETURN properties(null) as props")
|
|
1036
|
+
await runner.run()
|
|
1037
|
+
results = runner.results
|
|
1038
|
+
assert len(results) == 1
|
|
1039
|
+
assert results[0] == {"props": None}
|
|
1040
|
+
|
|
1041
|
+
@pytest.mark.asyncio
|
|
1042
|
+
async def test_nodes_function(self):
|
|
1043
|
+
"""Test nodes function with a graph path."""
|
|
1044
|
+
await Runner(
|
|
1045
|
+
"""
|
|
1046
|
+
CREATE VIRTUAL (:City) AS {
|
|
1047
|
+
UNWIND [
|
|
1048
|
+
{id: 1, name: 'New York'},
|
|
1049
|
+
{id: 2, name: 'Boston'}
|
|
1050
|
+
] AS record
|
|
1051
|
+
RETURN record.id AS id, record.name AS name
|
|
1052
|
+
}
|
|
1053
|
+
"""
|
|
1054
|
+
).run()
|
|
1055
|
+
await Runner(
|
|
1056
|
+
"""
|
|
1057
|
+
CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
|
|
1058
|
+
UNWIND [
|
|
1059
|
+
{left_id: 1, right_id: 2}
|
|
1060
|
+
] AS record
|
|
1061
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id
|
|
1062
|
+
}
|
|
1063
|
+
"""
|
|
1064
|
+
).run()
|
|
1065
|
+
match = Runner(
|
|
1066
|
+
"""
|
|
1067
|
+
MATCH p=(:City)-[:CONNECTED_TO]-(:City)
|
|
1068
|
+
RETURN nodes(p) AS cities
|
|
1069
|
+
"""
|
|
1070
|
+
)
|
|
1071
|
+
await match.run()
|
|
1072
|
+
results = match.results
|
|
1073
|
+
assert len(results) == 1
|
|
1074
|
+
assert len(results[0]["cities"]) == 2
|
|
1075
|
+
assert results[0]["cities"][0]["id"] == 1
|
|
1076
|
+
assert results[0]["cities"][0]["name"] == "New York"
|
|
1077
|
+
assert results[0]["cities"][1]["id"] == 2
|
|
1078
|
+
assert results[0]["cities"][1]["name"] == "Boston"
|
|
1079
|
+
|
|
1080
|
+
@pytest.mark.asyncio
|
|
1081
|
+
async def test_relationships_function(self):
|
|
1082
|
+
"""Test relationships function with a graph path."""
|
|
1083
|
+
await Runner(
|
|
1084
|
+
"""
|
|
1085
|
+
CREATE VIRTUAL (:City) AS {
|
|
1086
|
+
UNWIND [
|
|
1087
|
+
{id: 1, name: 'New York'},
|
|
1088
|
+
{id: 2, name: 'Boston'}
|
|
1089
|
+
] AS record
|
|
1090
|
+
RETURN record.id AS id, record.name AS name
|
|
1091
|
+
}
|
|
1092
|
+
"""
|
|
1093
|
+
).run()
|
|
1094
|
+
await Runner(
|
|
1095
|
+
"""
|
|
1096
|
+
CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
|
|
1097
|
+
UNWIND [
|
|
1098
|
+
{left_id: 1, right_id: 2, distance: 190}
|
|
1099
|
+
] AS record
|
|
1100
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id, record.distance AS distance
|
|
1101
|
+
}
|
|
1102
|
+
"""
|
|
1103
|
+
).run()
|
|
1104
|
+
match = Runner(
|
|
1105
|
+
"""
|
|
1106
|
+
MATCH p=(:City)-[:CONNECTED_TO]-(:City)
|
|
1107
|
+
RETURN relationships(p) AS rels
|
|
1108
|
+
"""
|
|
1109
|
+
)
|
|
1110
|
+
await match.run()
|
|
1111
|
+
results = match.results
|
|
1112
|
+
assert len(results) == 1
|
|
1113
|
+
assert len(results[0]["rels"]) == 1
|
|
1114
|
+
assert results[0]["rels"][0]["type"] == "CONNECTED_TO"
|
|
1115
|
+
assert results[0]["rels"][0]["properties"]["distance"] == 190
|
|
1116
|
+
|
|
1117
|
+
@pytest.mark.asyncio
|
|
1118
|
+
async def test_nodes_function_with_null(self):
|
|
1119
|
+
"""Test nodes function with null."""
|
|
1120
|
+
runner = Runner("RETURN nodes(null) as n")
|
|
1121
|
+
await runner.run()
|
|
1122
|
+
results = runner.results
|
|
1123
|
+
assert len(results) == 1
|
|
1124
|
+
assert results[0] == {"n": []}
|
|
1125
|
+
|
|
1126
|
+
@pytest.mark.asyncio
|
|
1127
|
+
async def test_relationships_function_with_null(self):
|
|
1128
|
+
"""Test relationships function with null."""
|
|
1129
|
+
runner = Runner("RETURN relationships(null) as r")
|
|
1130
|
+
await runner.run()
|
|
1131
|
+
results = runner.results
|
|
1132
|
+
assert len(results) == 1
|
|
1133
|
+
assert results[0] == {"r": []}
|
|
1134
|
+
|
|
904
1135
|
@pytest.mark.asyncio
|
|
905
1136
|
async def test_type_function(self):
|
|
906
1137
|
"""Test type function."""
|
|
@@ -1976,6 +2207,46 @@ class TestRunner:
|
|
|
1976
2207
|
assert results[2]["name"] == "Person 3"
|
|
1977
2208
|
assert results[2]["friend"] is None
|
|
1978
2209
|
|
|
2210
|
+
@pytest.mark.asyncio
|
|
2211
|
+
async def test_optional_match_property_access_on_null_node_returns_null(self):
|
|
2212
|
+
"""Test that accessing a property on a null node from optional match returns null."""
|
|
2213
|
+
await Runner(
|
|
2214
|
+
"""
|
|
2215
|
+
CREATE VIRTUAL (:OptPropPerson) AS {
|
|
2216
|
+
unwind [
|
|
2217
|
+
{id: 1, name: 'Person 1'},
|
|
2218
|
+
{id: 2, name: 'Person 2'},
|
|
2219
|
+
{id: 3, name: 'Person 3'}
|
|
2220
|
+
] as record
|
|
2221
|
+
RETURN record.id as id, record.name as name
|
|
2222
|
+
}
|
|
2223
|
+
"""
|
|
2224
|
+
).run()
|
|
2225
|
+
await Runner(
|
|
2226
|
+
"""
|
|
2227
|
+
CREATE VIRTUAL (:OptPropPerson)-[:KNOWS]-(:OptPropPerson) AS {
|
|
2228
|
+
unwind [
|
|
2229
|
+
{left_id: 1, right_id: 2}
|
|
2230
|
+
] as record
|
|
2231
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2232
|
+
}
|
|
2233
|
+
"""
|
|
2234
|
+
).run()
|
|
2235
|
+
# When accessing b.name and b is null (no match), should return null
|
|
2236
|
+
match = Runner(
|
|
2237
|
+
"""
|
|
2238
|
+
MATCH (a:OptPropPerson)
|
|
2239
|
+
OPTIONAL MATCH (a)-[:KNOWS]->(b:OptPropPerson)
|
|
2240
|
+
RETURN a.name AS name, b.name AS friend_name
|
|
2241
|
+
"""
|
|
2242
|
+
)
|
|
2243
|
+
await match.run()
|
|
2244
|
+
results = match.results
|
|
2245
|
+
assert len(results) == 3
|
|
2246
|
+
assert results[0] == {"name": "Person 1", "friend_name": "Person 2"}
|
|
2247
|
+
assert results[1] == {"name": "Person 2", "friend_name": None}
|
|
2248
|
+
assert results[2] == {"name": "Person 3", "friend_name": None}
|
|
2249
|
+
|
|
1979
2250
|
@pytest.mark.asyncio
|
|
1980
2251
|
async def test_optional_match_where_all_nodes_match(self):
|
|
1981
2252
|
"""Test optional match where all nodes have matching relationships."""
|
|
@@ -3099,4 +3370,566 @@ class TestRunner:
|
|
|
3099
3370
|
await runner.run()
|
|
3100
3371
|
results = runner.results
|
|
3101
3372
|
assert len(results) == 1
|
|
3102
|
-
assert results[0] == {"sum": 0}
|
|
3373
|
+
assert results[0] == {"sum": 0}
|
|
3374
|
+
|
|
3375
|
+
@pytest.mark.asyncio
|
|
3376
|
+
async def test_coalesce_returns_first_non_null(self):
|
|
3377
|
+
"""Test coalesce returns first non-null value."""
|
|
3378
|
+
runner = Runner("RETURN coalesce(null, null, 'hello', 'world') as result")
|
|
3379
|
+
await runner.run()
|
|
3380
|
+
results = runner.results
|
|
3381
|
+
assert len(results) == 1
|
|
3382
|
+
assert results[0] == {"result": "hello"}
|
|
3383
|
+
|
|
3384
|
+
@pytest.mark.asyncio
|
|
3385
|
+
async def test_coalesce_returns_first_argument_when_not_null(self):
|
|
3386
|
+
"""Test coalesce returns first argument when not null."""
|
|
3387
|
+
runner = Runner("RETURN coalesce('first', 'second') as result")
|
|
3388
|
+
await runner.run()
|
|
3389
|
+
results = runner.results
|
|
3390
|
+
assert len(results) == 1
|
|
3391
|
+
assert results[0] == {"result": "first"}
|
|
3392
|
+
|
|
3393
|
+
@pytest.mark.asyncio
|
|
3394
|
+
async def test_coalesce_returns_null_when_all_null(self):
|
|
3395
|
+
"""Test coalesce returns null when all arguments are null."""
|
|
3396
|
+
runner = Runner("RETURN coalesce(null, null, null) as result")
|
|
3397
|
+
await runner.run()
|
|
3398
|
+
results = runner.results
|
|
3399
|
+
assert len(results) == 1
|
|
3400
|
+
assert results[0] == {"result": None}
|
|
3401
|
+
|
|
3402
|
+
@pytest.mark.asyncio
|
|
3403
|
+
async def test_coalesce_with_single_non_null_argument(self):
|
|
3404
|
+
"""Test coalesce with single non-null argument."""
|
|
3405
|
+
runner = Runner("RETURN coalesce(42) as result")
|
|
3406
|
+
await runner.run()
|
|
3407
|
+
results = runner.results
|
|
3408
|
+
assert len(results) == 1
|
|
3409
|
+
assert results[0] == {"result": 42}
|
|
3410
|
+
|
|
3411
|
+
@pytest.mark.asyncio
|
|
3412
|
+
async def test_coalesce_with_mixed_types(self):
|
|
3413
|
+
"""Test coalesce with mixed types."""
|
|
3414
|
+
runner = Runner("RETURN coalesce(null, 42, 'hello') as result")
|
|
3415
|
+
await runner.run()
|
|
3416
|
+
results = runner.results
|
|
3417
|
+
assert len(results) == 1
|
|
3418
|
+
assert results[0] == {"result": 42}
|
|
3419
|
+
|
|
3420
|
+
@pytest.mark.asyncio
|
|
3421
|
+
async def test_coalesce_with_property_access(self):
|
|
3422
|
+
"""Test coalesce with property access."""
|
|
3423
|
+
runner = Runner("WITH {name: 'Alice'} AS person RETURN coalesce(person.nickname, person.name) as result")
|
|
3424
|
+
await runner.run()
|
|
3425
|
+
results = runner.results
|
|
3426
|
+
assert len(results) == 1
|
|
3427
|
+
assert results[0] == {"result": "Alice"}
|
|
3428
|
+
|
|
3429
|
+
# ============================================================
|
|
3430
|
+
# Temporal / Time Functions
|
|
3431
|
+
# ============================================================
|
|
3432
|
+
|
|
3433
|
+
@pytest.mark.asyncio
|
|
3434
|
+
async def test_datetime_returns_current_datetime_object(self):
|
|
3435
|
+
"""Test datetime() returns current datetime object."""
|
|
3436
|
+
import time
|
|
3437
|
+
before = int(time.time() * 1000)
|
|
3438
|
+
runner = Runner("RETURN datetime() AS dt")
|
|
3439
|
+
await runner.run()
|
|
3440
|
+
after = int(time.time() * 1000)
|
|
3441
|
+
results = runner.results
|
|
3442
|
+
assert len(results) == 1
|
|
3443
|
+
dt = results[0]["dt"]
|
|
3444
|
+
assert dt is not None
|
|
3445
|
+
assert isinstance(dt["year"], int)
|
|
3446
|
+
assert isinstance(dt["month"], int)
|
|
3447
|
+
assert isinstance(dt["day"], int)
|
|
3448
|
+
assert isinstance(dt["hour"], int)
|
|
3449
|
+
assert isinstance(dt["minute"], int)
|
|
3450
|
+
assert isinstance(dt["second"], int)
|
|
3451
|
+
assert isinstance(dt["millisecond"], int)
|
|
3452
|
+
assert isinstance(dt["epochMillis"], int)
|
|
3453
|
+
assert isinstance(dt["epochSeconds"], int)
|
|
3454
|
+
assert isinstance(dt["dayOfWeek"], int)
|
|
3455
|
+
assert isinstance(dt["dayOfYear"], int)
|
|
3456
|
+
assert isinstance(dt["quarter"], int)
|
|
3457
|
+
assert isinstance(dt["formatted"], str)
|
|
3458
|
+
# epochMillis should be between before and after
|
|
3459
|
+
assert dt["epochMillis"] >= before
|
|
3460
|
+
assert dt["epochMillis"] <= after
|
|
3461
|
+
|
|
3462
|
+
@pytest.mark.asyncio
|
|
3463
|
+
async def test_datetime_with_iso_string_argument(self):
|
|
3464
|
+
"""Test datetime() with ISO string argument."""
|
|
3465
|
+
runner = Runner("RETURN datetime('2025-06-15T12:30:45.123Z') AS dt")
|
|
3466
|
+
await runner.run()
|
|
3467
|
+
results = runner.results
|
|
3468
|
+
assert len(results) == 1
|
|
3469
|
+
dt = results[0]["dt"]
|
|
3470
|
+
assert dt["year"] == 2025
|
|
3471
|
+
assert dt["month"] == 6
|
|
3472
|
+
assert dt["day"] == 15
|
|
3473
|
+
assert dt["hour"] == 12
|
|
3474
|
+
assert dt["minute"] == 30
|
|
3475
|
+
assert dt["second"] == 45
|
|
3476
|
+
assert dt["millisecond"] == 123
|
|
3477
|
+
assert dt["formatted"] == "2025-06-15T12:30:45.123Z"
|
|
3478
|
+
|
|
3479
|
+
@pytest.mark.asyncio
|
|
3480
|
+
async def test_datetime_property_access(self):
|
|
3481
|
+
"""Test datetime() property access."""
|
|
3482
|
+
runner = Runner(
|
|
3483
|
+
"WITH datetime('2025-06-15T12:30:45.123Z') AS dt RETURN dt.year AS year, dt.month AS month, dt.day AS day"
|
|
3484
|
+
)
|
|
3485
|
+
await runner.run()
|
|
3486
|
+
results = runner.results
|
|
3487
|
+
assert len(results) == 1
|
|
3488
|
+
assert results[0] == {"year": 2025, "month": 6, "day": 15}
|
|
3489
|
+
|
|
3490
|
+
@pytest.mark.asyncio
|
|
3491
|
+
async def test_date_returns_current_date_object(self):
|
|
3492
|
+
"""Test date() returns current date object."""
|
|
3493
|
+
runner = Runner("RETURN date() AS d")
|
|
3494
|
+
await runner.run()
|
|
3495
|
+
results = runner.results
|
|
3496
|
+
assert len(results) == 1
|
|
3497
|
+
d = results[0]["d"]
|
|
3498
|
+
assert d is not None
|
|
3499
|
+
assert isinstance(d["year"], int)
|
|
3500
|
+
assert isinstance(d["month"], int)
|
|
3501
|
+
assert isinstance(d["day"], int)
|
|
3502
|
+
assert isinstance(d["epochMillis"], int)
|
|
3503
|
+
assert isinstance(d["dayOfWeek"], int)
|
|
3504
|
+
assert isinstance(d["dayOfYear"], int)
|
|
3505
|
+
assert isinstance(d["quarter"], int)
|
|
3506
|
+
assert isinstance(d["formatted"], str)
|
|
3507
|
+
# Should not have time fields
|
|
3508
|
+
assert "hour" not in d
|
|
3509
|
+
assert "minute" not in d
|
|
3510
|
+
|
|
3511
|
+
@pytest.mark.asyncio
|
|
3512
|
+
async def test_date_with_iso_date_string(self):
|
|
3513
|
+
"""Test date() with ISO date string."""
|
|
3514
|
+
runner = Runner("RETURN date('2025-06-15') AS d")
|
|
3515
|
+
await runner.run()
|
|
3516
|
+
results = runner.results
|
|
3517
|
+
assert len(results) == 1
|
|
3518
|
+
d = results[0]["d"]
|
|
3519
|
+
assert d["year"] == 2025
|
|
3520
|
+
assert d["month"] == 6
|
|
3521
|
+
assert d["day"] == 15
|
|
3522
|
+
assert d["formatted"] == "2025-06-15"
|
|
3523
|
+
|
|
3524
|
+
@pytest.mark.asyncio
|
|
3525
|
+
async def test_date_dayofweek_and_quarter(self):
|
|
3526
|
+
"""Test date() dayOfWeek and quarter."""
|
|
3527
|
+
# 2025-06-15 is a Sunday
|
|
3528
|
+
runner = Runner("RETURN date('2025-06-15') AS d")
|
|
3529
|
+
await runner.run()
|
|
3530
|
+
d = runner.results[0]["d"]
|
|
3531
|
+
assert d["dayOfWeek"] == 7 # Sunday = 7 in ISO
|
|
3532
|
+
assert d["quarter"] == 2 # June = Q2
|
|
3533
|
+
|
|
3534
|
+
@pytest.mark.asyncio
|
|
3535
|
+
async def test_time_returns_current_utc_time(self):
|
|
3536
|
+
"""Test time() returns current UTC time."""
|
|
3537
|
+
runner = Runner("RETURN time() AS t")
|
|
3538
|
+
await runner.run()
|
|
3539
|
+
results = runner.results
|
|
3540
|
+
assert len(results) == 1
|
|
3541
|
+
t = results[0]["t"]
|
|
3542
|
+
assert isinstance(t["hour"], int)
|
|
3543
|
+
assert isinstance(t["minute"], int)
|
|
3544
|
+
assert isinstance(t["second"], int)
|
|
3545
|
+
assert isinstance(t["millisecond"], int)
|
|
3546
|
+
assert isinstance(t["formatted"], str)
|
|
3547
|
+
assert t["formatted"].endswith("Z") # UTC time ends in Z
|
|
3548
|
+
|
|
3549
|
+
@pytest.mark.asyncio
|
|
3550
|
+
async def test_localtime_returns_current_local_time(self):
|
|
3551
|
+
"""Test localtime() returns current local time."""
|
|
3552
|
+
runner = Runner("RETURN localtime() AS t")
|
|
3553
|
+
await runner.run()
|
|
3554
|
+
results = runner.results
|
|
3555
|
+
assert len(results) == 1
|
|
3556
|
+
t = results[0]["t"]
|
|
3557
|
+
assert isinstance(t["hour"], int)
|
|
3558
|
+
assert isinstance(t["minute"], int)
|
|
3559
|
+
assert isinstance(t["second"], int)
|
|
3560
|
+
assert isinstance(t["millisecond"], int)
|
|
3561
|
+
assert isinstance(t["formatted"], str)
|
|
3562
|
+
assert not t["formatted"].endswith("Z") # Local time does not end in Z
|
|
3563
|
+
|
|
3564
|
+
@pytest.mark.asyncio
|
|
3565
|
+
async def test_localdatetime_returns_current_local_datetime(self):
|
|
3566
|
+
"""Test localdatetime() returns current local datetime."""
|
|
3567
|
+
runner = Runner("RETURN localdatetime() AS dt")
|
|
3568
|
+
await runner.run()
|
|
3569
|
+
results = runner.results
|
|
3570
|
+
assert len(results) == 1
|
|
3571
|
+
dt = results[0]["dt"]
|
|
3572
|
+
assert isinstance(dt["year"], int)
|
|
3573
|
+
assert isinstance(dt["month"], int)
|
|
3574
|
+
assert isinstance(dt["day"], int)
|
|
3575
|
+
assert isinstance(dt["hour"], int)
|
|
3576
|
+
assert isinstance(dt["minute"], int)
|
|
3577
|
+
assert isinstance(dt["second"], int)
|
|
3578
|
+
assert isinstance(dt["millisecond"], int)
|
|
3579
|
+
assert isinstance(dt["epochMillis"], int)
|
|
3580
|
+
assert isinstance(dt["formatted"], str)
|
|
3581
|
+
assert not dt["formatted"].endswith("Z") # Local datetime does not end in Z
|
|
3582
|
+
|
|
3583
|
+
@pytest.mark.asyncio
|
|
3584
|
+
async def test_localdatetime_with_string_argument(self):
|
|
3585
|
+
"""Test localdatetime() with string argument."""
|
|
3586
|
+
runner = Runner("RETURN localdatetime('2025-01-20T08:15:30.500') AS dt")
|
|
3587
|
+
await runner.run()
|
|
3588
|
+
dt = runner.results[0]["dt"]
|
|
3589
|
+
assert isinstance(dt["year"], int)
|
|
3590
|
+
assert isinstance(dt["hour"], int)
|
|
3591
|
+
assert dt["epochMillis"] is not None
|
|
3592
|
+
|
|
3593
|
+
@pytest.mark.asyncio
|
|
3594
|
+
async def test_timestamp_returns_epoch_millis(self):
|
|
3595
|
+
"""Test timestamp() returns epoch millis."""
|
|
3596
|
+
import time
|
|
3597
|
+
before = int(time.time() * 1000)
|
|
3598
|
+
runner = Runner("RETURN timestamp() AS ts")
|
|
3599
|
+
await runner.run()
|
|
3600
|
+
after = int(time.time() * 1000)
|
|
3601
|
+
results = runner.results
|
|
3602
|
+
assert len(results) == 1
|
|
3603
|
+
ts = results[0]["ts"]
|
|
3604
|
+
assert isinstance(ts, int)
|
|
3605
|
+
assert ts >= before
|
|
3606
|
+
assert ts <= after
|
|
3607
|
+
|
|
3608
|
+
@pytest.mark.asyncio
|
|
3609
|
+
async def test_datetime_epochmillis_matches_timestamp(self):
|
|
3610
|
+
"""Test datetime() epochMillis matches timestamp()."""
|
|
3611
|
+
runner = Runner(
|
|
3612
|
+
"WITH datetime() AS dt, timestamp() AS ts RETURN dt.epochMillis AS dtMillis, ts AS tsMillis"
|
|
3613
|
+
)
|
|
3614
|
+
await runner.run()
|
|
3615
|
+
results = runner.results
|
|
3616
|
+
assert len(results) == 1
|
|
3617
|
+
# They should be very close (within a few ms)
|
|
3618
|
+
assert abs(results[0]["dtMillis"] - results[0]["tsMillis"]) < 100
|
|
3619
|
+
|
|
3620
|
+
@pytest.mark.asyncio
|
|
3621
|
+
async def test_date_with_property_access_in_where(self):
|
|
3622
|
+
"""Test date() with property access in WHERE."""
|
|
3623
|
+
runner = Runner(
|
|
3624
|
+
"UNWIND [1, 2, 3] AS x WITH x, date('2025-06-15') AS d WHERE d.quarter = 2 RETURN x"
|
|
3625
|
+
)
|
|
3626
|
+
await runner.run()
|
|
3627
|
+
results = runner.results
|
|
3628
|
+
assert len(results) == 3 # All 3 pass through since Q2 = 2
|
|
3629
|
+
|
|
3630
|
+
@pytest.mark.asyncio
|
|
3631
|
+
async def test_datetime_with_map_argument(self):
|
|
3632
|
+
"""Test datetime() with map argument."""
|
|
3633
|
+
runner = Runner(
|
|
3634
|
+
"RETURN datetime({year: 2024, month: 12, day: 25, hour: 10, minute: 30}) AS dt"
|
|
3635
|
+
)
|
|
3636
|
+
await runner.run()
|
|
3637
|
+
dt = runner.results[0]["dt"]
|
|
3638
|
+
assert dt["year"] == 2024
|
|
3639
|
+
assert dt["month"] == 12
|
|
3640
|
+
assert dt["day"] == 25
|
|
3641
|
+
assert dt["quarter"] == 4 # December = Q4
|
|
3642
|
+
|
|
3643
|
+
@pytest.mark.asyncio
|
|
3644
|
+
async def test_date_with_map_argument(self):
|
|
3645
|
+
"""Test date() with map argument."""
|
|
3646
|
+
runner = Runner(
|
|
3647
|
+
"RETURN date({year: 2025, month: 3, day: 1}) AS d"
|
|
3648
|
+
)
|
|
3649
|
+
await runner.run()
|
|
3650
|
+
d = runner.results[0]["d"]
|
|
3651
|
+
assert d["year"] == 2025
|
|
3652
|
+
assert d["month"] == 3
|
|
3653
|
+
assert d["day"] == 1
|
|
3654
|
+
assert d["quarter"] == 1 # March = Q1
|
|
3655
|
+
|
|
3656
|
+
@pytest.mark.asyncio
|
|
3657
|
+
async def test_id_function_with_node(self):
|
|
3658
|
+
"""Test id() function with a graph node."""
|
|
3659
|
+
await Runner(
|
|
3660
|
+
"""
|
|
3661
|
+
CREATE VIRTUAL (:Person) AS {
|
|
3662
|
+
UNWIND [
|
|
3663
|
+
{id: 1, name: 'Alice'},
|
|
3664
|
+
{id: 2, name: 'Bob'}
|
|
3665
|
+
] AS record
|
|
3666
|
+
RETURN record.id AS id, record.name AS name
|
|
3667
|
+
}
|
|
3668
|
+
"""
|
|
3669
|
+
).run()
|
|
3670
|
+
match = Runner(
|
|
3671
|
+
"""
|
|
3672
|
+
MATCH (n:Person)
|
|
3673
|
+
RETURN id(n) AS nodeId
|
|
3674
|
+
"""
|
|
3675
|
+
)
|
|
3676
|
+
await match.run()
|
|
3677
|
+
results = match.results
|
|
3678
|
+
assert len(results) == 2
|
|
3679
|
+
assert results[0] == {"nodeId": 1}
|
|
3680
|
+
assert results[1] == {"nodeId": 2}
|
|
3681
|
+
|
|
3682
|
+
@pytest.mark.asyncio
|
|
3683
|
+
async def test_id_function_with_null(self):
|
|
3684
|
+
"""Test id() function with null."""
|
|
3685
|
+
runner = Runner("RETURN id(null) AS nodeId")
|
|
3686
|
+
await runner.run()
|
|
3687
|
+
results = runner.results
|
|
3688
|
+
assert len(results) == 1
|
|
3689
|
+
assert results[0] == {"nodeId": None}
|
|
3690
|
+
|
|
3691
|
+
@pytest.mark.asyncio
|
|
3692
|
+
async def test_id_function_with_relationship(self):
|
|
3693
|
+
"""Test id() function with a relationship."""
|
|
3694
|
+
await Runner(
|
|
3695
|
+
"""
|
|
3696
|
+
CREATE VIRTUAL (:City) AS {
|
|
3697
|
+
UNWIND [
|
|
3698
|
+
{id: 1, name: 'New York'},
|
|
3699
|
+
{id: 2, name: 'Boston'}
|
|
3700
|
+
] AS record
|
|
3701
|
+
RETURN record.id AS id, record.name AS name
|
|
3702
|
+
}
|
|
3703
|
+
"""
|
|
3704
|
+
).run()
|
|
3705
|
+
await Runner(
|
|
3706
|
+
"""
|
|
3707
|
+
CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
|
|
3708
|
+
UNWIND [
|
|
3709
|
+
{left_id: 1, right_id: 2}
|
|
3710
|
+
] AS record
|
|
3711
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id
|
|
3712
|
+
}
|
|
3713
|
+
"""
|
|
3714
|
+
).run()
|
|
3715
|
+
match = Runner(
|
|
3716
|
+
"""
|
|
3717
|
+
MATCH (a:City)-[r:CONNECTED_TO]->(b:City)
|
|
3718
|
+
RETURN id(r) AS relId
|
|
3719
|
+
"""
|
|
3720
|
+
)
|
|
3721
|
+
await match.run()
|
|
3722
|
+
results = match.results
|
|
3723
|
+
assert len(results) == 1
|
|
3724
|
+
assert results[0] == {"relId": "CONNECTED_TO"}
|
|
3725
|
+
|
|
3726
|
+
@pytest.mark.asyncio
|
|
3727
|
+
async def test_element_id_function_with_node(self):
|
|
3728
|
+
"""Test elementId() function with a graph node."""
|
|
3729
|
+
await Runner(
|
|
3730
|
+
"""
|
|
3731
|
+
CREATE VIRTUAL (:Person) AS {
|
|
3732
|
+
UNWIND [
|
|
3733
|
+
{id: 1, name: 'Alice'},
|
|
3734
|
+
{id: 2, name: 'Bob'}
|
|
3735
|
+
] AS record
|
|
3736
|
+
RETURN record.id AS id, record.name AS name
|
|
3737
|
+
}
|
|
3738
|
+
"""
|
|
3739
|
+
).run()
|
|
3740
|
+
match = Runner(
|
|
3741
|
+
"""
|
|
3742
|
+
MATCH (n:Person)
|
|
3743
|
+
RETURN elementId(n) AS eid
|
|
3744
|
+
"""
|
|
3745
|
+
)
|
|
3746
|
+
await match.run()
|
|
3747
|
+
results = match.results
|
|
3748
|
+
assert len(results) == 2
|
|
3749
|
+
assert results[0] == {"eid": "1"}
|
|
3750
|
+
assert results[1] == {"eid": "2"}
|
|
3751
|
+
|
|
3752
|
+
@pytest.mark.asyncio
|
|
3753
|
+
async def test_element_id_function_with_null(self):
|
|
3754
|
+
"""Test elementId() function with null."""
|
|
3755
|
+
runner = Runner("RETURN elementId(null) AS eid")
|
|
3756
|
+
await runner.run()
|
|
3757
|
+
results = runner.results
|
|
3758
|
+
assert len(results) == 1
|
|
3759
|
+
assert results[0] == {"eid": None}
|
|
3760
|
+
|
|
3761
|
+
@pytest.mark.asyncio
|
|
3762
|
+
async def test_head_function(self):
|
|
3763
|
+
"""Test head() function."""
|
|
3764
|
+
runner = Runner("RETURN head([1, 2, 3]) AS h")
|
|
3765
|
+
await runner.run()
|
|
3766
|
+
assert len(runner.results) == 1
|
|
3767
|
+
assert runner.results[0] == {"h": 1}
|
|
3768
|
+
|
|
3769
|
+
@pytest.mark.asyncio
|
|
3770
|
+
async def test_head_function_empty_list(self):
|
|
3771
|
+
"""Test head() function with empty list."""
|
|
3772
|
+
runner = Runner("RETURN head([]) AS h")
|
|
3773
|
+
await runner.run()
|
|
3774
|
+
assert runner.results[0] == {"h": None}
|
|
3775
|
+
|
|
3776
|
+
@pytest.mark.asyncio
|
|
3777
|
+
async def test_head_function_null(self):
|
|
3778
|
+
"""Test head() function with null."""
|
|
3779
|
+
runner = Runner("RETURN head(null) AS h")
|
|
3780
|
+
await runner.run()
|
|
3781
|
+
assert runner.results[0] == {"h": None}
|
|
3782
|
+
|
|
3783
|
+
@pytest.mark.asyncio
|
|
3784
|
+
async def test_tail_function(self):
|
|
3785
|
+
"""Test tail() function."""
|
|
3786
|
+
runner = Runner("RETURN tail([1, 2, 3]) AS t")
|
|
3787
|
+
await runner.run()
|
|
3788
|
+
assert len(runner.results) == 1
|
|
3789
|
+
assert runner.results[0] == {"t": [2, 3]}
|
|
3790
|
+
|
|
3791
|
+
@pytest.mark.asyncio
|
|
3792
|
+
async def test_tail_function_single_element(self):
|
|
3793
|
+
"""Test tail() function with single element."""
|
|
3794
|
+
runner = Runner("RETURN tail([1]) AS t")
|
|
3795
|
+
await runner.run()
|
|
3796
|
+
assert runner.results[0] == {"t": []}
|
|
3797
|
+
|
|
3798
|
+
@pytest.mark.asyncio
|
|
3799
|
+
async def test_tail_function_null(self):
|
|
3800
|
+
"""Test tail() function with null."""
|
|
3801
|
+
runner = Runner("RETURN tail(null) AS t")
|
|
3802
|
+
await runner.run()
|
|
3803
|
+
assert runner.results[0] == {"t": None}
|
|
3804
|
+
|
|
3805
|
+
@pytest.mark.asyncio
|
|
3806
|
+
async def test_last_function(self):
|
|
3807
|
+
"""Test last() function."""
|
|
3808
|
+
runner = Runner("RETURN last([1, 2, 3]) AS l")
|
|
3809
|
+
await runner.run()
|
|
3810
|
+
assert len(runner.results) == 1
|
|
3811
|
+
assert runner.results[0] == {"l": 3}
|
|
3812
|
+
|
|
3813
|
+
@pytest.mark.asyncio
|
|
3814
|
+
async def test_last_function_empty_list(self):
|
|
3815
|
+
"""Test last() function with empty list."""
|
|
3816
|
+
runner = Runner("RETURN last([]) AS l")
|
|
3817
|
+
await runner.run()
|
|
3818
|
+
assert runner.results[0] == {"l": None}
|
|
3819
|
+
|
|
3820
|
+
@pytest.mark.asyncio
|
|
3821
|
+
async def test_last_function_null(self):
|
|
3822
|
+
"""Test last() function with null."""
|
|
3823
|
+
runner = Runner("RETURN last(null) AS l")
|
|
3824
|
+
await runner.run()
|
|
3825
|
+
assert runner.results[0] == {"l": None}
|
|
3826
|
+
|
|
3827
|
+
@pytest.mark.asyncio
|
|
3828
|
+
async def test_to_integer_function_string(self):
|
|
3829
|
+
"""Test toInteger() function with string."""
|
|
3830
|
+
runner = Runner('RETURN toInteger("42") AS i')
|
|
3831
|
+
await runner.run()
|
|
3832
|
+
assert runner.results[0] == {"i": 42}
|
|
3833
|
+
|
|
3834
|
+
@pytest.mark.asyncio
|
|
3835
|
+
async def test_to_integer_function_float(self):
|
|
3836
|
+
"""Test toInteger() function with float."""
|
|
3837
|
+
runner = Runner("RETURN toInteger(3.14) AS i")
|
|
3838
|
+
await runner.run()
|
|
3839
|
+
assert runner.results[0] == {"i": 3}
|
|
3840
|
+
|
|
3841
|
+
@pytest.mark.asyncio
|
|
3842
|
+
async def test_to_integer_function_boolean(self):
|
|
3843
|
+
"""Test toInteger() function with boolean."""
|
|
3844
|
+
runner = Runner("RETURN toInteger(true) AS i")
|
|
3845
|
+
await runner.run()
|
|
3846
|
+
assert runner.results[0] == {"i": 1}
|
|
3847
|
+
|
|
3848
|
+
@pytest.mark.asyncio
|
|
3849
|
+
async def test_to_integer_function_null(self):
|
|
3850
|
+
"""Test toInteger() function with null."""
|
|
3851
|
+
runner = Runner("RETURN toInteger(null) AS i")
|
|
3852
|
+
await runner.run()
|
|
3853
|
+
assert runner.results[0] == {"i": None}
|
|
3854
|
+
|
|
3855
|
+
@pytest.mark.asyncio
|
|
3856
|
+
async def test_to_float_function_string(self):
|
|
3857
|
+
"""Test toFloat() function with string."""
|
|
3858
|
+
runner = Runner('RETURN toFloat("3.14") AS f')
|
|
3859
|
+
await runner.run()
|
|
3860
|
+
assert runner.results[0] == {"f": 3.14}
|
|
3861
|
+
|
|
3862
|
+
@pytest.mark.asyncio
|
|
3863
|
+
async def test_to_float_function_integer(self):
|
|
3864
|
+
"""Test toFloat() function with integer."""
|
|
3865
|
+
runner = Runner("RETURN toFloat(42) AS f")
|
|
3866
|
+
await runner.run()
|
|
3867
|
+
assert runner.results[0] == {"f": 42}
|
|
3868
|
+
|
|
3869
|
+
@pytest.mark.asyncio
|
|
3870
|
+
async def test_to_float_function_boolean(self):
|
|
3871
|
+
"""Test toFloat() function with boolean."""
|
|
3872
|
+
runner = Runner("RETURN toFloat(true) AS f")
|
|
3873
|
+
await runner.run()
|
|
3874
|
+
assert runner.results[0] == {"f": 1.0}
|
|
3875
|
+
|
|
3876
|
+
@pytest.mark.asyncio
|
|
3877
|
+
async def test_to_float_function_null(self):
|
|
3878
|
+
"""Test toFloat() function with null."""
|
|
3879
|
+
runner = Runner("RETURN toFloat(null) AS f")
|
|
3880
|
+
await runner.run()
|
|
3881
|
+
assert runner.results[0] == {"f": None}
|
|
3882
|
+
|
|
3883
|
+
@pytest.mark.asyncio
|
|
3884
|
+
async def test_duration_iso_string(self):
|
|
3885
|
+
"""Test duration() with ISO 8601 string."""
|
|
3886
|
+
runner = Runner("RETURN duration('P1Y2M3DT4H5M6S') AS d")
|
|
3887
|
+
await runner.run()
|
|
3888
|
+
d = runner.results[0]["d"]
|
|
3889
|
+
assert d["years"] == 1
|
|
3890
|
+
assert d["months"] == 2
|
|
3891
|
+
assert d["days"] == 3
|
|
3892
|
+
assert d["hours"] == 4
|
|
3893
|
+
assert d["minutes"] == 5
|
|
3894
|
+
assert d["seconds"] == 6
|
|
3895
|
+
assert d["totalMonths"] == 14
|
|
3896
|
+
assert d["formatted"] == "P1Y2M3DT4H5M6S"
|
|
3897
|
+
|
|
3898
|
+
@pytest.mark.asyncio
|
|
3899
|
+
async def test_duration_map_argument(self):
|
|
3900
|
+
"""Test duration() with map argument."""
|
|
3901
|
+
runner = Runner("RETURN duration({days: 14, hours: 16}) AS d")
|
|
3902
|
+
await runner.run()
|
|
3903
|
+
d = runner.results[0]["d"]
|
|
3904
|
+
assert d["days"] == 14
|
|
3905
|
+
assert d["hours"] == 16
|
|
3906
|
+
assert d["totalDays"] == 14
|
|
3907
|
+
assert d["totalSeconds"] == 57600
|
|
3908
|
+
|
|
3909
|
+
@pytest.mark.asyncio
|
|
3910
|
+
async def test_duration_weeks(self):
|
|
3911
|
+
"""Test duration() with weeks."""
|
|
3912
|
+
runner = Runner("RETURN duration('P2W') AS d")
|
|
3913
|
+
await runner.run()
|
|
3914
|
+
d = runner.results[0]["d"]
|
|
3915
|
+
assert d["weeks"] == 2
|
|
3916
|
+
assert d["days"] == 14
|
|
3917
|
+
assert d["totalDays"] == 14
|
|
3918
|
+
|
|
3919
|
+
@pytest.mark.asyncio
|
|
3920
|
+
async def test_duration_null(self):
|
|
3921
|
+
"""Test duration() with null."""
|
|
3922
|
+
runner = Runner("RETURN duration(null) AS d")
|
|
3923
|
+
await runner.run()
|
|
3924
|
+
assert runner.results[0] == {"d": None}
|
|
3925
|
+
|
|
3926
|
+
@pytest.mark.asyncio
|
|
3927
|
+
async def test_duration_time_only(self):
|
|
3928
|
+
"""Test duration() with time-only string."""
|
|
3929
|
+
runner = Runner("RETURN duration('PT2H30M') AS d")
|
|
3930
|
+
await runner.run()
|
|
3931
|
+
d = runner.results[0]["d"]
|
|
3932
|
+
assert d["hours"] == 2
|
|
3933
|
+
assert d["minutes"] == 30
|
|
3934
|
+
assert d["totalSeconds"] == 9000
|
|
3935
|
+
assert d["formatted"] == "PT2H30M"
|