flowquery 1.0.35 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/parsing/data_structures/lookup.d.ts.map +1 -1
  3. package/dist/parsing/data_structures/lookup.js +5 -1
  4. package/dist/parsing/data_structures/lookup.js.map +1 -1
  5. package/dist/parsing/functions/coalesce.d.ts +17 -0
  6. package/dist/parsing/functions/coalesce.d.ts.map +1 -0
  7. package/dist/parsing/functions/coalesce.js +61 -0
  8. package/dist/parsing/functions/coalesce.js.map +1 -0
  9. package/dist/parsing/functions/date.d.ts +22 -0
  10. package/dist/parsing/functions/date.d.ts.map +1 -0
  11. package/dist/parsing/functions/date.js +71 -0
  12. package/dist/parsing/functions/date.js.map +1 -0
  13. package/dist/parsing/functions/datetime.d.ts +22 -0
  14. package/dist/parsing/functions/datetime.d.ts.map +1 -0
  15. package/dist/parsing/functions/datetime.js +71 -0
  16. package/dist/parsing/functions/datetime.js.map +1 -0
  17. package/dist/parsing/functions/duration.d.ts +7 -0
  18. package/dist/parsing/functions/duration.d.ts.map +1 -0
  19. package/dist/parsing/functions/duration.js +145 -0
  20. package/dist/parsing/functions/duration.js.map +1 -0
  21. package/dist/parsing/functions/element_id.d.ts +7 -0
  22. package/dist/parsing/functions/element_id.d.ts.map +1 -0
  23. package/dist/parsing/functions/element_id.js +58 -0
  24. package/dist/parsing/functions/element_id.js.map +1 -0
  25. package/dist/parsing/functions/function_factory.d.ts +20 -0
  26. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  27. package/dist/parsing/functions/function_factory.js +20 -0
  28. package/dist/parsing/functions/function_factory.js.map +1 -1
  29. package/dist/parsing/functions/head.d.ts +7 -0
  30. package/dist/parsing/functions/head.d.ts.map +1 -0
  31. package/dist/parsing/functions/head.js +53 -0
  32. package/dist/parsing/functions/head.js.map +1 -0
  33. package/dist/parsing/functions/id.d.ts +7 -0
  34. package/dist/parsing/functions/id.d.ts.map +1 -0
  35. package/dist/parsing/functions/id.js +58 -0
  36. package/dist/parsing/functions/id.js.map +1 -0
  37. package/dist/parsing/functions/last.d.ts +7 -0
  38. package/dist/parsing/functions/last.d.ts.map +1 -0
  39. package/dist/parsing/functions/last.js +53 -0
  40. package/dist/parsing/functions/last.js.map +1 -0
  41. package/dist/parsing/functions/localdatetime.d.ts +21 -0
  42. package/dist/parsing/functions/localdatetime.d.ts.map +1 -0
  43. package/dist/parsing/functions/localdatetime.js +71 -0
  44. package/dist/parsing/functions/localdatetime.js.map +1 -0
  45. package/dist/parsing/functions/localtime.d.ts +20 -0
  46. package/dist/parsing/functions/localtime.d.ts.map +1 -0
  47. package/dist/parsing/functions/localtime.js +67 -0
  48. package/dist/parsing/functions/localtime.js.map +1 -0
  49. package/dist/parsing/functions/max.d.ts +14 -0
  50. package/dist/parsing/functions/max.d.ts.map +1 -0
  51. package/dist/parsing/functions/max.js +51 -0
  52. package/dist/parsing/functions/max.js.map +1 -0
  53. package/dist/parsing/functions/min.d.ts +14 -0
  54. package/dist/parsing/functions/min.d.ts.map +1 -0
  55. package/dist/parsing/functions/min.js +51 -0
  56. package/dist/parsing/functions/min.js.map +1 -0
  57. package/dist/parsing/functions/nodes.d.ts +7 -0
  58. package/dist/parsing/functions/nodes.d.ts.map +1 -0
  59. package/dist/parsing/functions/nodes.js +63 -0
  60. package/dist/parsing/functions/nodes.js.map +1 -0
  61. package/dist/parsing/functions/properties.d.ts +7 -0
  62. package/dist/parsing/functions/properties.d.ts.map +1 -0
  63. package/dist/parsing/functions/properties.js +74 -0
  64. package/dist/parsing/functions/properties.js.map +1 -0
  65. package/dist/parsing/functions/relationships.d.ts +7 -0
  66. package/dist/parsing/functions/relationships.d.ts.map +1 -0
  67. package/dist/parsing/functions/relationships.js +61 -0
  68. package/dist/parsing/functions/relationships.js.map +1 -0
  69. package/dist/parsing/functions/tail.d.ts +7 -0
  70. package/dist/parsing/functions/tail.d.ts.map +1 -0
  71. package/dist/parsing/functions/tail.js +50 -0
  72. package/dist/parsing/functions/tail.js.map +1 -0
  73. package/dist/parsing/functions/temporal_utils.d.ts +39 -0
  74. package/dist/parsing/functions/temporal_utils.d.ts.map +1 -0
  75. package/dist/parsing/functions/temporal_utils.js +168 -0
  76. package/dist/parsing/functions/temporal_utils.js.map +1 -0
  77. package/dist/parsing/functions/time.d.ts +20 -0
  78. package/dist/parsing/functions/time.d.ts.map +1 -0
  79. package/dist/parsing/functions/time.js +67 -0
  80. package/dist/parsing/functions/time.js.map +1 -0
  81. package/dist/parsing/functions/timestamp.d.ts +17 -0
  82. package/dist/parsing/functions/timestamp.d.ts.map +1 -0
  83. package/dist/parsing/functions/timestamp.js +51 -0
  84. package/dist/parsing/functions/timestamp.js.map +1 -0
  85. package/dist/parsing/functions/to_float.d.ts +7 -0
  86. package/dist/parsing/functions/to_float.d.ts.map +1 -0
  87. package/dist/parsing/functions/to_float.js +61 -0
  88. package/dist/parsing/functions/to_float.js.map +1 -0
  89. package/dist/parsing/functions/to_integer.d.ts +7 -0
  90. package/dist/parsing/functions/to_integer.d.ts.map +1 -0
  91. package/dist/parsing/functions/to_integer.js +61 -0
  92. package/dist/parsing/functions/to_integer.js.map +1 -0
  93. package/docs/flowquery.min.js +1 -1
  94. package/flowquery-py/pyproject.toml +1 -1
  95. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -0
  96. package/flowquery-py/src/parsing/functions/__init__.py +40 -2
  97. package/flowquery-py/src/parsing/functions/coalesce.py +44 -0
  98. package/flowquery-py/src/parsing/functions/date_.py +63 -0
  99. package/flowquery-py/src/parsing/functions/datetime_.py +64 -0
  100. package/flowquery-py/src/parsing/functions/duration.py +159 -0
  101. package/flowquery-py/src/parsing/functions/element_id.py +50 -0
  102. package/flowquery-py/src/parsing/functions/head.py +39 -0
  103. package/flowquery-py/src/parsing/functions/id_.py +49 -0
  104. package/flowquery-py/src/parsing/functions/last.py +39 -0
  105. package/flowquery-py/src/parsing/functions/localdatetime.py +62 -0
  106. package/flowquery-py/src/parsing/functions/localtime.py +59 -0
  107. package/flowquery-py/src/parsing/functions/max_.py +49 -0
  108. package/flowquery-py/src/parsing/functions/min_.py +49 -0
  109. package/flowquery-py/src/parsing/functions/nodes.py +48 -0
  110. package/flowquery-py/src/parsing/functions/properties.py +50 -0
  111. package/flowquery-py/src/parsing/functions/relationships.py +46 -0
  112. package/flowquery-py/src/parsing/functions/tail.py +37 -0
  113. package/flowquery-py/src/parsing/functions/temporal_utils.py +186 -0
  114. package/flowquery-py/src/parsing/functions/time_.py +59 -0
  115. package/flowquery-py/src/parsing/functions/timestamp.py +39 -0
  116. package/flowquery-py/src/parsing/functions/to_float.py +46 -0
  117. package/flowquery-py/src/parsing/functions/to_integer.py +46 -0
  118. package/flowquery-py/tests/compute/test_runner.py +834 -1
  119. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  120. package/package.json +1 -1
  121. package/src/parsing/data_structures/lookup.ts +8 -4
  122. package/src/parsing/functions/coalesce.ts +50 -0
  123. package/src/parsing/functions/date.ts +65 -0
  124. package/src/parsing/functions/datetime.ts +65 -0
  125. package/src/parsing/functions/duration.ts +143 -0
  126. package/src/parsing/functions/element_id.ts +51 -0
  127. package/src/parsing/functions/function_factory.ts +20 -0
  128. package/src/parsing/functions/head.ts +42 -0
  129. package/src/parsing/functions/id.ts +51 -0
  130. package/src/parsing/functions/last.ts +42 -0
  131. package/src/parsing/functions/localdatetime.ts +65 -0
  132. package/src/parsing/functions/localtime.ts +60 -0
  133. package/src/parsing/functions/max.ts +37 -0
  134. package/src/parsing/functions/min.ts +37 -0
  135. package/src/parsing/functions/nodes.ts +54 -0
  136. package/src/parsing/functions/properties.ts +56 -0
  137. package/src/parsing/functions/relationships.ts +52 -0
  138. package/src/parsing/functions/tail.ts +39 -0
  139. package/src/parsing/functions/temporal_utils.ts +180 -0
  140. package/src/parsing/functions/time.ts +60 -0
  141. package/src/parsing/functions/timestamp.ts +41 -0
  142. package/src/parsing/functions/to_float.ts +50 -0
  143. package/src/parsing/functions/to_integer.ts +50 -0
  144. package/tests/compute/runner.test.ts +726 -0
@@ -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 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
+
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 (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"