flowquery 1.0.34 → 1.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/database.d.ts +1 -0
- package/dist/graph/database.d.ts.map +1 -1
- package/dist/graph/database.js +43 -6
- package/dist/graph/database.js.map +1 -1
- package/dist/graph/relationship.d.ts +3 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +12 -4
- package/dist/graph/relationship.js.map +1 -1
- package/dist/graph/relationship_data.js +1 -1
- package/dist/graph/relationship_data.js.map +1 -1
- package/dist/graph/relationship_match_collector.d.ts.map +1 -1
- package/dist/graph/relationship_match_collector.js +6 -3
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/dist/graph/relationship_reference.js +1 -1
- package/dist/graph/relationship_reference.js.map +1 -1
- package/dist/parsing/data_structures/lookup.d.ts.map +1 -1
- package/dist/parsing/data_structures/lookup.js +5 -1
- package/dist/parsing/data_structures/lookup.js.map +1 -1
- package/dist/parsing/functions/coalesce.d.ts +17 -0
- package/dist/parsing/functions/coalesce.d.ts.map +1 -0
- package/dist/parsing/functions/coalesce.js +61 -0
- package/dist/parsing/functions/coalesce.js.map +1 -0
- package/dist/parsing/functions/date.d.ts +22 -0
- package/dist/parsing/functions/date.d.ts.map +1 -0
- package/dist/parsing/functions/date.js +71 -0
- package/dist/parsing/functions/date.js.map +1 -0
- package/dist/parsing/functions/datetime.d.ts +22 -0
- package/dist/parsing/functions/datetime.d.ts.map +1 -0
- package/dist/parsing/functions/datetime.js +71 -0
- package/dist/parsing/functions/datetime.js.map +1 -0
- package/dist/parsing/functions/duration.d.ts +7 -0
- package/dist/parsing/functions/duration.d.ts.map +1 -0
- package/dist/parsing/functions/duration.js +145 -0
- package/dist/parsing/functions/duration.js.map +1 -0
- package/dist/parsing/functions/element_id.d.ts +7 -0
- package/dist/parsing/functions/element_id.d.ts.map +1 -0
- package/dist/parsing/functions/element_id.js +58 -0
- package/dist/parsing/functions/element_id.js.map +1 -0
- package/dist/parsing/functions/function_factory.d.ts +21 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +21 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/head.d.ts +7 -0
- package/dist/parsing/functions/head.d.ts.map +1 -0
- package/dist/parsing/functions/head.js +53 -0
- package/dist/parsing/functions/head.js.map +1 -0
- package/dist/parsing/functions/id.d.ts +7 -0
- package/dist/parsing/functions/id.d.ts.map +1 -0
- package/dist/parsing/functions/id.js +58 -0
- package/dist/parsing/functions/id.js.map +1 -0
- package/dist/parsing/functions/last.d.ts +7 -0
- package/dist/parsing/functions/last.d.ts.map +1 -0
- package/dist/parsing/functions/last.js +53 -0
- package/dist/parsing/functions/last.js.map +1 -0
- package/dist/parsing/functions/localdatetime.d.ts +21 -0
- package/dist/parsing/functions/localdatetime.d.ts.map +1 -0
- package/dist/parsing/functions/localdatetime.js +71 -0
- package/dist/parsing/functions/localdatetime.js.map +1 -0
- package/dist/parsing/functions/localtime.d.ts +20 -0
- package/dist/parsing/functions/localtime.d.ts.map +1 -0
- package/dist/parsing/functions/localtime.js +67 -0
- package/dist/parsing/functions/localtime.js.map +1 -0
- package/dist/parsing/functions/max.d.ts +14 -0
- package/dist/parsing/functions/max.d.ts.map +1 -0
- package/dist/parsing/functions/max.js +51 -0
- package/dist/parsing/functions/max.js.map +1 -0
- package/dist/parsing/functions/min.d.ts +14 -0
- package/dist/parsing/functions/min.d.ts.map +1 -0
- package/dist/parsing/functions/min.js +51 -0
- package/dist/parsing/functions/min.js.map +1 -0
- package/dist/parsing/functions/nodes.d.ts +7 -0
- package/dist/parsing/functions/nodes.d.ts.map +1 -0
- package/dist/parsing/functions/nodes.js +63 -0
- package/dist/parsing/functions/nodes.js.map +1 -0
- package/dist/parsing/functions/predicate_sum.d.ts.map +1 -1
- package/dist/parsing/functions/predicate_sum.js +13 -10
- package/dist/parsing/functions/predicate_sum.js.map +1 -1
- package/dist/parsing/functions/properties.d.ts +7 -0
- package/dist/parsing/functions/properties.d.ts.map +1 -0
- package/dist/parsing/functions/properties.js +74 -0
- package/dist/parsing/functions/properties.js.map +1 -0
- package/dist/parsing/functions/relationships.d.ts +7 -0
- package/dist/parsing/functions/relationships.d.ts.map +1 -0
- package/dist/parsing/functions/relationships.js +61 -0
- package/dist/parsing/functions/relationships.js.map +1 -0
- package/dist/parsing/functions/schema.d.ts +5 -2
- package/dist/parsing/functions/schema.d.ts.map +1 -1
- package/dist/parsing/functions/schema.js +7 -4
- package/dist/parsing/functions/schema.js.map +1 -1
- package/dist/parsing/functions/tail.d.ts +7 -0
- package/dist/parsing/functions/tail.d.ts.map +1 -0
- package/dist/parsing/functions/tail.js +50 -0
- package/dist/parsing/functions/tail.js.map +1 -0
- package/dist/parsing/functions/temporal_utils.d.ts +39 -0
- package/dist/parsing/functions/temporal_utils.d.ts.map +1 -0
- package/dist/parsing/functions/temporal_utils.js +168 -0
- package/dist/parsing/functions/temporal_utils.js.map +1 -0
- package/dist/parsing/functions/time.d.ts +20 -0
- package/dist/parsing/functions/time.d.ts.map +1 -0
- package/dist/parsing/functions/time.js +67 -0
- package/dist/parsing/functions/time.js.map +1 -0
- package/dist/parsing/functions/timestamp.d.ts +17 -0
- package/dist/parsing/functions/timestamp.d.ts.map +1 -0
- package/dist/parsing/functions/timestamp.js +51 -0
- package/dist/parsing/functions/timestamp.js.map +1 -0
- package/dist/parsing/functions/to_float.d.ts +7 -0
- package/dist/parsing/functions/to_float.d.ts.map +1 -0
- package/dist/parsing/functions/to_float.js +61 -0
- package/dist/parsing/functions/to_float.js.map +1 -0
- package/dist/parsing/functions/to_integer.d.ts +7 -0
- package/dist/parsing/functions/to_integer.d.ts.map +1 -0
- package/dist/parsing/functions/to_integer.js +61 -0
- package/dist/parsing/functions/to_integer.js.map +1 -0
- package/dist/parsing/functions/trim.d.ts +7 -0
- package/dist/parsing/functions/trim.d.ts.map +1 -0
- package/dist/parsing/functions/trim.js +37 -0
- package/dist/parsing/functions/trim.js.map +1 -0
- package/dist/parsing/operations/group_by.d.ts.map +1 -1
- package/dist/parsing/operations/group_by.js +4 -2
- package/dist/parsing/operations/group_by.js.map +1 -1
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +15 -2
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/database.py +44 -11
- package/flowquery-py/src/graph/relationship.py +11 -3
- package/flowquery-py/src/graph/relationship_data.py +2 -1
- package/flowquery-py/src/graph/relationship_match_collector.py +7 -1
- package/flowquery-py/src/graph/relationship_reference.py +2 -2
- package/flowquery-py/src/parsing/data_structures/lookup.py +2 -0
- package/flowquery-py/src/parsing/functions/__init__.py +42 -2
- package/flowquery-py/src/parsing/functions/coalesce.py +44 -0
- package/flowquery-py/src/parsing/functions/date_.py +63 -0
- package/flowquery-py/src/parsing/functions/datetime_.py +64 -0
- package/flowquery-py/src/parsing/functions/duration.py +159 -0
- package/flowquery-py/src/parsing/functions/element_id.py +50 -0
- package/flowquery-py/src/parsing/functions/head.py +39 -0
- package/flowquery-py/src/parsing/functions/id_.py +49 -0
- package/flowquery-py/src/parsing/functions/last.py +39 -0
- package/flowquery-py/src/parsing/functions/localdatetime.py +62 -0
- package/flowquery-py/src/parsing/functions/localtime.py +59 -0
- package/flowquery-py/src/parsing/functions/max_.py +49 -0
- package/flowquery-py/src/parsing/functions/min_.py +49 -0
- package/flowquery-py/src/parsing/functions/nodes.py +48 -0
- package/flowquery-py/src/parsing/functions/predicate_sum.py +3 -6
- package/flowquery-py/src/parsing/functions/properties.py +50 -0
- package/flowquery-py/src/parsing/functions/relationships.py +46 -0
- package/flowquery-py/src/parsing/functions/schema.py +9 -5
- package/flowquery-py/src/parsing/functions/tail.py +37 -0
- package/flowquery-py/src/parsing/functions/temporal_utils.py +186 -0
- package/flowquery-py/src/parsing/functions/time_.py +59 -0
- package/flowquery-py/src/parsing/functions/timestamp.py +39 -0
- package/flowquery-py/src/parsing/functions/to_float.py +46 -0
- package/flowquery-py/src/parsing/functions/to_integer.py +46 -0
- package/flowquery-py/src/parsing/functions/trim.py +35 -0
- package/flowquery-py/src/parsing/operations/group_by.py +2 -0
- package/flowquery-py/src/parsing/parser.py +12 -2
- package/flowquery-py/tests/compute/test_runner.py +1082 -4
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/database.ts +42 -4
- package/src/graph/relationship.ts +12 -4
- package/src/graph/relationship_data.ts +1 -1
- package/src/graph/relationship_match_collector.ts +6 -2
- package/src/graph/relationship_reference.ts +1 -1
- package/src/parsing/data_structures/lookup.ts +8 -4
- package/src/parsing/functions/coalesce.ts +50 -0
- package/src/parsing/functions/date.ts +65 -0
- package/src/parsing/functions/datetime.ts +65 -0
- package/src/parsing/functions/duration.ts +143 -0
- package/src/parsing/functions/element_id.ts +51 -0
- package/src/parsing/functions/function_factory.ts +21 -0
- package/src/parsing/functions/head.ts +42 -0
- package/src/parsing/functions/id.ts +51 -0
- package/src/parsing/functions/last.ts +42 -0
- package/src/parsing/functions/localdatetime.ts +65 -0
- package/src/parsing/functions/localtime.ts +60 -0
- package/src/parsing/functions/max.ts +37 -0
- package/src/parsing/functions/min.ts +37 -0
- package/src/parsing/functions/nodes.ts +54 -0
- package/src/parsing/functions/predicate_sum.ts +17 -12
- package/src/parsing/functions/properties.ts +56 -0
- package/src/parsing/functions/relationships.ts +52 -0
- package/src/parsing/functions/schema.ts +7 -4
- package/src/parsing/functions/tail.ts +39 -0
- package/src/parsing/functions/temporal_utils.ts +180 -0
- package/src/parsing/functions/time.ts +60 -0
- package/src/parsing/functions/timestamp.ts +41 -0
- package/src/parsing/functions/to_float.ts +50 -0
- package/src/parsing/functions/to_integer.ts +50 -0
- package/src/parsing/functions/trim.ts +25 -0
- package/src/parsing/operations/group_by.ts +4 -1
- package/src/parsing/parser.ts +15 -2
- package/tests/compute/runner.test.ts +1005 -3
- package/tests/parsing/parser.test.ts +37 -0
|
@@ -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."""
|
|
@@ -681,6 +774,42 @@ class TestRunner:
|
|
|
681
774
|
assert len(results) == 1
|
|
682
775
|
assert results[0] == {"result": "foo bar"}
|
|
683
776
|
|
|
777
|
+
@pytest.mark.asyncio
|
|
778
|
+
async def test_trim_function(self):
|
|
779
|
+
"""Test trim function."""
|
|
780
|
+
runner = Runner('RETURN trim(" hello ") as result')
|
|
781
|
+
await runner.run()
|
|
782
|
+
results = runner.results
|
|
783
|
+
assert len(results) == 1
|
|
784
|
+
assert results[0] == {"result": "hello"}
|
|
785
|
+
|
|
786
|
+
@pytest.mark.asyncio
|
|
787
|
+
async def test_trim_function_with_tabs_and_newlines(self):
|
|
788
|
+
"""Test trim function with tabs and newlines."""
|
|
789
|
+
runner = Runner('WITH "\tfoo\n" AS s RETURN trim(s) as result')
|
|
790
|
+
await runner.run()
|
|
791
|
+
results = runner.results
|
|
792
|
+
assert len(results) == 1
|
|
793
|
+
assert results[0] == {"result": "foo"}
|
|
794
|
+
|
|
795
|
+
@pytest.mark.asyncio
|
|
796
|
+
async def test_trim_function_with_no_whitespace(self):
|
|
797
|
+
"""Test trim function with no whitespace."""
|
|
798
|
+
runner = Runner('RETURN trim("hello") as result')
|
|
799
|
+
await runner.run()
|
|
800
|
+
results = runner.results
|
|
801
|
+
assert len(results) == 1
|
|
802
|
+
assert results[0] == {"result": "hello"}
|
|
803
|
+
|
|
804
|
+
@pytest.mark.asyncio
|
|
805
|
+
async def test_trim_function_with_empty_string(self):
|
|
806
|
+
"""Test trim function with empty string."""
|
|
807
|
+
runner = Runner('RETURN trim("") as result')
|
|
808
|
+
await runner.run()
|
|
809
|
+
results = runner.results
|
|
810
|
+
assert len(results) == 1
|
|
811
|
+
assert results[0] == {"result": ""}
|
|
812
|
+
|
|
684
813
|
@pytest.mark.asyncio
|
|
685
814
|
async def test_associative_array_with_key_which_is_keyword(self):
|
|
686
815
|
"""Test associative array with key which is keyword."""
|
|
@@ -865,6 +994,144 @@ class TestRunner:
|
|
|
865
994
|
assert len(results) == 1
|
|
866
995
|
assert results[0] == {"keys": ["name", "age"]}
|
|
867
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
|
+
|
|
868
1135
|
@pytest.mark.asyncio
|
|
869
1136
|
async def test_type_function(self):
|
|
870
1137
|
"""Test type function."""
|
|
@@ -1940,6 +2207,46 @@ class TestRunner:
|
|
|
1940
2207
|
assert results[2]["name"] == "Person 3"
|
|
1941
2208
|
assert results[2]["friend"] is None
|
|
1942
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 like Neo4j
|
|
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
|
+
|
|
1943
2250
|
@pytest.mark.asyncio
|
|
1944
2251
|
async def test_optional_match_where_all_nodes_match(self):
|
|
1945
2252
|
"""Test optional match where all nodes have matching relationships."""
|
|
@@ -2152,20 +2459,24 @@ class TestRunner:
|
|
|
2152
2459
|
).run()
|
|
2153
2460
|
|
|
2154
2461
|
runner = Runner(
|
|
2155
|
-
"CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
|
|
2462
|
+
"CALL schema() YIELD kind, label, type, from_label, to_label, properties, sample RETURN kind, label, type, from_label, to_label, properties, sample"
|
|
2156
2463
|
)
|
|
2157
2464
|
await runner.run()
|
|
2158
2465
|
results = runner.results
|
|
2159
2466
|
|
|
2160
|
-
animal = next((r for r in results if r.get("kind") == "
|
|
2467
|
+
animal = next((r for r in results if r.get("kind") == "Node" and r.get("label") == "Animal"), None)
|
|
2161
2468
|
assert animal is not None
|
|
2469
|
+
assert animal["properties"] == ["species", "legs"]
|
|
2162
2470
|
assert animal["sample"] is not None
|
|
2163
2471
|
assert "id" not in animal["sample"]
|
|
2164
2472
|
assert "species" in animal["sample"]
|
|
2165
2473
|
assert "legs" in animal["sample"]
|
|
2166
2474
|
|
|
2167
|
-
chases = next((r for r in results if r.get("kind") == "
|
|
2475
|
+
chases = next((r for r in results if r.get("kind") == "Relationship" and r.get("type") == "CHASES"), None)
|
|
2168
2476
|
assert chases is not None
|
|
2477
|
+
assert chases["from_label"] == "Animal"
|
|
2478
|
+
assert chases["to_label"] == "Animal"
|
|
2479
|
+
assert chases["properties"] == ["speed"]
|
|
2169
2480
|
assert chases["sample"] is not None
|
|
2170
2481
|
assert "left_id" not in chases["sample"]
|
|
2171
2482
|
assert "right_id" not in chases["sample"]
|
|
@@ -2549,6 +2860,64 @@ class TestRunner:
|
|
|
2549
2860
|
# Add operator tests
|
|
2550
2861
|
# ============================================================
|
|
2551
2862
|
|
|
2863
|
+
@pytest.mark.asyncio
|
|
2864
|
+
async def test_collected_patterns_and_unwind(self):
|
|
2865
|
+
"""Test collecting graph patterns and unwinding them."""
|
|
2866
|
+
await Runner("""
|
|
2867
|
+
CREATE VIRTUAL (:Person) AS {
|
|
2868
|
+
unwind [
|
|
2869
|
+
{id: 1, name: 'Person 1'},
|
|
2870
|
+
{id: 2, name: 'Person 2'},
|
|
2871
|
+
{id: 3, name: 'Person 3'},
|
|
2872
|
+
{id: 4, name: 'Person 4'}
|
|
2873
|
+
] as record
|
|
2874
|
+
RETURN record.id as id, record.name as name
|
|
2875
|
+
}
|
|
2876
|
+
""").run()
|
|
2877
|
+
await Runner("""
|
|
2878
|
+
CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
|
|
2879
|
+
unwind [
|
|
2880
|
+
{left_id: 1, right_id: 2},
|
|
2881
|
+
{left_id: 2, right_id: 3},
|
|
2882
|
+
{left_id: 3, right_id: 4}
|
|
2883
|
+
] as record
|
|
2884
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2885
|
+
}
|
|
2886
|
+
""").run()
|
|
2887
|
+
runner = Runner("""
|
|
2888
|
+
MATCH p=(a:Person)-[:KNOWS*0..3]->(b:Person)
|
|
2889
|
+
WITH collect(p) AS patterns
|
|
2890
|
+
UNWIND patterns AS pattern
|
|
2891
|
+
RETURN pattern
|
|
2892
|
+
""")
|
|
2893
|
+
await runner.run()
|
|
2894
|
+
results = runner.results
|
|
2895
|
+
assert len(results) == 10
|
|
2896
|
+
# Index 0: Person 1 zero-hop - pattern = [node1] (single node)
|
|
2897
|
+
assert len(results[0]["pattern"]) == 1
|
|
2898
|
+
assert results[0]["pattern"][0]["id"] == 1
|
|
2899
|
+
# Index 1: Person 1 -> Person 2 (1-hop)
|
|
2900
|
+
assert len(results[1]["pattern"]) == 3
|
|
2901
|
+
# Index 2: Person 1 -> Person 2 -> Person 3 (2-hop)
|
|
2902
|
+
assert len(results[2]["pattern"]) == 5
|
|
2903
|
+
# Index 3: Person 1 -> Person 2 -> Person 3 -> Person 4 (3-hop)
|
|
2904
|
+
assert len(results[3]["pattern"]) == 7
|
|
2905
|
+
# Index 4: Person 2 zero-hop
|
|
2906
|
+
assert len(results[4]["pattern"]) == 1
|
|
2907
|
+
assert results[4]["pattern"][0]["id"] == 2
|
|
2908
|
+
# Index 5: Person 2 -> Person 3 (1-hop)
|
|
2909
|
+
assert len(results[5]["pattern"]) == 3
|
|
2910
|
+
# Index 6: Person 2 -> Person 3 -> Person 4 (2-hop)
|
|
2911
|
+
assert len(results[6]["pattern"]) == 5
|
|
2912
|
+
# Index 7: Person 3 zero-hop
|
|
2913
|
+
assert len(results[7]["pattern"]) == 1
|
|
2914
|
+
assert results[7]["pattern"][0]["id"] == 3
|
|
2915
|
+
# Index 8: Person 3 -> Person 4 (1-hop)
|
|
2916
|
+
assert len(results[8]["pattern"]) == 3
|
|
2917
|
+
# Index 9: Person 4 zero-hop
|
|
2918
|
+
assert len(results[9]["pattern"]) == 1
|
|
2919
|
+
assert results[9]["pattern"][0]["id"] == 4
|
|
2920
|
+
|
|
2552
2921
|
@pytest.mark.asyncio
|
|
2553
2922
|
async def test_add_two_integers(self):
|
|
2554
2923
|
"""Test add two integers."""
|
|
@@ -2854,4 +3223,713 @@ class TestRunner:
|
|
|
2854
3223
|
await runner.run()
|
|
2855
3224
|
results = runner.results
|
|
2856
3225
|
assert len(results) == 1
|
|
2857
|
-
assert results == [{"x": 1}]
|
|
3226
|
+
assert results == [{"x": 1}]
|
|
3227
|
+
|
|
3228
|
+
@pytest.mark.asyncio
|
|
3229
|
+
async def test_language_name_hits_query_with_virtual_graph(self):
|
|
3230
|
+
"""Test full language-name-hits query with virtual graph.
|
|
3231
|
+
|
|
3232
|
+
Reproduces the original bug: collect(distinct ...) on MATCH results,
|
|
3233
|
+
then sum(lang IN langs | ...) in a WITH clause, was throwing
|
|
3234
|
+
"Invalid array for sum function" because collect() returned null
|
|
3235
|
+
instead of [] when no rows entered aggregation.
|
|
3236
|
+
"""
|
|
3237
|
+
# Create Language nodes
|
|
3238
|
+
await Runner(
|
|
3239
|
+
"""
|
|
3240
|
+
CREATE VIRTUAL (:Language) AS {
|
|
3241
|
+
UNWIND [
|
|
3242
|
+
{id: 1, name: 'Python'},
|
|
3243
|
+
{id: 2, name: 'JavaScript'},
|
|
3244
|
+
{id: 3, name: 'TypeScript'}
|
|
3245
|
+
] AS record
|
|
3246
|
+
RETURN record.id AS id, record.name AS name
|
|
3247
|
+
}
|
|
3248
|
+
"""
|
|
3249
|
+
).run()
|
|
3250
|
+
|
|
3251
|
+
# Create Chat nodes with messages
|
|
3252
|
+
await Runner(
|
|
3253
|
+
"""
|
|
3254
|
+
CREATE VIRTUAL (:Chat) AS {
|
|
3255
|
+
UNWIND [
|
|
3256
|
+
{id: 1, name: 'Dev Discussion', messages: [
|
|
3257
|
+
{From: 'Alice', SentDateTime: '2025-01-01T10:00:00', Content: 'I love Python and JavaScript'},
|
|
3258
|
+
{From: 'Bob', SentDateTime: '2025-01-01T10:05:00', Content: 'What languages do you prefer?'}
|
|
3259
|
+
]},
|
|
3260
|
+
{id: 2, name: 'General', messages: [
|
|
3261
|
+
{From: 'Charlie', SentDateTime: '2025-01-02T09:00:00', Content: 'The weather is nice today'},
|
|
3262
|
+
{From: 'Alice', SentDateTime: '2025-01-02T09:05:00', Content: 'TypeScript is great for language tooling'}
|
|
3263
|
+
]}
|
|
3264
|
+
] AS record
|
|
3265
|
+
RETURN record.id AS id, record.name AS name, record.messages AS messages
|
|
3266
|
+
}
|
|
3267
|
+
"""
|
|
3268
|
+
).run()
|
|
3269
|
+
|
|
3270
|
+
# Create User nodes
|
|
3271
|
+
await Runner(
|
|
3272
|
+
"""
|
|
3273
|
+
CREATE VIRTUAL (:User) AS {
|
|
3274
|
+
UNWIND [
|
|
3275
|
+
{id: 1, displayName: 'Alice'},
|
|
3276
|
+
{id: 2, displayName: 'Bob'},
|
|
3277
|
+
{id: 3, displayName: 'Charlie'}
|
|
3278
|
+
] AS record
|
|
3279
|
+
RETURN record.id AS id, record.displayName AS displayName
|
|
3280
|
+
}
|
|
3281
|
+
"""
|
|
3282
|
+
).run()
|
|
3283
|
+
|
|
3284
|
+
# Create PARTICIPATES_IN relationships
|
|
3285
|
+
await Runner(
|
|
3286
|
+
"""
|
|
3287
|
+
CREATE VIRTUAL (:User)-[:PARTICIPATES_IN]-(:Chat) AS {
|
|
3288
|
+
UNWIND [
|
|
3289
|
+
{left_id: 1, right_id: 1},
|
|
3290
|
+
{left_id: 2, right_id: 1},
|
|
3291
|
+
{left_id: 3, right_id: 2},
|
|
3292
|
+
{left_id: 1, right_id: 2}
|
|
3293
|
+
] AS record
|
|
3294
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id
|
|
3295
|
+
}
|
|
3296
|
+
"""
|
|
3297
|
+
).run()
|
|
3298
|
+
|
|
3299
|
+
# Run the original query (using 'sender' alias since 'from' is a reserved keyword)
|
|
3300
|
+
runner = Runner(
|
|
3301
|
+
"""
|
|
3302
|
+
MATCH (l:Language)
|
|
3303
|
+
WITH collect(distinct l.name) AS langs
|
|
3304
|
+
MATCH (c:Chat)
|
|
3305
|
+
UNWIND c.messages AS msg
|
|
3306
|
+
WITH c, msg, langs,
|
|
3307
|
+
sum(lang IN langs | 1 where toLower(msg.Content) CONTAINS toLower(lang)) AS langNameHits
|
|
3308
|
+
WHERE toLower(msg.Content) CONTAINS "language"
|
|
3309
|
+
OR toLower(msg.Content) CONTAINS "languages"
|
|
3310
|
+
OR langNameHits > 0
|
|
3311
|
+
OPTIONAL MATCH (u:User)-[:PARTICIPATES_IN]->(c)
|
|
3312
|
+
RETURN
|
|
3313
|
+
c.name AS chat,
|
|
3314
|
+
collect(distinct u.displayName) AS participants,
|
|
3315
|
+
msg.From AS sender,
|
|
3316
|
+
msg.SentDateTime AS sentDateTime,
|
|
3317
|
+
msg.Content AS message
|
|
3318
|
+
"""
|
|
3319
|
+
)
|
|
3320
|
+
await runner.run()
|
|
3321
|
+
results = runner.results
|
|
3322
|
+
|
|
3323
|
+
# Messages that mention a language name or the word "language(s)":
|
|
3324
|
+
# 1. "I love Python and JavaScript" - langNameHits=2
|
|
3325
|
+
# 2. "What languages do you prefer?" - contains "languages"
|
|
3326
|
+
# 3. "TypeScript is great for language tooling" - langNameHits=1, also "language"
|
|
3327
|
+
assert len(results) == 3
|
|
3328
|
+
assert results[0]["chat"] == "Dev Discussion"
|
|
3329
|
+
assert results[0]["message"] == "I love Python and JavaScript"
|
|
3330
|
+
assert results[0]["sender"] == "Alice"
|
|
3331
|
+
assert results[1]["chat"] == "Dev Discussion"
|
|
3332
|
+
assert results[1]["message"] == "What languages do you prefer?"
|
|
3333
|
+
assert results[1]["sender"] == "Bob"
|
|
3334
|
+
assert results[2]["chat"] == "General"
|
|
3335
|
+
assert results[2]["message"] == "TypeScript is great for language tooling"
|
|
3336
|
+
assert results[2]["sender"] == "Alice"
|
|
3337
|
+
|
|
3338
|
+
@pytest.mark.asyncio
|
|
3339
|
+
async def test_sum_with_empty_collected_array(self):
|
|
3340
|
+
"""Reproduces the original bug: collect on empty input should yield []
|
|
3341
|
+
and sum over that empty array should return 0, not throw."""
|
|
3342
|
+
runner = Runner(
|
|
3343
|
+
"""
|
|
3344
|
+
UNWIND [] AS lang
|
|
3345
|
+
WITH collect(distinct lang) AS langs
|
|
3346
|
+
UNWIND ['hello', 'world'] AS msg
|
|
3347
|
+
WITH msg, langs, sum(l IN langs | 1 where toLower(msg) CONTAINS toLower(l)) AS hits
|
|
3348
|
+
RETURN msg, hits
|
|
3349
|
+
"""
|
|
3350
|
+
)
|
|
3351
|
+
await runner.run()
|
|
3352
|
+
results = runner.results
|
|
3353
|
+
assert len(results) == 2
|
|
3354
|
+
assert results[0] == {"msg": "hello", "hits": 0}
|
|
3355
|
+
assert results[1] == {"msg": "world", "hits": 0}
|
|
3356
|
+
|
|
3357
|
+
@pytest.mark.asyncio
|
|
3358
|
+
async def test_sum_where_all_elements_filtered_returns_0(self):
|
|
3359
|
+
"""Test sum returns 0 when where clause filters everything."""
|
|
3360
|
+
runner = Runner("RETURN sum(n in [1, 2, 3] | n where n > 100) as sum")
|
|
3361
|
+
await runner.run()
|
|
3362
|
+
results = runner.results
|
|
3363
|
+
assert len(results) == 1
|
|
3364
|
+
assert results[0] == {"sum": 0}
|
|
3365
|
+
|
|
3366
|
+
@pytest.mark.asyncio
|
|
3367
|
+
async def test_sum_over_empty_array_returns_0(self):
|
|
3368
|
+
"""Test sum over empty array returns 0."""
|
|
3369
|
+
runner = Runner("WITH [] AS arr RETURN sum(n in arr | n) as sum")
|
|
3370
|
+
await runner.run()
|
|
3371
|
+
results = runner.results
|
|
3372
|
+
assert len(results) == 1
|
|
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 (Neo4j-style)
|
|
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"
|