flowquery 1.0.34 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/graph/database.d.ts +1 -0
  3. package/dist/graph/database.d.ts.map +1 -1
  4. package/dist/graph/database.js +43 -6
  5. package/dist/graph/database.js.map +1 -1
  6. package/dist/graph/relationship.d.ts +3 -1
  7. package/dist/graph/relationship.d.ts.map +1 -1
  8. package/dist/graph/relationship.js +12 -4
  9. package/dist/graph/relationship.js.map +1 -1
  10. package/dist/graph/relationship_data.js +1 -1
  11. package/dist/graph/relationship_data.js.map +1 -1
  12. package/dist/graph/relationship_match_collector.d.ts.map +1 -1
  13. package/dist/graph/relationship_match_collector.js +6 -3
  14. package/dist/graph/relationship_match_collector.js.map +1 -1
  15. package/dist/graph/relationship_reference.js +1 -1
  16. package/dist/graph/relationship_reference.js.map +1 -1
  17. package/dist/parsing/data_structures/lookup.d.ts.map +1 -1
  18. package/dist/parsing/data_structures/lookup.js +5 -1
  19. package/dist/parsing/data_structures/lookup.js.map +1 -1
  20. package/dist/parsing/functions/coalesce.d.ts +17 -0
  21. package/dist/parsing/functions/coalesce.d.ts.map +1 -0
  22. package/dist/parsing/functions/coalesce.js +61 -0
  23. package/dist/parsing/functions/coalesce.js.map +1 -0
  24. package/dist/parsing/functions/date.d.ts +22 -0
  25. package/dist/parsing/functions/date.d.ts.map +1 -0
  26. package/dist/parsing/functions/date.js +71 -0
  27. package/dist/parsing/functions/date.js.map +1 -0
  28. package/dist/parsing/functions/datetime.d.ts +22 -0
  29. package/dist/parsing/functions/datetime.d.ts.map +1 -0
  30. package/dist/parsing/functions/datetime.js +71 -0
  31. package/dist/parsing/functions/datetime.js.map +1 -0
  32. package/dist/parsing/functions/duration.d.ts +7 -0
  33. package/dist/parsing/functions/duration.d.ts.map +1 -0
  34. package/dist/parsing/functions/duration.js +145 -0
  35. package/dist/parsing/functions/duration.js.map +1 -0
  36. package/dist/parsing/functions/element_id.d.ts +7 -0
  37. package/dist/parsing/functions/element_id.d.ts.map +1 -0
  38. package/dist/parsing/functions/element_id.js +58 -0
  39. package/dist/parsing/functions/element_id.js.map +1 -0
  40. package/dist/parsing/functions/function_factory.d.ts +21 -0
  41. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  42. package/dist/parsing/functions/function_factory.js +21 -0
  43. package/dist/parsing/functions/function_factory.js.map +1 -1
  44. package/dist/parsing/functions/head.d.ts +7 -0
  45. package/dist/parsing/functions/head.d.ts.map +1 -0
  46. package/dist/parsing/functions/head.js +53 -0
  47. package/dist/parsing/functions/head.js.map +1 -0
  48. package/dist/parsing/functions/id.d.ts +7 -0
  49. package/dist/parsing/functions/id.d.ts.map +1 -0
  50. package/dist/parsing/functions/id.js +58 -0
  51. package/dist/parsing/functions/id.js.map +1 -0
  52. package/dist/parsing/functions/last.d.ts +7 -0
  53. package/dist/parsing/functions/last.d.ts.map +1 -0
  54. package/dist/parsing/functions/last.js +53 -0
  55. package/dist/parsing/functions/last.js.map +1 -0
  56. package/dist/parsing/functions/localdatetime.d.ts +21 -0
  57. package/dist/parsing/functions/localdatetime.d.ts.map +1 -0
  58. package/dist/parsing/functions/localdatetime.js +71 -0
  59. package/dist/parsing/functions/localdatetime.js.map +1 -0
  60. package/dist/parsing/functions/localtime.d.ts +20 -0
  61. package/dist/parsing/functions/localtime.d.ts.map +1 -0
  62. package/dist/parsing/functions/localtime.js +67 -0
  63. package/dist/parsing/functions/localtime.js.map +1 -0
  64. package/dist/parsing/functions/max.d.ts +14 -0
  65. package/dist/parsing/functions/max.d.ts.map +1 -0
  66. package/dist/parsing/functions/max.js +51 -0
  67. package/dist/parsing/functions/max.js.map +1 -0
  68. package/dist/parsing/functions/min.d.ts +14 -0
  69. package/dist/parsing/functions/min.d.ts.map +1 -0
  70. package/dist/parsing/functions/min.js +51 -0
  71. package/dist/parsing/functions/min.js.map +1 -0
  72. package/dist/parsing/functions/nodes.d.ts +7 -0
  73. package/dist/parsing/functions/nodes.d.ts.map +1 -0
  74. package/dist/parsing/functions/nodes.js +63 -0
  75. package/dist/parsing/functions/nodes.js.map +1 -0
  76. package/dist/parsing/functions/predicate_sum.d.ts.map +1 -1
  77. package/dist/parsing/functions/predicate_sum.js +13 -10
  78. package/dist/parsing/functions/predicate_sum.js.map +1 -1
  79. package/dist/parsing/functions/properties.d.ts +7 -0
  80. package/dist/parsing/functions/properties.d.ts.map +1 -0
  81. package/dist/parsing/functions/properties.js +74 -0
  82. package/dist/parsing/functions/properties.js.map +1 -0
  83. package/dist/parsing/functions/relationships.d.ts +7 -0
  84. package/dist/parsing/functions/relationships.d.ts.map +1 -0
  85. package/dist/parsing/functions/relationships.js +61 -0
  86. package/dist/parsing/functions/relationships.js.map +1 -0
  87. package/dist/parsing/functions/schema.d.ts +5 -2
  88. package/dist/parsing/functions/schema.d.ts.map +1 -1
  89. package/dist/parsing/functions/schema.js +7 -4
  90. package/dist/parsing/functions/schema.js.map +1 -1
  91. package/dist/parsing/functions/tail.d.ts +7 -0
  92. package/dist/parsing/functions/tail.d.ts.map +1 -0
  93. package/dist/parsing/functions/tail.js +50 -0
  94. package/dist/parsing/functions/tail.js.map +1 -0
  95. package/dist/parsing/functions/temporal_utils.d.ts +39 -0
  96. package/dist/parsing/functions/temporal_utils.d.ts.map +1 -0
  97. package/dist/parsing/functions/temporal_utils.js +168 -0
  98. package/dist/parsing/functions/temporal_utils.js.map +1 -0
  99. package/dist/parsing/functions/time.d.ts +20 -0
  100. package/dist/parsing/functions/time.d.ts.map +1 -0
  101. package/dist/parsing/functions/time.js +67 -0
  102. package/dist/parsing/functions/time.js.map +1 -0
  103. package/dist/parsing/functions/timestamp.d.ts +17 -0
  104. package/dist/parsing/functions/timestamp.d.ts.map +1 -0
  105. package/dist/parsing/functions/timestamp.js +51 -0
  106. package/dist/parsing/functions/timestamp.js.map +1 -0
  107. package/dist/parsing/functions/to_float.d.ts +7 -0
  108. package/dist/parsing/functions/to_float.d.ts.map +1 -0
  109. package/dist/parsing/functions/to_float.js +61 -0
  110. package/dist/parsing/functions/to_float.js.map +1 -0
  111. package/dist/parsing/functions/to_integer.d.ts +7 -0
  112. package/dist/parsing/functions/to_integer.d.ts.map +1 -0
  113. package/dist/parsing/functions/to_integer.js +61 -0
  114. package/dist/parsing/functions/to_integer.js.map +1 -0
  115. package/dist/parsing/functions/trim.d.ts +7 -0
  116. package/dist/parsing/functions/trim.d.ts.map +1 -0
  117. package/dist/parsing/functions/trim.js +37 -0
  118. package/dist/parsing/functions/trim.js.map +1 -0
  119. package/dist/parsing/operations/group_by.d.ts.map +1 -1
  120. package/dist/parsing/operations/group_by.js +4 -2
  121. package/dist/parsing/operations/group_by.js.map +1 -1
  122. package/dist/parsing/parser.d.ts.map +1 -1
  123. package/dist/parsing/parser.js +15 -2
  124. package/dist/parsing/parser.js.map +1 -1
  125. package/docs/flowquery.min.js +1 -1
  126. package/flowquery-py/pyproject.toml +1 -1
  127. package/flowquery-py/src/graph/database.py +44 -11
  128. package/flowquery-py/src/graph/relationship.py +11 -3
  129. package/flowquery-py/src/graph/relationship_data.py +2 -1
  130. package/flowquery-py/src/graph/relationship_match_collector.py +7 -1
  131. package/flowquery-py/src/graph/relationship_reference.py +2 -2
  132. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -0
  133. package/flowquery-py/src/parsing/functions/__init__.py +42 -2
  134. package/flowquery-py/src/parsing/functions/coalesce.py +44 -0
  135. package/flowquery-py/src/parsing/functions/date_.py +63 -0
  136. package/flowquery-py/src/parsing/functions/datetime_.py +64 -0
  137. package/flowquery-py/src/parsing/functions/duration.py +159 -0
  138. package/flowquery-py/src/parsing/functions/element_id.py +50 -0
  139. package/flowquery-py/src/parsing/functions/head.py +39 -0
  140. package/flowquery-py/src/parsing/functions/id_.py +49 -0
  141. package/flowquery-py/src/parsing/functions/last.py +39 -0
  142. package/flowquery-py/src/parsing/functions/localdatetime.py +62 -0
  143. package/flowquery-py/src/parsing/functions/localtime.py +59 -0
  144. package/flowquery-py/src/parsing/functions/max_.py +49 -0
  145. package/flowquery-py/src/parsing/functions/min_.py +49 -0
  146. package/flowquery-py/src/parsing/functions/nodes.py +48 -0
  147. package/flowquery-py/src/parsing/functions/predicate_sum.py +3 -6
  148. package/flowquery-py/src/parsing/functions/properties.py +50 -0
  149. package/flowquery-py/src/parsing/functions/relationships.py +46 -0
  150. package/flowquery-py/src/parsing/functions/schema.py +9 -5
  151. package/flowquery-py/src/parsing/functions/tail.py +37 -0
  152. package/flowquery-py/src/parsing/functions/temporal_utils.py +186 -0
  153. package/flowquery-py/src/parsing/functions/time_.py +59 -0
  154. package/flowquery-py/src/parsing/functions/timestamp.py +39 -0
  155. package/flowquery-py/src/parsing/functions/to_float.py +46 -0
  156. package/flowquery-py/src/parsing/functions/to_integer.py +46 -0
  157. package/flowquery-py/src/parsing/functions/trim.py +35 -0
  158. package/flowquery-py/src/parsing/operations/group_by.py +2 -0
  159. package/flowquery-py/src/parsing/parser.py +12 -2
  160. package/flowquery-py/tests/compute/test_runner.py +1082 -4
  161. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  162. package/package.json +1 -1
  163. package/src/graph/database.ts +42 -4
  164. package/src/graph/relationship.ts +12 -4
  165. package/src/graph/relationship_data.ts +1 -1
  166. package/src/graph/relationship_match_collector.ts +6 -2
  167. package/src/graph/relationship_reference.ts +1 -1
  168. package/src/parsing/data_structures/lookup.ts +8 -4
  169. package/src/parsing/functions/coalesce.ts +50 -0
  170. package/src/parsing/functions/date.ts +65 -0
  171. package/src/parsing/functions/datetime.ts +65 -0
  172. package/src/parsing/functions/duration.ts +143 -0
  173. package/src/parsing/functions/element_id.ts +51 -0
  174. package/src/parsing/functions/function_factory.ts +21 -0
  175. package/src/parsing/functions/head.ts +42 -0
  176. package/src/parsing/functions/id.ts +51 -0
  177. package/src/parsing/functions/last.ts +42 -0
  178. package/src/parsing/functions/localdatetime.ts +65 -0
  179. package/src/parsing/functions/localtime.ts +60 -0
  180. package/src/parsing/functions/max.ts +37 -0
  181. package/src/parsing/functions/min.ts +37 -0
  182. package/src/parsing/functions/nodes.ts +54 -0
  183. package/src/parsing/functions/predicate_sum.ts +17 -12
  184. package/src/parsing/functions/properties.ts +56 -0
  185. package/src/parsing/functions/relationships.ts +52 -0
  186. package/src/parsing/functions/schema.ts +7 -4
  187. package/src/parsing/functions/tail.ts +39 -0
  188. package/src/parsing/functions/temporal_utils.ts +180 -0
  189. package/src/parsing/functions/time.ts +60 -0
  190. package/src/parsing/functions/timestamp.ts +41 -0
  191. package/src/parsing/functions/to_float.ts +50 -0
  192. package/src/parsing/functions/to_integer.ts +50 -0
  193. package/src/parsing/functions/trim.ts +25 -0
  194. package/src/parsing/operations/group_by.ts +4 -1
  195. package/src/parsing/parser.ts +15 -2
  196. package/tests/compute/runner.test.ts +1005 -3
  197. package/tests/parsing/parser.test.ts +37 -0
@@ -216,6 +216,86 @@ test("Test avg with one value", async () => {
216
216
  expect(results[0]).toEqual({ avg: 1 });
217
217
  });
218
218
 
219
+ test("Test min", async () => {
220
+ const runner = new Runner("unwind [3, 1, 4, 1, 5, 9] as n return min(n) as minimum");
221
+ await runner.run();
222
+ const results = runner.results;
223
+ expect(results.length).toBe(1);
224
+ expect(results[0]).toEqual({ minimum: 1 });
225
+ });
226
+
227
+ test("Test max", async () => {
228
+ const runner = new Runner("unwind [3, 1, 4, 1, 5, 9] as n return max(n) as maximum");
229
+ await runner.run();
230
+ const results = runner.results;
231
+ expect(results.length).toBe(1);
232
+ expect(results[0]).toEqual({ maximum: 9 });
233
+ });
234
+
235
+ test("Test min with grouped values", async () => {
236
+ const runner = new Runner(
237
+ "unwind [1, 1, 2, 2] as i unwind [10, 20, 30, 40] as j return i, min(j) as minimum"
238
+ );
239
+ await runner.run();
240
+ const results = runner.results;
241
+ expect(results.length).toBe(2);
242
+ expect(results[0]).toEqual({ i: 1, minimum: 10 });
243
+ expect(results[1]).toEqual({ i: 2, minimum: 10 });
244
+ });
245
+
246
+ test("Test max with grouped values", async () => {
247
+ const runner = new Runner(
248
+ "unwind [1, 1, 2, 2] as i unwind [10, 20, 30, 40] as j return i, max(j) as maximum"
249
+ );
250
+ await runner.run();
251
+ const results = runner.results;
252
+ expect(results.length).toBe(2);
253
+ expect(results[0]).toEqual({ i: 1, maximum: 40 });
254
+ expect(results[1]).toEqual({ i: 2, maximum: 40 });
255
+ });
256
+
257
+ test("Test min with null", async () => {
258
+ const runner = new Runner("return min(null) as minimum");
259
+ await runner.run();
260
+ const results = runner.results;
261
+ expect(results.length).toBe(1);
262
+ expect(results[0]).toEqual({ minimum: null });
263
+ });
264
+
265
+ test("Test max with null", async () => {
266
+ const runner = new Runner("return max(null) as maximum");
267
+ await runner.run();
268
+ const results = runner.results;
269
+ expect(results.length).toBe(1);
270
+ expect(results[0]).toEqual({ maximum: null });
271
+ });
272
+
273
+ test("Test min with strings", async () => {
274
+ const runner = new Runner('unwind ["cherry", "apple", "banana"] as s return min(s) as minimum');
275
+ await runner.run();
276
+ const results = runner.results;
277
+ expect(results.length).toBe(1);
278
+ expect(results[0]).toEqual({ minimum: "apple" });
279
+ });
280
+
281
+ test("Test max with strings", async () => {
282
+ const runner = new Runner('unwind ["cherry", "apple", "banana"] as s return max(s) as maximum');
283
+ await runner.run();
284
+ const results = runner.results;
285
+ expect(results.length).toBe(1);
286
+ expect(results[0]).toEqual({ maximum: "cherry" });
287
+ });
288
+
289
+ test("Test min and max together", async () => {
290
+ const runner = new Runner(
291
+ "unwind [3, 1, 4, 1, 5, 9] as n return min(n) as minimum, max(n) as maximum"
292
+ );
293
+ await runner.run();
294
+ const results = runner.results;
295
+ expect(results.length).toBe(1);
296
+ expect(results[0]).toEqual({ minimum: 1, maximum: 9 });
297
+ });
298
+
219
299
  test("Test with and return", async () => {
220
300
  const runner = new Runner("with 1 as a return a");
221
301
  await runner.run();
@@ -613,6 +693,38 @@ test("Test toLower function with all uppercase", async () => {
613
693
  expect(results[0]).toEqual({ result: "foo bar" });
614
694
  });
615
695
 
696
+ test("Test trim function", async () => {
697
+ const runner = new Runner('RETURN trim(" hello ") as result');
698
+ await runner.run();
699
+ const results = runner.results;
700
+ expect(results.length).toBe(1);
701
+ expect(results[0]).toEqual({ result: "hello" });
702
+ });
703
+
704
+ test("Test trim function with tabs and newlines", async () => {
705
+ const runner = new Runner('WITH "\tfoo\n" AS s RETURN trim(s) as result');
706
+ await runner.run();
707
+ const results = runner.results;
708
+ expect(results.length).toBe(1);
709
+ expect(results[0]).toEqual({ result: "foo" });
710
+ });
711
+
712
+ test("Test trim function with no whitespace", async () => {
713
+ const runner = new Runner('RETURN trim("hello") as result');
714
+ await runner.run();
715
+ const results = runner.results;
716
+ expect(results.length).toBe(1);
717
+ expect(results[0]).toEqual({ result: "hello" });
718
+ });
719
+
720
+ test("Test trim function with empty string", async () => {
721
+ const runner = new Runner('RETURN trim("") as result');
722
+ await runner.run();
723
+ const results = runner.results;
724
+ expect(results.length).toBe(1);
725
+ expect(results[0]).toEqual({ result: "" });
726
+ });
727
+
616
728
  test("Test associative array with key which is keyword", async () => {
617
729
  const runner = new Runner("RETURN {return: 1} as aa");
618
730
  await runner.run();
@@ -790,6 +902,121 @@ test("Test keys function", async () => {
790
902
  expect(results[0]).toEqual({ keys: ["name", "age"] });
791
903
  });
792
904
 
905
+ test("Test properties function with map", async () => {
906
+ const runner = new Runner('RETURN properties({name: "Alice", age: 30}) as props');
907
+ await runner.run();
908
+ const results = runner.results;
909
+ expect(results.length).toBe(1);
910
+ expect(results[0]).toEqual({ props: { name: "Alice", age: 30 } });
911
+ });
912
+
913
+ test("Test properties function with node", async () => {
914
+ await new Runner(`
915
+ CREATE VIRTUAL (:Animal) AS {
916
+ UNWIND [
917
+ {id: 1, name: 'Dog', legs: 4},
918
+ {id: 2, name: 'Cat', legs: 4}
919
+ ] AS record
920
+ RETURN record.id AS id, record.name AS name, record.legs AS legs
921
+ }
922
+ `).run();
923
+ const match = new Runner(`
924
+ MATCH (a:Animal)
925
+ RETURN properties(a) AS props
926
+ `);
927
+ await match.run();
928
+ const results = match.results;
929
+ expect(results.length).toBe(2);
930
+ expect(results[0]).toEqual({ props: { name: "Dog", legs: 4 } });
931
+ expect(results[1]).toEqual({ props: { name: "Cat", legs: 4 } });
932
+ });
933
+
934
+ test("Test properties function with null", async () => {
935
+ const runner = new Runner("RETURN properties(null) as props");
936
+ await runner.run();
937
+ const results = runner.results;
938
+ expect(results.length).toBe(1);
939
+ expect(results[0]).toEqual({ props: null });
940
+ });
941
+
942
+ test("Test nodes function", async () => {
943
+ await new Runner(`
944
+ CREATE VIRTUAL (:City) AS {
945
+ UNWIND [
946
+ {id: 1, name: 'New York'},
947
+ {id: 2, name: 'Boston'}
948
+ ] AS record
949
+ RETURN record.id AS id, record.name AS name
950
+ }
951
+ `).run();
952
+ await new Runner(`
953
+ CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
954
+ UNWIND [
955
+ {left_id: 1, right_id: 2}
956
+ ] AS record
957
+ RETURN record.left_id AS left_id, record.right_id AS right_id
958
+ }
959
+ `).run();
960
+ const match = new Runner(`
961
+ MATCH p=(:City)-[:CONNECTED_TO]-(:City)
962
+ RETURN nodes(p) AS cities
963
+ `);
964
+ await match.run();
965
+ const results = match.results;
966
+ expect(results.length).toBe(1);
967
+ expect(results[0].cities.length).toBe(2);
968
+ expect(results[0].cities[0].id).toBe(1);
969
+ expect(results[0].cities[0].name).toBe("New York");
970
+ expect(results[0].cities[1].id).toBe(2);
971
+ expect(results[0].cities[1].name).toBe("Boston");
972
+ });
973
+
974
+ test("Test relationships function", async () => {
975
+ await new Runner(`
976
+ CREATE VIRTUAL (:City) AS {
977
+ UNWIND [
978
+ {id: 1, name: 'New York'},
979
+ {id: 2, name: 'Boston'}
980
+ ] AS record
981
+ RETURN record.id AS id, record.name AS name
982
+ }
983
+ `).run();
984
+ await new Runner(`
985
+ CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
986
+ UNWIND [
987
+ {left_id: 1, right_id: 2, distance: 190}
988
+ ] AS record
989
+ RETURN record.left_id AS left_id, record.right_id AS right_id, record.distance AS distance
990
+ }
991
+ `).run();
992
+ const match = new Runner(`
993
+ MATCH p=(:City)-[:CONNECTED_TO]-(:City)
994
+ RETURN relationships(p) AS rels
995
+ `);
996
+ await match.run();
997
+ const results = match.results;
998
+ expect(results.length).toBe(1);
999
+ expect(results[0].rels.length).toBe(1);
1000
+ expect(results[0].rels[0].type).toBe("CONNECTED_TO");
1001
+ expect(results[0].rels[0].properties.distance).toBe(190);
1002
+ });
1003
+
1004
+ test("Test nodes function with null", async () => {
1005
+ const runner = new Runner("RETURN nodes(null) as n");
1006
+ await runner.run();
1007
+ const results = runner.results;
1008
+ expect(results.length).toBe(1);
1009
+ expect(results[0]).toEqual({ n: [] });
1010
+ });
1011
+
1012
+ test("Test relationships function with null", async () => {
1013
+ const runner = new Runner("RETURN relationships(null) as r");
1014
+ await runner.run();
1015
+ const results = runner.results;
1016
+ expect(results.length).toBe(1);
1017
+ expect(results[0]).toEqual({ r: [] });
1018
+ });
1019
+
793
1020
  test("Test type function", async () => {
794
1021
  const runner = new Runner(`
795
1022
  RETURN type(123) as type1,
@@ -1793,6 +2020,39 @@ test("Test optional match with no matching relationship", async () => {
1793
2020
  expect(results[2].friend).toBeNull();
1794
2021
  });
1795
2022
 
2023
+ test("Test optional match property access on null node returns null", async () => {
2024
+ await new Runner(`
2025
+ CREATE VIRTUAL (:Person) AS {
2026
+ unwind [
2027
+ {id: 1, name: 'Person 1'},
2028
+ {id: 2, name: 'Person 2'},
2029
+ {id: 3, name: 'Person 3'}
2030
+ ] as record
2031
+ RETURN record.id as id, record.name as name
2032
+ }
2033
+ `).run();
2034
+ await new Runner(`
2035
+ CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
2036
+ unwind [
2037
+ {left_id: 1, right_id: 2}
2038
+ ] as record
2039
+ RETURN record.left_id as left_id, record.right_id as right_id
2040
+ }
2041
+ `).run();
2042
+ // When accessing b.name and b is null (no match), should return null like Neo4j
2043
+ const match = new Runner(`
2044
+ MATCH (a:Person)
2045
+ OPTIONAL MATCH (a)-[:KNOWS]->(b:Person)
2046
+ RETURN a.name AS name, b.name AS friend_name
2047
+ `);
2048
+ await match.run();
2049
+ const results = match.results;
2050
+ expect(results.length).toBe(3);
2051
+ expect(results[0]).toEqual({ name: "Person 1", friend_name: "Person 2" });
2052
+ expect(results[1]).toEqual({ name: "Person 2", friend_name: null });
2053
+ expect(results[2]).toEqual({ name: "Person 3", friend_name: null });
2054
+ });
2055
+
1796
2056
  test("Test optional match where all nodes match", async () => {
1797
2057
  await new Runner(`
1798
2058
  CREATE VIRTUAL (:Person) AS {
@@ -1964,20 +2224,24 @@ test("Test schema() returns nodes and relationships with sample data", async ()
1964
2224
  `).run();
1965
2225
 
1966
2226
  const runner = new Runner(
1967
- "CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
2227
+ "CALL schema() YIELD kind, label, type, from_label, to_label, properties, sample RETURN kind, label, type, from_label, to_label, properties, sample"
1968
2228
  );
1969
2229
  await runner.run();
1970
2230
  const results = runner.results;
1971
2231
 
1972
- const animal = results.find((r: any) => r.kind === "node" && r.label === "Animal");
2232
+ const animal = results.find((r: any) => r.kind === "Node" && r.label === "Animal");
1973
2233
  expect(animal).toBeDefined();
2234
+ expect(animal.properties).toEqual(["species", "legs"]);
1974
2235
  expect(animal.sample).toBeDefined();
1975
2236
  expect(animal.sample).not.toHaveProperty("id");
1976
2237
  expect(animal.sample).toHaveProperty("species");
1977
2238
  expect(animal.sample).toHaveProperty("legs");
1978
2239
 
1979
- const chases = results.find((r: any) => r.kind === "relationship" && r.type === "CHASES");
2240
+ const chases = results.find((r: any) => r.kind === "Relationship" && r.type === "CHASES");
1980
2241
  expect(chases).toBeDefined();
2242
+ expect(chases.from_label).toBe("Animal");
2243
+ expect(chases.to_label).toBe("Animal");
2244
+ expect(chases.properties).toEqual(["speed"]);
1981
2245
  expect(chases.sample).toBeDefined();
1982
2246
  expect(chases.sample).not.toHaveProperty("left_id");
1983
2247
  expect(chases.sample).not.toHaveProperty("right_id");
@@ -2690,3 +2954,741 @@ test("Test UNION with empty right side", async () => {
2690
2954
  expect(results.length).toBe(1);
2691
2955
  expect(results).toEqual([{ x: 1 }]);
2692
2956
  });
2957
+
2958
+ test("Test language name hits query with virtual graph", async () => {
2959
+ // Create Language nodes
2960
+ await new Runner(`
2961
+ CREATE VIRTUAL (:Language) AS {
2962
+ UNWIND [
2963
+ {id: 1, name: 'Python'},
2964
+ {id: 2, name: 'JavaScript'},
2965
+ {id: 3, name: 'TypeScript'}
2966
+ ] AS record
2967
+ RETURN record.id AS id, record.name AS name
2968
+ }
2969
+ `).run();
2970
+
2971
+ // Create Chat nodes with messages
2972
+ await new Runner(`
2973
+ CREATE VIRTUAL (:Chat) AS {
2974
+ UNWIND [
2975
+ {id: 1, name: 'Dev Discussion', messages: [
2976
+ {From: 'Alice', SentDateTime: '2025-01-01T10:00:00', Content: 'I love Python and JavaScript'},
2977
+ {From: 'Bob', SentDateTime: '2025-01-01T10:05:00', Content: 'What languages do you prefer?'}
2978
+ ]},
2979
+ {id: 2, name: 'General', messages: [
2980
+ {From: 'Charlie', SentDateTime: '2025-01-02T09:00:00', Content: 'The weather is nice today'},
2981
+ {From: 'Alice', SentDateTime: '2025-01-02T09:05:00', Content: 'TypeScript is great for language tooling'}
2982
+ ]}
2983
+ ] AS record
2984
+ RETURN record.id AS id, record.name AS name, record.messages AS messages
2985
+ }
2986
+ `).run();
2987
+
2988
+ // Create User nodes
2989
+ await new Runner(`
2990
+ CREATE VIRTUAL (:User) AS {
2991
+ UNWIND [
2992
+ {id: 1, displayName: 'Alice'},
2993
+ {id: 2, displayName: 'Bob'},
2994
+ {id: 3, displayName: 'Charlie'}
2995
+ ] AS record
2996
+ RETURN record.id AS id, record.displayName AS displayName
2997
+ }
2998
+ `).run();
2999
+
3000
+ // Create PARTICIPATES_IN relationships
3001
+ await new Runner(`
3002
+ CREATE VIRTUAL (:User)-[:PARTICIPATES_IN]-(:Chat) AS {
3003
+ UNWIND [
3004
+ {left_id: 1, right_id: 1},
3005
+ {left_id: 2, right_id: 1},
3006
+ {left_id: 3, right_id: 2},
3007
+ {left_id: 1, right_id: 2}
3008
+ ] AS record
3009
+ RETURN record.left_id AS left_id, record.right_id AS right_id
3010
+ }
3011
+ `).run();
3012
+
3013
+ // Run the original query (using 'sender' alias since 'from' is a reserved keyword)
3014
+ const runner = new Runner(`
3015
+ MATCH (l:Language)
3016
+ WITH collect(distinct l.name) AS langs
3017
+ MATCH (c:Chat)
3018
+ UNWIND c.messages AS msg
3019
+ WITH c, msg, langs,
3020
+ sum(lang IN langs | 1 where toLower(msg.Content) CONTAINS toLower(lang)) AS langNameHits
3021
+ WHERE toLower(msg.Content) CONTAINS "language"
3022
+ OR toLower(msg.Content) CONTAINS "languages"
3023
+ OR langNameHits > 0
3024
+ OPTIONAL MATCH (u:User)-[:PARTICIPATES_IN]->(c)
3025
+ RETURN
3026
+ c.name AS chat,
3027
+ collect(distinct u.displayName) AS participants,
3028
+ msg.From AS sender,
3029
+ msg.SentDateTime AS sentDateTime,
3030
+ msg.Content AS message
3031
+ `);
3032
+ await runner.run();
3033
+ const results = runner.results;
3034
+
3035
+ // Messages that mention a language name or the word "language(s)":
3036
+ // 1. "I love Python and JavaScript" - langNameHits=2 (matches Python and JavaScript)
3037
+ // 2. "What languages do you prefer?" - contains "languages"
3038
+ // 3. "TypeScript is great for language tooling" - langNameHits=1, also contains "language"
3039
+ expect(results.length).toBe(3);
3040
+ expect(results[0].chat).toBe("Dev Discussion");
3041
+ expect(results[0].message).toBe("I love Python and JavaScript");
3042
+ expect(results[0].sender).toBe("Alice");
3043
+ expect(results[1].chat).toBe("Dev Discussion");
3044
+ expect(results[1].message).toBe("What languages do you prefer?");
3045
+ expect(results[1].sender).toBe("Bob");
3046
+ expect(results[2].chat).toBe("General");
3047
+ expect(results[2].message).toBe("TypeScript is great for language tooling");
3048
+ expect(results[2].sender).toBe("Alice");
3049
+ });
3050
+
3051
+ test("Test sum with empty collected array", async () => {
3052
+ // Reproduces the original bug: collect on empty input should yield []
3053
+ // and sum over that empty array should return 0, not throw
3054
+ const runner = new Runner(`
3055
+ UNWIND [] AS lang
3056
+ WITH collect(distinct lang) AS langs
3057
+ UNWIND ['hello', 'world'] AS msg
3058
+ WITH msg, langs, sum(l IN langs | 1 where toLower(msg) CONTAINS toLower(l)) AS hits
3059
+ RETURN msg, hits
3060
+ `);
3061
+ await runner.run();
3062
+ const results = runner.results;
3063
+ expect(results.length).toBe(2);
3064
+ expect(results[0]).toEqual({ msg: "hello", hits: 0 });
3065
+ expect(results[1]).toEqual({ msg: "world", hits: 0 });
3066
+ });
3067
+
3068
+ test("Test sum where all elements filtered returns 0", async () => {
3069
+ const runner = new Runner("RETURN sum(n in [1, 2, 3] | n where n > 100) as sum");
3070
+ await runner.run();
3071
+ const results = runner.results;
3072
+ expect(results.length).toBe(1);
3073
+ expect(results[0]).toEqual({ sum: 0 });
3074
+ });
3075
+
3076
+ test("Test sum over empty array returns 0", async () => {
3077
+ const runner = new Runner("WITH [] AS arr RETURN sum(n in arr | n) as sum");
3078
+ await runner.run();
3079
+ const results = runner.results;
3080
+ expect(results.length).toBe(1);
3081
+ expect(results[0]).toEqual({ sum: 0 });
3082
+ });
3083
+
3084
+ test("Test match with ORed relationship types", async () => {
3085
+ await new Runner(`
3086
+ CREATE VIRTUAL (:Person) AS {
3087
+ unwind [
3088
+ {id: 1, name: 'Alice'},
3089
+ {id: 2, name: 'Bob'},
3090
+ {id: 3, name: 'Charlie'}
3091
+ ] as record
3092
+ RETURN record.id as id, record.name as name
3093
+ }
3094
+ `).run();
3095
+ await new Runner(`
3096
+ CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
3097
+ unwind [
3098
+ {left_id: 1, right_id: 2}
3099
+ ] as record
3100
+ RETURN record.left_id as left_id, record.right_id as right_id
3101
+ }
3102
+ `).run();
3103
+ await new Runner(`
3104
+ CREATE VIRTUAL (:Person)-[:FOLLOWS]-(:Person) AS {
3105
+ unwind [
3106
+ {left_id: 2, right_id: 3}
3107
+ ] as record
3108
+ RETURN record.left_id as left_id, record.right_id as right_id
3109
+ }
3110
+ `).run();
3111
+ const match = new Runner(`
3112
+ MATCH (a:Person)-[:KNOWS|FOLLOWS]->(b:Person)
3113
+ RETURN a.name AS name1, b.name AS name2
3114
+ `);
3115
+ await match.run();
3116
+ const results = match.results;
3117
+ expect(results.length).toBe(2);
3118
+ expect(results[0]).toEqual({ name1: "Alice", name2: "Bob" });
3119
+ expect(results[1]).toEqual({ name1: "Bob", name2: "Charlie" });
3120
+ });
3121
+
3122
+ test("Test match with ORed relationship types with optional colon syntax", async () => {
3123
+ await new Runner(`
3124
+ CREATE VIRTUAL (:Animal) AS {
3125
+ unwind [
3126
+ {id: 1, name: 'Cat'},
3127
+ {id: 2, name: 'Dog'},
3128
+ {id: 3, name: 'Fish'}
3129
+ ] as record
3130
+ RETURN record.id as id, record.name as name
3131
+ }
3132
+ `).run();
3133
+ await new Runner(`
3134
+ CREATE VIRTUAL (:Animal)-[:CHASES]-(:Animal) AS {
3135
+ unwind [
3136
+ {left_id: 1, right_id: 2}
3137
+ ] as record
3138
+ RETURN record.left_id as left_id, record.right_id as right_id
3139
+ }
3140
+ `).run();
3141
+ await new Runner(`
3142
+ CREATE VIRTUAL (:Animal)-[:EATS]-(:Animal) AS {
3143
+ unwind [
3144
+ {left_id: 1, right_id: 3}
3145
+ ] as record
3146
+ RETURN record.left_id as left_id, record.right_id as right_id
3147
+ }
3148
+ `).run();
3149
+ const match = new Runner(`
3150
+ MATCH (a:Animal)-[:CHASES|:EATS]->(b:Animal)
3151
+ RETURN a.name AS name1, b.name AS name2
3152
+ `);
3153
+ await match.run();
3154
+ const results = match.results;
3155
+ expect(results.length).toBe(2);
3156
+ expect(results[0]).toEqual({ name1: "Cat", name2: "Dog" });
3157
+ expect(results[1]).toEqual({ name1: "Cat", name2: "Fish" });
3158
+ });
3159
+
3160
+ test("Test match with ORed relationship types returns correct type in relationship variable", async () => {
3161
+ await new Runner(`
3162
+ CREATE VIRTUAL (:City) AS {
3163
+ unwind [
3164
+ {id: 1, name: 'NYC'},
3165
+ {id: 2, name: 'LA'},
3166
+ {id: 3, name: 'Chicago'}
3167
+ ] as record
3168
+ RETURN record.id as id, record.name as name
3169
+ }
3170
+ `).run();
3171
+ await new Runner(`
3172
+ CREATE VIRTUAL (:City)-[:FLIGHT]-(:City) AS {
3173
+ unwind [
3174
+ {left_id: 1, right_id: 2, airline: 'Delta'}
3175
+ ] as record
3176
+ RETURN record.left_id as left_id, record.right_id as right_id, record.airline as airline
3177
+ }
3178
+ `).run();
3179
+ await new Runner(`
3180
+ CREATE VIRTUAL (:City)-[:TRAIN]-(:City) AS {
3181
+ unwind [
3182
+ {left_id: 1, right_id: 3, line: 'Amtrak'}
3183
+ ] as record
3184
+ RETURN record.left_id as left_id, record.right_id as right_id, record.line as line
3185
+ }
3186
+ `).run();
3187
+ const match = new Runner(`
3188
+ MATCH (a:City)-[r:FLIGHT|TRAIN]->(b:City)
3189
+ RETURN a.name AS from, b.name AS to, r.type AS type
3190
+ `);
3191
+ await match.run();
3192
+ const results = match.results;
3193
+ expect(results.length).toBe(2);
3194
+ expect(results[0]).toEqual({ from: "NYC", to: "LA", type: "FLIGHT" });
3195
+ expect(results[1]).toEqual({ from: "NYC", to: "Chicago", type: "TRAIN" });
3196
+ });
3197
+
3198
+ test("Test coalesce returns first non-null value", async () => {
3199
+ const runner = new Runner("RETURN coalesce(null, null, 'hello', 'world') as result");
3200
+ await runner.run();
3201
+ const results = runner.results;
3202
+ expect(results.length).toBe(1);
3203
+ expect(results[0]).toEqual({ result: "hello" });
3204
+ });
3205
+
3206
+ test("Test coalesce returns first argument when not null", async () => {
3207
+ const runner = new Runner("RETURN coalesce('first', 'second') as result");
3208
+ await runner.run();
3209
+ const results = runner.results;
3210
+ expect(results.length).toBe(1);
3211
+ expect(results[0]).toEqual({ result: "first" });
3212
+ });
3213
+
3214
+ test("Test coalesce returns null when all arguments are null", async () => {
3215
+ const runner = new Runner("RETURN coalesce(null, null, null) as result");
3216
+ await runner.run();
3217
+ const results = runner.results;
3218
+ expect(results.length).toBe(1);
3219
+ expect(results[0]).toEqual({ result: null });
3220
+ });
3221
+
3222
+ test("Test coalesce with single non-null argument", async () => {
3223
+ const runner = new Runner("RETURN coalesce(42) as result");
3224
+ await runner.run();
3225
+ const results = runner.results;
3226
+ expect(results.length).toBe(1);
3227
+ expect(results[0]).toEqual({ result: 42 });
3228
+ });
3229
+
3230
+ test("Test coalesce with mixed types", async () => {
3231
+ const runner = new Runner("RETURN coalesce(null, 42, 'hello') as result");
3232
+ await runner.run();
3233
+ const results = runner.results;
3234
+ expect(results.length).toBe(1);
3235
+ expect(results[0]).toEqual({ result: 42 });
3236
+ });
3237
+
3238
+ test("Test coalesce with property access", async () => {
3239
+ const runner = new Runner(
3240
+ "WITH {name: 'Alice'} AS person RETURN coalesce(person.nickname, person.name) as result"
3241
+ );
3242
+ await runner.run();
3243
+ const results = runner.results;
3244
+ expect(results.length).toBe(1);
3245
+ expect(results[0]).toEqual({ result: "Alice" });
3246
+ });
3247
+
3248
+ // ============================================================
3249
+ // Temporal / Time Functions (Neo4j-style)
3250
+ // ============================================================
3251
+
3252
+ test("Test datetime() returns current datetime object", async () => {
3253
+ const before = Date.now();
3254
+ const runner = new Runner("RETURN datetime() AS dt");
3255
+ await runner.run();
3256
+ const after = Date.now();
3257
+ const results = runner.results;
3258
+ expect(results.length).toBe(1);
3259
+ const dt = results[0].dt;
3260
+ expect(dt).toBeDefined();
3261
+ expect(typeof dt.year).toBe("number");
3262
+ expect(typeof dt.month).toBe("number");
3263
+ expect(typeof dt.day).toBe("number");
3264
+ expect(typeof dt.hour).toBe("number");
3265
+ expect(typeof dt.minute).toBe("number");
3266
+ expect(typeof dt.second).toBe("number");
3267
+ expect(typeof dt.millisecond).toBe("number");
3268
+ expect(typeof dt.epochMillis).toBe("number");
3269
+ expect(typeof dt.epochSeconds).toBe("number");
3270
+ expect(typeof dt.dayOfWeek).toBe("number");
3271
+ expect(typeof dt.dayOfYear).toBe("number");
3272
+ expect(typeof dt.quarter).toBe("number");
3273
+ expect(typeof dt.formatted).toBe("string");
3274
+ // epochMillis should be between before and after
3275
+ expect(dt.epochMillis).toBeGreaterThanOrEqual(before);
3276
+ expect(dt.epochMillis).toBeLessThanOrEqual(after);
3277
+ });
3278
+
3279
+ test("Test datetime() with ISO string argument", async () => {
3280
+ const runner = new Runner("RETURN datetime('2025-06-15T12:30:45.123Z') AS dt");
3281
+ await runner.run();
3282
+ const results = runner.results;
3283
+ expect(results.length).toBe(1);
3284
+ const dt = results[0].dt;
3285
+ expect(dt.year).toBe(2025);
3286
+ expect(dt.month).toBe(6);
3287
+ expect(dt.day).toBe(15);
3288
+ expect(dt.hour).toBe(12);
3289
+ expect(dt.minute).toBe(30);
3290
+ expect(dt.second).toBe(45);
3291
+ expect(dt.millisecond).toBe(123);
3292
+ expect(dt.formatted).toBe("2025-06-15T12:30:45.123Z");
3293
+ });
3294
+
3295
+ test("Test datetime() property access", async () => {
3296
+ const runner = new Runner(
3297
+ "WITH datetime('2025-06-15T12:30:45.123Z') AS dt RETURN dt.year AS year, dt.month AS month, dt.day AS day"
3298
+ );
3299
+ await runner.run();
3300
+ const results = runner.results;
3301
+ expect(results.length).toBe(1);
3302
+ expect(results[0]).toEqual({ year: 2025, month: 6, day: 15 });
3303
+ });
3304
+
3305
+ test("Test date() returns current date object", async () => {
3306
+ const runner = new Runner("RETURN date() AS d");
3307
+ await runner.run();
3308
+ const results = runner.results;
3309
+ expect(results.length).toBe(1);
3310
+ const d = results[0].d;
3311
+ expect(d).toBeDefined();
3312
+ expect(typeof d.year).toBe("number");
3313
+ expect(typeof d.month).toBe("number");
3314
+ expect(typeof d.day).toBe("number");
3315
+ expect(typeof d.epochMillis).toBe("number");
3316
+ expect(typeof d.dayOfWeek).toBe("number");
3317
+ expect(typeof d.dayOfYear).toBe("number");
3318
+ expect(typeof d.quarter).toBe("number");
3319
+ expect(typeof d.formatted).toBe("string");
3320
+ // Should not have time fields
3321
+ expect(d.hour).toBeUndefined();
3322
+ expect(d.minute).toBeUndefined();
3323
+ });
3324
+
3325
+ test("Test date() with ISO date string", async () => {
3326
+ const runner = new Runner("RETURN date('2025-06-15') AS d");
3327
+ await runner.run();
3328
+ const results = runner.results;
3329
+ expect(results.length).toBe(1);
3330
+ const d = results[0].d;
3331
+ expect(d.year).toBe(2025);
3332
+ expect(d.month).toBe(6);
3333
+ expect(d.day).toBe(15);
3334
+ expect(d.formatted).toBe("2025-06-15");
3335
+ });
3336
+
3337
+ test("Test date() dayOfWeek and quarter", async () => {
3338
+ // 2025-06-15 is a Sunday
3339
+ const runner = new Runner("RETURN date('2025-06-15') AS d");
3340
+ await runner.run();
3341
+ const d = runner.results[0].d;
3342
+ expect(d.dayOfWeek).toBe(7); // Sunday = 7 in ISO
3343
+ expect(d.quarter).toBe(2); // June = Q2
3344
+ });
3345
+
3346
+ test("Test time() returns current UTC time", async () => {
3347
+ const runner = new Runner("RETURN time() AS t");
3348
+ await runner.run();
3349
+ const results = runner.results;
3350
+ expect(results.length).toBe(1);
3351
+ const t = results[0].t;
3352
+ expect(typeof t.hour).toBe("number");
3353
+ expect(typeof t.minute).toBe("number");
3354
+ expect(typeof t.second).toBe("number");
3355
+ expect(typeof t.millisecond).toBe("number");
3356
+ expect(typeof t.formatted).toBe("string");
3357
+ expect(t.formatted).toMatch(/Z$/); // UTC time ends in Z
3358
+ });
3359
+
3360
+ test("Test localtime() returns current local time", async () => {
3361
+ const runner = new Runner("RETURN localtime() AS t");
3362
+ await runner.run();
3363
+ const results = runner.results;
3364
+ expect(results.length).toBe(1);
3365
+ const t = results[0].t;
3366
+ expect(typeof t.hour).toBe("number");
3367
+ expect(typeof t.minute).toBe("number");
3368
+ expect(typeof t.second).toBe("number");
3369
+ expect(typeof t.millisecond).toBe("number");
3370
+ expect(typeof t.formatted).toBe("string");
3371
+ expect(t.formatted).not.toMatch(/Z$/); // Local time does not end in Z
3372
+ });
3373
+
3374
+ test("Test localdatetime() returns current local datetime", async () => {
3375
+ const runner = new Runner("RETURN localdatetime() AS dt");
3376
+ await runner.run();
3377
+ const results = runner.results;
3378
+ expect(results.length).toBe(1);
3379
+ const dt = results[0].dt;
3380
+ expect(typeof dt.year).toBe("number");
3381
+ expect(typeof dt.month).toBe("number");
3382
+ expect(typeof dt.day).toBe("number");
3383
+ expect(typeof dt.hour).toBe("number");
3384
+ expect(typeof dt.minute).toBe("number");
3385
+ expect(typeof dt.second).toBe("number");
3386
+ expect(typeof dt.millisecond).toBe("number");
3387
+ expect(typeof dt.epochMillis).toBe("number");
3388
+ expect(typeof dt.formatted).toBe("string");
3389
+ expect(dt.formatted).not.toMatch(/Z$/); // Local datetime does not end in Z
3390
+ });
3391
+
3392
+ test("Test localdatetime() with string argument", async () => {
3393
+ const runner = new Runner("RETURN localdatetime('2025-01-20T08:15:30.500Z') AS dt");
3394
+ await runner.run();
3395
+ const dt = runner.results[0].dt;
3396
+ expect(typeof dt.year).toBe("number");
3397
+ expect(typeof dt.hour).toBe("number");
3398
+ expect(dt.epochMillis).toBeDefined();
3399
+ });
3400
+
3401
+ test("Test timestamp() returns epoch millis", async () => {
3402
+ const before = Date.now();
3403
+ const runner = new Runner("RETURN timestamp() AS ts");
3404
+ await runner.run();
3405
+ const after = Date.now();
3406
+ const results = runner.results;
3407
+ expect(results.length).toBe(1);
3408
+ const ts = results[0].ts;
3409
+ expect(typeof ts).toBe("number");
3410
+ expect(ts).toBeGreaterThanOrEqual(before);
3411
+ expect(ts).toBeLessThanOrEqual(after);
3412
+ });
3413
+
3414
+ test("Test datetime() epochMillis matches timestamp()", async () => {
3415
+ const runner = new Runner(
3416
+ "WITH datetime() AS dt, timestamp() AS ts RETURN dt.epochMillis AS dtMillis, ts AS tsMillis"
3417
+ );
3418
+ await runner.run();
3419
+ const results = runner.results;
3420
+ expect(results.length).toBe(1);
3421
+ // They should be very close (within a few ms)
3422
+ expect(Math.abs(results[0].dtMillis - results[0].tsMillis)).toBeLessThan(100);
3423
+ });
3424
+
3425
+ test("Test date() with property access in WHERE", async () => {
3426
+ const runner = new Runner(
3427
+ "UNWIND [1, 2, 3] AS x WITH x, date('2025-06-15') AS d WHERE d.quarter = 2 RETURN x"
3428
+ );
3429
+ await runner.run();
3430
+ const results = runner.results;
3431
+ expect(results.length).toBe(3); // All 3 pass through since Q2 = 2
3432
+ });
3433
+
3434
+ test("Test datetime() with map argument", async () => {
3435
+ const runner = new Runner(
3436
+ "RETURN datetime({year: 2024, month: 12, day: 25, hour: 10, minute: 30}) AS dt"
3437
+ );
3438
+ await runner.run();
3439
+ const dt = runner.results[0].dt;
3440
+ expect(dt.year).toBe(2024);
3441
+ expect(dt.month).toBe(12);
3442
+ expect(dt.day).toBe(25);
3443
+ expect(dt.quarter).toBe(4); // December = Q4
3444
+ });
3445
+
3446
+ test("Test date() with map argument", async () => {
3447
+ const runner = new Runner("RETURN date({year: 2025, month: 3, day: 1}) AS d");
3448
+ await runner.run();
3449
+ const d = runner.results[0].d;
3450
+ expect(d.year).toBe(2025);
3451
+ expect(d.month).toBe(3);
3452
+ expect(d.day).toBe(1);
3453
+ expect(d.quarter).toBe(1); // March = Q1
3454
+ });
3455
+
3456
+ test("Test id() function with node", async () => {
3457
+ await new Runner(`
3458
+ CREATE VIRTUAL (:Person) AS {
3459
+ UNWIND [
3460
+ {id: 1, name: 'Alice'},
3461
+ {id: 2, name: 'Bob'}
3462
+ ] AS record
3463
+ RETURN record.id AS id, record.name AS name
3464
+ }
3465
+ `).run();
3466
+ const match = new Runner(`
3467
+ MATCH (n:Person)
3468
+ RETURN id(n) AS nodeId
3469
+ `);
3470
+ await match.run();
3471
+ const results = match.results;
3472
+ expect(results.length).toBe(2);
3473
+ expect(results[0]).toEqual({ nodeId: 1 });
3474
+ expect(results[1]).toEqual({ nodeId: 2 });
3475
+ });
3476
+
3477
+ test("Test id() function with null", async () => {
3478
+ const runner = new Runner("RETURN id(null) AS nodeId");
3479
+ await runner.run();
3480
+ const results = runner.results;
3481
+ expect(results.length).toBe(1);
3482
+ expect(results[0]).toEqual({ nodeId: null });
3483
+ });
3484
+
3485
+ test("Test id() function with relationship", async () => {
3486
+ await new Runner(`
3487
+ CREATE VIRTUAL (:City) AS {
3488
+ UNWIND [
3489
+ {id: 1, name: 'New York'},
3490
+ {id: 2, name: 'Boston'}
3491
+ ] AS record
3492
+ RETURN record.id AS id, record.name AS name
3493
+ }
3494
+ `).run();
3495
+ await new Runner(`
3496
+ CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
3497
+ UNWIND [
3498
+ {left_id: 1, right_id: 2}
3499
+ ] AS record
3500
+ RETURN record.left_id AS left_id, record.right_id AS right_id
3501
+ }
3502
+ `).run();
3503
+ const match = new Runner(`
3504
+ MATCH (a:City)-[r:CONNECTED_TO]->(b:City)
3505
+ RETURN id(r) AS relId
3506
+ `);
3507
+ await match.run();
3508
+ const results = match.results;
3509
+ expect(results.length).toBe(1);
3510
+ expect(results[0]).toEqual({ relId: "CONNECTED_TO" });
3511
+ });
3512
+
3513
+ test("Test elementId() function with node", async () => {
3514
+ await new Runner(`
3515
+ CREATE VIRTUAL (:Person) AS {
3516
+ UNWIND [
3517
+ {id: 1, name: 'Alice'},
3518
+ {id: 2, name: 'Bob'}
3519
+ ] AS record
3520
+ RETURN record.id AS id, record.name AS name
3521
+ }
3522
+ `).run();
3523
+ const match = new Runner(`
3524
+ MATCH (n:Person)
3525
+ RETURN elementId(n) AS eid
3526
+ `);
3527
+ await match.run();
3528
+ const results = match.results;
3529
+ expect(results.length).toBe(2);
3530
+ expect(results[0]).toEqual({ eid: "1" });
3531
+ expect(results[1]).toEqual({ eid: "2" });
3532
+ });
3533
+
3534
+ test("Test elementId() function with null", async () => {
3535
+ const runner = new Runner("RETURN elementId(null) AS eid");
3536
+ await runner.run();
3537
+ const results = runner.results;
3538
+ expect(results.length).toBe(1);
3539
+ expect(results[0]).toEqual({ eid: null });
3540
+ });
3541
+
3542
+ test("Test head() function", async () => {
3543
+ const runner = new Runner("RETURN head([1, 2, 3]) AS h");
3544
+ await runner.run();
3545
+ expect(runner.results.length).toBe(1);
3546
+ expect(runner.results[0]).toEqual({ h: 1 });
3547
+ });
3548
+
3549
+ test("Test head() function with empty list", async () => {
3550
+ const runner = new Runner("RETURN head([]) AS h");
3551
+ await runner.run();
3552
+ expect(runner.results[0]).toEqual({ h: null });
3553
+ });
3554
+
3555
+ test("Test head() function with null", async () => {
3556
+ const runner = new Runner("RETURN head(null) AS h");
3557
+ await runner.run();
3558
+ expect(runner.results[0]).toEqual({ h: null });
3559
+ });
3560
+
3561
+ test("Test tail() function", async () => {
3562
+ const runner = new Runner("RETURN tail([1, 2, 3]) AS t");
3563
+ await runner.run();
3564
+ expect(runner.results.length).toBe(1);
3565
+ expect(runner.results[0]).toEqual({ t: [2, 3] });
3566
+ });
3567
+
3568
+ test("Test tail() function with single element", async () => {
3569
+ const runner = new Runner("RETURN tail([1]) AS t");
3570
+ await runner.run();
3571
+ expect(runner.results[0]).toEqual({ t: [] });
3572
+ });
3573
+
3574
+ test("Test tail() function with null", async () => {
3575
+ const runner = new Runner("RETURN tail(null) AS t");
3576
+ await runner.run();
3577
+ expect(runner.results[0]).toEqual({ t: null });
3578
+ });
3579
+
3580
+ test("Test last() function", async () => {
3581
+ const runner = new Runner("RETURN last([1, 2, 3]) AS l");
3582
+ await runner.run();
3583
+ expect(runner.results.length).toBe(1);
3584
+ expect(runner.results[0]).toEqual({ l: 3 });
3585
+ });
3586
+
3587
+ test("Test last() function with empty list", async () => {
3588
+ const runner = new Runner("RETURN last([]) AS l");
3589
+ await runner.run();
3590
+ expect(runner.results[0]).toEqual({ l: null });
3591
+ });
3592
+
3593
+ test("Test last() function with null", async () => {
3594
+ const runner = new Runner("RETURN last(null) AS l");
3595
+ await runner.run();
3596
+ expect(runner.results[0]).toEqual({ l: null });
3597
+ });
3598
+
3599
+ test("Test toInteger() function with string", async () => {
3600
+ const runner = new Runner('RETURN toInteger("42") AS i');
3601
+ await runner.run();
3602
+ expect(runner.results[0]).toEqual({ i: 42 });
3603
+ });
3604
+
3605
+ test("Test toInteger() function with float", async () => {
3606
+ const runner = new Runner("RETURN toInteger(3.14) AS i");
3607
+ await runner.run();
3608
+ expect(runner.results[0]).toEqual({ i: 3 });
3609
+ });
3610
+
3611
+ test("Test toInteger() function with boolean", async () => {
3612
+ const runner = new Runner("RETURN toInteger(true) AS i");
3613
+ await runner.run();
3614
+ expect(runner.results[0]).toEqual({ i: 1 });
3615
+ });
3616
+
3617
+ test("Test toInteger() function with null", async () => {
3618
+ const runner = new Runner("RETURN toInteger(null) AS i");
3619
+ await runner.run();
3620
+ expect(runner.results[0]).toEqual({ i: null });
3621
+ });
3622
+
3623
+ test("Test toFloat() function with string", async () => {
3624
+ const runner = new Runner('RETURN toFloat("3.14") AS f');
3625
+ await runner.run();
3626
+ expect(runner.results[0]).toEqual({ f: 3.14 });
3627
+ });
3628
+
3629
+ test("Test toFloat() function with integer", async () => {
3630
+ const runner = new Runner("RETURN toFloat(42) AS f");
3631
+ await runner.run();
3632
+ expect(runner.results[0]).toEqual({ f: 42 });
3633
+ });
3634
+
3635
+ test("Test toFloat() function with boolean", async () => {
3636
+ const runner = new Runner("RETURN toFloat(true) AS f");
3637
+ await runner.run();
3638
+ expect(runner.results[0]).toEqual({ f: 1.0 });
3639
+ });
3640
+
3641
+ test("Test toFloat() function with null", async () => {
3642
+ const runner = new Runner("RETURN toFloat(null) AS f");
3643
+ await runner.run();
3644
+ expect(runner.results[0]).toEqual({ f: null });
3645
+ });
3646
+
3647
+ test("Test duration() with ISO 8601 string", async () => {
3648
+ const runner = new Runner("RETURN duration('P1Y2M3DT4H5M6S') AS d");
3649
+ await runner.run();
3650
+ const d = runner.results[0].d;
3651
+ expect(d.years).toBe(1);
3652
+ expect(d.months).toBe(2);
3653
+ expect(d.days).toBe(3);
3654
+ expect(d.hours).toBe(4);
3655
+ expect(d.minutes).toBe(5);
3656
+ expect(d.seconds).toBe(6);
3657
+ expect(d.totalMonths).toBe(14);
3658
+ expect(d.formatted).toBe("P1Y2M3DT4H5M6S");
3659
+ });
3660
+
3661
+ test("Test duration() with map argument", async () => {
3662
+ const runner = new Runner("RETURN duration({days: 14, hours: 16}) AS d");
3663
+ await runner.run();
3664
+ const d = runner.results[0].d;
3665
+ expect(d.days).toBe(14);
3666
+ expect(d.hours).toBe(16);
3667
+ expect(d.totalDays).toBe(14);
3668
+ expect(d.totalSeconds).toBe(57600);
3669
+ });
3670
+
3671
+ test("Test duration() with weeks", async () => {
3672
+ const runner = new Runner("RETURN duration('P2W') AS d");
3673
+ await runner.run();
3674
+ const d = runner.results[0].d;
3675
+ expect(d.weeks).toBe(2);
3676
+ expect(d.days).toBe(14);
3677
+ expect(d.totalDays).toBe(14);
3678
+ });
3679
+
3680
+ test("Test duration() with null", async () => {
3681
+ const runner = new Runner("RETURN duration(null) AS d");
3682
+ await runner.run();
3683
+ expect(runner.results[0]).toEqual({ d: null });
3684
+ });
3685
+
3686
+ test("Test duration() with time only", async () => {
3687
+ const runner = new Runner("RETURN duration('PT2H30M') AS d");
3688
+ await runner.run();
3689
+ const d = runner.results[0].d;
3690
+ expect(d.hours).toBe(2);
3691
+ expect(d.minutes).toBe(30);
3692
+ expect(d.totalSeconds).toBe(9000);
3693
+ expect(d.formatted).toBe("PT2H30M");
3694
+ });