flowquery 1.0.46 → 1.0.47

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 (413) hide show
  1. package/dist/index.d.ts +0 -7
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -4
  4. package/dist/index.js.map +1 -1
  5. package/package.json +4 -1
  6. package/.editorconfig +0 -21
  7. package/.gitattributes +0 -3
  8. package/.github/workflows/npm-publish.yml +0 -32
  9. package/.github/workflows/python-publish.yml +0 -143
  10. package/.github/workflows/release.yml +0 -107
  11. package/.husky/pre-commit +0 -28
  12. package/.prettierrc +0 -22
  13. package/CODE_OF_CONDUCT.md +0 -10
  14. package/FlowQueryLogoIcon.png +0 -0
  15. package/SECURITY.md +0 -14
  16. package/SUPPORT.md +0 -13
  17. package/docs/flowquery.min.js +0 -1
  18. package/docs/index.html +0 -105
  19. package/flowquery-py/CONTRIBUTING.md +0 -127
  20. package/flowquery-py/README.md +0 -67
  21. package/flowquery-py/misc/data/test.json +0 -10
  22. package/flowquery-py/misc/data/users.json +0 -242
  23. package/flowquery-py/notebooks/TestFlowQuery.ipynb +0 -440
  24. package/flowquery-py/pyproject.toml +0 -121
  25. package/flowquery-py/setup_env.ps1 +0 -92
  26. package/flowquery-py/setup_env.sh +0 -87
  27. package/flowquery-py/src/__init__.py +0 -38
  28. package/flowquery-py/src/__main__.py +0 -10
  29. package/flowquery-py/src/compute/__init__.py +0 -6
  30. package/flowquery-py/src/compute/flowquery.py +0 -68
  31. package/flowquery-py/src/compute/runner.py +0 -64
  32. package/flowquery-py/src/extensibility.py +0 -52
  33. package/flowquery-py/src/graph/__init__.py +0 -31
  34. package/flowquery-py/src/graph/data.py +0 -136
  35. package/flowquery-py/src/graph/database.py +0 -141
  36. package/flowquery-py/src/graph/hops.py +0 -43
  37. package/flowquery-py/src/graph/node.py +0 -143
  38. package/flowquery-py/src/graph/node_data.py +0 -26
  39. package/flowquery-py/src/graph/node_reference.py +0 -50
  40. package/flowquery-py/src/graph/pattern.py +0 -115
  41. package/flowquery-py/src/graph/pattern_expression.py +0 -67
  42. package/flowquery-py/src/graph/patterns.py +0 -42
  43. package/flowquery-py/src/graph/physical_node.py +0 -41
  44. package/flowquery-py/src/graph/physical_relationship.py +0 -36
  45. package/flowquery-py/src/graph/relationship.py +0 -193
  46. package/flowquery-py/src/graph/relationship_data.py +0 -36
  47. package/flowquery-py/src/graph/relationship_match_collector.py +0 -85
  48. package/flowquery-py/src/graph/relationship_reference.py +0 -21
  49. package/flowquery-py/src/io/__init__.py +0 -5
  50. package/flowquery-py/src/io/command_line.py +0 -108
  51. package/flowquery-py/src/parsing/__init__.py +0 -17
  52. package/flowquery-py/src/parsing/alias.py +0 -20
  53. package/flowquery-py/src/parsing/alias_option.py +0 -11
  54. package/flowquery-py/src/parsing/ast_node.py +0 -147
  55. package/flowquery-py/src/parsing/base_parser.py +0 -84
  56. package/flowquery-py/src/parsing/components/__init__.py +0 -19
  57. package/flowquery-py/src/parsing/components/csv.py +0 -8
  58. package/flowquery-py/src/parsing/components/from_.py +0 -12
  59. package/flowquery-py/src/parsing/components/headers.py +0 -12
  60. package/flowquery-py/src/parsing/components/json.py +0 -8
  61. package/flowquery-py/src/parsing/components/null.py +0 -10
  62. package/flowquery-py/src/parsing/components/post.py +0 -8
  63. package/flowquery-py/src/parsing/components/text.py +0 -8
  64. package/flowquery-py/src/parsing/context.py +0 -50
  65. package/flowquery-py/src/parsing/data_structures/__init__.py +0 -15
  66. package/flowquery-py/src/parsing/data_structures/associative_array.py +0 -41
  67. package/flowquery-py/src/parsing/data_structures/json_array.py +0 -30
  68. package/flowquery-py/src/parsing/data_structures/key_value_pair.py +0 -38
  69. package/flowquery-py/src/parsing/data_structures/lookup.py +0 -51
  70. package/flowquery-py/src/parsing/data_structures/range_lookup.py +0 -42
  71. package/flowquery-py/src/parsing/expressions/__init__.py +0 -61
  72. package/flowquery-py/src/parsing/expressions/boolean.py +0 -20
  73. package/flowquery-py/src/parsing/expressions/expression.py +0 -141
  74. package/flowquery-py/src/parsing/expressions/expression_map.py +0 -26
  75. package/flowquery-py/src/parsing/expressions/f_string.py +0 -27
  76. package/flowquery-py/src/parsing/expressions/identifier.py +0 -21
  77. package/flowquery-py/src/parsing/expressions/number.py +0 -32
  78. package/flowquery-py/src/parsing/expressions/operator.py +0 -271
  79. package/flowquery-py/src/parsing/expressions/reference.py +0 -47
  80. package/flowquery-py/src/parsing/expressions/string.py +0 -27
  81. package/flowquery-py/src/parsing/functions/__init__.py +0 -127
  82. package/flowquery-py/src/parsing/functions/aggregate_function.py +0 -60
  83. package/flowquery-py/src/parsing/functions/async_function.py +0 -65
  84. package/flowquery-py/src/parsing/functions/avg.py +0 -55
  85. package/flowquery-py/src/parsing/functions/coalesce.py +0 -43
  86. package/flowquery-py/src/parsing/functions/collect.py +0 -75
  87. package/flowquery-py/src/parsing/functions/count.py +0 -79
  88. package/flowquery-py/src/parsing/functions/date_.py +0 -61
  89. package/flowquery-py/src/parsing/functions/datetime_.py +0 -62
  90. package/flowquery-py/src/parsing/functions/duration.py +0 -159
  91. package/flowquery-py/src/parsing/functions/element_id.py +0 -50
  92. package/flowquery-py/src/parsing/functions/function.py +0 -68
  93. package/flowquery-py/src/parsing/functions/function_factory.py +0 -170
  94. package/flowquery-py/src/parsing/functions/function_metadata.py +0 -148
  95. package/flowquery-py/src/parsing/functions/functions.py +0 -67
  96. package/flowquery-py/src/parsing/functions/head.py +0 -39
  97. package/flowquery-py/src/parsing/functions/id_.py +0 -49
  98. package/flowquery-py/src/parsing/functions/join.py +0 -49
  99. package/flowquery-py/src/parsing/functions/keys.py +0 -34
  100. package/flowquery-py/src/parsing/functions/last.py +0 -39
  101. package/flowquery-py/src/parsing/functions/localdatetime.py +0 -60
  102. package/flowquery-py/src/parsing/functions/localtime.py +0 -57
  103. package/flowquery-py/src/parsing/functions/max_.py +0 -49
  104. package/flowquery-py/src/parsing/functions/min_.py +0 -49
  105. package/flowquery-py/src/parsing/functions/nodes.py +0 -48
  106. package/flowquery-py/src/parsing/functions/predicate_function.py +0 -47
  107. package/flowquery-py/src/parsing/functions/predicate_sum.py +0 -49
  108. package/flowquery-py/src/parsing/functions/properties.py +0 -50
  109. package/flowquery-py/src/parsing/functions/rand.py +0 -28
  110. package/flowquery-py/src/parsing/functions/range_.py +0 -41
  111. package/flowquery-py/src/parsing/functions/reducer_element.py +0 -15
  112. package/flowquery-py/src/parsing/functions/relationships.py +0 -46
  113. package/flowquery-py/src/parsing/functions/replace.py +0 -39
  114. package/flowquery-py/src/parsing/functions/round_.py +0 -34
  115. package/flowquery-py/src/parsing/functions/schema.py +0 -40
  116. package/flowquery-py/src/parsing/functions/size.py +0 -34
  117. package/flowquery-py/src/parsing/functions/split.py +0 -54
  118. package/flowquery-py/src/parsing/functions/string_distance.py +0 -92
  119. package/flowquery-py/src/parsing/functions/stringify.py +0 -49
  120. package/flowquery-py/src/parsing/functions/substring.py +0 -76
  121. package/flowquery-py/src/parsing/functions/sum.py +0 -51
  122. package/flowquery-py/src/parsing/functions/tail.py +0 -37
  123. package/flowquery-py/src/parsing/functions/temporal_utils.py +0 -186
  124. package/flowquery-py/src/parsing/functions/time_.py +0 -57
  125. package/flowquery-py/src/parsing/functions/timestamp.py +0 -37
  126. package/flowquery-py/src/parsing/functions/to_float.py +0 -46
  127. package/flowquery-py/src/parsing/functions/to_integer.py +0 -46
  128. package/flowquery-py/src/parsing/functions/to_json.py +0 -35
  129. package/flowquery-py/src/parsing/functions/to_lower.py +0 -37
  130. package/flowquery-py/src/parsing/functions/to_string.py +0 -41
  131. package/flowquery-py/src/parsing/functions/trim.py +0 -37
  132. package/flowquery-py/src/parsing/functions/type_.py +0 -47
  133. package/flowquery-py/src/parsing/functions/value_holder.py +0 -24
  134. package/flowquery-py/src/parsing/logic/__init__.py +0 -15
  135. package/flowquery-py/src/parsing/logic/case.py +0 -28
  136. package/flowquery-py/src/parsing/logic/else_.py +0 -12
  137. package/flowquery-py/src/parsing/logic/end.py +0 -8
  138. package/flowquery-py/src/parsing/logic/then.py +0 -12
  139. package/flowquery-py/src/parsing/logic/when.py +0 -12
  140. package/flowquery-py/src/parsing/operations/__init__.py +0 -46
  141. package/flowquery-py/src/parsing/operations/aggregated_return.py +0 -25
  142. package/flowquery-py/src/parsing/operations/aggregated_with.py +0 -22
  143. package/flowquery-py/src/parsing/operations/call.py +0 -73
  144. package/flowquery-py/src/parsing/operations/create_node.py +0 -35
  145. package/flowquery-py/src/parsing/operations/create_relationship.py +0 -35
  146. package/flowquery-py/src/parsing/operations/delete_node.py +0 -29
  147. package/flowquery-py/src/parsing/operations/delete_relationship.py +0 -29
  148. package/flowquery-py/src/parsing/operations/group_by.py +0 -148
  149. package/flowquery-py/src/parsing/operations/limit.py +0 -33
  150. package/flowquery-py/src/parsing/operations/load.py +0 -148
  151. package/flowquery-py/src/parsing/operations/match.py +0 -52
  152. package/flowquery-py/src/parsing/operations/operation.py +0 -69
  153. package/flowquery-py/src/parsing/operations/order_by.py +0 -114
  154. package/flowquery-py/src/parsing/operations/projection.py +0 -21
  155. package/flowquery-py/src/parsing/operations/return_op.py +0 -88
  156. package/flowquery-py/src/parsing/operations/union.py +0 -115
  157. package/flowquery-py/src/parsing/operations/union_all.py +0 -17
  158. package/flowquery-py/src/parsing/operations/unwind.py +0 -42
  159. package/flowquery-py/src/parsing/operations/where.py +0 -43
  160. package/flowquery-py/src/parsing/operations/with_op.py +0 -18
  161. package/flowquery-py/src/parsing/parser.py +0 -1384
  162. package/flowquery-py/src/parsing/parser_state.py +0 -26
  163. package/flowquery-py/src/parsing/token_to_node.py +0 -109
  164. package/flowquery-py/src/tokenization/__init__.py +0 -23
  165. package/flowquery-py/src/tokenization/keyword.py +0 -54
  166. package/flowquery-py/src/tokenization/operator.py +0 -29
  167. package/flowquery-py/src/tokenization/string_walker.py +0 -158
  168. package/flowquery-py/src/tokenization/symbol.py +0 -19
  169. package/flowquery-py/src/tokenization/token.py +0 -693
  170. package/flowquery-py/src/tokenization/token_mapper.py +0 -53
  171. package/flowquery-py/src/tokenization/token_type.py +0 -21
  172. package/flowquery-py/src/tokenization/tokenizer.py +0 -214
  173. package/flowquery-py/src/tokenization/trie.py +0 -125
  174. package/flowquery-py/src/utils/__init__.py +0 -6
  175. package/flowquery-py/src/utils/object_utils.py +0 -20
  176. package/flowquery-py/src/utils/string_utils.py +0 -113
  177. package/flowquery-py/tests/__init__.py +0 -1
  178. package/flowquery-py/tests/compute/__init__.py +0 -1
  179. package/flowquery-py/tests/compute/test_runner.py +0 -4902
  180. package/flowquery-py/tests/graph/__init__.py +0 -1
  181. package/flowquery-py/tests/graph/test_create.py +0 -56
  182. package/flowquery-py/tests/graph/test_data.py +0 -73
  183. package/flowquery-py/tests/graph/test_match.py +0 -40
  184. package/flowquery-py/tests/parsing/__init__.py +0 -1
  185. package/flowquery-py/tests/parsing/test_context.py +0 -34
  186. package/flowquery-py/tests/parsing/test_expression.py +0 -248
  187. package/flowquery-py/tests/parsing/test_parser.py +0 -1237
  188. package/flowquery-py/tests/test_extensibility.py +0 -611
  189. package/flowquery-py/tests/tokenization/__init__.py +0 -1
  190. package/flowquery-py/tests/tokenization/test_token_mapper.py +0 -60
  191. package/flowquery-py/tests/tokenization/test_tokenizer.py +0 -198
  192. package/flowquery-py/tests/tokenization/test_trie.py +0 -30
  193. package/flowquery-vscode/.vscode-test.mjs +0 -5
  194. package/flowquery-vscode/.vscodeignore +0 -13
  195. package/flowquery-vscode/LICENSE +0 -21
  196. package/flowquery-vscode/README.md +0 -11
  197. package/flowquery-vscode/demo/FlowQueryVSCodeDemo.gif +0 -0
  198. package/flowquery-vscode/eslint.config.mjs +0 -25
  199. package/flowquery-vscode/extension.js +0 -508
  200. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +0 -1
  201. package/flowquery-vscode/flowquery-worker.js +0 -66
  202. package/flowquery-vscode/images/FlowQueryLogoIcon.png +0 -0
  203. package/flowquery-vscode/jsconfig.json +0 -13
  204. package/flowquery-vscode/libs/page.css +0 -53
  205. package/flowquery-vscode/libs/table.css +0 -13
  206. package/flowquery-vscode/libs/tabs.css +0 -66
  207. package/flowquery-vscode/package-lock.json +0 -2917
  208. package/flowquery-vscode/package.json +0 -51
  209. package/flowquery-vscode/test/extension.test.js +0 -196
  210. package/flowquery-vscode/test/worker.test.js +0 -25
  211. package/flowquery-vscode/vsc-extension-quickstart.md +0 -42
  212. package/jest.config.js +0 -14
  213. package/misc/apps/RAG/README.md +0 -29
  214. package/misc/apps/RAG/data/chats.json +0 -302
  215. package/misc/apps/RAG/data/emails.json +0 -182
  216. package/misc/apps/RAG/data/events.json +0 -226
  217. package/misc/apps/RAG/data/files.json +0 -172
  218. package/misc/apps/RAG/data/users.json +0 -158
  219. package/misc/apps/RAG/jest.config.js +0 -21
  220. package/misc/apps/RAG/package.json +0 -48
  221. package/misc/apps/RAG/public/index.html +0 -18
  222. package/misc/apps/RAG/src/App.css +0 -42
  223. package/misc/apps/RAG/src/App.tsx +0 -50
  224. package/misc/apps/RAG/src/components/AdaptiveCardRenderer.css +0 -172
  225. package/misc/apps/RAG/src/components/AdaptiveCardRenderer.tsx +0 -380
  226. package/misc/apps/RAG/src/components/ApiKeySettings.tsx +0 -245
  227. package/misc/apps/RAG/src/components/ChatContainer.css +0 -67
  228. package/misc/apps/RAG/src/components/ChatContainer.tsx +0 -242
  229. package/misc/apps/RAG/src/components/ChatInput.css +0 -23
  230. package/misc/apps/RAG/src/components/ChatInput.tsx +0 -76
  231. package/misc/apps/RAG/src/components/ChatMessage.css +0 -160
  232. package/misc/apps/RAG/src/components/ChatMessage.tsx +0 -286
  233. package/misc/apps/RAG/src/components/FlowQueryAgent.ts +0 -708
  234. package/misc/apps/RAG/src/components/FlowQueryRunner.css +0 -113
  235. package/misc/apps/RAG/src/components/FlowQueryRunner.tsx +0 -371
  236. package/misc/apps/RAG/src/components/index.ts +0 -28
  237. package/misc/apps/RAG/src/graph/index.ts +0 -19
  238. package/misc/apps/RAG/src/graph/initializeGraph.ts +0 -254
  239. package/misc/apps/RAG/src/index.tsx +0 -29
  240. package/misc/apps/RAG/src/prompts/FlowQuerySystemPrompt.ts +0 -327
  241. package/misc/apps/RAG/src/prompts/index.ts +0 -10
  242. package/misc/apps/RAG/src/tests/graph.test.ts +0 -35
  243. package/misc/apps/RAG/src/utils/FlowQueryExecutor.ts +0 -130
  244. package/misc/apps/RAG/src/utils/FlowQueryExtractor.ts +0 -208
  245. package/misc/apps/RAG/src/utils/Llm.ts +0 -248
  246. package/misc/apps/RAG/src/utils/index.ts +0 -12
  247. package/misc/apps/RAG/tsconfig.json +0 -22
  248. package/misc/apps/RAG/webpack.config.js +0 -43
  249. package/misc/apps/README.md +0 -1
  250. package/misc/queries/analyze_catfacts.cql +0 -75
  251. package/misc/queries/azure_openai_completions.cql +0 -13
  252. package/misc/queries/azure_openai_models.cql +0 -9
  253. package/misc/queries/mock_pipeline.cql +0 -84
  254. package/misc/queries/openai_completions.cql +0 -15
  255. package/misc/queries/openai_models.cql +0 -13
  256. package/misc/queries/test.cql +0 -6
  257. package/misc/queries/tool_inference.cql +0 -24
  258. package/misc/queries/wisdom.cql +0 -6
  259. package/misc/queries/wisdom_letter_histogram.cql +0 -8
  260. package/src/compute/flowquery.ts +0 -46
  261. package/src/compute/runner.ts +0 -66
  262. package/src/extensibility.ts +0 -45
  263. package/src/graph/data.ts +0 -130
  264. package/src/graph/database.ts +0 -143
  265. package/src/graph/hops.ts +0 -22
  266. package/src/graph/node.ts +0 -122
  267. package/src/graph/node_data.ts +0 -18
  268. package/src/graph/node_reference.ts +0 -38
  269. package/src/graph/pattern.ts +0 -110
  270. package/src/graph/pattern_expression.ts +0 -48
  271. package/src/graph/patterns.ts +0 -36
  272. package/src/graph/physical_node.ts +0 -23
  273. package/src/graph/physical_relationship.ts +0 -23
  274. package/src/graph/relationship.ts +0 -167
  275. package/src/graph/relationship_data.ts +0 -31
  276. package/src/graph/relationship_match_collector.ts +0 -64
  277. package/src/graph/relationship_reference.ts +0 -25
  278. package/src/index.browser.ts +0 -46
  279. package/src/index.node.ts +0 -55
  280. package/src/index.ts +0 -12
  281. package/src/io/command_line.ts +0 -74
  282. package/src/parsing/alias.ts +0 -23
  283. package/src/parsing/alias_option.ts +0 -5
  284. package/src/parsing/ast_node.ts +0 -153
  285. package/src/parsing/base_parser.ts +0 -98
  286. package/src/parsing/components/csv.ts +0 -9
  287. package/src/parsing/components/from.ts +0 -12
  288. package/src/parsing/components/headers.ts +0 -12
  289. package/src/parsing/components/json.ts +0 -9
  290. package/src/parsing/components/null.ts +0 -9
  291. package/src/parsing/components/post.ts +0 -9
  292. package/src/parsing/components/text.ts +0 -9
  293. package/src/parsing/context.ts +0 -54
  294. package/src/parsing/data_structures/associative_array.ts +0 -43
  295. package/src/parsing/data_structures/json_array.ts +0 -31
  296. package/src/parsing/data_structures/key_value_pair.ts +0 -37
  297. package/src/parsing/data_structures/lookup.ts +0 -44
  298. package/src/parsing/data_structures/range_lookup.ts +0 -36
  299. package/src/parsing/expressions/boolean.ts +0 -21
  300. package/src/parsing/expressions/expression.ts +0 -150
  301. package/src/parsing/expressions/expression_map.ts +0 -22
  302. package/src/parsing/expressions/f_string.ts +0 -26
  303. package/src/parsing/expressions/identifier.ts +0 -22
  304. package/src/parsing/expressions/number.ts +0 -40
  305. package/src/parsing/expressions/operator.ts +0 -354
  306. package/src/parsing/expressions/reference.ts +0 -45
  307. package/src/parsing/expressions/string.ts +0 -34
  308. package/src/parsing/functions/aggregate_function.ts +0 -58
  309. package/src/parsing/functions/async_function.ts +0 -64
  310. package/src/parsing/functions/avg.ts +0 -47
  311. package/src/parsing/functions/coalesce.ts +0 -49
  312. package/src/parsing/functions/collect.ts +0 -54
  313. package/src/parsing/functions/count.ts +0 -54
  314. package/src/parsing/functions/date.ts +0 -63
  315. package/src/parsing/functions/datetime.ts +0 -63
  316. package/src/parsing/functions/duration.ts +0 -143
  317. package/src/parsing/functions/element_id.ts +0 -51
  318. package/src/parsing/functions/function.ts +0 -60
  319. package/src/parsing/functions/function_factory.ts +0 -195
  320. package/src/parsing/functions/function_metadata.ts +0 -217
  321. package/src/parsing/functions/functions.ts +0 -70
  322. package/src/parsing/functions/head.ts +0 -42
  323. package/src/parsing/functions/id.ts +0 -51
  324. package/src/parsing/functions/join.ts +0 -40
  325. package/src/parsing/functions/keys.ts +0 -29
  326. package/src/parsing/functions/last.ts +0 -42
  327. package/src/parsing/functions/localdatetime.ts +0 -63
  328. package/src/parsing/functions/localtime.ts +0 -58
  329. package/src/parsing/functions/max.ts +0 -37
  330. package/src/parsing/functions/min.ts +0 -37
  331. package/src/parsing/functions/nodes.ts +0 -54
  332. package/src/parsing/functions/predicate_function.ts +0 -48
  333. package/src/parsing/functions/predicate_sum.ts +0 -47
  334. package/src/parsing/functions/properties.ts +0 -56
  335. package/src/parsing/functions/rand.ts +0 -21
  336. package/src/parsing/functions/range.ts +0 -37
  337. package/src/parsing/functions/reducer_element.ts +0 -10
  338. package/src/parsing/functions/relationships.ts +0 -52
  339. package/src/parsing/functions/replace.ts +0 -38
  340. package/src/parsing/functions/round.ts +0 -28
  341. package/src/parsing/functions/schema.ts +0 -39
  342. package/src/parsing/functions/size.ts +0 -28
  343. package/src/parsing/functions/split.ts +0 -45
  344. package/src/parsing/functions/string_distance.ts +0 -83
  345. package/src/parsing/functions/stringify.ts +0 -37
  346. package/src/parsing/functions/substring.ts +0 -68
  347. package/src/parsing/functions/sum.ts +0 -41
  348. package/src/parsing/functions/tail.ts +0 -39
  349. package/src/parsing/functions/temporal_utils.ts +0 -180
  350. package/src/parsing/functions/time.ts +0 -58
  351. package/src/parsing/functions/timestamp.ts +0 -37
  352. package/src/parsing/functions/to_float.ts +0 -50
  353. package/src/parsing/functions/to_integer.ts +0 -50
  354. package/src/parsing/functions/to_json.ts +0 -28
  355. package/src/parsing/functions/to_lower.ts +0 -28
  356. package/src/parsing/functions/to_string.ts +0 -32
  357. package/src/parsing/functions/trim.ts +0 -28
  358. package/src/parsing/functions/type.ts +0 -39
  359. package/src/parsing/functions/value_holder.ts +0 -13
  360. package/src/parsing/logic/case.ts +0 -26
  361. package/src/parsing/logic/else.ts +0 -12
  362. package/src/parsing/logic/end.ts +0 -9
  363. package/src/parsing/logic/then.ts +0 -12
  364. package/src/parsing/logic/when.ts +0 -12
  365. package/src/parsing/operations/aggregated_return.ts +0 -22
  366. package/src/parsing/operations/aggregated_with.ts +0 -18
  367. package/src/parsing/operations/call.ts +0 -69
  368. package/src/parsing/operations/create_node.ts +0 -39
  369. package/src/parsing/operations/create_relationship.ts +0 -38
  370. package/src/parsing/operations/delete_node.ts +0 -33
  371. package/src/parsing/operations/delete_relationship.ts +0 -32
  372. package/src/parsing/operations/group_by.ts +0 -137
  373. package/src/parsing/operations/limit.ts +0 -31
  374. package/src/parsing/operations/load.ts +0 -146
  375. package/src/parsing/operations/match.ts +0 -54
  376. package/src/parsing/operations/operation.ts +0 -69
  377. package/src/parsing/operations/order_by.ts +0 -126
  378. package/src/parsing/operations/projection.ts +0 -18
  379. package/src/parsing/operations/return.ts +0 -76
  380. package/src/parsing/operations/union.ts +0 -114
  381. package/src/parsing/operations/union_all.ts +0 -16
  382. package/src/parsing/operations/unwind.ts +0 -36
  383. package/src/parsing/operations/where.ts +0 -42
  384. package/src/parsing/operations/with.ts +0 -20
  385. package/src/parsing/parser.ts +0 -1641
  386. package/src/parsing/parser_state.ts +0 -25
  387. package/src/parsing/token_to_node.ts +0 -114
  388. package/src/tokenization/keyword.ts +0 -50
  389. package/src/tokenization/operator.ts +0 -25
  390. package/src/tokenization/string_walker.ts +0 -197
  391. package/src/tokenization/symbol.ts +0 -15
  392. package/src/tokenization/token.ts +0 -764
  393. package/src/tokenization/token_mapper.ts +0 -53
  394. package/src/tokenization/token_type.ts +0 -16
  395. package/src/tokenization/tokenizer.ts +0 -250
  396. package/src/tokenization/trie.ts +0 -117
  397. package/src/utils/object_utils.ts +0 -17
  398. package/src/utils/string_utils.ts +0 -114
  399. package/tests/compute/runner.test.ts +0 -4559
  400. package/tests/extensibility.test.ts +0 -643
  401. package/tests/graph/create.test.ts +0 -36
  402. package/tests/graph/data.test.ts +0 -58
  403. package/tests/graph/match.test.ts +0 -29
  404. package/tests/parsing/context.test.ts +0 -27
  405. package/tests/parsing/expression.test.ts +0 -303
  406. package/tests/parsing/parser.test.ts +0 -1327
  407. package/tests/tokenization/token_mapper.test.ts +0 -47
  408. package/tests/tokenization/tokenizer.test.ts +0 -191
  409. package/tests/tokenization/trie.test.ts +0 -20
  410. package/tsconfig.json +0 -19
  411. package/typedoc.json +0 -16
  412. package/vscode-settings.json.recommended +0 -16
  413. package/webpack.config.js +0 -26
@@ -1,4902 +0,0 @@
1
- """Tests for the FlowQuery Runner."""
2
-
3
- import pytest
4
- from typing import AsyncIterator
5
- from flowquery.compute.runner import Runner
6
- from flowquery.graph.node import Node
7
- from flowquery.graph.relationship import Relationship
8
- from flowquery.graph.database import Database
9
- from flowquery.parsing.functions.async_function import AsyncFunction
10
- from flowquery.parsing.functions.function_metadata import FunctionDef
11
-
12
-
13
- # Test classes for CALL operation tests
14
- @FunctionDef({
15
- "description": "Asynchronous function for testing CALL operation",
16
- "category": "async",
17
- "parameters": [],
18
- "output": {"description": "Yields test values", "type": "any"},
19
- })
20
- class _CallTestFunction(AsyncFunction):
21
- """Test async function for CALL operation."""
22
-
23
- def __init__(self):
24
- super().__init__("calltestfunction")
25
- self._expected_parameter_count = 0
26
-
27
- async def generate(self) -> AsyncIterator:
28
- yield {"result": 1, "dummy": "a"}
29
- yield {"result": 2, "dummy": "b"}
30
- yield {"result": 3, "dummy": "c"}
31
-
32
-
33
- @FunctionDef({
34
- "description": "Asynchronous function for testing CALL operation with no yielded expressions",
35
- "category": "async",
36
- "parameters": [],
37
- "output": {"description": "Yields test values", "type": "any"},
38
- })
39
- class _CallTestFunctionNoObject(AsyncFunction):
40
- """Test async function for CALL operation without object output."""
41
-
42
- def __init__(self):
43
- super().__init__("calltestfunctionnoobject")
44
- self._expected_parameter_count = 0
45
-
46
- async def generate(self) -> AsyncIterator:
47
- yield 1
48
- yield 2
49
- yield 3
50
-
51
-
52
- class TestRunner:
53
- """Test cases for the Runner class."""
54
-
55
- @pytest.mark.asyncio
56
- async def test_return(self):
57
- """Test return operation."""
58
- runner = Runner("return 1 + 2 as sum")
59
- await runner.run()
60
- results = runner.results
61
- assert len(results) == 1
62
- assert results[0] == {"sum": 3}
63
-
64
- @pytest.mark.asyncio
65
- async def test_return_with_multiple_expressions(self):
66
- """Test return with multiple expressions."""
67
- runner = Runner("return 1 + 2 as sum, 3 + 4 as sum2")
68
- await runner.run()
69
- results = runner.results
70
- assert len(results) == 1
71
- assert results[0] == {"sum": 3, "sum2": 7}
72
-
73
- @pytest.mark.asyncio
74
- async def test_unwind_and_return(self):
75
- """Test unwind and return."""
76
- runner = Runner("unwind [1, 2, 3] as num return num")
77
- await runner.run()
78
- results = runner.results
79
- assert len(results) == 3
80
- assert results[0] == {"num": 1}
81
- assert results[1] == {"num": 2}
82
- assert results[2] == {"num": 3}
83
-
84
- @pytest.mark.asyncio
85
- async def test_load_and_return(self):
86
- """Test load and return."""
87
- runner = Runner(
88
- 'load json from "https://jsonplaceholder.typicode.com/todos" as todo return todo'
89
- )
90
- await runner.run()
91
- results = runner.results
92
- assert len(results) > 0
93
-
94
- @pytest.mark.asyncio
95
- async def test_load_with_post_and_return(self):
96
- """Test load with post and return."""
97
- runner = Runner(
98
- 'load json from "https://jsonplaceholder.typicode.com/posts" post {userId: 1} as data return data'
99
- )
100
- await runner.run()
101
- results = runner.results
102
- assert len(results) == 1
103
-
104
- @pytest.mark.asyncio
105
- async def test_load_which_should_throw_error(self):
106
- """Test load which should throw error."""
107
- runner = Runner('load json from "http://non_existing" as data return data')
108
- with pytest.raises(Exception) as exc_info:
109
- await runner.run()
110
- assert "non_existing" in str(exc_info.value).lower() or "failed" in str(exc_info.value).lower()
111
-
112
- @pytest.mark.asyncio
113
- async def test_aggregated_return(self):
114
- """Test aggregated return."""
115
- runner = Runner(
116
- "unwind [1, 1, 2, 2] as i unwind [1, 2, 3, 4] as j return i, sum(j) as sum"
117
- )
118
- await runner.run()
119
- results = runner.results
120
- assert len(results) == 2
121
- assert results[0] == {"i": 1, "sum": 20}
122
- assert results[1] == {"i": 2, "sum": 20}
123
-
124
- @pytest.mark.asyncio
125
- async def test_aggregated_return_with_string(self):
126
- """Test aggregated return with string."""
127
- runner = Runner(
128
- 'unwind [1, 1, 2, 2] as i unwind ["a", "b", "c", "d"] as j return i, sum(j) as sum'
129
- )
130
- await runner.run()
131
- results = runner.results
132
- assert len(results) == 2
133
- assert results[0] == {"i": 1, "sum": "abcdabcd"}
134
- assert results[1] == {"i": 2, "sum": "abcdabcd"}
135
-
136
- @pytest.mark.asyncio
137
- async def test_aggregated_return_with_object(self):
138
- """Test aggregated return with object."""
139
- runner = Runner(
140
- "unwind [1, 1, 2, 2] as i unwind [1, 2, 3, 4] as j return i, {sum: sum(j)} as sum"
141
- )
142
- await runner.run()
143
- results = runner.results
144
- assert len(results) == 2
145
- assert results[0] == {"i": 1, "sum": {"sum": 20}}
146
- assert results[1] == {"i": 2, "sum": {"sum": 20}}
147
-
148
- @pytest.mark.asyncio
149
- async def test_aggregated_return_with_array(self):
150
- """Test aggregated return with array."""
151
- runner = Runner(
152
- "unwind [1, 1, 2, 2] as i unwind [1, 2, 3, 4] as j return i, [sum(j)] as sum"
153
- )
154
- await runner.run()
155
- results = runner.results
156
- assert len(results) == 2
157
- assert results[0] == {"i": 1, "sum": [20]}
158
- assert results[1] == {"i": 2, "sum": [20]}
159
-
160
- @pytest.mark.asyncio
161
- async def test_aggregated_return_with_multiple_aggregates(self):
162
- """Test aggregated return with multiple aggregates."""
163
- runner = Runner(
164
- "unwind [1, 1, 2, 2] as i unwind [1, 2, 3, 4] as j return i, sum(j) as sum, avg(j) as avg"
165
- )
166
- await runner.run()
167
- results = runner.results
168
- assert len(results) == 2
169
- assert results[0] == {"i": 1, "sum": 20, "avg": 2.5}
170
- assert results[1] == {"i": 2, "sum": 20, "avg": 2.5}
171
-
172
- @pytest.mark.asyncio
173
- async def test_count(self):
174
- """Test count aggregate function."""
175
- runner = Runner(
176
- "unwind [1, 1, 2, 2] as i unwind [1, 2, 3, 4] as j return i, count(j) as cnt"
177
- )
178
- await runner.run()
179
- results = runner.results
180
- assert len(results) == 2
181
- assert results[0] == {"i": 1, "cnt": 8}
182
- assert results[1] == {"i": 2, "cnt": 8}
183
-
184
- @pytest.mark.asyncio
185
- async def test_count_distinct(self):
186
- """Test count with distinct modifier."""
187
- runner = Runner(
188
- """
189
- unwind [1, 1, 2, 2] as i
190
- unwind [1, 2, 1, 2] as j
191
- return i, count(distinct j) as cnt
192
- """
193
- )
194
- await runner.run()
195
- results = runner.results
196
- assert len(results) == 2
197
- assert results[0] == {"i": 1, "cnt": 2}
198
- assert results[1] == {"i": 2, "cnt": 2}
199
-
200
- @pytest.mark.asyncio
201
- async def test_count_with_strings(self):
202
- """Test count with string values."""
203
- runner = Runner(
204
- """
205
- unwind ["a", "b", "a", "c"] as s
206
- return count(s) as cnt
207
- """
208
- )
209
- await runner.run()
210
- results = runner.results
211
- assert len(results) == 1
212
- assert results[0] == {"cnt": 4}
213
-
214
- @pytest.mark.asyncio
215
- async def test_count_distinct_with_strings(self):
216
- """Test count distinct with string values."""
217
- runner = Runner(
218
- """
219
- unwind ["a", "b", "a", "c"] as s
220
- return count(distinct s) as cnt
221
- """
222
- )
223
- await runner.run()
224
- results = runner.results
225
- assert len(results) == 1
226
- assert results[0] == {"cnt": 3}
227
-
228
- @pytest.mark.asyncio
229
- async def test_avg_with_null(self):
230
- """Test avg with null."""
231
- runner = Runner("return avg(null) as avg")
232
- await runner.run()
233
- results = runner.results
234
- assert len(results) == 1
235
- assert results[0] == {"avg": None}
236
-
237
- @pytest.mark.asyncio
238
- async def test_sum_with_null(self):
239
- """Test sum with null."""
240
- runner = Runner("return sum(null) as sum")
241
- await runner.run()
242
- results = runner.results
243
- assert len(results) == 1
244
- assert results[0] == {"sum": None}
245
-
246
- @pytest.mark.asyncio
247
- async def test_avg_with_one_value(self):
248
- """Test avg with one value."""
249
- runner = Runner("return avg(1) as avg")
250
- await runner.run()
251
- results = runner.results
252
- assert len(results) == 1
253
- assert results[0] == {"avg": 1}
254
-
255
- @pytest.mark.asyncio
256
- async def test_min(self):
257
- """Test min aggregate function."""
258
- runner = Runner("unwind [3, 1, 4, 1, 5, 9] as n return min(n) as minimum")
259
- await runner.run()
260
- results = runner.results
261
- assert len(results) == 1
262
- assert results[0] == {"minimum": 1}
263
-
264
- @pytest.mark.asyncio
265
- async def test_max(self):
266
- """Test max aggregate function."""
267
- runner = Runner("unwind [3, 1, 4, 1, 5, 9] as n return max(n) as maximum")
268
- await runner.run()
269
- results = runner.results
270
- assert len(results) == 1
271
- assert results[0] == {"maximum": 9}
272
-
273
- @pytest.mark.asyncio
274
- async def test_min_with_grouped_values(self):
275
- """Test min with grouped values."""
276
- runner = Runner(
277
- "unwind [1, 1, 2, 2] as i unwind [10, 20, 30, 40] as j return i, min(j) as minimum"
278
- )
279
- await runner.run()
280
- results = runner.results
281
- assert len(results) == 2
282
- assert results[0] == {"i": 1, "minimum": 10}
283
- assert results[1] == {"i": 2, "minimum": 10}
284
-
285
- @pytest.mark.asyncio
286
- async def test_max_with_grouped_values(self):
287
- """Test max with grouped values."""
288
- runner = Runner(
289
- "unwind [1, 1, 2, 2] as i unwind [10, 20, 30, 40] as j return i, max(j) as maximum"
290
- )
291
- await runner.run()
292
- results = runner.results
293
- assert len(results) == 2
294
- assert results[0] == {"i": 1, "maximum": 40}
295
- assert results[1] == {"i": 2, "maximum": 40}
296
-
297
- @pytest.mark.asyncio
298
- async def test_min_with_null(self):
299
- """Test min with null."""
300
- runner = Runner("return min(null) as minimum")
301
- await runner.run()
302
- results = runner.results
303
- assert len(results) == 1
304
- assert results[0] == {"minimum": None}
305
-
306
- @pytest.mark.asyncio
307
- async def test_max_with_null(self):
308
- """Test max with null."""
309
- runner = Runner("return max(null) as maximum")
310
- await runner.run()
311
- results = runner.results
312
- assert len(results) == 1
313
- assert results[0] == {"maximum": None}
314
-
315
- @pytest.mark.asyncio
316
- async def test_min_with_strings(self):
317
- """Test min with string values."""
318
- runner = Runner(
319
- 'unwind ["cherry", "apple", "banana"] as s return min(s) as minimum'
320
- )
321
- await runner.run()
322
- results = runner.results
323
- assert len(results) == 1
324
- assert results[0] == {"minimum": "apple"}
325
-
326
- @pytest.mark.asyncio
327
- async def test_max_with_strings(self):
328
- """Test max with string values."""
329
- runner = Runner(
330
- 'unwind ["cherry", "apple", "banana"] as s return max(s) as maximum'
331
- )
332
- await runner.run()
333
- results = runner.results
334
- assert len(results) == 1
335
- assert results[0] == {"maximum": "cherry"}
336
-
337
- @pytest.mark.asyncio
338
- async def test_min_and_max_together(self):
339
- """Test min and max together."""
340
- runner = Runner(
341
- "unwind [3, 1, 4, 1, 5, 9] as n return min(n) as minimum, max(n) as maximum"
342
- )
343
- await runner.run()
344
- results = runner.results
345
- assert len(results) == 1
346
- assert results[0] == {"minimum": 1, "maximum": 9}
347
-
348
- @pytest.mark.asyncio
349
- async def test_with_and_return(self):
350
- """Test with and return."""
351
- runner = Runner("with 1 as a return a")
352
- await runner.run()
353
- results = runner.results
354
- assert len(results) == 1
355
- assert results[0] == {"a": 1}
356
-
357
- def test_nested_aggregate_functions(self):
358
- """Test nested aggregate functions throw error."""
359
- with pytest.raises(Exception, match="Aggregate functions cannot be nested"):
360
- Runner("unwind [1, 2, 3, 4] as i return sum(sum(i)) as sum")
361
-
362
- @pytest.mark.asyncio
363
- async def test_with_and_return_with_unwind(self):
364
- """Test with and return with unwind."""
365
- runner = Runner("with [1, 2, 3] as a unwind a as b return b as renamed")
366
- await runner.run()
367
- results = runner.results
368
- assert len(results) == 3
369
- assert results[0] == {"renamed": 1}
370
- assert results[1] == {"renamed": 2}
371
- assert results[2] == {"renamed": 3}
372
-
373
- @pytest.mark.asyncio
374
- async def test_predicate_function(self):
375
- """Test predicate function."""
376
- runner = Runner("RETURN sum(n in [1, 2, 3] | n where n > 1) as sum")
377
- await runner.run()
378
- results = runner.results
379
- assert len(results) == 1
380
- assert results[0] == {"sum": 5}
381
-
382
- @pytest.mark.asyncio
383
- async def test_predicate_without_where(self):
384
- """Test predicate without where."""
385
- runner = Runner("RETURN sum(n in [1, 2, 3] | n) as sum")
386
- await runner.run()
387
- results = runner.results
388
- assert len(results) == 1
389
- assert results[0] == {"sum": 6}
390
-
391
- @pytest.mark.asyncio
392
- async def test_predicate_with_return_expression(self):
393
- """Test predicate with return expression."""
394
- runner = Runner("RETURN sum(n in [1+2+3, 2, 3] | n^2) as sum")
395
- await runner.run()
396
- results = runner.results
397
- assert len(results) == 1
398
- assert results[0] == {"sum": 49}
399
-
400
- @pytest.mark.asyncio
401
- async def test_range_function(self):
402
- """Test range function."""
403
- runner = Runner("RETURN range(1, 3) as range")
404
- await runner.run()
405
- results = runner.results
406
- assert len(results) == 1
407
- assert results[0] == {"range": [1, 2, 3]}
408
-
409
- @pytest.mark.asyncio
410
- async def test_range_function_with_unwind_and_case(self):
411
- """Test range function with unwind and case."""
412
- runner = Runner(
413
- "unwind range(1, 3) as num return case when num > 1 then num else null end as ret"
414
- )
415
- await runner.run()
416
- results = runner.results
417
- assert len(results) == 3
418
- assert results[0] == {"ret": None}
419
- assert results[1] == {"ret": 2}
420
- assert results[2] == {"ret": 3}
421
-
422
- @pytest.mark.asyncio
423
- async def test_size_function(self):
424
- """Test size function."""
425
- runner = Runner("RETURN size([1, 2, 3]) as size")
426
- await runner.run()
427
- results = runner.results
428
- assert len(results) == 1
429
- assert results[0] == {"size": 3}
430
-
431
- @pytest.mark.asyncio
432
- async def test_rand_and_round_functions(self):
433
- """Test rand and round functions."""
434
- runner = Runner("RETURN round(rand() * 10) as rand")
435
- await runner.run()
436
- results = runner.results
437
- assert len(results) == 1
438
- assert results[0]["rand"] <= 10
439
-
440
- @pytest.mark.asyncio
441
- async def test_split_function(self):
442
- """Test split function."""
443
- runner = Runner('RETURN split("a,b,c", ",") as split')
444
- await runner.run()
445
- results = runner.results
446
- assert len(results) == 1
447
- assert results[0] == {"split": ["a", "b", "c"]}
448
-
449
- @pytest.mark.asyncio
450
- async def test_f_string(self):
451
- """Test f-string."""
452
- runner = Runner(
453
- 'with range(1,3) as numbers RETURN f"hello {sum(n in numbers | n)}" as f'
454
- )
455
- await runner.run()
456
- results = runner.results
457
- assert len(results) == 1
458
- assert results[0] == {"f": "hello 6"}
459
-
460
- @pytest.mark.asyncio
461
- async def test_aggregated_with_and_return(self):
462
- """Test aggregated with and return."""
463
- runner = Runner(
464
- """
465
- unwind [1, 1, 2, 2] as i
466
- unwind range(1, 3) as j
467
- with i, sum(j) as sum
468
- return i, sum
469
- """
470
- )
471
- await runner.run()
472
- results = runner.results
473
- assert len(results) == 2
474
- assert results[0] == {"i": 1, "sum": 12}
475
- assert results[1] == {"i": 2, "sum": 12}
476
-
477
- @pytest.mark.asyncio
478
- async def test_unwind_null_produces_zero_rows(self):
479
- """Test that UNWIND null produces zero rows (Neo4j-compatible)."""
480
- runner = Runner("WITH null AS x UNWIND x AS i RETURN i")
481
- await runner.run()
482
- results = runner.results
483
- assert len(results) == 0
484
-
485
- @pytest.mark.asyncio
486
- async def test_unwind_null_in_pipeline_preserves_no_rows(self):
487
- """Test that UNWIND null stops the pipeline producing no rows."""
488
- runner = Runner(
489
- """
490
- WITH null AS arr
491
- UNWIND arr AS i
492
- UNWIND [1, 2] AS j
493
- RETURN i, j
494
- """
495
- )
496
- await runner.run()
497
- results = runner.results
498
- assert len(results) == 0
499
-
500
- @pytest.mark.asyncio
501
- async def test_aggregated_with_on_empty_result_set(self):
502
- """Test aggregated with on empty result set does not crash."""
503
- runner = Runner(
504
- """
505
- unwind [] as i
506
- unwind [1, 2] as j
507
- with i, count(j) as cnt
508
- return i, cnt
509
- """
510
- )
511
- await runner.run()
512
- results = runner.results
513
- assert len(results) == 0
514
-
515
- @pytest.mark.asyncio
516
- async def test_aggregated_with_using_collect_and_return(self):
517
- """Test aggregated with using collect and return."""
518
- runner = Runner(
519
- """
520
- unwind [1, 1, 2, 2] as i
521
- unwind range(1, 3) as j
522
- with i, collect(j) as collected
523
- return i, collected
524
- """
525
- )
526
- await runner.run()
527
- results = runner.results
528
- assert len(results) == 2
529
- assert results[0] == {"i": 1, "collected": [1, 2, 3, 1, 2, 3]}
530
- assert results[1] == {"i": 2, "collected": [1, 2, 3, 1, 2, 3]}
531
-
532
- @pytest.mark.asyncio
533
- async def test_collect_distinct(self):
534
- """Test collect distinct."""
535
- runner = Runner(
536
- """
537
- unwind [1, 1, 2, 2] as i
538
- unwind range(1, 3) as j
539
- with i, collect(distinct j) as collected
540
- return i, collected
541
- """
542
- )
543
- await runner.run()
544
- results = runner.results
545
- assert len(results) == 2
546
- assert results[0] == {"i": 1, "collected": [1, 2, 3]}
547
- assert results[1] == {"i": 2, "collected": [1, 2, 3]}
548
-
549
- @pytest.mark.asyncio
550
- async def test_collect_distinct_with_associative_array(self):
551
- """Test collect distinct with associative array."""
552
- runner = Runner(
553
- """
554
- unwind [1, 1, 2, 2] as i
555
- unwind range(1, 3) as j
556
- with i, collect(distinct {j: j}) as collected
557
- return i, collected
558
- """
559
- )
560
- await runner.run()
561
- results = runner.results
562
- assert len(results) == 2
563
- assert results[0] == {"i": 1, "collected": [{"j": 1}, {"j": 2}, {"j": 3}]}
564
- assert results[1] == {"i": 2, "collected": [{"j": 1}, {"j": 2}, {"j": 3}]}
565
-
566
- @pytest.mark.asyncio
567
- async def test_return_distinct(self):
568
- """Test return distinct."""
569
- runner = Runner(
570
- """
571
- unwind [1, 1, 2, 2, 3, 3] as i
572
- return distinct i
573
- """
574
- )
575
- await runner.run()
576
- results = runner.results
577
- assert len(results) == 3
578
- assert results[0] == {"i": 1}
579
- assert results[1] == {"i": 2}
580
- assert results[2] == {"i": 3}
581
-
582
- @pytest.mark.asyncio
583
- async def test_return_distinct_with_multiple_expressions(self):
584
- """Test return distinct with multiple expressions."""
585
- runner = Runner(
586
- """
587
- unwind [1, 1, 2, 2] as i
588
- unwind [10, 10, 20, 20] as j
589
- return distinct i, j
590
- """
591
- )
592
- await runner.run()
593
- results = runner.results
594
- assert len(results) == 4
595
- assert results[0] == {"i": 1, "j": 10}
596
- assert results[1] == {"i": 1, "j": 20}
597
- assert results[2] == {"i": 2, "j": 10}
598
- assert results[3] == {"i": 2, "j": 20}
599
-
600
- @pytest.mark.asyncio
601
- async def test_with_distinct(self):
602
- """Test with distinct."""
603
- runner = Runner(
604
- """
605
- unwind [1, 1, 2, 2, 3, 3] as i
606
- with distinct i as i
607
- return i
608
- """
609
- )
610
- await runner.run()
611
- results = runner.results
612
- assert len(results) == 3
613
- assert results[0] == {"i": 1}
614
- assert results[1] == {"i": 2}
615
- assert results[2] == {"i": 3}
616
-
617
- @pytest.mark.asyncio
618
- async def test_with_distinct_and_aggregation(self):
619
- """Test with distinct followed by aggregation."""
620
- runner = Runner(
621
- """
622
- unwind [1, 1, 2, 2] as i
623
- with distinct i as i
624
- return sum(i) as total
625
- """
626
- )
627
- await runner.run()
628
- results = runner.results
629
- assert len(results) == 1
630
- assert results[0] == {"total": 3}
631
-
632
- @pytest.mark.asyncio
633
- async def test_return_distinct_with_strings(self):
634
- """Test return distinct with strings."""
635
- runner = Runner(
636
- """
637
- unwind ["a", "b", "a", "c", "b"] as x
638
- return distinct x
639
- """
640
- )
641
- await runner.run()
642
- results = runner.results
643
- assert len(results) == 3
644
- assert results[0] == {"x": "a"}
645
- assert results[1] == {"x": "b"}
646
- assert results[2] == {"x": "c"}
647
-
648
- @pytest.mark.asyncio
649
- async def test_join_function(self):
650
- """Test join function."""
651
- runner = Runner('RETURN join(["a", "b", "c"], ",") as join')
652
- await runner.run()
653
- results = runner.results
654
- assert len(results) == 1
655
- assert results[0] == {"join": "a,b,c"}
656
-
657
- @pytest.mark.asyncio
658
- async def test_join_function_with_empty_array(self):
659
- """Test join function with empty array."""
660
- runner = Runner('RETURN join([], ",") as join')
661
- await runner.run()
662
- results = runner.results
663
- assert len(results) == 1
664
- assert results[0] == {"join": ""}
665
-
666
- @pytest.mark.asyncio
667
- async def test_tojson_function(self):
668
- """Test tojson function."""
669
- runner = Runner("RETURN tojson('{\"a\": 1, \"b\": 2}') as tojson")
670
- await runner.run()
671
- results = runner.results
672
- assert len(results) == 1
673
- assert results[0] == {"tojson": {"a": 1, "b": 2}}
674
-
675
- @pytest.mark.asyncio
676
- async def test_tojson_function_with_lookup(self):
677
- """Test tojson function with lookup."""
678
- runner = Runner("RETURN tojson('{\"a\": 1, \"b\": 2}').a as tojson")
679
- await runner.run()
680
- results = runner.results
681
- assert len(results) == 1
682
- assert results[0] == {"tojson": 1}
683
-
684
- @pytest.mark.asyncio
685
- async def test_replace_function(self):
686
- """Test replace function."""
687
- runner = Runner('RETURN replace("hello", "l", "x") as replace')
688
- await runner.run()
689
- results = runner.results
690
- assert len(results) == 1
691
- assert results[0] == {"replace": "hexxo"}
692
-
693
- @pytest.mark.asyncio
694
- async def test_string_distance_function(self):
695
- """Test string_distance function."""
696
- runner = Runner('RETURN string_distance("kitten", "sitting") as dist')
697
- await runner.run()
698
- results = runner.results
699
- assert len(results) == 1
700
- assert results[0]["dist"] == pytest.approx(3 / 7)
701
-
702
- @pytest.mark.asyncio
703
- async def test_string_distance_identical_strings(self):
704
- """Test string_distance function with identical strings."""
705
- runner = Runner('RETURN string_distance("hello", "hello") as dist')
706
- await runner.run()
707
- results = runner.results
708
- assert len(results) == 1
709
- assert results[0] == {"dist": 0}
710
-
711
- @pytest.mark.asyncio
712
- async def test_string_distance_empty_string(self):
713
- """Test string_distance function with empty string."""
714
- runner = Runner('RETURN string_distance("", "abc") as dist')
715
- await runner.run()
716
- results = runner.results
717
- assert len(results) == 1
718
- assert results[0] == {"dist": 1}
719
-
720
- @pytest.mark.asyncio
721
- async def test_string_distance_both_empty(self):
722
- """Test string_distance function with both empty strings."""
723
- runner = Runner('RETURN string_distance("", "") as dist')
724
- await runner.run()
725
- results = runner.results
726
- assert len(results) == 1
727
- assert results[0] == {"dist": 0}
728
-
729
- @pytest.mark.asyncio
730
- async def test_f_string_with_escaped_braces(self):
731
- """Test f-string with escaped braces."""
732
- runner = Runner(
733
- 'with range(1,3) as numbers RETURN f"hello {{sum(n in numbers | n)}}" as f'
734
- )
735
- await runner.run()
736
- results = runner.results
737
- assert len(results) == 1
738
- assert results[0] == {"f": "hello {sum(n in numbers | n)}"}
739
-
740
- @pytest.mark.asyncio
741
- async def test_predicate_function_with_collection_from_lookup(self):
742
- """Test predicate function with collection from lookup."""
743
- runner = Runner("RETURN sum(n in tojson('{\"a\": [1, 2, 3]}').a | n) as sum")
744
- await runner.run()
745
- results = runner.results
746
- assert len(results) == 1
747
- assert results[0] == {"sum": 6}
748
-
749
- @pytest.mark.asyncio
750
- async def test_stringify_function(self):
751
- """Test stringify function."""
752
- runner = Runner("RETURN stringify({a: 1, b: 2}) as stringify")
753
- await runner.run()
754
- results = runner.results
755
- assert len(results) == 1
756
- assert results[0] == {"stringify": '{\n "a": 1,\n "b": 2\n}'}
757
-
758
- @pytest.mark.asyncio
759
- async def test_to_string_function_with_number(self):
760
- """Test toString function with a number."""
761
- runner = Runner("RETURN toString(42) as result")
762
- await runner.run()
763
- results = runner.results
764
- assert len(results) == 1
765
- assert results[0] == {"result": "42"}
766
-
767
- @pytest.mark.asyncio
768
- async def test_to_string_function_with_boolean(self):
769
- """Test toString function with a boolean."""
770
- runner = Runner("RETURN toString(true) as result")
771
- await runner.run()
772
- results = runner.results
773
- assert len(results) == 1
774
- assert results[0] == {"result": "true"}
775
-
776
- @pytest.mark.asyncio
777
- async def test_to_string_function_with_object(self):
778
- """Test toString function with an object."""
779
- runner = Runner("RETURN toString({a: 1}) as result")
780
- await runner.run()
781
- results = runner.results
782
- assert len(results) == 1
783
- assert results[0] == {"result": '{"a": 1}'}
784
-
785
- @pytest.mark.asyncio
786
- async def test_to_lower_function(self):
787
- """Test toLower function."""
788
- runner = Runner('RETURN toLower("Hello World") as result')
789
- await runner.run()
790
- results = runner.results
791
- assert len(results) == 1
792
- assert results[0] == {"result": "hello world"}
793
-
794
- @pytest.mark.asyncio
795
- async def test_to_lower_function_with_all_uppercase(self):
796
- """Test toLower function with all uppercase."""
797
- runner = Runner('RETURN toLower("FOO BAR") as result')
798
- await runner.run()
799
- results = runner.results
800
- assert len(results) == 1
801
- assert results[0] == {"result": "foo bar"}
802
-
803
- @pytest.mark.asyncio
804
- async def test_trim_function(self):
805
- """Test trim function."""
806
- runner = Runner('RETURN trim(" hello ") as result')
807
- await runner.run()
808
- results = runner.results
809
- assert len(results) == 1
810
- assert results[0] == {"result": "hello"}
811
-
812
- @pytest.mark.asyncio
813
- async def test_trim_function_with_tabs_and_newlines(self):
814
- """Test trim function with tabs and newlines."""
815
- runner = Runner('WITH "\tfoo\n" AS s RETURN trim(s) as result')
816
- await runner.run()
817
- results = runner.results
818
- assert len(results) == 1
819
- assert results[0] == {"result": "foo"}
820
-
821
- @pytest.mark.asyncio
822
- async def test_trim_function_with_no_whitespace(self):
823
- """Test trim function with no whitespace."""
824
- runner = Runner('RETURN trim("hello") as result')
825
- await runner.run()
826
- results = runner.results
827
- assert len(results) == 1
828
- assert results[0] == {"result": "hello"}
829
-
830
- @pytest.mark.asyncio
831
- async def test_trim_function_with_empty_string(self):
832
- """Test trim function with empty string."""
833
- runner = Runner('RETURN trim("") as result')
834
- await runner.run()
835
- results = runner.results
836
- assert len(results) == 1
837
- assert results[0] == {"result": ""}
838
-
839
- @pytest.mark.asyncio
840
- async def test_substring_function_with_start_and_length(self):
841
- """Test substring function with start and length."""
842
- runner = Runner('RETURN substring("hello", 1, 3) as result')
843
- await runner.run()
844
- results = runner.results
845
- assert len(results) == 1
846
- assert results[0] == {"result": "ell"}
847
-
848
- @pytest.mark.asyncio
849
- async def test_substring_function_with_start_only(self):
850
- """Test substring function with start only."""
851
- runner = Runner('RETURN substring("hello", 2) as result')
852
- await runner.run()
853
- results = runner.results
854
- assert len(results) == 1
855
- assert results[0] == {"result": "llo"}
856
-
857
- @pytest.mark.asyncio
858
- async def test_substring_function_with_zero_start(self):
859
- """Test substring function with zero start."""
860
- runner = Runner('RETURN substring("hello", 0, 5) as result')
861
- await runner.run()
862
- results = runner.results
863
- assert len(results) == 1
864
- assert results[0] == {"result": "hello"}
865
-
866
- @pytest.mark.asyncio
867
- async def test_substring_function_with_zero_length(self):
868
- """Test substring function with zero length."""
869
- runner = Runner('RETURN substring("hello", 1, 0) as result')
870
- await runner.run()
871
- results = runner.results
872
- assert len(results) == 1
873
- assert results[0] == {"result": ""}
874
-
875
- # --- Null propagation tests ---
876
-
877
- @pytest.mark.asyncio
878
- async def test_to_lower_with_null_returns_null(self):
879
- """Test toLower with null returns null."""
880
- runner = Runner("RETURN toLower(null) as result")
881
- await runner.run()
882
- results = runner.results
883
- assert len(results) == 1
884
- assert results[0] == {"result": None}
885
-
886
- @pytest.mark.asyncio
887
- async def test_trim_with_null_returns_null(self):
888
- """Test trim with null returns null."""
889
- runner = Runner("RETURN trim(null) as result")
890
- await runner.run()
891
- results = runner.results
892
- assert len(results) == 1
893
- assert results[0] == {"result": None}
894
-
895
- @pytest.mark.asyncio
896
- async def test_replace_with_null_returns_null(self):
897
- """Test replace with null returns null."""
898
- runner = Runner("RETURN replace(null, 'a', 'b') as result")
899
- await runner.run()
900
- results = runner.results
901
- assert len(results) == 1
902
- assert results[0] == {"result": None}
903
-
904
- @pytest.mark.asyncio
905
- async def test_substring_with_null_returns_null(self):
906
- """Test substring with null returns null."""
907
- runner = Runner("RETURN substring(null, 0, 3) as result")
908
- await runner.run()
909
- results = runner.results
910
- assert len(results) == 1
911
- assert results[0] == {"result": None}
912
-
913
- @pytest.mark.asyncio
914
- async def test_split_with_null_returns_null(self):
915
- """Test split with null returns null."""
916
- runner = Runner("RETURN split(null, ',') as result")
917
- await runner.run()
918
- results = runner.results
919
- assert len(results) == 1
920
- assert results[0] == {"result": None}
921
-
922
- @pytest.mark.asyncio
923
- async def test_size_with_null_returns_null(self):
924
- """Test size with null returns null."""
925
- runner = Runner("RETURN size(null) as result")
926
- await runner.run()
927
- results = runner.results
928
- assert len(results) == 1
929
- assert results[0] == {"result": None}
930
-
931
- @pytest.mark.asyncio
932
- async def test_round_with_null_returns_null(self):
933
- """Test round with null returns null."""
934
- runner = Runner("RETURN round(null) as result")
935
- await runner.run()
936
- results = runner.results
937
- assert len(results) == 1
938
- assert results[0] == {"result": None}
939
-
940
- @pytest.mark.asyncio
941
- async def test_join_with_null_returns_null(self):
942
- """Test join with null returns null."""
943
- runner = Runner("RETURN join(null, ',') as result")
944
- await runner.run()
945
- results = runner.results
946
- assert len(results) == 1
947
- assert results[0] == {"result": None}
948
-
949
- @pytest.mark.asyncio
950
- async def test_string_distance_with_null_returns_null(self):
951
- """Test string_distance with null returns null."""
952
- runner = Runner("RETURN string_distance(null, 'hello') as result")
953
- await runner.run()
954
- results = runner.results
955
- assert len(results) == 1
956
- assert results[0] == {"result": None}
957
-
958
- @pytest.mark.asyncio
959
- async def test_stringify_with_null_returns_null(self):
960
- """Test stringify with null returns null."""
961
- runner = Runner("RETURN stringify(null) as result")
962
- await runner.run()
963
- results = runner.results
964
- assert len(results) == 1
965
- assert results[0] == {"result": None}
966
-
967
- @pytest.mark.asyncio
968
- async def test_tojson_with_null_returns_null(self):
969
- """Test tojson with null returns null."""
970
- runner = Runner("RETURN tojson(null) as result")
971
- await runner.run()
972
- results = runner.results
973
- assert len(results) == 1
974
- assert results[0] == {"result": None}
975
-
976
- @pytest.mark.asyncio
977
- async def test_range_with_null_returns_null(self):
978
- """Test range with null returns null."""
979
- runner = Runner("RETURN range(null, 5) as result")
980
- await runner.run()
981
- results = runner.results
982
- assert len(results) == 1
983
- assert results[0] == {"result": None}
984
-
985
- @pytest.mark.asyncio
986
- async def test_to_string_with_null_returns_null(self):
987
- """Test toString with null returns null."""
988
- runner = Runner("RETURN toString(null) as result")
989
- await runner.run()
990
- results = runner.results
991
- assert len(results) == 1
992
- assert results[0] == {"result": None}
993
-
994
- @pytest.mark.asyncio
995
- async def test_keys_with_null_returns_null(self):
996
- """Test keys with null returns null."""
997
- runner = Runner("RETURN keys(null) as result")
998
- await runner.run()
999
- results = runner.results
1000
- assert len(results) == 1
1001
- assert results[0] == {"result": None}
1002
-
1003
- @pytest.mark.asyncio
1004
- async def test_associative_array_with_key_which_is_keyword(self):
1005
- """Test associative array with key which is keyword."""
1006
- runner = Runner("RETURN {return: 1} as aa")
1007
- await runner.run()
1008
- results = runner.results
1009
- assert len(results) == 1
1010
- assert results[0] == {"aa": {"return": 1}}
1011
-
1012
- @pytest.mark.asyncio
1013
- async def test_lookup_which_is_keyword(self):
1014
- """Test lookup which is keyword."""
1015
- runner = Runner("RETURN {return: 1}.return as aa")
1016
- await runner.run()
1017
- results = runner.results
1018
- assert len(results) == 1
1019
- assert results[0] == {"aa": 1}
1020
-
1021
- @pytest.mark.asyncio
1022
- async def test_lookup_which_is_keyword_bracket(self):
1023
- """Test lookup which is keyword with bracket notation."""
1024
- runner = Runner('RETURN {return: 1}["return"] as aa')
1025
- await runner.run()
1026
- results = runner.results
1027
- assert len(results) == 1
1028
- assert results[0] == {"aa": 1}
1029
-
1030
- @pytest.mark.asyncio
1031
- async def test_return_with_expression_alias_which_starts_with_keyword(self):
1032
- """Test return with expression alias which starts with keyword."""
1033
- runner = Runner('RETURN 1 as return1, ["hello", "world"] as notes')
1034
- await runner.run()
1035
- results = runner.results
1036
- assert len(results) == 1
1037
- assert results[0] == {"return1": 1, "notes": ["hello", "world"]}
1038
-
1039
- @pytest.mark.asyncio
1040
- async def test_return_with_where_clause(self):
1041
- """Test return with where clause."""
1042
- runner = Runner("unwind range(1,100) as n with n return n where n >= 20 and n <= 30")
1043
- await runner.run()
1044
- results = runner.results
1045
- assert len(results) == 11
1046
- assert results[0] == {"n": 20}
1047
- assert results[10] == {"n": 30}
1048
-
1049
- @pytest.mark.asyncio
1050
- async def test_return_with_where_clause_and_expression_alias(self):
1051
- """Test return with where clause and expression alias."""
1052
- runner = Runner(
1053
- "unwind range(1,100) as n with n return n as number where n >= 20 and n <= 30"
1054
- )
1055
- await runner.run()
1056
- results = runner.results
1057
- assert len(results) == 11
1058
- assert results[0] == {"number": 20}
1059
- assert results[10] == {"number": 30}
1060
-
1061
- @pytest.mark.asyncio
1062
- async def test_aggregated_return_with_where_clause(self):
1063
- """Test aggregated return with where clause."""
1064
- runner = Runner(
1065
- "unwind range(1,100) as n with n where n >= 20 and n <= 30 return sum(n) as sum"
1066
- )
1067
- await runner.run()
1068
- results = runner.results
1069
- assert len(results) == 1
1070
- assert results[0] == {"sum": 275}
1071
-
1072
- @pytest.mark.asyncio
1073
- async def test_chained_aggregated_return_with_where_clause(self):
1074
- """Test chained aggregated return with where clause."""
1075
- runner = Runner(
1076
- """
1077
- unwind [1, 1, 2, 2] as i
1078
- unwind range(1, 4) as j
1079
- return i, sum(j) as sum
1080
- where i = 1
1081
- """
1082
- )
1083
- await runner.run()
1084
- results = runner.results
1085
- assert len(results) == 1
1086
- assert results[0] == {"i": 1, "sum": 20}
1087
-
1088
- @pytest.mark.asyncio
1089
- async def test_predicate_function_with_collection_from_function(self):
1090
- """Test predicate function with collection from function."""
1091
- runner = Runner(
1092
- """
1093
- unwind range(1, 10) as i
1094
- unwind range(1, 10) as j
1095
- return i, sum(j), avg(j), sum(n in collect(j) | n) as sum
1096
- """
1097
- )
1098
- await runner.run()
1099
- results = runner.results
1100
- assert len(results) == 10
1101
- assert results[0] == {"i": 1, "expr1": 55, "expr2": 5.5, "sum": 55}
1102
-
1103
- @pytest.mark.asyncio
1104
- async def test_limit(self):
1105
- """Test limit."""
1106
- runner = Runner(
1107
- """
1108
- unwind range(1, 10) as i
1109
- unwind range(1, 10) as j
1110
- limit 5
1111
- return j
1112
- """
1113
- )
1114
- await runner.run()
1115
- results = runner.results
1116
- assert len(results) == 50
1117
-
1118
- @pytest.mark.asyncio
1119
- async def test_limit_as_last_operation(self):
1120
- """Test limit as the last operation after return."""
1121
- runner = Runner(
1122
- """
1123
- unwind range(1, 10) as i
1124
- return i
1125
- limit 5
1126
- """
1127
- )
1128
- await runner.run()
1129
- results = runner.results
1130
- assert len(results) == 5
1131
-
1132
- @pytest.mark.asyncio
1133
- async def test_range_lookup(self):
1134
- """Test range lookup."""
1135
- runner = Runner(
1136
- """
1137
- with range(1, 10) as numbers
1138
- return
1139
- numbers[:] as subset1,
1140
- numbers[0:3] as subset2,
1141
- numbers[:-2] as subset3
1142
- """
1143
- )
1144
- await runner.run()
1145
- results = runner.results
1146
- assert len(results) == 1
1147
- assert results[0] == {
1148
- "subset1": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
1149
- "subset2": [1, 2, 3],
1150
- "subset3": [1, 2, 3, 4, 5, 6, 7, 8],
1151
- }
1152
-
1153
- @pytest.mark.asyncio
1154
- async def test_return_negative_number(self):
1155
- """Test return -1."""
1156
- runner = Runner("return -1 as num")
1157
- await runner.run()
1158
- results = runner.results
1159
- assert len(results) == 1
1160
- assert results[0] == {"num": -1}
1161
-
1162
- @pytest.mark.asyncio
1163
- async def test_unwind_range_lookup(self):
1164
- """Test unwind range lookup."""
1165
- runner = Runner(
1166
- """
1167
- with range(1,10) as arr
1168
- unwind arr[2:-2] as a
1169
- return a
1170
- """
1171
- )
1172
- await runner.run()
1173
- results = runner.results
1174
- assert len(results) == 6
1175
- assert results[0] == {"a": 3}
1176
- assert results[5] == {"a": 8}
1177
-
1178
- @pytest.mark.asyncio
1179
- async def test_range_with_size(self):
1180
- """Test range with size."""
1181
- runner = Runner(
1182
- """
1183
- with range(1,10) as data
1184
- return range(0, size(data)-1) as indices
1185
- """
1186
- )
1187
- await runner.run()
1188
- results = runner.results
1189
- assert len(results) == 1
1190
- assert results[0] == {"indices": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
1191
-
1192
- @pytest.mark.asyncio
1193
- async def test_keys_function(self):
1194
- """Test keys function."""
1195
- runner = Runner('RETURN keys({name: "Alice", age: 30}) as keys')
1196
- await runner.run()
1197
- results = runner.results
1198
- assert len(results) == 1
1199
- assert results[0] == {"keys": ["name", "age"]}
1200
-
1201
- @pytest.mark.asyncio
1202
- async def test_properties_function_with_map(self):
1203
- """Test properties function with a plain map."""
1204
- runner = Runner('RETURN properties({name: "Alice", age: 30}) as props')
1205
- await runner.run()
1206
- results = runner.results
1207
- assert len(results) == 1
1208
- assert results[0] == {"props": {"name": "Alice", "age": 30}}
1209
-
1210
- @pytest.mark.asyncio
1211
- async def test_properties_function_with_node(self):
1212
- """Test properties function with a graph node."""
1213
- await Runner(
1214
- """
1215
- CREATE VIRTUAL (:Animal) AS {
1216
- UNWIND [
1217
- {id: 1, name: 'Dog', legs: 4},
1218
- {id: 2, name: 'Cat', legs: 4}
1219
- ] AS record
1220
- RETURN record.id AS id, record.name AS name, record.legs AS legs
1221
- }
1222
- """
1223
- ).run()
1224
- match = Runner(
1225
- """
1226
- MATCH (a:Animal)
1227
- RETURN properties(a) AS props
1228
- """
1229
- )
1230
- await match.run()
1231
- results = match.results
1232
- assert len(results) == 2
1233
- assert results[0] == {"props": {"name": "Dog", "legs": 4}}
1234
- assert results[1] == {"props": {"name": "Cat", "legs": 4}}
1235
-
1236
- @pytest.mark.asyncio
1237
- async def test_properties_function_with_null(self):
1238
- """Test properties function with null."""
1239
- runner = Runner("RETURN properties(null) as props")
1240
- await runner.run()
1241
- results = runner.results
1242
- assert len(results) == 1
1243
- assert results[0] == {"props": None}
1244
-
1245
- @pytest.mark.asyncio
1246
- async def test_nodes_function(self):
1247
- """Test nodes function with a graph path."""
1248
- await Runner(
1249
- """
1250
- CREATE VIRTUAL (:City) AS {
1251
- UNWIND [
1252
- {id: 1, name: 'New York'},
1253
- {id: 2, name: 'Boston'}
1254
- ] AS record
1255
- RETURN record.id AS id, record.name AS name
1256
- }
1257
- """
1258
- ).run()
1259
- await Runner(
1260
- """
1261
- CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
1262
- UNWIND [
1263
- {left_id: 1, right_id: 2}
1264
- ] AS record
1265
- RETURN record.left_id AS left_id, record.right_id AS right_id
1266
- }
1267
- """
1268
- ).run()
1269
- match = Runner(
1270
- """
1271
- MATCH p=(:City)-[:CONNECTED_TO]-(:City)
1272
- RETURN nodes(p) AS cities
1273
- """
1274
- )
1275
- await match.run()
1276
- results = match.results
1277
- assert len(results) == 1
1278
- assert len(results[0]["cities"]) == 2
1279
- assert results[0]["cities"][0]["id"] == 1
1280
- assert results[0]["cities"][0]["name"] == "New York"
1281
- assert results[0]["cities"][1]["id"] == 2
1282
- assert results[0]["cities"][1]["name"] == "Boston"
1283
-
1284
- @pytest.mark.asyncio
1285
- async def test_relationships_function(self):
1286
- """Test relationships function with a graph path."""
1287
- await Runner(
1288
- """
1289
- CREATE VIRTUAL (:City) AS {
1290
- UNWIND [
1291
- {id: 1, name: 'New York'},
1292
- {id: 2, name: 'Boston'}
1293
- ] AS record
1294
- RETURN record.id AS id, record.name AS name
1295
- }
1296
- """
1297
- ).run()
1298
- await Runner(
1299
- """
1300
- CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
1301
- UNWIND [
1302
- {left_id: 1, right_id: 2, distance: 190}
1303
- ] AS record
1304
- RETURN record.left_id AS left_id, record.right_id AS right_id, record.distance AS distance
1305
- }
1306
- """
1307
- ).run()
1308
- match = Runner(
1309
- """
1310
- MATCH p=(:City)-[:CONNECTED_TO]-(:City)
1311
- RETURN relationships(p) AS rels
1312
- """
1313
- )
1314
- await match.run()
1315
- results = match.results
1316
- assert len(results) == 1
1317
- assert len(results[0]["rels"]) == 1
1318
- assert results[0]["rels"][0]["type"] == "CONNECTED_TO"
1319
- assert results[0]["rels"][0]["properties"]["distance"] == 190
1320
-
1321
- @pytest.mark.asyncio
1322
- async def test_nodes_function_with_null(self):
1323
- """Test nodes function with null."""
1324
- runner = Runner("RETURN nodes(null) as n")
1325
- await runner.run()
1326
- results = runner.results
1327
- assert len(results) == 1
1328
- assert results[0] == {"n": []}
1329
-
1330
- @pytest.mark.asyncio
1331
- async def test_relationships_function_with_null(self):
1332
- """Test relationships function with null."""
1333
- runner = Runner("RETURN relationships(null) as r")
1334
- await runner.run()
1335
- results = runner.results
1336
- assert len(results) == 1
1337
- assert results[0] == {"r": []}
1338
-
1339
- @pytest.mark.asyncio
1340
- async def test_type_function(self):
1341
- """Test type function."""
1342
- runner = Runner(
1343
- """
1344
- RETURN type(123) as type1,
1345
- type("hello") as type2,
1346
- type([1, 2, 3]) as type3,
1347
- type({a: 1, b: 2}) as type4,
1348
- type(null) as type5
1349
- """
1350
- )
1351
- await runner.run()
1352
- results = runner.results
1353
- assert len(results) == 1
1354
- assert results[0] == {
1355
- "type1": "number",
1356
- "type2": "string",
1357
- "type3": "array",
1358
- "type4": "object",
1359
- "type5": "null",
1360
- }
1361
-
1362
- @pytest.mark.asyncio
1363
- async def test_equality_comparison(self):
1364
- """Test equality comparison."""
1365
- runner = Runner(
1366
- """
1367
- unwind range(1,10) as i
1368
- return i=5 as `isEqual`, i<>5 as `isNotEqual`
1369
- """
1370
- )
1371
- await runner.run()
1372
- results = runner.results
1373
- assert len(results) == 10
1374
- for index, result in enumerate(results):
1375
- if index + 1 == 5:
1376
- assert result == {"isEqual": 1, "isNotEqual": 0}
1377
- else:
1378
- assert result == {"isEqual": 0, "isNotEqual": 1}
1379
-
1380
- @pytest.mark.asyncio
1381
- async def test_create_node_operation(self):
1382
- """Test create node operation."""
1383
- runner = Runner(
1384
- """
1385
- CREATE VIRTUAL (:TestPerson) AS {
1386
- with 1 as x
1387
- RETURN x
1388
- }
1389
- """
1390
- )
1391
- await runner.run()
1392
- results = runner.results
1393
- assert len(results) == 0
1394
-
1395
- @pytest.mark.asyncio
1396
- async def test_create_node_and_match_operations(self):
1397
- """Test create node and match operations."""
1398
- create = Runner(
1399
- """
1400
- CREATE VIRTUAL (:MatchPerson) AS {
1401
- unwind [
1402
- {id: 1, name: 'Person 1'},
1403
- {id: 2, name: 'Person 2'}
1404
- ] as record
1405
- RETURN record.id as id, record.name as name
1406
- }
1407
- """
1408
- )
1409
- await create.run()
1410
- match = Runner("MATCH (n:MatchPerson) RETURN n")
1411
- await match.run()
1412
- results = match.results
1413
- assert len(results) == 2
1414
- assert results[0]["n"] is not None
1415
- assert results[0]["n"]["id"] == 1
1416
- assert results[0]["n"]["name"] == "Person 1"
1417
- assert results[1]["n"] is not None
1418
- assert results[1]["n"]["id"] == 2
1419
- assert results[1]["n"]["name"] == "Person 2"
1420
-
1421
- @pytest.mark.asyncio
1422
- async def test_complex_match_operation(self):
1423
- """Test complex match operation."""
1424
- await Runner(
1425
- """
1426
- CREATE VIRTUAL (:AgePerson) AS {
1427
- unwind [
1428
- {id: 1, name: 'Person 1', age: 30},
1429
- {id: 2, name: 'Person 2', age: 25},
1430
- {id: 3, name: 'Person 3', age: 35}
1431
- ] as record
1432
- RETURN record.id as id, record.name as name, record.age as age
1433
- }
1434
- """
1435
- ).run()
1436
- match = Runner(
1437
- """
1438
- MATCH (n:AgePerson)
1439
- WHERE n.age > 29
1440
- RETURN n.name AS name, n.age AS age
1441
- """
1442
- )
1443
- await match.run()
1444
- results = match.results
1445
- assert len(results) == 2
1446
- assert results[0] == {"name": "Person 1", "age": 30}
1447
- assert results[1] == {"name": "Person 3", "age": 35}
1448
-
1449
- @pytest.mark.asyncio
1450
- async def test_match(self):
1451
- """Test match operation."""
1452
- await Runner(
1453
- """
1454
- CREATE VIRTUAL (:SimplePerson) AS {
1455
- unwind [
1456
- {id: 1, name: 'Person 1'},
1457
- {id: 2, name: 'Person 2'}
1458
- ] as record
1459
- RETURN record.id as id, record.name as name
1460
- }
1461
- """
1462
- ).run()
1463
- match = Runner(
1464
- """
1465
- MATCH (n:SimplePerson)
1466
- RETURN n.name AS name
1467
- """
1468
- )
1469
- await match.run()
1470
- results = match.results
1471
- assert len(results) == 2
1472
- assert results[0] == {"name": "Person 1"}
1473
- assert results[1] == {"name": "Person 2"}
1474
-
1475
- @pytest.mark.asyncio
1476
- async def test_match_with_nested_join(self):
1477
- """Test match with nested join."""
1478
- await Runner(
1479
- """
1480
- CREATE VIRTUAL (:JoinPerson) AS {
1481
- unwind [
1482
- {id: 1, name: 'Person 1'},
1483
- {id: 2, name: 'Person 2'}
1484
- ] as record
1485
- RETURN record.id as id, record.name as name
1486
- }
1487
- """
1488
- ).run()
1489
- match = Runner(
1490
- """
1491
- MATCH (a:JoinPerson), (b:JoinPerson)
1492
- WHERE a.id <> b.id
1493
- RETURN a.name AS name1, b.name AS name2
1494
- """
1495
- )
1496
- await match.run()
1497
- results = match.results
1498
- assert len(results) == 2
1499
- assert results[0] == {"name1": "Person 1", "name2": "Person 2"}
1500
- assert results[1] == {"name1": "Person 2", "name2": "Person 1"}
1501
-
1502
- @pytest.mark.asyncio
1503
- async def test_match_with_graph_pattern(self):
1504
- """Test match with graph pattern."""
1505
- await Runner(
1506
- """
1507
- CREATE VIRTUAL (:User) AS {
1508
- UNWIND [
1509
- {id: 1, name: 'User 1', manager_id: null},
1510
- {id: 2, name: 'User 2', manager_id: 1},
1511
- {id: 3, name: 'User 3', manager_id: 1},
1512
- {id: 4, name: 'User 4', manager_id: 2}
1513
- ] AS record
1514
- RETURN record.id AS id, record.name AS name, record.manager_id AS manager_id
1515
- }
1516
- """
1517
- ).run()
1518
- await Runner(
1519
- """
1520
- CREATE VIRTUAL (:User)-[:MANAGED_BY]-(:User) AS {
1521
- UNWIND [
1522
- {id: 1, manager_id: null},
1523
- {id: 2, manager_id: 1},
1524
- {id: 3, manager_id: 1},
1525
- {id: 4, manager_id: 2}
1526
- ] AS record
1527
- RETURN record.id AS left_id, record.manager_id AS right_id
1528
- }
1529
- """
1530
- ).run()
1531
- match = Runner(
1532
- """
1533
- MATCH (user:User)-[r:MANAGED_BY]-(manager:User)
1534
- RETURN user.name AS user, manager.name AS manager
1535
- """
1536
- )
1537
- await match.run()
1538
- results = match.results
1539
- assert len(results) == 3
1540
- assert results[0] == {"user": "User 2", "manager": "User 1"}
1541
- assert results[1] == {"user": "User 3", "manager": "User 1"}
1542
- assert results[2] == {"user": "User 4", "manager": "User 2"}
1543
-
1544
- @pytest.mark.asyncio
1545
- async def test_match_with_multiple_hop_graph_pattern(self):
1546
- """Test match with multiple hop graph pattern."""
1547
- await Runner(
1548
- """
1549
- CREATE VIRTUAL (:HopPerson) AS {
1550
- unwind [
1551
- {id: 1, name: 'Person 1'},
1552
- {id: 2, name: 'Person 2'},
1553
- {id: 3, name: 'Person 3'},
1554
- {id: 4, name: 'Person 4'}
1555
- ] as record
1556
- RETURN record.id as id, record.name as name
1557
- }
1558
- """
1559
- ).run()
1560
- await Runner(
1561
- """
1562
- CREATE VIRTUAL (:HopPerson)-[:KNOWS]-(:HopPerson) AS {
1563
- unwind [
1564
- {left_id: 1, right_id: 2},
1565
- {left_id: 2, right_id: 3}
1566
- ] as record
1567
- RETURN record.left_id as left_id, record.right_id as right_id
1568
- }
1569
- """
1570
- ).run()
1571
- match = Runner(
1572
- """
1573
- MATCH (a:HopPerson)-[:KNOWS*]-(c:HopPerson)
1574
- RETURN a.name AS name1, c.name AS name2
1575
- """
1576
- )
1577
- await match.run()
1578
- results = match.results
1579
- # With * meaning 0+ hops, each person also matches itself (zero-hop)
1580
- # Person 1→1, 1→2, 1→3, Person 2→2, 2→3, Person 3→3 + bidirectional = 7
1581
- assert len(results) == 7
1582
-
1583
- @pytest.mark.asyncio
1584
- async def test_match_with_double_graph_pattern(self):
1585
- """Test match with double graph pattern."""
1586
- await Runner(
1587
- """
1588
- CREATE VIRTUAL (:DoublePerson) AS {
1589
- unwind [
1590
- {id: 1, name: 'Person 1'},
1591
- {id: 2, name: 'Person 2'},
1592
- {id: 3, name: 'Person 3'},
1593
- {id: 4, name: 'Person 4'}
1594
- ] as record
1595
- RETURN record.id as id, record.name as name
1596
- }
1597
- """
1598
- ).run()
1599
- await Runner(
1600
- """
1601
- CREATE VIRTUAL (:DoublePerson)-[:KNOWS]-(:DoublePerson) AS {
1602
- unwind [
1603
- {left_id: 1, right_id: 2},
1604
- {left_id: 2, right_id: 3},
1605
- {left_id: 3, right_id: 4}
1606
- ] as record
1607
- RETURN record.left_id as left_id, record.right_id as right_id
1608
- }
1609
- """
1610
- ).run()
1611
- match = Runner(
1612
- """
1613
- MATCH (a:DoublePerson)-[:KNOWS]-(b:DoublePerson)-[:KNOWS]-(c:DoublePerson)
1614
- RETURN a.name AS name1, b.name AS name2, c.name AS name3
1615
- """
1616
- )
1617
- await match.run()
1618
- results = match.results
1619
- assert len(results) == 2
1620
- assert results[0] == {"name1": "Person 1", "name2": "Person 2", "name3": "Person 3"}
1621
- assert results[1] == {"name1": "Person 2", "name2": "Person 3", "name3": "Person 4"}
1622
-
1623
- @pytest.mark.asyncio
1624
- async def test_match_with_referenced_to_previous_variable(self):
1625
- """Test match with referenced to previous variable."""
1626
- await Runner(
1627
- """
1628
- CREATE VIRTUAL (:RefPerson) AS {
1629
- unwind [
1630
- {id: 1, name: 'Person 1'},
1631
- {id: 2, name: 'Person 2'},
1632
- {id: 3, name: 'Person 3'},
1633
- {id: 4, name: 'Person 4'}
1634
- ] as record
1635
- RETURN record.id as id, record.name as name
1636
- }
1637
- """
1638
- ).run()
1639
- await Runner(
1640
- """
1641
- CREATE VIRTUAL (:RefPerson)-[:KNOWS]-(:RefPerson) AS {
1642
- unwind [
1643
- {left_id: 1, right_id: 2},
1644
- {left_id: 2, right_id: 3},
1645
- {left_id: 3, right_id: 4}
1646
- ] as record
1647
- RETURN record.left_id as left_id, record.right_id as right_id
1648
- }
1649
- """
1650
- ).run()
1651
- match = Runner(
1652
- """
1653
- MATCH (a:RefPerson)-[:KNOWS]-(b:RefPerson)
1654
- MATCH (b)-[:KNOWS]-(c:RefPerson)
1655
- RETURN a.name AS name1, b.name AS name2, c.name AS name3
1656
- """
1657
- )
1658
- await match.run()
1659
- results = match.results
1660
- assert len(results) == 2
1661
- assert results[0] == {"name1": "Person 1", "name2": "Person 2", "name3": "Person 3"}
1662
- assert results[1] == {"name1": "Person 2", "name2": "Person 3", "name3": "Person 4"}
1663
-
1664
- @pytest.mark.asyncio
1665
- async def test_match_with_aggregated_with_and_subsequent_match(self):
1666
- """Test match with aggregated WITH followed by another match using the same node reference."""
1667
- await Runner(
1668
- """
1669
- CREATE VIRTUAL (:AggUser) AS {
1670
- unwind [
1671
- {id: 1, name: 'Alice'},
1672
- {id: 2, name: 'Bob'},
1673
- {id: 3, name: 'Carol'}
1674
- ] as record
1675
- RETURN record.id as id, record.name as name
1676
- }
1677
- """
1678
- ).run()
1679
- await Runner(
1680
- """
1681
- CREATE VIRTUAL (:AggUser)-[:KNOWS]-(:AggUser) AS {
1682
- unwind [
1683
- {left_id: 1, right_id: 2},
1684
- {left_id: 1, right_id: 3}
1685
- ] as record
1686
- RETURN record.left_id as left_id, record.right_id as right_id
1687
- }
1688
- """
1689
- ).run()
1690
- await Runner(
1691
- """
1692
- CREATE VIRTUAL (:AggProject) AS {
1693
- unwind [
1694
- {id: 1, name: 'Project A'},
1695
- {id: 2, name: 'Project B'}
1696
- ] as record
1697
- RETURN record.id as id, record.name as name
1698
- }
1699
- """
1700
- ).run()
1701
- await Runner(
1702
- """
1703
- CREATE VIRTUAL (:AggUser)-[:WORKS_ON]-(:AggProject) AS {
1704
- unwind [
1705
- {left_id: 1, right_id: 1},
1706
- {left_id: 1, right_id: 2}
1707
- ] as record
1708
- RETURN record.left_id as left_id, record.right_id as right_id
1709
- }
1710
- """
1711
- ).run()
1712
- match = Runner(
1713
- """
1714
- MATCH (u:AggUser)-[:KNOWS]->(s:AggUser)
1715
- WITH u, count(s) as acquaintances
1716
- MATCH (u)-[:WORKS_ON]->(p:AggProject)
1717
- RETURN u.name as name, acquaintances, collect(p.name) as projects
1718
- """
1719
- )
1720
- await match.run()
1721
- results = match.results
1722
- assert len(results) == 1
1723
- assert results[0] == {
1724
- "name": "Alice",
1725
- "acquaintances": 2,
1726
- "projects": ["Project A", "Project B"],
1727
- }
1728
-
1729
- @pytest.mark.asyncio
1730
- async def test_match_and_return_full_node(self):
1731
- """Test match and return full node."""
1732
- await Runner(
1733
- """
1734
- CREATE VIRTUAL (:FullPerson) AS {
1735
- unwind [
1736
- {id: 1, name: 'Person 1'},
1737
- {id: 2, name: 'Person 2'}
1738
- ] as record
1739
- RETURN record.id as id, record.name as name
1740
- }
1741
- """
1742
- ).run()
1743
- match = Runner(
1744
- """
1745
- MATCH (n:FullPerson)
1746
- RETURN n
1747
- """
1748
- )
1749
- await match.run()
1750
- results = match.results
1751
- assert len(results) == 2
1752
- assert results[0]["n"] is not None
1753
- assert results[0]["n"]["id"] == 1
1754
- assert results[0]["n"]["name"] == "Person 1"
1755
- assert results[1]["n"] is not None
1756
- assert results[1]["n"]["id"] == 2
1757
- assert results[1]["n"]["name"] == "Person 2"
1758
-
1759
- @pytest.mark.asyncio
1760
- async def test_call_operation_with_async_function(self):
1761
- """Test call operation with async function."""
1762
- runner = Runner("CALL calltestfunction() YIELD result RETURN result")
1763
- await runner.run()
1764
- results = runner.results
1765
- assert len(results) == 3
1766
- assert results[0] == {"result": 1}
1767
- assert results[1] == {"result": 2}
1768
- assert results[2] == {"result": 3}
1769
-
1770
- @pytest.mark.asyncio
1771
- async def test_call_operation_with_aggregation(self):
1772
- """Test call operation with aggregation."""
1773
- runner = Runner("CALL calltestfunction() YIELD result RETURN sum(result) as total")
1774
- await runner.run()
1775
- results = runner.results
1776
- assert len(results) == 1
1777
- assert results[0] == {"total": 6}
1778
-
1779
- @pytest.mark.asyncio
1780
- async def test_call_operation_as_last_operation(self):
1781
- """Test call operation as last operation."""
1782
- runner = Runner("CALL calltestfunction()")
1783
- await runner.run()
1784
- results = runner.results
1785
- assert len(results) == 3
1786
- assert results[0] == {"result": 1, "dummy": "a"}
1787
- assert results[1] == {"result": 2, "dummy": "b"}
1788
- assert results[2] == {"result": 3, "dummy": "c"}
1789
-
1790
- @pytest.mark.asyncio
1791
- async def test_call_operation_as_last_operation_with_yield(self):
1792
- """Test call operation as last operation with yield."""
1793
- runner = Runner("CALL calltestfunction() YIELD result")
1794
- await runner.run()
1795
- results = runner.results
1796
- assert len(results) == 3
1797
- assert results[0] == {"result": 1}
1798
- assert results[1] == {"result": 2}
1799
- assert results[2] == {"result": 3}
1800
-
1801
- def test_call_operation_with_no_yielded_expressions(self):
1802
- """Test call operation with no yielded expressions throws error."""
1803
- with pytest.raises(ValueError, match="CALL operations must have a YIELD clause"):
1804
- Runner("CALL calltestfunctionnoobject() RETURN 1")
1805
-
1806
- @pytest.mark.asyncio
1807
- async def test_return_graph_pattern(self):
1808
- """Test return graph pattern."""
1809
- await Runner(
1810
- """
1811
- CREATE VIRTUAL (:PatternPerson) AS {
1812
- unwind [
1813
- {id: 1, name: 'Person 1'},
1814
- {id: 2, name: 'Person 2'}
1815
- ] as record
1816
- RETURN record.id as id, record.name as name
1817
- }
1818
- """
1819
- ).run()
1820
- await Runner(
1821
- """
1822
- CREATE VIRTUAL (:PatternPerson)-[:KNOWS]-(:PatternPerson) AS {
1823
- unwind [
1824
- {left_id: 1, since: '2020-01-01', right_id: 2}
1825
- ] as record
1826
- RETURN record.left_id as left_id, record.since as since, record.right_id as right_id
1827
- }
1828
- """
1829
- ).run()
1830
- match = Runner(
1831
- """
1832
- MATCH p=(:PatternPerson)-[:KNOWS]-(:PatternPerson)
1833
- RETURN p AS pattern
1834
- """
1835
- )
1836
- await match.run()
1837
- results = match.results
1838
- assert len(results) == 1
1839
- assert results[0]["pattern"] is not None
1840
- assert len(results[0]["pattern"]) == 3
1841
-
1842
- @pytest.mark.asyncio
1843
- async def test_circular_graph_pattern(self):
1844
- """Test circular graph pattern."""
1845
- await Runner(
1846
- """
1847
- CREATE VIRTUAL (:CircularPerson) AS {
1848
- unwind [
1849
- {id: 1, name: 'Person 1'},
1850
- {id: 2, name: 'Person 2'}
1851
- ] as record
1852
- RETURN record.id as id, record.name as name
1853
- }
1854
- """
1855
- ).run()
1856
- await Runner(
1857
- """
1858
- CREATE VIRTUAL (:CircularPerson)-[:KNOWS]-(:CircularPerson) AS {
1859
- unwind [
1860
- {left_id: 1, right_id: 2},
1861
- {left_id: 2, right_id: 1}
1862
- ] as record
1863
- RETURN record.left_id as left_id, record.right_id as right_id
1864
- }
1865
- """
1866
- ).run()
1867
- match = Runner(
1868
- """
1869
- MATCH p=(:CircularPerson)-[:KNOWS]-(:CircularPerson)-[:KNOWS]-(:CircularPerson)
1870
- RETURN p AS pattern
1871
- """
1872
- )
1873
- await match.run()
1874
- results = match.results
1875
- assert len(results) == 2
1876
-
1877
- @pytest.mark.asyncio
1878
- async def test_circular_graph_pattern_with_variable_length_should_not_revisit_nodes(self):
1879
- """Test circular graph pattern with variable length should not revisit nodes."""
1880
- await Runner(
1881
- """
1882
- CREATE VIRTUAL (:CircularVarPerson) AS {
1883
- unwind [
1884
- {id: 1, name: 'Person 1'},
1885
- {id: 2, name: 'Person 2'}
1886
- ] as record
1887
- RETURN record.id as id, record.name as name
1888
- }
1889
- """
1890
- ).run()
1891
- await Runner(
1892
- """
1893
- CREATE VIRTUAL (:CircularVarPerson)-[:KNOWS]-(:CircularVarPerson) AS {
1894
- unwind [
1895
- {left_id: 1, right_id: 2},
1896
- {left_id: 2, right_id: 1}
1897
- ] as record
1898
- RETURN record.left_id as left_id, record.right_id as right_id
1899
- }
1900
- """
1901
- ).run()
1902
- match = Runner(
1903
- """
1904
- MATCH p=(:CircularVarPerson)-[:KNOWS*]-(:CircularVarPerson)
1905
- RETURN p AS pattern
1906
- """
1907
- )
1908
- await match.run()
1909
- results = match.results
1910
- # Circular graph 1↔2: cycles are skipped, only acyclic paths are returned
1911
- assert len(results) == 6
1912
-
1913
- @pytest.mark.asyncio
1914
- async def test_multi_hop_match_with_min_hops_constraint_1(self):
1915
- """Test multi-hop match with min hops constraint *1.."""
1916
- await Runner(
1917
- """
1918
- CREATE VIRTUAL (:MinHop1Person) AS {
1919
- unwind [
1920
- {id: 1, name: 'Person 1'},
1921
- {id: 2, name: 'Person 2'},
1922
- {id: 3, name: 'Person 3'},
1923
- {id: 4, name: 'Person 4'}
1924
- ] as record
1925
- RETURN record.id as id, record.name as name
1926
- }
1927
- """
1928
- ).run()
1929
- await Runner(
1930
- """
1931
- CREATE VIRTUAL (:MinHop1Person)-[:KNOWS]-(:MinHop1Person) AS {
1932
- unwind [
1933
- {left_id: 1, right_id: 2},
1934
- {left_id: 2, right_id: 3},
1935
- {left_id: 3, right_id: 4}
1936
- ] as record
1937
- RETURN record.left_id as left_id, record.right_id as right_id
1938
- }
1939
- """
1940
- ).run()
1941
- match = Runner(
1942
- """
1943
- MATCH (a:MinHop1Person)-[:KNOWS*1..]->(b:MinHop1Person)
1944
- RETURN a.name AS name1, b.name AS name2
1945
- """
1946
- )
1947
- await match.run()
1948
- results = match.results
1949
- # *1.. means at least 1 hop, so no zero-hop (self) matches
1950
- # Person 1: 1-hop to P2, 2-hop to P3, 3-hop to P4
1951
- # Person 2: 1-hop to P3, 2-hop to P4
1952
- # Person 3: 1-hop to P4
1953
- # Person 4: no outgoing edges
1954
- assert len(results) == 6
1955
- assert results[0] == {"name1": "Person 1", "name2": "Person 2"}
1956
- assert results[1] == {"name1": "Person 1", "name2": "Person 3"}
1957
- assert results[2] == {"name1": "Person 1", "name2": "Person 4"}
1958
- assert results[3] == {"name1": "Person 2", "name2": "Person 3"}
1959
- assert results[4] == {"name1": "Person 2", "name2": "Person 4"}
1960
- assert results[5] == {"name1": "Person 3", "name2": "Person 4"}
1961
-
1962
- @pytest.mark.asyncio
1963
- async def test_multi_hop_match_with_min_hops_constraint_2(self):
1964
- """Test multi-hop match with min hops constraint *2.."""
1965
- await Runner(
1966
- """
1967
- CREATE VIRTUAL (:MinHop2Person) AS {
1968
- unwind [
1969
- {id: 1, name: 'Person 1'},
1970
- {id: 2, name: 'Person 2'},
1971
- {id: 3, name: 'Person 3'},
1972
- {id: 4, name: 'Person 4'}
1973
- ] as record
1974
- RETURN record.id as id, record.name as name
1975
- }
1976
- """
1977
- ).run()
1978
- await Runner(
1979
- """
1980
- CREATE VIRTUAL (:MinHop2Person)-[:KNOWS]-(:MinHop2Person) AS {
1981
- unwind [
1982
- {left_id: 1, right_id: 2},
1983
- {left_id: 2, right_id: 3},
1984
- {left_id: 3, right_id: 4}
1985
- ] as record
1986
- RETURN record.left_id as left_id, record.right_id as right_id
1987
- }
1988
- """
1989
- ).run()
1990
- match = Runner(
1991
- """
1992
- MATCH (a:MinHop2Person)-[:KNOWS*2..]->(b:MinHop2Person)
1993
- RETURN a.name AS name1, b.name AS name2
1994
- """
1995
- )
1996
- await match.run()
1997
- results = match.results
1998
- # *2.. means at least 2 hops
1999
- # Person 1: 2-hop to P3, 3-hop to P4
2000
- # Person 2: 2-hop to P4
2001
- assert len(results) == 3
2002
- assert results[0] == {"name1": "Person 1", "name2": "Person 3"}
2003
- assert results[1] == {"name1": "Person 1", "name2": "Person 4"}
2004
- assert results[2] == {"name1": "Person 2", "name2": "Person 4"}
2005
-
2006
- @pytest.mark.asyncio
2007
- async def test_multi_hop_match_with_variable_length_relationships(self):
2008
- """Test multi-hop match with variable length relationships."""
2009
- await Runner(
2010
- """
2011
- CREATE VIRTUAL (:MultiHopPerson) AS {
2012
- unwind [
2013
- {id: 1, name: 'Person 1'},
2014
- {id: 2, name: 'Person 2'},
2015
- {id: 3, name: 'Person 3'},
2016
- {id: 4, name: 'Person 4'}
2017
- ] as record
2018
- RETURN record.id as id, record.name as name
2019
- }
2020
- """
2021
- ).run()
2022
- await Runner(
2023
- """
2024
- CREATE VIRTUAL (:MultiHopPerson)-[:KNOWS]-(:MultiHopPerson) AS {
2025
- unwind [
2026
- {left_id: 1, right_id: 2},
2027
- {left_id: 2, right_id: 3},
2028
- {left_id: 3, right_id: 4}
2029
- ] as record
2030
- RETURN record.left_id as left_id, record.right_id as right_id
2031
- }
2032
- """
2033
- ).run()
2034
- match = Runner(
2035
- """
2036
- MATCH (a:MultiHopPerson)-[r:KNOWS*0..3]->(b:MultiHopPerson)
2037
- RETURN a, r, b
2038
- """
2039
- )
2040
- await match.run()
2041
- results = match.results
2042
- # With *0..3: Person 1 has 4 matches (0,1,2,3 hops), Person 2 has 3, Person 3 has 2, Person 4 has 1 = 10 total
2043
- assert len(results) == 10
2044
-
2045
- @pytest.mark.asyncio
2046
- async def test_return_match_pattern_with_variable_length_relationships(self):
2047
- """Test return match pattern with variable length relationships."""
2048
- await Runner(
2049
- """
2050
- CREATE VIRTUAL (:VarLenPerson) AS {
2051
- unwind [
2052
- {id: 1, name: 'Person 1'},
2053
- {id: 2, name: 'Person 2'},
2054
- {id: 3, name: 'Person 3'},
2055
- {id: 4, name: 'Person 4'}
2056
- ] as record
2057
- RETURN record.id as id, record.name as name
2058
- }
2059
- """
2060
- ).run()
2061
- await Runner(
2062
- """
2063
- CREATE VIRTUAL (:VarLenPerson)-[:KNOWS]-(:VarLenPerson) AS {
2064
- unwind [
2065
- {left_id: 1, right_id: 2},
2066
- {left_id: 2, right_id: 3},
2067
- {left_id: 3, right_id: 4}
2068
- ] as record
2069
- RETURN record.left_id as left_id, record.right_id as right_id
2070
- }
2071
- """
2072
- ).run()
2073
- match = Runner(
2074
- """
2075
- MATCH p=(a:VarLenPerson)-[:KNOWS*0..3]->(b:VarLenPerson)
2076
- RETURN p AS pattern
2077
- """
2078
- )
2079
- await match.run()
2080
- results = match.results
2081
- # With *0..3: Person 1 has 4 matches (0,1,2,3 hops), Person 2 has 3, Person 3 has 2, Person 4 has 1 = 10 total
2082
- assert len(results) == 10
2083
-
2084
- @pytest.mark.asyncio
2085
- async def test_statement_with_graph_pattern_in_where_clause(self):
2086
- """Test statement with graph pattern in where clause."""
2087
- await Runner(
2088
- """
2089
- CREATE VIRTUAL (:WherePerson) AS {
2090
- unwind [
2091
- {id: 1, name: 'Person 1'},
2092
- {id: 2, name: 'Person 2'},
2093
- {id: 3, name: 'Person 3'},
2094
- {id: 4, name: 'Person 4'}
2095
- ] as record
2096
- RETURN record.id as id, record.name as name
2097
- }
2098
- """
2099
- ).run()
2100
- await Runner(
2101
- """
2102
- CREATE VIRTUAL (:WherePerson)-[:KNOWS]-(:WherePerson) AS {
2103
- unwind [
2104
- {left_id: 1, right_id: 2},
2105
- {left_id: 2, right_id: 3},
2106
- {left_id: 3, right_id: 4}
2107
- ] as record
2108
- RETURN record.left_id as left_id, record.right_id as right_id
2109
- }
2110
- """
2111
- ).run()
2112
- match = Runner(
2113
- """
2114
- MATCH (a:WherePerson), (b:WherePerson)
2115
- WHERE (a)-[:KNOWS]->(b)
2116
- RETURN a.name AS name1, b.name AS name2
2117
- """
2118
- )
2119
- await match.run()
2120
- results = match.results
2121
- assert len(results) == 3
2122
- assert results[0] == {"name1": "Person 1", "name2": "Person 2"}
2123
- assert results[1] == {"name1": "Person 2", "name2": "Person 3"}
2124
- assert results[2] == {"name1": "Person 3", "name2": "Person 4"}
2125
-
2126
- # Test negative match
2127
- nomatch = Runner(
2128
- """
2129
- MATCH (a:WherePerson), (b:WherePerson)
2130
- WHERE (a)-[:KNOWS]->(b) <> true
2131
- RETURN a.name AS name1, b.name AS name2
2132
- """
2133
- )
2134
- await nomatch.run()
2135
- noresults = nomatch.results
2136
- assert len(noresults) == 13
2137
- assert noresults[0] == {"name1": "Person 1", "name2": "Person 1"}
2138
- assert noresults[1] == {"name1": "Person 1", "name2": "Person 3"}
2139
- assert noresults[2] == {"name1": "Person 1", "name2": "Person 4"}
2140
- assert noresults[3] == {"name1": "Person 2", "name2": "Person 1"}
2141
- assert noresults[4] == {"name1": "Person 2", "name2": "Person 2"}
2142
- assert noresults[5] == {"name1": "Person 2", "name2": "Person 4"}
2143
- assert noresults[6] == {"name1": "Person 3", "name2": "Person 1"}
2144
- assert noresults[7] == {"name1": "Person 3", "name2": "Person 2"}
2145
- assert noresults[8] == {"name1": "Person 3", "name2": "Person 3"}
2146
- assert noresults[9] == {"name1": "Person 4", "name2": "Person 1"}
2147
- assert noresults[10] == {"name1": "Person 4", "name2": "Person 2"}
2148
- assert noresults[11] == {"name1": "Person 4", "name2": "Person 3"}
2149
- assert noresults[12] == {"name1": "Person 4", "name2": "Person 4"}
2150
-
2151
- @pytest.mark.asyncio
2152
- async def test_person_who_does_not_know_anyone(self):
2153
- """Test person who does not know anyone."""
2154
- await Runner(
2155
- """
2156
- CREATE VIRTUAL (:LonePerson) AS {
2157
- unwind [
2158
- {id: 1, name: 'Person 1'},
2159
- {id: 2, name: 'Person 2'},
2160
- {id: 3, name: 'Person 3'}
2161
- ] as record
2162
- RETURN record.id as id, record.name as name
2163
- }
2164
- """
2165
- ).run()
2166
- await Runner(
2167
- """
2168
- CREATE VIRTUAL (:LonePerson)-[:KNOWS]-(:LonePerson) AS {
2169
- unwind [
2170
- {left_id: 1, right_id: 2},
2171
- {left_id: 2, right_id: 1}
2172
- ] as record
2173
- RETURN record.left_id as left_id, record.right_id as right_id
2174
- }
2175
- """
2176
- ).run()
2177
- match = Runner(
2178
- """
2179
- MATCH (a:LonePerson)
2180
- WHERE NOT (a)-[:KNOWS]->(:LonePerson)
2181
- RETURN a.name AS name
2182
- """
2183
- )
2184
- await match.run()
2185
- results = match.results
2186
- assert len(results) == 1
2187
- assert results[0] == {"name": "Person 3"}
2188
-
2189
- @pytest.mark.asyncio
2190
- async def test_manager_chain(self):
2191
- """Test manager chain."""
2192
- await Runner(
2193
- """
2194
- CREATE VIRTUAL (:ChainEmployee) AS {
2195
- unwind [
2196
- {id: 1, name: 'Employee 1'},
2197
- {id: 2, name: 'Employee 2'},
2198
- {id: 3, name: 'Employee 3'},
2199
- {id: 4, name: 'Employee 4'}
2200
- ] as record
2201
- RETURN record.id as id, record.name as name
2202
- }
2203
- """
2204
- ).run()
2205
- await Runner(
2206
- """
2207
- CREATE VIRTUAL (:ChainEmployee)-[:MANAGED_BY]-(:ChainEmployee) AS {
2208
- unwind [
2209
- {left_id: 2, right_id: 1},
2210
- {left_id: 3, right_id: 2},
2211
- {left_id: 4, right_id: 2}
2212
- ] as record
2213
- RETURN record.left_id as left_id, record.right_id as right_id
2214
- }
2215
- """
2216
- ).run()
2217
- match = Runner(
2218
- """
2219
- MATCH p=(e:ChainEmployee)-[:MANAGED_BY*]->(m:ChainEmployee)
2220
- WHERE NOT (m)-[:MANAGED_BY]->(:ChainEmployee)
2221
- RETURN p
2222
- """
2223
- )
2224
- await match.run()
2225
- results = match.results
2226
- # With * meaning 0+ hops, Employee 1 (CEO) also matches itself (zero-hop)
2227
- # Employee 1→1 (zero-hop), 2→1, 3→2→1, 4→2→1 = 4 results
2228
- assert len(results) == 4
2229
-
2230
- @pytest.mark.asyncio
2231
- async def test_match_with_leftward_relationship_direction(self):
2232
- """Test match with leftward relationship direction."""
2233
- await Runner(
2234
- """
2235
- CREATE VIRTUAL (:DirPerson) AS {
2236
- unwind [
2237
- {id: 1, name: 'Person 1'},
2238
- {id: 2, name: 'Person 2'},
2239
- {id: 3, name: 'Person 3'}
2240
- ] as record
2241
- RETURN record.id as id, record.name as name
2242
- }
2243
- """
2244
- ).run()
2245
- await Runner(
2246
- """
2247
- CREATE VIRTUAL (:DirPerson)-[:REPORTS_TO]-(:DirPerson) AS {
2248
- unwind [
2249
- {left_id: 2, right_id: 1},
2250
- {left_id: 3, right_id: 1}
2251
- ] as record
2252
- RETURN record.left_id as left_id, record.right_id as right_id
2253
- }
2254
- """
2255
- ).run()
2256
- # Rightward: left_id -> right_id (2->1, 3->1)
2257
- right_match = Runner(
2258
- """
2259
- MATCH (a:DirPerson)-[:REPORTS_TO]->(b:DirPerson)
2260
- RETURN a.name AS employee, b.name AS manager
2261
- """
2262
- )
2263
- await right_match.run()
2264
- right_results = right_match.results
2265
- assert len(right_results) == 2
2266
- assert right_results[0] == {"employee": "Person 2", "manager": "Person 1"}
2267
- assert right_results[1] == {"employee": "Person 3", "manager": "Person 1"}
2268
-
2269
- # Leftward: right_id -> left_id (1->2, 1->3) - reverse traversal
2270
- left_match = Runner(
2271
- """
2272
- MATCH (m:DirPerson)<-[:REPORTS_TO]-(e:DirPerson)
2273
- RETURN m.name AS manager, e.name AS employee
2274
- """
2275
- )
2276
- await left_match.run()
2277
- left_results = left_match.results
2278
- assert len(left_results) == 2
2279
- assert left_results[0] == {"manager": "Person 1", "employee": "Person 2"}
2280
- assert left_results[1] == {"manager": "Person 1", "employee": "Person 3"}
2281
-
2282
- @pytest.mark.asyncio
2283
- async def test_match_with_leftward_direction_swapped_data(self):
2284
- """Test match with leftward direction produces same results as rightward with swapped data."""
2285
- await Runner(
2286
- """
2287
- CREATE VIRTUAL (:DirCity) AS {
2288
- unwind [
2289
- {id: 1, name: 'New York'},
2290
- {id: 2, name: 'Boston'},
2291
- {id: 3, name: 'Chicago'}
2292
- ] as record
2293
- RETURN record.id as id, record.name as name
2294
- }
2295
- """
2296
- ).run()
2297
- await Runner(
2298
- """
2299
- CREATE VIRTUAL (:DirCity)-[:ROUTE]-(:DirCity) AS {
2300
- unwind [
2301
- {left_id: 1, right_id: 2},
2302
- {left_id: 1, right_id: 3}
2303
- ] as record
2304
- RETURN record.left_id as left_id, record.right_id as right_id
2305
- }
2306
- """
2307
- ).run()
2308
- # Leftward from destination: find where right_id matches, follow left_id
2309
- match = Runner(
2310
- """
2311
- MATCH (dest:DirCity)<-[:ROUTE]-(origin:DirCity)
2312
- RETURN dest.name AS destination, origin.name AS origin
2313
- """
2314
- )
2315
- await match.run()
2316
- results = match.results
2317
- assert len(results) == 2
2318
- assert results[0] == {"destination": "Boston", "origin": "New York"}
2319
- assert results[1] == {"destination": "Chicago", "origin": "New York"}
2320
-
2321
- @pytest.mark.asyncio
2322
- async def test_match_with_leftward_variable_length(self):
2323
- """Test match with leftward variable-length relationships."""
2324
- await Runner(
2325
- """
2326
- CREATE VIRTUAL (:DirVarPerson) AS {
2327
- unwind [
2328
- {id: 1, name: 'Person 1'},
2329
- {id: 2, name: 'Person 2'},
2330
- {id: 3, name: 'Person 3'}
2331
- ] as record
2332
- RETURN record.id as id, record.name as name
2333
- }
2334
- """
2335
- ).run()
2336
- await Runner(
2337
- """
2338
- CREATE VIRTUAL (:DirVarPerson)-[:MANAGES]-(:DirVarPerson) AS {
2339
- unwind [
2340
- {left_id: 1, right_id: 2},
2341
- {left_id: 2, right_id: 3}
2342
- ] as record
2343
- RETURN record.left_id as left_id, record.right_id as right_id
2344
- }
2345
- """
2346
- ).run()
2347
- # Leftward variable-length: traverse from right_id to left_id
2348
- match = Runner(
2349
- """
2350
- MATCH (a:DirVarPerson)<-[:MANAGES*]-(b:DirVarPerson)
2351
- RETURN a.name AS name1, b.name AS name2
2352
- """
2353
- )
2354
- await match.run()
2355
- results = match.results
2356
- # Leftward indexes on right_id. find(id) looks up right_id=id, follows left_id.
2357
- # Person 1: zero-hop only (no right_id=1)
2358
- # Person 2: zero-hop, then left_id=1 (1 hop)
2359
- # Person 3: zero-hop, then left_id=2 (1 hop), then left_id=1 (2 hops)
2360
- assert len(results) == 6
2361
- assert results[0] == {"name1": "Person 1", "name2": "Person 1"}
2362
- assert results[1] == {"name1": "Person 2", "name2": "Person 2"}
2363
- assert results[2] == {"name1": "Person 2", "name2": "Person 1"}
2364
- assert results[3] == {"name1": "Person 3", "name2": "Person 3"}
2365
- assert results[4] == {"name1": "Person 3", "name2": "Person 2"}
2366
- assert results[5] == {"name1": "Person 3", "name2": "Person 1"}
2367
-
2368
- @pytest.mark.asyncio
2369
- async def test_match_with_leftward_double_graph_pattern(self):
2370
- """Test match with leftward double graph pattern."""
2371
- await Runner(
2372
- """
2373
- CREATE VIRTUAL (:DirDoublePerson) AS {
2374
- unwind [
2375
- {id: 1, name: 'Person 1'},
2376
- {id: 2, name: 'Person 2'},
2377
- {id: 3, name: 'Person 3'},
2378
- {id: 4, name: 'Person 4'}
2379
- ] as record
2380
- RETURN record.id as id, record.name as name
2381
- }
2382
- """
2383
- ).run()
2384
- await Runner(
2385
- """
2386
- CREATE VIRTUAL (:DirDoublePerson)-[:KNOWS]-(:DirDoublePerson) AS {
2387
- unwind [
2388
- {left_id: 1, right_id: 2},
2389
- {left_id: 2, right_id: 3},
2390
- {left_id: 3, right_id: 4}
2391
- ] as record
2392
- RETURN record.left_id as left_id, record.right_id as right_id
2393
- }
2394
- """
2395
- ).run()
2396
- # Leftward chain: (c)<-[:KNOWS]-(b)<-[:KNOWS]-(a)
2397
- match = Runner(
2398
- """
2399
- MATCH (c:DirDoublePerson)<-[:KNOWS]-(b:DirDoublePerson)<-[:KNOWS]-(a:DirDoublePerson)
2400
- RETURN a.name AS name1, b.name AS name2, c.name AS name3
2401
- """
2402
- )
2403
- await match.run()
2404
- results = match.results
2405
- assert len(results) == 2
2406
- assert results[0] == {"name1": "Person 1", "name2": "Person 2", "name3": "Person 3"}
2407
- assert results[1] == {"name1": "Person 2", "name2": "Person 3", "name3": "Person 4"}
2408
-
2409
- @pytest.mark.asyncio
2410
- async def test_match_with_constraints(self):
2411
- await Runner(
2412
- """
2413
- CREATE VIRTUAL (:ConstraintEmployee) AS {
2414
- unwind [
2415
- {id: 1, name: 'Employee 1'},
2416
- {id: 2, name: 'Employee 2'},
2417
- {id: 3, name: 'Employee 3'},
2418
- {id: 4, name: 'Employee 4'}
2419
- ] as record
2420
- RETURN record.id as id, record.name as name
2421
- }
2422
- """
2423
- ).run()
2424
- match = Runner(
2425
- """
2426
- match (e:ConstraintEmployee{name:'Employee 1'})
2427
- return e.name as name
2428
- """
2429
- )
2430
- await match.run()
2431
- results = match.results
2432
- assert len(results) == 1
2433
- assert results[0]["name"] == "Employee 1"
2434
-
2435
- @pytest.mark.asyncio
2436
- async def test_optional_match_with_no_matching_relationship(self):
2437
- """Test optional match with no matching relationship returns null."""
2438
- await Runner(
2439
- """
2440
- CREATE VIRTUAL (:OptPerson) AS {
2441
- unwind [
2442
- {id: 1, name: 'Person 1'},
2443
- {id: 2, name: 'Person 2'},
2444
- {id: 3, name: 'Person 3'}
2445
- ] as record
2446
- RETURN record.id as id, record.name as name
2447
- }
2448
- """
2449
- ).run()
2450
- await Runner(
2451
- """
2452
- CREATE VIRTUAL (:OptPerson)-[:KNOWS]-(:OptPerson) AS {
2453
- unwind [
2454
- {left_id: 1, right_id: 2}
2455
- ] as record
2456
- RETURN record.left_id as left_id, record.right_id as right_id
2457
- }
2458
- """
2459
- ).run()
2460
- # Person 3 has no KNOWS relationship, so OPTIONAL MATCH should return null for friend
2461
- match = Runner(
2462
- """
2463
- MATCH (a:OptPerson)
2464
- OPTIONAL MATCH (a)-[:KNOWS]->(b:OptPerson)
2465
- RETURN a.name AS name, b AS friend
2466
- """
2467
- )
2468
- await match.run()
2469
- results = match.results
2470
- assert len(results) == 3
2471
- assert results[0]["name"] == "Person 1"
2472
- assert results[0]["friend"] is not None
2473
- assert results[0]["friend"]["name"] == "Person 2"
2474
- assert results[1]["name"] == "Person 2"
2475
- assert results[1]["friend"] is None
2476
- assert results[2]["name"] == "Person 3"
2477
- assert results[2]["friend"] is None
2478
-
2479
- @pytest.mark.asyncio
2480
- async def test_optional_match_property_access_on_null_node_returns_null(self):
2481
- """Test that accessing a property on a null node from optional match returns null."""
2482
- await Runner(
2483
- """
2484
- CREATE VIRTUAL (:OptPropPerson) AS {
2485
- unwind [
2486
- {id: 1, name: 'Person 1'},
2487
- {id: 2, name: 'Person 2'},
2488
- {id: 3, name: 'Person 3'}
2489
- ] as record
2490
- RETURN record.id as id, record.name as name
2491
- }
2492
- """
2493
- ).run()
2494
- await Runner(
2495
- """
2496
- CREATE VIRTUAL (:OptPropPerson)-[:KNOWS]-(:OptPropPerson) AS {
2497
- unwind [
2498
- {left_id: 1, right_id: 2}
2499
- ] as record
2500
- RETURN record.left_id as left_id, record.right_id as right_id
2501
- }
2502
- """
2503
- ).run()
2504
- # When accessing b.name and b is null (no match), should return null
2505
- match = Runner(
2506
- """
2507
- MATCH (a:OptPropPerson)
2508
- OPTIONAL MATCH (a)-[:KNOWS]->(b:OptPropPerson)
2509
- RETURN a.name AS name, b.name AS friend_name
2510
- """
2511
- )
2512
- await match.run()
2513
- results = match.results
2514
- assert len(results) == 3
2515
- assert results[0] == {"name": "Person 1", "friend_name": "Person 2"}
2516
- assert results[1] == {"name": "Person 2", "friend_name": None}
2517
- assert results[2] == {"name": "Person 3", "friend_name": None}
2518
-
2519
- @pytest.mark.asyncio
2520
- async def test_optional_match_where_all_nodes_match(self):
2521
- """Test optional match where all nodes have matching relationships."""
2522
- await Runner(
2523
- """
2524
- CREATE VIRTUAL (:OptAllPerson) AS {
2525
- unwind [
2526
- {id: 1, name: 'Person 1'},
2527
- {id: 2, name: 'Person 2'}
2528
- ] as record
2529
- RETURN record.id as id, record.name as name
2530
- }
2531
- """
2532
- ).run()
2533
- await Runner(
2534
- """
2535
- CREATE VIRTUAL (:OptAllPerson)-[:KNOWS]-(:OptAllPerson) AS {
2536
- unwind [
2537
- {left_id: 1, right_id: 2},
2538
- {left_id: 2, right_id: 1}
2539
- ] as record
2540
- RETURN record.left_id as left_id, record.right_id as right_id
2541
- }
2542
- """
2543
- ).run()
2544
- # All persons have KNOWS relationships, so no null values
2545
- match = Runner(
2546
- """
2547
- MATCH (a:OptAllPerson)
2548
- OPTIONAL MATCH (a)-[:KNOWS]->(b:OptAllPerson)
2549
- RETURN a.name AS name, b AS friend
2550
- """
2551
- )
2552
- await match.run()
2553
- results = match.results
2554
- assert len(results) == 2
2555
- assert results[0]["name"] == "Person 1"
2556
- assert results[0]["friend"]["name"] == "Person 2"
2557
- assert results[1]["name"] == "Person 2"
2558
- assert results[1]["friend"]["name"] == "Person 1"
2559
-
2560
- @pytest.mark.asyncio
2561
- async def test_optional_match_with_no_data_returns_nulls(self):
2562
- """Test optional match with no matching data returns nulls."""
2563
- await Runner(
2564
- """
2565
- CREATE VIRTUAL (:OptNullPerson) AS {
2566
- unwind [
2567
- {id: 1, name: 'Person 1'},
2568
- {id: 2, name: 'Person 2'}
2569
- ] as record
2570
- RETURN record.id as id, record.name as name
2571
- }
2572
- """
2573
- ).run()
2574
- await Runner(
2575
- """
2576
- CREATE VIRTUAL (:OptNullPerson)-[:KNOWS]-(:OptNullPerson) AS {
2577
- unwind [] as record
2578
- RETURN record.left_id as left_id, record.right_id as right_id
2579
- }
2580
- """
2581
- ).run()
2582
- # KNOWS relationship type exists but has no data
2583
- match = Runner(
2584
- """
2585
- MATCH (a:OptNullPerson)
2586
- OPTIONAL MATCH (a)-[:KNOWS]->(b:OptNullPerson)
2587
- RETURN a.name AS name, b AS friend
2588
- """
2589
- )
2590
- await match.run()
2591
- results = match.results
2592
- assert len(results) == 2
2593
- assert results[0]["name"] == "Person 1"
2594
- assert results[0]["friend"] is None
2595
- assert results[1]["name"] == "Person 2"
2596
- assert results[1]["friend"] is None
2597
-
2598
- @pytest.mark.asyncio
2599
- async def test_optional_match_with_aggregation(self):
2600
- """Test optional match with aggregation (collect friends)."""
2601
- await Runner(
2602
- """
2603
- CREATE VIRTUAL (:OptAggPerson) AS {
2604
- unwind [
2605
- {id: 1, name: 'Person 1'},
2606
- {id: 2, name: 'Person 2'},
2607
- {id: 3, name: 'Person 3'}
2608
- ] as record
2609
- RETURN record.id as id, record.name as name
2610
- }
2611
- """
2612
- ).run()
2613
- await Runner(
2614
- """
2615
- CREATE VIRTUAL (:OptAggPerson)-[:KNOWS]-(:OptAggPerson) AS {
2616
- unwind [
2617
- {left_id: 1, right_id: 2},
2618
- {left_id: 1, right_id: 3}
2619
- ] as record
2620
- RETURN record.left_id as left_id, record.right_id as right_id
2621
- }
2622
- """
2623
- ).run()
2624
- # Collect friends per person; Person 2 and 3 have no friends
2625
- match = Runner(
2626
- """
2627
- MATCH (a:OptAggPerson)
2628
- OPTIONAL MATCH (a)-[:KNOWS]->(b:OptAggPerson)
2629
- RETURN a.name AS name, collect(b) AS friends
2630
- """
2631
- )
2632
- await match.run()
2633
- results = match.results
2634
- assert len(results) == 3
2635
- assert results[0]["name"] == "Person 1"
2636
- assert len(results[0]["friends"]) == 2
2637
- assert results[1]["name"] == "Person 2"
2638
- assert len(results[1]["friends"]) == 1 # null is collected
2639
- assert results[2]["name"] == "Person 3"
2640
- assert len(results[2]["friends"]) == 1 # null is collected
2641
-
2642
- @pytest.mark.asyncio
2643
- async def test_standalone_optional_match_returns_data(self):
2644
- """Test standalone optional match returns data when label exists."""
2645
- await Runner(
2646
- """
2647
- CREATE VIRTUAL (:OptStandalonePerson) AS {
2648
- unwind [
2649
- {id: 1, name: 'Person 1'},
2650
- {id: 2, name: 'Person 2'}
2651
- ] as record
2652
- RETURN record.id as id, record.name as name
2653
- }
2654
- """
2655
- ).run()
2656
- await Runner(
2657
- """
2658
- CREATE VIRTUAL (:OptStandalonePerson)-[:KNOWS]-(:OptStandalonePerson) AS {
2659
- unwind [
2660
- {left_id: 1, right_id: 2}
2661
- ] as record
2662
- RETURN record.left_id as left_id, record.right_id as right_id
2663
- }
2664
- """
2665
- ).run()
2666
- # Standalone OPTIONAL MATCH with relationship where only Person 1 has a match
2667
- match = Runner(
2668
- """
2669
- OPTIONAL MATCH (a:OptStandalonePerson)-[:KNOWS]->(b:OptStandalonePerson)
2670
- RETURN a.name AS name, b.name AS friend
2671
- """
2672
- )
2673
- await match.run()
2674
- results = match.results
2675
- assert len(results) == 1
2676
- assert results[0] == {"name": "Person 1", "friend": "Person 2"}
2677
-
2678
- @pytest.mark.asyncio
2679
- async def test_optional_match_returns_full_node_when_matched(self):
2680
- """Test optional match on existing label returns actual nodes."""
2681
- await Runner(
2682
- """
2683
- CREATE VIRTUAL (:OptFullPerson) AS {
2684
- unwind [
2685
- {id: 1, name: 'Person 1'},
2686
- {id: 2, name: 'Person 2'}
2687
- ] as record
2688
- RETURN record.id as id, record.name as name
2689
- }
2690
- """
2691
- ).run()
2692
- # OPTIONAL MATCH on existing label returns actual nodes
2693
- match = Runner(
2694
- """
2695
- OPTIONAL MATCH (n:OptFullPerson)
2696
- RETURN n.name AS name
2697
- """
2698
- )
2699
- await match.run()
2700
- results = match.results
2701
- assert len(results) == 2
2702
- assert results[0] == {"name": "Person 1"}
2703
- assert results[1] == {"name": "Person 2"}
2704
-
2705
- @pytest.mark.asyncio
2706
- async def test_schema_returns_nodes_and_relationships_with_sample_data(self):
2707
- """Test schema() returns nodes and relationships with sample data."""
2708
- await Runner(
2709
- """
2710
- CREATE VIRTUAL (:Animal) AS {
2711
- UNWIND [
2712
- {id: 1, species: 'Cat', legs: 4},
2713
- {id: 2, species: 'Dog', legs: 4}
2714
- ] AS record
2715
- RETURN record.id AS id, record.species AS species, record.legs AS legs
2716
- }
2717
- """
2718
- ).run()
2719
- await Runner(
2720
- """
2721
- CREATE VIRTUAL (:Animal)-[:CHASES]-(:Animal) AS {
2722
- UNWIND [
2723
- {left_id: 2, right_id: 1, speed: 'fast'}
2724
- ] AS record
2725
- RETURN record.left_id AS left_id, record.right_id AS right_id, record.speed AS speed
2726
- }
2727
- """
2728
- ).run()
2729
-
2730
- runner = Runner(
2731
- "CALL schema() YIELD kind, label, type, from_label, to_label, properties, sample RETURN kind, label, type, from_label, to_label, properties, sample"
2732
- )
2733
- await runner.run()
2734
- results = runner.results
2735
-
2736
- animal = next((r for r in results if r.get("kind") == "Node" and r.get("label") == "Animal"), None)
2737
- assert animal is not None
2738
- assert animal["properties"] == ["species", "legs"]
2739
- assert animal["sample"] is not None
2740
- assert "id" not in animal["sample"]
2741
- assert "species" in animal["sample"]
2742
- assert "legs" in animal["sample"]
2743
-
2744
- chases = next((r for r in results if r.get("kind") == "Relationship" and r.get("type") == "CHASES"), None)
2745
- assert chases is not None
2746
- assert chases["from_label"] == "Animal"
2747
- assert chases["to_label"] == "Animal"
2748
- assert chases["properties"] == ["speed"]
2749
- assert chases["sample"] is not None
2750
- assert "left_id" not in chases["sample"]
2751
- assert "right_id" not in chases["sample"]
2752
- assert "speed" in chases["sample"]
2753
-
2754
- @pytest.mark.asyncio
2755
- async def test_reserved_keywords_as_identifiers(self):
2756
- """Test reserved keywords as identifiers."""
2757
- runner = Runner("""
2758
- WITH 1 AS return
2759
- RETURN return
2760
- """)
2761
- await runner.run()
2762
- results = runner.results
2763
- assert len(results) == 1
2764
- assert results[0]["return"] == 1
2765
-
2766
- @pytest.mark.asyncio
2767
- async def test_reserved_keywords_as_parts_of_identifiers(self):
2768
- """Test reserved keywords as parts of identifiers."""
2769
- runner = Runner("""
2770
- unwind [
2771
- {from: "Alice", to: "Bob", organizer: "Charlie"},
2772
- {from: "Bob", to: "Charlie", organizer: "Alice"},
2773
- {from: "Charlie", to: "Alice", organizer: "Bob"}
2774
- ] as data
2775
- return data.from as from, data.to as to, data.organizer as organizer
2776
- """)
2777
- await runner.run()
2778
- results = runner.results
2779
- assert len(results) == 3
2780
- assert results[0] == {"from": "Alice", "to": "Bob", "organizer": "Charlie"}
2781
- assert results[1] == {"from": "Bob", "to": "Charlie", "organizer": "Alice"}
2782
- assert results[2] == {"from": "Charlie", "to": "Alice", "organizer": "Bob"}
2783
-
2784
- @pytest.mark.asyncio
2785
- async def test_reserved_keywords_as_relationship_types_and_labels(self):
2786
- """Test reserved keywords as relationship types and labels."""
2787
- await Runner("""
2788
- CREATE VIRTUAL (:Return) AS {
2789
- unwind [
2790
- {id: 1, name: 'Node 1'},
2791
- {id: 2, name: 'Node 2'}
2792
- ] as record
2793
- RETURN record.id as id, record.name as name
2794
- }
2795
- """).run()
2796
- await Runner("""
2797
- CREATE VIRTUAL (:Return)-[:With]-(:Return) AS {
2798
- unwind [
2799
- {left_id: 1, right_id: 2}
2800
- ] as record
2801
- RETURN record.left_id as left_id, record.right_id as right_id
2802
- }
2803
- """).run()
2804
- runner = Runner("""
2805
- MATCH (a:Return)-[:With]->(b:Return)
2806
- RETURN a.name AS name1, b.name AS name2
2807
- """)
2808
- await runner.run()
2809
- results = runner.results
2810
- assert len(results) == 1
2811
- assert results[0] == {"name1": "Node 1", "name2": "Node 2"}
2812
-
2813
- @pytest.mark.asyncio
2814
- async def test_match_with_node_reference_passed_through_with(self):
2815
- """Test that node variables passed through WITH can be re-referenced in subsequent MATCH."""
2816
- await Runner("""
2817
- CREATE VIRTUAL (:WithRefUser) AS {
2818
- UNWIND [
2819
- {id: 1, name: 'Alice', mail: 'alice@test.com', jobTitle: 'CEO'},
2820
- {id: 2, name: 'Bob', mail: 'bob@test.com', jobTitle: 'VP'},
2821
- {id: 3, name: 'Carol', mail: 'carol@test.com', jobTitle: 'VP'},
2822
- {id: 4, name: 'Dave', mail: 'dave@test.com', jobTitle: 'Engineer'}
2823
- ] AS record
2824
- RETURN record.id AS id, record.name AS name, record.mail AS mail, record.jobTitle AS jobTitle
2825
- }
2826
- """).run()
2827
- await Runner("""
2828
- CREATE VIRTUAL (:WithRefUser)-[:MANAGES]-(:WithRefUser) AS {
2829
- UNWIND [
2830
- {left_id: 1, right_id: 2},
2831
- {left_id: 1, right_id: 3},
2832
- {left_id: 2, right_id: 4}
2833
- ] AS record
2834
- RETURN record.left_id AS left_id, record.right_id AS right_id
2835
- }
2836
- """).run()
2837
- runner = Runner("""
2838
- MATCH (ceo:WithRefUser)-[:MANAGES]->(dr1:WithRefUser)
2839
- WHERE ceo.jobTitle = 'CEO'
2840
- WITH ceo, dr1
2841
- MATCH (ceo)-[:MANAGES]->(dr2:WithRefUser)
2842
- WHERE dr1.mail <> dr2.mail
2843
- RETURN ceo.name AS ceo, dr1.name AS dr1, dr2.name AS dr2
2844
- """)
2845
- await runner.run()
2846
- results = runner.results
2847
- # CEO (Alice) manages Bob and Carol. All distinct pairs:
2848
- # (Alice, Bob, Carol) and (Alice, Carol, Bob)
2849
- assert len(results) == 2
2850
- assert results[0] == {"ceo": "Alice", "dr1": "Bob", "dr2": "Carol"}
2851
- assert results[1] == {"ceo": "Alice", "dr1": "Carol", "dr2": "Bob"}
2852
-
2853
- async def test_match_with_node_reference_reuse_with_label(self):
2854
- """Test that reusing a node variable with a label creates a NodeReference, not a new node."""
2855
- await Runner("""
2856
- CREATE VIRTUAL (:RefLabelUser) AS {
2857
- UNWIND [
2858
- {id: 1, name: 'Alice', jobTitle: 'CEO'},
2859
- {id: 2, name: 'Bob', jobTitle: 'VP'},
2860
- {id: 3, name: 'Carol', jobTitle: 'VP'},
2861
- {id: 4, name: 'Dave', jobTitle: 'Engineer'}
2862
- ] AS record
2863
- RETURN record.id AS id, record.name AS name, record.jobTitle AS jobTitle
2864
- }
2865
- """).run()
2866
- await Runner("""
2867
- CREATE VIRTUAL (:RefLabelUser)-[:MANAGES]-(:RefLabelUser) AS {
2868
- UNWIND [
2869
- {left_id: 1, right_id: 2},
2870
- {left_id: 1, right_id: 3},
2871
- {left_id: 2, right_id: 4}
2872
- ] AS record
2873
- RETURN record.left_id AS left_id, record.right_id AS right_id
2874
- }
2875
- """).run()
2876
- # Uses (ceo:RefLabelUser) with label in both MATCH clauses.
2877
- # Previously this would create a new node instead of a NodeReference.
2878
- runner = Runner("""
2879
- MATCH (ceo:RefLabelUser)-[:MANAGES]->(dr1:RefLabelUser)
2880
- WHERE ceo.jobTitle = 'CEO'
2881
- WITH ceo, dr1
2882
- MATCH (ceo:RefLabelUser)-[:MANAGES]->(dr2:RefLabelUser)
2883
- WHERE dr1.name <> dr2.name
2884
- RETURN ceo.name AS ceo, dr1.name AS dr1, dr2.name AS dr2
2885
- """)
2886
- await runner.run()
2887
- results = runner.results
2888
- assert len(results) == 2
2889
- assert results[0] == {"ceo": "Alice", "dr1": "Bob", "dr2": "Carol"}
2890
- assert results[1] == {"ceo": "Alice", "dr1": "Carol", "dr2": "Bob"}
2891
-
2892
- @pytest.mark.asyncio
2893
- async def test_where_with_is_null(self):
2894
- """Test WHERE with IS NULL."""
2895
- runner = Runner("""
2896
- unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: null}] as person
2897
- with person.name as name, person.age as age
2898
- where age IS NULL
2899
- return name
2900
- """)
2901
- await runner.run()
2902
- results = runner.results
2903
- assert len(results) == 1
2904
- assert results[0] == {"name": "Bob"}
2905
-
2906
- @pytest.mark.asyncio
2907
- async def test_where_with_is_not_null(self):
2908
- """Test WHERE with IS NOT NULL."""
2909
- runner = Runner("""
2910
- unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: null}] as person
2911
- with person.name as name, person.age as age
2912
- where age IS NOT NULL
2913
- return name, age
2914
- """)
2915
- await runner.run()
2916
- results = runner.results
2917
- assert len(results) == 1
2918
- assert results[0] == {"name": "Alice", "age": 30}
2919
-
2920
- @pytest.mark.asyncio
2921
- async def test_where_with_is_not_null_multiple_results(self):
2922
- """Test WHERE with IS NOT NULL filters multiple results."""
2923
- runner = Runner("""
2924
- unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: null}, {name: 'Carol', age: 25}] as person
2925
- with person.name as name, person.age as age
2926
- where age IS NOT NULL
2927
- return name, age
2928
- """)
2929
- await runner.run()
2930
- results = runner.results
2931
- assert len(results) == 2
2932
- assert results[0] == {"name": "Alice", "age": 30}
2933
- assert results[1] == {"name": "Carol", "age": 25}
2934
-
2935
- @pytest.mark.asyncio
2936
- async def test_where_with_in_list_check(self):
2937
- """Test WHERE with IN list check."""
2938
- runner = Runner("""
2939
- unwind range(1, 10) as n
2940
- with n
2941
- where n IN [2, 4, 6, 8]
2942
- return n
2943
- """)
2944
- await runner.run()
2945
- results = runner.results
2946
- assert len(results) == 4
2947
- assert [r["n"] for r in results] == [2, 4, 6, 8]
2948
-
2949
- @pytest.mark.asyncio
2950
- async def test_where_with_not_in_list_check(self):
2951
- """Test WHERE with NOT IN list check."""
2952
- runner = Runner("""
2953
- unwind range(1, 5) as n
2954
- with n
2955
- where n NOT IN [2, 4]
2956
- return n
2957
- """)
2958
- await runner.run()
2959
- results = runner.results
2960
- assert len(results) == 3
2961
- assert [r["n"] for r in results] == [1, 3, 5]
2962
-
2963
- @pytest.mark.asyncio
2964
- async def test_where_with_in_string_list(self):
2965
- """Test WHERE with IN string list."""
2966
- runner = Runner("""
2967
- unwind ['apple', 'banana', 'cherry', 'date'] as fruit
2968
- with fruit
2969
- where fruit IN ['banana', 'date']
2970
- return fruit
2971
- """)
2972
- await runner.run()
2973
- results = runner.results
2974
- assert len(results) == 2
2975
- assert [r["fruit"] for r in results] == ["banana", "date"]
2976
-
2977
- @pytest.mark.asyncio
2978
- async def test_where_with_in_combined_with_and(self):
2979
- """Test WHERE with IN combined with AND."""
2980
- runner = Runner("""
2981
- unwind range(1, 20) as n
2982
- with n
2983
- where n IN [1, 5, 10, 15, 20] AND n > 5
2984
- return n
2985
- """)
2986
- await runner.run()
2987
- results = runner.results
2988
- assert len(results) == 3
2989
- assert [r["n"] for r in results] == [10, 15, 20]
2990
-
2991
- @pytest.mark.asyncio
2992
- async def test_where_with_and_before_in(self):
2993
- """Test WHERE with AND before IN (IN on right side of AND)."""
2994
- runner = Runner("""
2995
- unwind ['expert', 'intermediate', 'beginner'] as proficiency
2996
- with proficiency where 1=1 and proficiency in ['expert']
2997
- return proficiency
2998
- """)
2999
- await runner.run()
3000
- results = runner.results
3001
- assert len(results) == 1
3002
- assert results[0] == {"proficiency": "expert"}
3003
-
3004
- @pytest.mark.asyncio
3005
- async def test_where_with_and_before_not_in(self):
3006
- """Test WHERE with AND before NOT IN."""
3007
- runner = Runner("""
3008
- unwind ['expert', 'intermediate', 'beginner'] as proficiency
3009
- with proficiency where 1=1 and proficiency not in ['expert']
3010
- return proficiency
3011
- """)
3012
- await runner.run()
3013
- results = runner.results
3014
- assert len(results) == 2
3015
- assert [r["proficiency"] for r in results] == ["intermediate", "beginner"]
3016
-
3017
- @pytest.mark.asyncio
3018
- async def test_where_with_or_before_in(self):
3019
- """Test WHERE with OR before IN."""
3020
- runner = Runner("""
3021
- unwind range(1, 10) as n
3022
- with n where 1=0 or n in [3, 7]
3023
- return n
3024
- """)
3025
- await runner.run()
3026
- results = runner.results
3027
- assert len(results) == 2
3028
- assert [r["n"] for r in results] == [3, 7]
3029
-
3030
- @pytest.mark.asyncio
3031
- async def test_in_as_return_expression_with_and_in_where(self):
3032
- """Test IN as return expression with AND in WHERE."""
3033
- runner = Runner("""
3034
- unwind ['expert', 'intermediate', 'beginner'] as proficiency
3035
- with proficiency where 1=1 and proficiency in ['expert']
3036
- return proficiency, proficiency in ['expert'] as isExpert
3037
- """)
3038
- await runner.run()
3039
- results = runner.results
3040
- assert len(results) == 1
3041
- assert results[0] == {"proficiency": "expert", "isExpert": 1}
3042
-
3043
- @pytest.mark.asyncio
3044
- async def test_where_with_contains(self):
3045
- """Test WHERE with CONTAINS."""
3046
- runner = Runner("""
3047
- unwind ['apple', 'banana', 'grape', 'pineapple'] as fruit
3048
- with fruit
3049
- where fruit CONTAINS 'apple'
3050
- return fruit
3051
- """)
3052
- await runner.run()
3053
- results = runner.results
3054
- assert len(results) == 2
3055
- assert [r["fruit"] for r in results] == ["apple", "pineapple"]
3056
-
3057
- @pytest.mark.asyncio
3058
- async def test_where_with_not_contains(self):
3059
- """Test WHERE with NOT CONTAINS."""
3060
- runner = Runner("""
3061
- unwind ['apple', 'banana', 'grape', 'pineapple'] as fruit
3062
- with fruit
3063
- where fruit NOT CONTAINS 'apple'
3064
- return fruit
3065
- """)
3066
- await runner.run()
3067
- results = runner.results
3068
- assert len(results) == 2
3069
- assert [r["fruit"] for r in results] == ["banana", "grape"]
3070
-
3071
- @pytest.mark.asyncio
3072
- async def test_where_with_starts_with(self):
3073
- """Test WHERE with STARTS WITH."""
3074
- runner = Runner("""
3075
- unwind ['apple', 'apricot', 'banana', 'avocado'] as fruit
3076
- with fruit
3077
- where fruit STARTS WITH 'ap'
3078
- return fruit
3079
- """)
3080
- await runner.run()
3081
- results = runner.results
3082
- assert len(results) == 2
3083
- assert [r["fruit"] for r in results] == ["apple", "apricot"]
3084
-
3085
- @pytest.mark.asyncio
3086
- async def test_where_with_not_starts_with(self):
3087
- """Test WHERE with NOT STARTS WITH."""
3088
- runner = Runner("""
3089
- unwind ['apple', 'apricot', 'banana', 'avocado'] as fruit
3090
- with fruit
3091
- where fruit NOT STARTS WITH 'ap'
3092
- return fruit
3093
- """)
3094
- await runner.run()
3095
- results = runner.results
3096
- assert len(results) == 2
3097
- assert [r["fruit"] for r in results] == ["banana", "avocado"]
3098
-
3099
- @pytest.mark.asyncio
3100
- async def test_where_with_ends_with(self):
3101
- """Test WHERE with ENDS WITH."""
3102
- runner = Runner("""
3103
- unwind ['apple', 'pineapple', 'banana', 'grape'] as fruit
3104
- with fruit
3105
- where fruit ENDS WITH 'ple'
3106
- return fruit
3107
- """)
3108
- await runner.run()
3109
- results = runner.results
3110
- assert len(results) == 2
3111
- assert [r["fruit"] for r in results] == ["apple", "pineapple"]
3112
-
3113
- @pytest.mark.asyncio
3114
- async def test_where_with_not_ends_with(self):
3115
- """Test WHERE with NOT ENDS WITH."""
3116
- runner = Runner("""
3117
- unwind ['apple', 'pineapple', 'banana', 'grape'] as fruit
3118
- with fruit
3119
- where fruit NOT ENDS WITH 'ple'
3120
- return fruit
3121
- """)
3122
- await runner.run()
3123
- results = runner.results
3124
- assert len(results) == 2
3125
- assert [r["fruit"] for r in results] == ["banana", "grape"]
3126
-
3127
- @pytest.mark.asyncio
3128
- async def test_where_with_contains_combined_with_and(self):
3129
- """Test WHERE with CONTAINS combined with AND."""
3130
- runner = Runner("""
3131
- unwind ['apple', 'pineapple', 'applesauce', 'banana'] as fruit
3132
- with fruit
3133
- where fruit CONTAINS 'apple' AND fruit STARTS WITH 'pine'
3134
- return fruit
3135
- """)
3136
- await runner.run()
3137
- results = runner.results
3138
- assert len(results) == 1
3139
- assert results[0]["fruit"] == "pineapple"
3140
-
3141
- @pytest.mark.asyncio
3142
- async def test_collected_nodes_and_re_matching(self):
3143
- """Test that collected nodes can be unwound and used as node references in subsequent MATCH."""
3144
- await Runner("""
3145
- CREATE VIRTUAL (:Person) AS {
3146
- unwind [
3147
- {id: 1, name: 'Person 1'},
3148
- {id: 2, name: 'Person 2'},
3149
- {id: 3, name: 'Person 3'},
3150
- {id: 4, name: 'Person 4'}
3151
- ] as record
3152
- RETURN record.id as id, record.name as name
3153
- }
3154
- """).run()
3155
- await Runner("""
3156
- CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
3157
- unwind [
3158
- {left_id: 1, right_id: 2},
3159
- {left_id: 2, right_id: 3},
3160
- {left_id: 3, right_id: 4}
3161
- ] as record
3162
- RETURN record.left_id as left_id, record.right_id as right_id
3163
- }
3164
- """).run()
3165
- runner = Runner("""
3166
- MATCH (a:Person)-[:KNOWS*0..3]->(b:Person)
3167
- WITH collect(a) AS persons, b
3168
- UNWIND persons AS p
3169
- match (p)-[:KNOWS]->(:Person)
3170
- return p.name AS name
3171
- """)
3172
- await runner.run()
3173
- results = runner.results
3174
- assert len(results) == 9
3175
- names = [r["name"] for r in results]
3176
- assert "Person 1" in names
3177
- assert "Person 2" in names
3178
- assert "Person 3" in names
3179
-
3180
- # ============================================================
3181
- # Add operator tests
3182
- # ============================================================
3183
-
3184
- @pytest.mark.asyncio
3185
- async def test_collected_patterns_and_unwind(self):
3186
- """Test collecting graph patterns and unwinding them."""
3187
- await Runner("""
3188
- CREATE VIRTUAL (:Person) AS {
3189
- unwind [
3190
- {id: 1, name: 'Person 1'},
3191
- {id: 2, name: 'Person 2'},
3192
- {id: 3, name: 'Person 3'},
3193
- {id: 4, name: 'Person 4'}
3194
- ] as record
3195
- RETURN record.id as id, record.name as name
3196
- }
3197
- """).run()
3198
- await Runner("""
3199
- CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
3200
- unwind [
3201
- {left_id: 1, right_id: 2},
3202
- {left_id: 2, right_id: 3},
3203
- {left_id: 3, right_id: 4}
3204
- ] as record
3205
- RETURN record.left_id as left_id, record.right_id as right_id
3206
- }
3207
- """).run()
3208
- runner = Runner("""
3209
- MATCH p=(a:Person)-[:KNOWS*0..3]->(b:Person)
3210
- WITH collect(p) AS patterns
3211
- UNWIND patterns AS pattern
3212
- RETURN pattern
3213
- """)
3214
- await runner.run()
3215
- results = runner.results
3216
- assert len(results) == 10
3217
- # Index 0: Person 1 zero-hop - pattern = [node1] (single node)
3218
- assert len(results[0]["pattern"]) == 1
3219
- assert results[0]["pattern"][0]["id"] == 1
3220
- # Index 1: Person 1 -> Person 2 (1-hop)
3221
- assert len(results[1]["pattern"]) == 3
3222
- # Index 2: Person 1 -> Person 2 -> Person 3 (2-hop)
3223
- assert len(results[2]["pattern"]) == 5
3224
- # Index 3: Person 1 -> Person 2 -> Person 3 -> Person 4 (3-hop)
3225
- assert len(results[3]["pattern"]) == 7
3226
- # Index 4: Person 2 zero-hop
3227
- assert len(results[4]["pattern"]) == 1
3228
- assert results[4]["pattern"][0]["id"] == 2
3229
- # Index 5: Person 2 -> Person 3 (1-hop)
3230
- assert len(results[5]["pattern"]) == 3
3231
- # Index 6: Person 2 -> Person 3 -> Person 4 (2-hop)
3232
- assert len(results[6]["pattern"]) == 5
3233
- # Index 7: Person 3 zero-hop
3234
- assert len(results[7]["pattern"]) == 1
3235
- assert results[7]["pattern"][0]["id"] == 3
3236
- # Index 8: Person 3 -> Person 4 (1-hop)
3237
- assert len(results[8]["pattern"]) == 3
3238
- # Index 9: Person 4 zero-hop
3239
- assert len(results[9]["pattern"]) == 1
3240
- assert results[9]["pattern"][0]["id"] == 4
3241
-
3242
- @pytest.mark.asyncio
3243
- async def test_add_two_integers(self):
3244
- """Test add two integers."""
3245
- runner = Runner("return 1 + 2 as result")
3246
- await runner.run()
3247
- results = runner.results
3248
- assert len(results) == 1
3249
- assert results[0] == {"result": 3}
3250
-
3251
- @pytest.mark.asyncio
3252
- async def test_add_negative_number(self):
3253
- """Test add with a negative number."""
3254
- runner = Runner("return -3 + 7 as result")
3255
- await runner.run()
3256
- results = runner.results
3257
- assert len(results) == 1
3258
- assert results[0] == {"result": 4}
3259
-
3260
- @pytest.mark.asyncio
3261
- async def test_add_to_negative_result(self):
3262
- """Test add to negative result."""
3263
- runner = Runner("return 0 - 10 + 4 as result")
3264
- await runner.run()
3265
- results = runner.results
3266
- assert len(results) == 1
3267
- assert results[0] == {"result": -6}
3268
-
3269
- @pytest.mark.asyncio
3270
- async def test_add_zero(self):
3271
- """Test add zero."""
3272
- runner = Runner("return 42 + 0 as result")
3273
- await runner.run()
3274
- results = runner.results
3275
- assert len(results) == 1
3276
- assert results[0] == {"result": 42}
3277
-
3278
- @pytest.mark.asyncio
3279
- async def test_add_floating_point_numbers(self):
3280
- """Test add floating point numbers."""
3281
- runner = Runner("return 1.5 + 2.3 as result")
3282
- await runner.run()
3283
- results = runner.results
3284
- assert len(results) == 1
3285
- assert results[0]["result"] == pytest.approx(3.8)
3286
-
3287
- @pytest.mark.asyncio
3288
- async def test_add_integer_and_float(self):
3289
- """Test add integer and float."""
3290
- runner = Runner("return 1 + 0.5 as result")
3291
- await runner.run()
3292
- results = runner.results
3293
- assert len(results) == 1
3294
- assert results[0]["result"] == pytest.approx(1.5)
3295
-
3296
- @pytest.mark.asyncio
3297
- async def test_add_strings(self):
3298
- """Test add strings."""
3299
- runner = Runner('return "hello" + " world" as result')
3300
- await runner.run()
3301
- results = runner.results
3302
- assert len(results) == 1
3303
- assert results[0] == {"result": "hello world"}
3304
-
3305
- @pytest.mark.asyncio
3306
- async def test_add_empty_strings(self):
3307
- """Test add empty strings."""
3308
- runner = Runner('return "" + "" as result')
3309
- await runner.run()
3310
- results = runner.results
3311
- assert len(results) == 1
3312
- assert results[0] == {"result": ""}
3313
-
3314
- @pytest.mark.asyncio
3315
- async def test_add_string_and_empty_string(self):
3316
- """Test add string and empty string."""
3317
- runner = Runner('return "hello" + "" as result')
3318
- await runner.run()
3319
- results = runner.results
3320
- assert len(results) == 1
3321
- assert results[0] == {"result": "hello"}
3322
-
3323
- @pytest.mark.asyncio
3324
- async def test_add_two_lists(self):
3325
- """Test add two lists."""
3326
- runner = Runner("return [1, 2] + [3, 4] as result")
3327
- await runner.run()
3328
- results = runner.results
3329
- assert len(results) == 1
3330
- assert results[0] == {"result": [1, 2, 3, 4]}
3331
-
3332
- @pytest.mark.asyncio
3333
- async def test_add_empty_list_to_list(self):
3334
- """Test add empty list to list."""
3335
- runner = Runner("return [1, 2, 3] + [] as result")
3336
- await runner.run()
3337
- results = runner.results
3338
- assert len(results) == 1
3339
- assert results[0] == {"result": [1, 2, 3]}
3340
-
3341
- @pytest.mark.asyncio
3342
- async def test_add_two_empty_lists(self):
3343
- """Test add two empty lists."""
3344
- runner = Runner("return [] + [] as result")
3345
- await runner.run()
3346
- results = runner.results
3347
- assert len(results) == 1
3348
- assert results[0] == {"result": []}
3349
-
3350
- @pytest.mark.asyncio
3351
- async def test_add_lists_with_mixed_types(self):
3352
- """Test add lists with mixed types."""
3353
- runner = Runner('return [1, "a"] + [2, "b"] as result')
3354
- await runner.run()
3355
- results = runner.results
3356
- assert len(results) == 1
3357
- assert results[0] == {"result": [1, "a", 2, "b"]}
3358
-
3359
- @pytest.mark.asyncio
3360
- async def test_add_chained_three_numbers(self):
3361
- """Test add chained three numbers."""
3362
- runner = Runner("return 1 + 2 + 3 as result")
3363
- await runner.run()
3364
- results = runner.results
3365
- assert len(results) == 1
3366
- assert results[0] == {"result": 6}
3367
-
3368
- @pytest.mark.asyncio
3369
- async def test_add_chained_multiple_numbers(self):
3370
- """Test add chained multiple numbers."""
3371
- runner = Runner("return 10 + 20 + 30 + 40 as result")
3372
- await runner.run()
3373
- results = runner.results
3374
- assert len(results) == 1
3375
- assert results[0] == {"result": 100}
3376
-
3377
- @pytest.mark.asyncio
3378
- async def test_add_large_numbers(self):
3379
- """Test add large numbers."""
3380
- runner = Runner("return 1000000 + 2000000 as result")
3381
- await runner.run()
3382
- results = runner.results
3383
- assert len(results) == 1
3384
- assert results[0] == {"result": 3000000}
3385
-
3386
- @pytest.mark.asyncio
3387
- async def test_add_with_unwind(self):
3388
- """Test add with unwind."""
3389
- runner = Runner("unwind [1, 2, 3] as x return x + 10 as result")
3390
- await runner.run()
3391
- results = runner.results
3392
- assert len(results) == 3
3393
- assert results[0] == {"result": 11}
3394
- assert results[1] == {"result": 12}
3395
- assert results[2] == {"result": 13}
3396
-
3397
- @pytest.mark.asyncio
3398
- async def test_add_with_multiple_return_expressions(self):
3399
- """Test add with multiple return expressions."""
3400
- runner = Runner("return 1 + 2 as sum1, 3 + 4 as sum2, 5 + 6 as sum3")
3401
- await runner.run()
3402
- results = runner.results
3403
- assert len(results) == 1
3404
- assert results[0] == {"sum1": 3, "sum2": 7, "sum3": 11}
3405
-
3406
- @pytest.mark.asyncio
3407
- async def test_add_mixed_with_other_operators(self):
3408
- """Test add mixed with other operators (precedence)."""
3409
- runner = Runner("return 2 + 3 * 4 as result")
3410
- await runner.run()
3411
- results = runner.results
3412
- assert len(results) == 1
3413
- assert results[0] == {"result": 14}
3414
-
3415
- @pytest.mark.asyncio
3416
- async def test_add_with_parentheses(self):
3417
- """Test add with parentheses."""
3418
- runner = Runner("return (2 + 3) * 4 as result")
3419
- await runner.run()
3420
- results = runner.results
3421
- assert len(results) == 1
3422
- assert results[0] == {"result": 20}
3423
-
3424
- @pytest.mark.asyncio
3425
- async def test_add_nested_lists(self):
3426
- """Test add nested lists."""
3427
- runner = Runner("return [[1, 2]] + [[3, 4]] as result")
3428
- await runner.run()
3429
- results = runner.results
3430
- assert len(results) == 1
3431
- assert results[0] == {"result": [[1, 2], [3, 4]]}
3432
-
3433
- @pytest.mark.asyncio
3434
- async def test_add_with_with_clause(self):
3435
- """Test add with with clause."""
3436
- runner = Runner("with 5 as a, 10 as b return a + b as result")
3437
- await runner.run()
3438
- results = runner.results
3439
- assert len(results) == 1
3440
- assert results[0] == {"result": 15}
3441
-
3442
- # ============================================================
3443
- # UNION and UNION ALL tests
3444
- # ============================================================
3445
-
3446
- @pytest.mark.asyncio
3447
- async def test_union_with_simple_values(self):
3448
- """Test UNION with simple values."""
3449
- runner = Runner("WITH 1 AS x RETURN x UNION WITH 2 AS x RETURN x")
3450
- await runner.run()
3451
- results = runner.results
3452
- assert len(results) == 2
3453
- assert results == [{"x": 1}, {"x": 2}]
3454
-
3455
- @pytest.mark.asyncio
3456
- async def test_union_removes_duplicates(self):
3457
- """Test UNION removes duplicates."""
3458
- runner = Runner("WITH 1 AS x RETURN x UNION WITH 1 AS x RETURN x")
3459
- await runner.run()
3460
- results = runner.results
3461
- assert len(results) == 1
3462
- assert results == [{"x": 1}]
3463
-
3464
- @pytest.mark.asyncio
3465
- async def test_union_all_keeps_duplicates(self):
3466
- """Test UNION ALL keeps duplicates."""
3467
- runner = Runner("WITH 1 AS x RETURN x UNION ALL WITH 1 AS x RETURN x")
3468
- await runner.run()
3469
- results = runner.results
3470
- assert len(results) == 2
3471
- assert results == [{"x": 1}, {"x": 1}]
3472
-
3473
- @pytest.mark.asyncio
3474
- async def test_union_with_multiple_columns(self):
3475
- """Test UNION with multiple columns."""
3476
- runner = Runner(
3477
- "WITH 1 AS a, 'hello' AS b RETURN a, b UNION WITH 2 AS a, 'world' AS b RETURN a, b"
3478
- )
3479
- await runner.run()
3480
- results = runner.results
3481
- assert len(results) == 2
3482
- assert results == [
3483
- {"a": 1, "b": "hello"},
3484
- {"a": 2, "b": "world"},
3485
- ]
3486
-
3487
- @pytest.mark.asyncio
3488
- async def test_union_all_with_multiple_columns(self):
3489
- """Test chained UNION ALL with three branches."""
3490
- runner = Runner(
3491
- "WITH 1 AS a RETURN a UNION ALL WITH 2 AS a RETURN a UNION ALL WITH 3 AS a RETURN a"
3492
- )
3493
- await runner.run()
3494
- results = runner.results
3495
- assert len(results) == 3
3496
- assert results == [{"a": 1}, {"a": 2}, {"a": 3}]
3497
-
3498
- @pytest.mark.asyncio
3499
- async def test_chained_union_removes_duplicates(self):
3500
- """Test chained UNION removes duplicates across all branches."""
3501
- runner = Runner(
3502
- "WITH 1 AS x RETURN x UNION WITH 2 AS x RETURN x UNION WITH 1 AS x RETURN x"
3503
- )
3504
- await runner.run()
3505
- results = runner.results
3506
- assert len(results) == 2
3507
- assert results == [{"x": 1}, {"x": 2}]
3508
-
3509
- @pytest.mark.asyncio
3510
- async def test_union_with_unwind(self):
3511
- """Test UNION with UNWIND."""
3512
- runner = Runner(
3513
- "UNWIND [1, 2] AS x RETURN x UNION UNWIND [3, 4] AS x RETURN x"
3514
- )
3515
- await runner.run()
3516
- results = runner.results
3517
- assert len(results) == 4
3518
- assert results == [{"x": 1}, {"x": 2}, {"x": 3}, {"x": 4}]
3519
-
3520
- @pytest.mark.asyncio
3521
- async def test_union_with_mismatched_columns(self):
3522
- """Test UNION with mismatched columns throws error."""
3523
- runner = Runner("WITH 1 AS x RETURN x UNION WITH 2 AS y RETURN y")
3524
- with pytest.raises(ValueError, match="All sub queries in a UNION must have the same return column names"):
3525
- await runner.run()
3526
-
3527
- @pytest.mark.asyncio
3528
- async def test_union_with_empty_left_side(self):
3529
- """Test UNION with empty left side."""
3530
- runner = Runner(
3531
- "UNWIND [] AS x RETURN x UNION WITH 1 AS x RETURN x"
3532
- )
3533
- await runner.run()
3534
- results = runner.results
3535
- assert len(results) == 1
3536
- assert results == [{"x": 1}]
3537
-
3538
- @pytest.mark.asyncio
3539
- async def test_union_with_empty_right_side(self):
3540
- """Test UNION with empty right side."""
3541
- runner = Runner(
3542
- "WITH 1 AS x RETURN x UNION UNWIND [] AS x RETURN x"
3543
- )
3544
- await runner.run()
3545
- results = runner.results
3546
- assert len(results) == 1
3547
- assert results == [{"x": 1}]
3548
-
3549
- @pytest.mark.asyncio
3550
- async def test_language_name_hits_query_with_virtual_graph(self):
3551
- """Test full language-name-hits query with virtual graph.
3552
-
3553
- Reproduces the original bug: collect(distinct ...) on MATCH results,
3554
- then sum(lang IN langs | ...) in a WITH clause, was throwing
3555
- "Invalid array for sum function" because collect() returned null
3556
- instead of [] when no rows entered aggregation.
3557
- """
3558
- # Create Language nodes
3559
- await Runner(
3560
- """
3561
- CREATE VIRTUAL (:Language) AS {
3562
- UNWIND [
3563
- {id: 1, name: 'Python'},
3564
- {id: 2, name: 'JavaScript'},
3565
- {id: 3, name: 'TypeScript'}
3566
- ] AS record
3567
- RETURN record.id AS id, record.name AS name
3568
- }
3569
- """
3570
- ).run()
3571
-
3572
- # Create Chat nodes with messages
3573
- await Runner(
3574
- """
3575
- CREATE VIRTUAL (:Chat) AS {
3576
- UNWIND [
3577
- {id: 1, name: 'Dev Discussion', messages: [
3578
- {From: 'Alice', SentDateTime: '2025-01-01T10:00:00', Content: 'I love Python and JavaScript'},
3579
- {From: 'Bob', SentDateTime: '2025-01-01T10:05:00', Content: 'What languages do you prefer?'}
3580
- ]},
3581
- {id: 2, name: 'General', messages: [
3582
- {From: 'Charlie', SentDateTime: '2025-01-02T09:00:00', Content: 'The weather is nice today'},
3583
- {From: 'Alice', SentDateTime: '2025-01-02T09:05:00', Content: 'TypeScript is great for language tooling'}
3584
- ]}
3585
- ] AS record
3586
- RETURN record.id AS id, record.name AS name, record.messages AS messages
3587
- }
3588
- """
3589
- ).run()
3590
-
3591
- # Create User nodes
3592
- await Runner(
3593
- """
3594
- CREATE VIRTUAL (:User) AS {
3595
- UNWIND [
3596
- {id: 1, displayName: 'Alice'},
3597
- {id: 2, displayName: 'Bob'},
3598
- {id: 3, displayName: 'Charlie'}
3599
- ] AS record
3600
- RETURN record.id AS id, record.displayName AS displayName
3601
- }
3602
- """
3603
- ).run()
3604
-
3605
- # Create PARTICIPATES_IN relationships
3606
- await Runner(
3607
- """
3608
- CREATE VIRTUAL (:User)-[:PARTICIPATES_IN]-(:Chat) AS {
3609
- UNWIND [
3610
- {left_id: 1, right_id: 1},
3611
- {left_id: 2, right_id: 1},
3612
- {left_id: 3, right_id: 2},
3613
- {left_id: 1, right_id: 2}
3614
- ] AS record
3615
- RETURN record.left_id AS left_id, record.right_id AS right_id
3616
- }
3617
- """
3618
- ).run()
3619
-
3620
- # Run the original query (using 'sender' alias since 'from' is a reserved keyword)
3621
- runner = Runner(
3622
- """
3623
- MATCH (l:Language)
3624
- WITH collect(distinct l.name) AS langs
3625
- MATCH (c:Chat)
3626
- UNWIND c.messages AS msg
3627
- WITH c, msg, langs,
3628
- sum(lang IN langs | 1 where toLower(msg.Content) CONTAINS toLower(lang)) AS langNameHits
3629
- WHERE toLower(msg.Content) CONTAINS "language"
3630
- OR toLower(msg.Content) CONTAINS "languages"
3631
- OR langNameHits > 0
3632
- OPTIONAL MATCH (u:User)-[:PARTICIPATES_IN]->(c)
3633
- RETURN
3634
- c.name AS chat,
3635
- collect(distinct u.displayName) AS participants,
3636
- msg.From AS sender,
3637
- msg.SentDateTime AS sentDateTime,
3638
- msg.Content AS message
3639
- """
3640
- )
3641
- await runner.run()
3642
- results = runner.results
3643
-
3644
- # Messages that mention a language name or the word "language(s)":
3645
- # 1. "I love Python and JavaScript" - langNameHits=2
3646
- # 2. "What languages do you prefer?" - contains "languages"
3647
- # 3. "TypeScript is great for language tooling" - langNameHits=1, also "language"
3648
- assert len(results) == 3
3649
- assert results[0]["chat"] == "Dev Discussion"
3650
- assert results[0]["message"] == "I love Python and JavaScript"
3651
- assert results[0]["sender"] == "Alice"
3652
- assert results[1]["chat"] == "Dev Discussion"
3653
- assert results[1]["message"] == "What languages do you prefer?"
3654
- assert results[1]["sender"] == "Bob"
3655
- assert results[2]["chat"] == "General"
3656
- assert results[2]["message"] == "TypeScript is great for language tooling"
3657
- assert results[2]["sender"] == "Alice"
3658
-
3659
- @pytest.mark.asyncio
3660
- async def test_sum_with_empty_collected_array(self):
3661
- """Reproduces the original bug: collect on empty input should yield []
3662
- and sum over that empty array should return 0, not throw."""
3663
- runner = Runner(
3664
- """
3665
- UNWIND [] AS lang
3666
- WITH collect(distinct lang) AS langs
3667
- UNWIND ['hello', 'world'] AS msg
3668
- WITH msg, langs, sum(l IN langs | 1 where toLower(msg) CONTAINS toLower(l)) AS hits
3669
- RETURN msg, hits
3670
- """
3671
- )
3672
- await runner.run()
3673
- results = runner.results
3674
- assert len(results) == 2
3675
- assert results[0] == {"msg": "hello", "hits": 0}
3676
- assert results[1] == {"msg": "world", "hits": 0}
3677
-
3678
- @pytest.mark.asyncio
3679
- async def test_sum_where_all_elements_filtered_returns_0(self):
3680
- """Test sum returns 0 when where clause filters everything."""
3681
- runner = Runner("RETURN sum(n in [1, 2, 3] | n where n > 100) as sum")
3682
- await runner.run()
3683
- results = runner.results
3684
- assert len(results) == 1
3685
- assert results[0] == {"sum": 0}
3686
-
3687
- @pytest.mark.asyncio
3688
- async def test_sum_over_empty_array_returns_0(self):
3689
- """Test sum over empty array returns 0."""
3690
- runner = Runner("WITH [] AS arr RETURN sum(n in arr | n) as sum")
3691
- await runner.run()
3692
- results = runner.results
3693
- assert len(results) == 1
3694
- assert results[0] == {"sum": 0}
3695
-
3696
- @pytest.mark.asyncio
3697
- async def test_relationship_properties_direct_dot_notation(self):
3698
- """Test relationship properties can be accessed directly via dot notation."""
3699
- await Runner(
3700
- """
3701
- CREATE VIRTUAL (:RCity) AS {
3702
- unwind [
3703
- {id: 1, name: 'NYC'},
3704
- {id: 2, name: 'LA'}
3705
- ] as record
3706
- RETURN record.id as id, record.name as name
3707
- }
3708
- """
3709
- ).run()
3710
- await Runner(
3711
- """
3712
- CREATE VIRTUAL (:RCity)-[:RFLIGHT]-(:RCity) AS {
3713
- unwind [
3714
- {left_id: 1, right_id: 2, airline: 'Delta', duration: 5}
3715
- ] as record
3716
- RETURN record.left_id as left_id, record.right_id as right_id, record.airline as airline, record.duration as duration
3717
- }
3718
- """
3719
- ).run()
3720
- match = Runner(
3721
- """
3722
- MATCH (a:RCity)-[r:RFLIGHT]->(b:RCity)
3723
- RETURN a.name AS from, b.name AS to, r.airline AS airline, r.duration AS duration
3724
- """
3725
- )
3726
- await match.run()
3727
- results = match.results
3728
- assert len(results) == 1
3729
- assert results[0] == {"from": "NYC", "to": "LA", "airline": "Delta", "duration": 5}
3730
-
3731
- @pytest.mark.asyncio
3732
- async def test_relationship_properties_direct_and_via_properties_function(self):
3733
- """Test relationship properties accessible via both direct access and properties()."""
3734
- await Runner(
3735
- """
3736
- CREATE VIRTUAL (:RPerson) AS {
3737
- unwind [
3738
- {id: 1, name: 'Alice'},
3739
- {id: 2, name: 'Bob'}
3740
- ] as record
3741
- RETURN record.id as id, record.name as name
3742
- }
3743
- """
3744
- ).run()
3745
- await Runner(
3746
- """
3747
- CREATE VIRTUAL (:RPerson)-[:RKNOWS]-(:RPerson) AS {
3748
- unwind [
3749
- {left_id: 1, right_id: 2, since: 2020, strength: 'strong'}
3750
- ] as record
3751
- RETURN record.left_id as left_id, record.right_id as right_id, record.since as since, record.strength as strength
3752
- }
3753
- """
3754
- ).run()
3755
- match = Runner(
3756
- """
3757
- MATCH (a:RPerson)-[r:RKNOWS]->(b:RPerson)
3758
- RETURN a.name AS from, b.name AS to, r.since AS since, r.strength AS strength, properties(r).since AS propSince
3759
- """
3760
- )
3761
- await match.run()
3762
- results = match.results
3763
- assert len(results) == 1
3764
- assert results[0] == {"from": "Alice", "to": "Bob", "since": 2020, "strength": "strong", "propSince": 2020}
3765
-
3766
- @pytest.mark.asyncio
3767
- async def test_coalesce_returns_first_non_null(self):
3768
- """Test coalesce returns first non-null value."""
3769
- runner = Runner("RETURN coalesce(null, null, 'hello', 'world') as result")
3770
- await runner.run()
3771
- results = runner.results
3772
- assert len(results) == 1
3773
- assert results[0] == {"result": "hello"}
3774
-
3775
- @pytest.mark.asyncio
3776
- async def test_coalesce_returns_first_argument_when_not_null(self):
3777
- """Test coalesce returns first argument when not null."""
3778
- runner = Runner("RETURN coalesce('first', 'second') as result")
3779
- await runner.run()
3780
- results = runner.results
3781
- assert len(results) == 1
3782
- assert results[0] == {"result": "first"}
3783
-
3784
- @pytest.mark.asyncio
3785
- async def test_coalesce_returns_null_when_all_null(self):
3786
- """Test coalesce returns null when all arguments are null."""
3787
- runner = Runner("RETURN coalesce(null, null, null) as result")
3788
- await runner.run()
3789
- results = runner.results
3790
- assert len(results) == 1
3791
- assert results[0] == {"result": None}
3792
-
3793
- @pytest.mark.asyncio
3794
- async def test_coalesce_with_single_non_null_argument(self):
3795
- """Test coalesce with single non-null argument."""
3796
- runner = Runner("RETURN coalesce(42) as result")
3797
- await runner.run()
3798
- results = runner.results
3799
- assert len(results) == 1
3800
- assert results[0] == {"result": 42}
3801
-
3802
- @pytest.mark.asyncio
3803
- async def test_coalesce_with_mixed_types(self):
3804
- """Test coalesce with mixed types."""
3805
- runner = Runner("RETURN coalesce(null, 42, 'hello') as result")
3806
- await runner.run()
3807
- results = runner.results
3808
- assert len(results) == 1
3809
- assert results[0] == {"result": 42}
3810
-
3811
- @pytest.mark.asyncio
3812
- async def test_coalesce_with_property_access(self):
3813
- """Test coalesce with property access."""
3814
- runner = Runner("WITH {name: 'Alice'} AS person RETURN coalesce(person.nickname, person.name) as result")
3815
- await runner.run()
3816
- results = runner.results
3817
- assert len(results) == 1
3818
- assert results[0] == {"result": "Alice"}
3819
-
3820
- # ============================================================
3821
- # Temporal / Time Functions
3822
- # ============================================================
3823
-
3824
- @pytest.mark.asyncio
3825
- async def test_datetime_returns_current_datetime_object(self):
3826
- """Test datetime() returns current datetime object."""
3827
- import time
3828
- before = int(time.time() * 1000)
3829
- runner = Runner("RETURN datetime() AS dt")
3830
- await runner.run()
3831
- after = int(time.time() * 1000)
3832
- results = runner.results
3833
- assert len(results) == 1
3834
- dt = results[0]["dt"]
3835
- assert dt is not None
3836
- assert isinstance(dt["year"], int)
3837
- assert isinstance(dt["month"], int)
3838
- assert isinstance(dt["day"], int)
3839
- assert isinstance(dt["hour"], int)
3840
- assert isinstance(dt["minute"], int)
3841
- assert isinstance(dt["second"], int)
3842
- assert isinstance(dt["millisecond"], int)
3843
- assert isinstance(dt["epochMillis"], int)
3844
- assert isinstance(dt["epochSeconds"], int)
3845
- assert isinstance(dt["dayOfWeek"], int)
3846
- assert isinstance(dt["dayOfYear"], int)
3847
- assert isinstance(dt["quarter"], int)
3848
- assert isinstance(dt["formatted"], str)
3849
- # epochMillis should be between before and after
3850
- assert dt["epochMillis"] >= before
3851
- assert dt["epochMillis"] <= after
3852
-
3853
- @pytest.mark.asyncio
3854
- async def test_datetime_with_iso_string_argument(self):
3855
- """Test datetime() with ISO string argument."""
3856
- runner = Runner("RETURN datetime('2025-06-15T12:30:45.123Z') AS dt")
3857
- await runner.run()
3858
- results = runner.results
3859
- assert len(results) == 1
3860
- dt = results[0]["dt"]
3861
- assert dt["year"] == 2025
3862
- assert dt["month"] == 6
3863
- assert dt["day"] == 15
3864
- assert dt["hour"] == 12
3865
- assert dt["minute"] == 30
3866
- assert dt["second"] == 45
3867
- assert dt["millisecond"] == 123
3868
- assert dt["formatted"] == "2025-06-15T12:30:45.123Z"
3869
-
3870
- @pytest.mark.asyncio
3871
- async def test_datetime_property_access(self):
3872
- """Test datetime() property access."""
3873
- runner = Runner(
3874
- "WITH datetime('2025-06-15T12:30:45.123Z') AS dt RETURN dt.year AS year, dt.month AS month, dt.day AS day"
3875
- )
3876
- await runner.run()
3877
- results = runner.results
3878
- assert len(results) == 1
3879
- assert results[0] == {"year": 2025, "month": 6, "day": 15}
3880
-
3881
- @pytest.mark.asyncio
3882
- async def test_date_returns_current_date_object(self):
3883
- """Test date() returns current date object."""
3884
- runner = Runner("RETURN date() AS d")
3885
- await runner.run()
3886
- results = runner.results
3887
- assert len(results) == 1
3888
- d = results[0]["d"]
3889
- assert d is not None
3890
- assert isinstance(d["year"], int)
3891
- assert isinstance(d["month"], int)
3892
- assert isinstance(d["day"], int)
3893
- assert isinstance(d["epochMillis"], int)
3894
- assert isinstance(d["dayOfWeek"], int)
3895
- assert isinstance(d["dayOfYear"], int)
3896
- assert isinstance(d["quarter"], int)
3897
- assert isinstance(d["formatted"], str)
3898
- # Should not have time fields
3899
- assert "hour" not in d
3900
- assert "minute" not in d
3901
-
3902
- @pytest.mark.asyncio
3903
- async def test_date_with_iso_date_string(self):
3904
- """Test date() with ISO date string."""
3905
- runner = Runner("RETURN date('2025-06-15') AS d")
3906
- await runner.run()
3907
- results = runner.results
3908
- assert len(results) == 1
3909
- d = results[0]["d"]
3910
- assert d["year"] == 2025
3911
- assert d["month"] == 6
3912
- assert d["day"] == 15
3913
- assert d["formatted"] == "2025-06-15"
3914
-
3915
- @pytest.mark.asyncio
3916
- async def test_date_dayofweek_and_quarter(self):
3917
- """Test date() dayOfWeek and quarter."""
3918
- # 2025-06-15 is a Sunday
3919
- runner = Runner("RETURN date('2025-06-15') AS d")
3920
- await runner.run()
3921
- d = runner.results[0]["d"]
3922
- assert d["dayOfWeek"] == 7 # Sunday = 7 in ISO
3923
- assert d["quarter"] == 2 # June = Q2
3924
-
3925
- @pytest.mark.asyncio
3926
- async def test_time_returns_current_utc_time(self):
3927
- """Test time() returns current UTC time."""
3928
- runner = Runner("RETURN time() AS t")
3929
- await runner.run()
3930
- results = runner.results
3931
- assert len(results) == 1
3932
- t = results[0]["t"]
3933
- assert isinstance(t["hour"], int)
3934
- assert isinstance(t["minute"], int)
3935
- assert isinstance(t["second"], int)
3936
- assert isinstance(t["millisecond"], int)
3937
- assert isinstance(t["formatted"], str)
3938
- assert t["formatted"].endswith("Z") # UTC time ends in Z
3939
-
3940
- @pytest.mark.asyncio
3941
- async def test_localtime_returns_current_local_time(self):
3942
- """Test localtime() returns current local time."""
3943
- runner = Runner("RETURN localtime() AS t")
3944
- await runner.run()
3945
- results = runner.results
3946
- assert len(results) == 1
3947
- t = results[0]["t"]
3948
- assert isinstance(t["hour"], int)
3949
- assert isinstance(t["minute"], int)
3950
- assert isinstance(t["second"], int)
3951
- assert isinstance(t["millisecond"], int)
3952
- assert isinstance(t["formatted"], str)
3953
- assert not t["formatted"].endswith("Z") # Local time does not end in Z
3954
-
3955
- @pytest.mark.asyncio
3956
- async def test_localdatetime_returns_current_local_datetime(self):
3957
- """Test localdatetime() returns current local datetime."""
3958
- runner = Runner("RETURN localdatetime() AS dt")
3959
- await runner.run()
3960
- results = runner.results
3961
- assert len(results) == 1
3962
- dt = results[0]["dt"]
3963
- assert isinstance(dt["year"], int)
3964
- assert isinstance(dt["month"], int)
3965
- assert isinstance(dt["day"], int)
3966
- assert isinstance(dt["hour"], int)
3967
- assert isinstance(dt["minute"], int)
3968
- assert isinstance(dt["second"], int)
3969
- assert isinstance(dt["millisecond"], int)
3970
- assert isinstance(dt["epochMillis"], int)
3971
- assert isinstance(dt["formatted"], str)
3972
- assert not dt["formatted"].endswith("Z") # Local datetime does not end in Z
3973
-
3974
- @pytest.mark.asyncio
3975
- async def test_localdatetime_with_string_argument(self):
3976
- """Test localdatetime() with string argument."""
3977
- runner = Runner("RETURN localdatetime('2025-01-20T08:15:30.500') AS dt")
3978
- await runner.run()
3979
- dt = runner.results[0]["dt"]
3980
- assert isinstance(dt["year"], int)
3981
- assert isinstance(dt["hour"], int)
3982
- assert dt["epochMillis"] is not None
3983
-
3984
- @pytest.mark.asyncio
3985
- async def test_timestamp_returns_epoch_millis(self):
3986
- """Test timestamp() returns epoch millis."""
3987
- import time
3988
- before = int(time.time() * 1000)
3989
- runner = Runner("RETURN timestamp() AS ts")
3990
- await runner.run()
3991
- after = int(time.time() * 1000)
3992
- results = runner.results
3993
- assert len(results) == 1
3994
- ts = results[0]["ts"]
3995
- assert isinstance(ts, int)
3996
- assert ts >= before
3997
- assert ts <= after
3998
-
3999
- @pytest.mark.asyncio
4000
- async def test_datetime_epochmillis_matches_timestamp(self):
4001
- """Test datetime() epochMillis matches timestamp()."""
4002
- runner = Runner(
4003
- "WITH datetime() AS dt, timestamp() AS ts RETURN dt.epochMillis AS dtMillis, ts AS tsMillis"
4004
- )
4005
- await runner.run()
4006
- results = runner.results
4007
- assert len(results) == 1
4008
- # They should be very close (within a few ms)
4009
- assert abs(results[0]["dtMillis"] - results[0]["tsMillis"]) < 100
4010
-
4011
- @pytest.mark.asyncio
4012
- async def test_date_with_property_access_in_where(self):
4013
- """Test date() with property access in WHERE."""
4014
- runner = Runner(
4015
- "UNWIND [1, 2, 3] AS x WITH x, date('2025-06-15') AS d WHERE d.quarter = 2 RETURN x"
4016
- )
4017
- await runner.run()
4018
- results = runner.results
4019
- assert len(results) == 3 # All 3 pass through since Q2 = 2
4020
-
4021
- @pytest.mark.asyncio
4022
- async def test_datetime_with_map_argument(self):
4023
- """Test datetime() with map argument."""
4024
- runner = Runner(
4025
- "RETURN datetime({year: 2024, month: 12, day: 25, hour: 10, minute: 30}) AS dt"
4026
- )
4027
- await runner.run()
4028
- dt = runner.results[0]["dt"]
4029
- assert dt["year"] == 2024
4030
- assert dt["month"] == 12
4031
- assert dt["day"] == 25
4032
- assert dt["quarter"] == 4 # December = Q4
4033
-
4034
- @pytest.mark.asyncio
4035
- async def test_date_with_map_argument(self):
4036
- """Test date() with map argument."""
4037
- runner = Runner(
4038
- "RETURN date({year: 2025, month: 3, day: 1}) AS d"
4039
- )
4040
- await runner.run()
4041
- d = runner.results[0]["d"]
4042
- assert d["year"] == 2025
4043
- assert d["month"] == 3
4044
- assert d["day"] == 1
4045
- assert d["quarter"] == 1 # March = Q1
4046
-
4047
- @pytest.mark.asyncio
4048
- async def test_id_function_with_node(self):
4049
- """Test id() function with a graph node."""
4050
- await Runner(
4051
- """
4052
- CREATE VIRTUAL (:Person) AS {
4053
- UNWIND [
4054
- {id: 1, name: 'Alice'},
4055
- {id: 2, name: 'Bob'}
4056
- ] AS record
4057
- RETURN record.id AS id, record.name AS name
4058
- }
4059
- """
4060
- ).run()
4061
- match = Runner(
4062
- """
4063
- MATCH (n:Person)
4064
- RETURN id(n) AS nodeId
4065
- """
4066
- )
4067
- await match.run()
4068
- results = match.results
4069
- assert len(results) == 2
4070
- assert results[0] == {"nodeId": 1}
4071
- assert results[1] == {"nodeId": 2}
4072
-
4073
- @pytest.mark.asyncio
4074
- async def test_id_function_with_null(self):
4075
- """Test id() function with null."""
4076
- runner = Runner("RETURN id(null) AS nodeId")
4077
- await runner.run()
4078
- results = runner.results
4079
- assert len(results) == 1
4080
- assert results[0] == {"nodeId": None}
4081
-
4082
- @pytest.mark.asyncio
4083
- async def test_id_function_with_relationship(self):
4084
- """Test id() function with a relationship."""
4085
- await Runner(
4086
- """
4087
- CREATE VIRTUAL (:City) AS {
4088
- UNWIND [
4089
- {id: 1, name: 'New York'},
4090
- {id: 2, name: 'Boston'}
4091
- ] AS record
4092
- RETURN record.id AS id, record.name AS name
4093
- }
4094
- """
4095
- ).run()
4096
- await Runner(
4097
- """
4098
- CREATE VIRTUAL (:City)-[:CONNECTED_TO]-(:City) AS {
4099
- UNWIND [
4100
- {left_id: 1, right_id: 2}
4101
- ] AS record
4102
- RETURN record.left_id AS left_id, record.right_id AS right_id
4103
- }
4104
- """
4105
- ).run()
4106
- match = Runner(
4107
- """
4108
- MATCH (a:City)-[r:CONNECTED_TO]->(b:City)
4109
- RETURN id(r) AS relId
4110
- """
4111
- )
4112
- await match.run()
4113
- results = match.results
4114
- assert len(results) == 1
4115
- assert results[0] == {"relId": "CONNECTED_TO"}
4116
-
4117
- @pytest.mark.asyncio
4118
- async def test_element_id_function_with_node(self):
4119
- """Test elementId() function with a graph node."""
4120
- await Runner(
4121
- """
4122
- CREATE VIRTUAL (:Person) AS {
4123
- UNWIND [
4124
- {id: 1, name: 'Alice'},
4125
- {id: 2, name: 'Bob'}
4126
- ] AS record
4127
- RETURN record.id AS id, record.name AS name
4128
- }
4129
- """
4130
- ).run()
4131
- match = Runner(
4132
- """
4133
- MATCH (n:Person)
4134
- RETURN elementId(n) AS eid
4135
- """
4136
- )
4137
- await match.run()
4138
- results = match.results
4139
- assert len(results) == 2
4140
- assert results[0] == {"eid": "1"}
4141
- assert results[1] == {"eid": "2"}
4142
-
4143
- @pytest.mark.asyncio
4144
- async def test_element_id_function_with_null(self):
4145
- """Test elementId() function with null."""
4146
- runner = Runner("RETURN elementId(null) AS eid")
4147
- await runner.run()
4148
- results = runner.results
4149
- assert len(results) == 1
4150
- assert results[0] == {"eid": None}
4151
-
4152
- @pytest.mark.asyncio
4153
- async def test_head_function(self):
4154
- """Test head() function."""
4155
- runner = Runner("RETURN head([1, 2, 3]) AS h")
4156
- await runner.run()
4157
- assert len(runner.results) == 1
4158
- assert runner.results[0] == {"h": 1}
4159
-
4160
- @pytest.mark.asyncio
4161
- async def test_head_function_empty_list(self):
4162
- """Test head() function with empty list."""
4163
- runner = Runner("RETURN head([]) AS h")
4164
- await runner.run()
4165
- assert runner.results[0] == {"h": None}
4166
-
4167
- @pytest.mark.asyncio
4168
- async def test_head_function_null(self):
4169
- """Test head() function with null."""
4170
- runner = Runner("RETURN head(null) AS h")
4171
- await runner.run()
4172
- assert runner.results[0] == {"h": None}
4173
-
4174
- @pytest.mark.asyncio
4175
- async def test_tail_function(self):
4176
- """Test tail() function."""
4177
- runner = Runner("RETURN tail([1, 2, 3]) AS t")
4178
- await runner.run()
4179
- assert len(runner.results) == 1
4180
- assert runner.results[0] == {"t": [2, 3]}
4181
-
4182
- @pytest.mark.asyncio
4183
- async def test_tail_function_single_element(self):
4184
- """Test tail() function with single element."""
4185
- runner = Runner("RETURN tail([1]) AS t")
4186
- await runner.run()
4187
- assert runner.results[0] == {"t": []}
4188
-
4189
- @pytest.mark.asyncio
4190
- async def test_tail_function_null(self):
4191
- """Test tail() function with null."""
4192
- runner = Runner("RETURN tail(null) AS t")
4193
- await runner.run()
4194
- assert runner.results[0] == {"t": None}
4195
-
4196
- @pytest.mark.asyncio
4197
- async def test_last_function(self):
4198
- """Test last() function."""
4199
- runner = Runner("RETURN last([1, 2, 3]) AS l")
4200
- await runner.run()
4201
- assert len(runner.results) == 1
4202
- assert runner.results[0] == {"l": 3}
4203
-
4204
- @pytest.mark.asyncio
4205
- async def test_last_function_empty_list(self):
4206
- """Test last() function with empty list."""
4207
- runner = Runner("RETURN last([]) AS l")
4208
- await runner.run()
4209
- assert runner.results[0] == {"l": None}
4210
-
4211
- @pytest.mark.asyncio
4212
- async def test_last_function_null(self):
4213
- """Test last() function with null."""
4214
- runner = Runner("RETURN last(null) AS l")
4215
- await runner.run()
4216
- assert runner.results[0] == {"l": None}
4217
-
4218
- @pytest.mark.asyncio
4219
- async def test_to_integer_function_string(self):
4220
- """Test toInteger() function with string."""
4221
- runner = Runner('RETURN toInteger("42") AS i')
4222
- await runner.run()
4223
- assert runner.results[0] == {"i": 42}
4224
-
4225
- @pytest.mark.asyncio
4226
- async def test_to_integer_function_float(self):
4227
- """Test toInteger() function with float."""
4228
- runner = Runner("RETURN toInteger(3.14) AS i")
4229
- await runner.run()
4230
- assert runner.results[0] == {"i": 3}
4231
-
4232
- @pytest.mark.asyncio
4233
- async def test_to_integer_function_boolean(self):
4234
- """Test toInteger() function with boolean."""
4235
- runner = Runner("RETURN toInteger(true) AS i")
4236
- await runner.run()
4237
- assert runner.results[0] == {"i": 1}
4238
-
4239
- @pytest.mark.asyncio
4240
- async def test_to_integer_function_null(self):
4241
- """Test toInteger() function with null."""
4242
- runner = Runner("RETURN toInteger(null) AS i")
4243
- await runner.run()
4244
- assert runner.results[0] == {"i": None}
4245
-
4246
- @pytest.mark.asyncio
4247
- async def test_to_float_function_string(self):
4248
- """Test toFloat() function with string."""
4249
- runner = Runner('RETURN toFloat("3.14") AS f')
4250
- await runner.run()
4251
- assert runner.results[0] == {"f": 3.14}
4252
-
4253
- @pytest.mark.asyncio
4254
- async def test_to_float_function_integer(self):
4255
- """Test toFloat() function with integer."""
4256
- runner = Runner("RETURN toFloat(42) AS f")
4257
- await runner.run()
4258
- assert runner.results[0] == {"f": 42}
4259
-
4260
- @pytest.mark.asyncio
4261
- async def test_to_float_function_boolean(self):
4262
- """Test toFloat() function with boolean."""
4263
- runner = Runner("RETURN toFloat(true) AS f")
4264
- await runner.run()
4265
- assert runner.results[0] == {"f": 1.0}
4266
-
4267
- @pytest.mark.asyncio
4268
- async def test_to_float_function_null(self):
4269
- """Test toFloat() function with null."""
4270
- runner = Runner("RETURN toFloat(null) AS f")
4271
- await runner.run()
4272
- assert runner.results[0] == {"f": None}
4273
-
4274
- @pytest.mark.asyncio
4275
- async def test_duration_iso_string(self):
4276
- """Test duration() with ISO 8601 string."""
4277
- runner = Runner("RETURN duration('P1Y2M3DT4H5M6S') AS d")
4278
- await runner.run()
4279
- d = runner.results[0]["d"]
4280
- assert d["years"] == 1
4281
- assert d["months"] == 2
4282
- assert d["days"] == 3
4283
- assert d["hours"] == 4
4284
- assert d["minutes"] == 5
4285
- assert d["seconds"] == 6
4286
- assert d["totalMonths"] == 14
4287
- assert d["formatted"] == "P1Y2M3DT4H5M6S"
4288
-
4289
- @pytest.mark.asyncio
4290
- async def test_duration_map_argument(self):
4291
- """Test duration() with map argument."""
4292
- runner = Runner("RETURN duration({days: 14, hours: 16}) AS d")
4293
- await runner.run()
4294
- d = runner.results[0]["d"]
4295
- assert d["days"] == 14
4296
- assert d["hours"] == 16
4297
- assert d["totalDays"] == 14
4298
- assert d["totalSeconds"] == 57600
4299
-
4300
- @pytest.mark.asyncio
4301
- async def test_duration_weeks(self):
4302
- """Test duration() with weeks."""
4303
- runner = Runner("RETURN duration('P2W') AS d")
4304
- await runner.run()
4305
- d = runner.results[0]["d"]
4306
- assert d["weeks"] == 2
4307
- assert d["days"] == 14
4308
- assert d["totalDays"] == 14
4309
-
4310
- @pytest.mark.asyncio
4311
- async def test_duration_null(self):
4312
- """Test duration() with null."""
4313
- runner = Runner("RETURN duration(null) AS d")
4314
- await runner.run()
4315
- assert runner.results[0] == {"d": None}
4316
-
4317
- @pytest.mark.asyncio
4318
- async def test_duration_time_only(self):
4319
- """Test duration() with time-only string."""
4320
- runner = Runner("RETURN duration('PT2H30M') AS d")
4321
- await runner.run()
4322
- d = runner.results[0]["d"]
4323
- assert d["hours"] == 2
4324
- assert d["minutes"] == 30
4325
- assert d["totalSeconds"] == 9000
4326
- assert d["formatted"] == "PT2H30M"
4327
-
4328
- # ORDER BY tests
4329
-
4330
- @pytest.mark.asyncio
4331
- async def test_order_by_ascending(self):
4332
- """Test ORDER BY ascending (default)."""
4333
- runner = Runner("unwind [3, 1, 2] as x return x order by x")
4334
- await runner.run()
4335
- results = runner.results
4336
- assert len(results) == 3
4337
- assert results[0] == {"x": 1}
4338
- assert results[1] == {"x": 2}
4339
- assert results[2] == {"x": 3}
4340
-
4341
- @pytest.mark.asyncio
4342
- async def test_order_by_descending(self):
4343
- """Test ORDER BY descending."""
4344
- runner = Runner("unwind [3, 1, 2] as x return x order by x desc")
4345
- await runner.run()
4346
- results = runner.results
4347
- assert len(results) == 3
4348
- assert results[0] == {"x": 3}
4349
- assert results[1] == {"x": 2}
4350
- assert results[2] == {"x": 1}
4351
-
4352
- @pytest.mark.asyncio
4353
- async def test_order_by_ascending_explicit(self):
4354
- """Test ORDER BY with explicit ASC."""
4355
- runner = Runner("unwind [3, 1, 2] as x return x order by x asc")
4356
- await runner.run()
4357
- results = runner.results
4358
- assert len(results) == 3
4359
- assert results[0] == {"x": 1}
4360
- assert results[1] == {"x": 2}
4361
- assert results[2] == {"x": 3}
4362
-
4363
- @pytest.mark.asyncio
4364
- async def test_order_by_with_multiple_fields(self):
4365
- """Test ORDER BY with multiple sort fields."""
4366
- runner = Runner(
4367
- "unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: 25}, {name: 'Alice', age: 25}] as person "
4368
- "return person.name as name, person.age as age "
4369
- "order by name asc, age asc"
4370
- )
4371
- await runner.run()
4372
- results = runner.results
4373
- assert len(results) == 3
4374
- assert results[0] == {"name": "Alice", "age": 25}
4375
- assert results[1] == {"name": "Alice", "age": 30}
4376
- assert results[2] == {"name": "Bob", "age": 25}
4377
-
4378
- @pytest.mark.asyncio
4379
- async def test_order_by_with_strings(self):
4380
- """Test ORDER BY with string values."""
4381
- runner = Runner(
4382
- "unwind ['banana', 'apple', 'cherry'] as fruit return fruit order by fruit"
4383
- )
4384
- await runner.run()
4385
- results = runner.results
4386
- assert len(results) == 3
4387
- assert results[0] == {"fruit": "apple"}
4388
- assert results[1] == {"fruit": "banana"}
4389
- assert results[2] == {"fruit": "cherry"}
4390
-
4391
- @pytest.mark.asyncio
4392
- async def test_order_by_with_aggregated_return(self):
4393
- """Test ORDER BY with aggregated RETURN."""
4394
- runner = Runner(
4395
- "unwind [1, 1, 2, 2, 3, 3] as x "
4396
- "return x, count(x) as cnt "
4397
- "order by x desc"
4398
- )
4399
- await runner.run()
4400
- results = runner.results
4401
- assert len(results) == 3
4402
- assert results[0] == {"x": 3, "cnt": 2}
4403
- assert results[1] == {"x": 2, "cnt": 2}
4404
- assert results[2] == {"x": 1, "cnt": 2}
4405
-
4406
- @pytest.mark.asyncio
4407
- async def test_order_by_with_limit(self):
4408
- """Test ORDER BY combined with LIMIT."""
4409
- runner = Runner(
4410
- "unwind [3, 1, 4, 1, 5, 9, 2, 6] as x return x order by x limit 3"
4411
- )
4412
- await runner.run()
4413
- results = runner.results
4414
- assert len(results) == 3
4415
- assert results[0] == {"x": 1}
4416
- assert results[1] == {"x": 1}
4417
- assert results[2] == {"x": 2}
4418
-
4419
- @pytest.mark.asyncio
4420
- async def test_order_by_with_where(self):
4421
- """Test ORDER BY combined with WHERE."""
4422
- runner = Runner(
4423
- "unwind [3, 1, 4, 1, 5, 9, 2, 6] as x return x where x > 2 order by x desc"
4424
- )
4425
- await runner.run()
4426
- results = runner.results
4427
- assert len(results) == 5
4428
- assert results[0] == {"x": 9}
4429
- assert results[1] == {"x": 6}
4430
- assert results[2] == {"x": 5}
4431
- assert results[3] == {"x": 4}
4432
- assert results[4] == {"x": 3}
4433
-
4434
- @pytest.mark.asyncio
4435
- async def test_order_by_with_property_access_expression(self):
4436
- """Test ORDER BY with property access expression."""
4437
- runner = Runner(
4438
- "unwind [{name: 'Charlie', age: 30}, {name: 'Alice', age: 25}, {name: 'Bob', age: 35}] as person "
4439
- "return person.name as name, person.age as age "
4440
- "order by person.name asc"
4441
- )
4442
- await runner.run()
4443
- results = runner.results
4444
- assert len(results) == 3
4445
- assert results[0] == {"name": "Alice", "age": 25}
4446
- assert results[1] == {"name": "Bob", "age": 35}
4447
- assert results[2] == {"name": "Charlie", "age": 30}
4448
-
4449
- @pytest.mark.asyncio
4450
- async def test_order_by_with_function_expression(self):
4451
- """Test ORDER BY with function expression."""
4452
- runner = Runner(
4453
- "unwind ['BANANA', 'apple', 'Cherry'] as fruit "
4454
- "return fruit "
4455
- "order by toLower(fruit)"
4456
- )
4457
- await runner.run()
4458
- results = runner.results
4459
- assert len(results) == 3
4460
- assert results[0] == {"fruit": "apple"}
4461
- assert results[1] == {"fruit": "BANANA"}
4462
- assert results[2] == {"fruit": "Cherry"}
4463
-
4464
- @pytest.mark.asyncio
4465
- async def test_order_by_with_function_expression_descending(self):
4466
- """Test ORDER BY with function expression descending."""
4467
- runner = Runner(
4468
- "unwind ['BANANA', 'apple', 'Cherry'] as fruit "
4469
- "return fruit "
4470
- "order by toLower(fruit) desc"
4471
- )
4472
- await runner.run()
4473
- results = runner.results
4474
- assert len(results) == 3
4475
- assert results[0] == {"fruit": "Cherry"}
4476
- assert results[1] == {"fruit": "BANANA"}
4477
- assert results[2] == {"fruit": "apple"}
4478
-
4479
- @pytest.mark.asyncio
4480
- async def test_order_by_with_nested_function_expression(self):
4481
- """Test ORDER BY with nested function expression."""
4482
- runner = Runner(
4483
- "unwind ['Alice', 'Bob', 'ALICE', 'bob'] as name "
4484
- "return name "
4485
- "order by string_distance(toLower(name), toLower('alice')) asc"
4486
- )
4487
- await runner.run()
4488
- results = runner.results
4489
- assert len(results) == 4
4490
- # 'Alice' and 'ALICE' have distance 0 from 'alice', should come first
4491
- assert results[0]["name"] == "Alice"
4492
- assert results[1]["name"] == "ALICE"
4493
- # 'Bob' and 'bob' have higher distance from 'alice'
4494
- assert results[2]["name"] == "Bob"
4495
- assert results[3]["name"] == "bob"
4496
-
4497
- @pytest.mark.asyncio
4498
- async def test_order_by_with_arithmetic_expression(self):
4499
- """Test ORDER BY with arithmetic expression."""
4500
- runner = Runner(
4501
- "unwind [{a: 3, b: 1}, {a: 1, b: 5}, {a: 2, b: 2}] as item "
4502
- "return item.a as a, item.b as b "
4503
- "order by item.a + item.b asc"
4504
- )
4505
- await runner.run()
4506
- results = runner.results
4507
- assert len(results) == 3
4508
- assert results[0] == {"a": 3, "b": 1} # sum = 4
4509
- assert results[1] == {"a": 2, "b": 2} # sum = 4
4510
- assert results[2] == {"a": 1, "b": 5} # sum = 6
4511
-
4512
- @pytest.mark.asyncio
4513
- async def test_order_by_expression_does_not_leak_synthetic_keys(self):
4514
- """Test ORDER BY expression does not leak synthetic keys."""
4515
- runner = Runner(
4516
- "unwind ['B', 'a', 'C'] as x "
4517
- "return x "
4518
- "order by toLower(x) asc"
4519
- )
4520
- await runner.run()
4521
- results = runner.results
4522
- assert len(results) == 3
4523
- # Results should only contain 'x', no extra keys
4524
- for r in results:
4525
- assert list(r.keys()) == ["x"]
4526
- assert results[0] == {"x": "a"}
4527
- assert results[1] == {"x": "B"}
4528
- assert results[2] == {"x": "C"}
4529
-
4530
- @pytest.mark.asyncio
4531
- async def test_order_by_with_expression_and_limit(self):
4532
- """Test ORDER BY with expression and limit."""
4533
- runner = Runner(
4534
- "unwind ['BANANA', 'apple', 'Cherry', 'date', 'ELDERBERRY'] as fruit "
4535
- "return fruit "
4536
- "order by toLower(fruit) asc "
4537
- "limit 3"
4538
- )
4539
- await runner.run()
4540
- results = runner.results
4541
- assert len(results) == 3
4542
- assert results[0] == {"fruit": "apple"}
4543
- assert results[1] == {"fruit": "BANANA"}
4544
- assert results[2] == {"fruit": "Cherry"}
4545
-
4546
- @pytest.mark.asyncio
4547
- async def test_order_by_with_mixed_simple_and_expression_fields(self):
4548
- """Test ORDER BY with mixed simple and expression fields."""
4549
- runner = Runner(
4550
- "unwind [{name: 'Alice', score: 3}, {name: 'Alice', score: 1}, {name: 'Bob', score: 2}] as item "
4551
- "return item.name as name, item.score as score "
4552
- "order by name asc, item.score desc"
4553
- )
4554
- await runner.run()
4555
- results = runner.results
4556
- assert len(results) == 3
4557
- assert results[0] == {"name": "Alice", "score": 3} # Alice, score 3 desc
4558
- assert results[1] == {"name": "Alice", "score": 1} # Alice, score 1 desc
4559
- assert results[2] == {"name": "Bob", "score": 2} # Bob
4560
-
4561
- @pytest.mark.asyncio
4562
- async def test_delete_virtual_node_operation(self):
4563
- """Test delete virtual node operation."""
4564
- db = Database.get_instance()
4565
- # Create a virtual node first
4566
- create = Runner(
4567
- """
4568
- CREATE VIRTUAL (:PyDeleteTestPerson) AS {
4569
- unwind [
4570
- {id: 1, name: 'Person 1'},
4571
- {id: 2, name: 'Person 2'}
4572
- ] as record
4573
- RETURN record.id as id, record.name as name
4574
- }
4575
- """
4576
- )
4577
- await create.run()
4578
- assert db.get_node(Node(None, "PyDeleteTestPerson")) is not None
4579
-
4580
- # Delete the virtual node
4581
- del_runner = Runner("DELETE VIRTUAL (:PyDeleteTestPerson)")
4582
- await del_runner.run()
4583
- assert len(del_runner.results) == 0
4584
- assert db.get_node(Node(None, "PyDeleteTestPerson")) is None
4585
-
4586
- @pytest.mark.asyncio
4587
- async def test_delete_virtual_node_then_match_throws(self):
4588
- """Test that matching a deleted virtual node throws."""
4589
- # Create a virtual node
4590
- create = Runner(
4591
- """
4592
- CREATE VIRTUAL (:PyDeleteMatchPerson) AS {
4593
- unwind [{id: 1, name: 'Alice'}] as record
4594
- RETURN record.id as id, record.name as name
4595
- }
4596
- """
4597
- )
4598
- await create.run()
4599
-
4600
- # Verify it can be matched
4601
- match1 = Runner("MATCH (n:PyDeleteMatchPerson) RETURN n")
4602
- await match1.run()
4603
- assert len(match1.results) == 1
4604
-
4605
- # Delete the virtual node
4606
- del_runner = Runner("DELETE VIRTUAL (:PyDeleteMatchPerson)")
4607
- await del_runner.run()
4608
-
4609
- # Matching should now throw since the node is gone
4610
- match2 = Runner("MATCH (n:PyDeleteMatchPerson) RETURN n")
4611
- with pytest.raises(ValueError):
4612
- await match2.run()
4613
-
4614
- @pytest.mark.asyncio
4615
- async def test_delete_virtual_relationship_operation(self):
4616
- """Test delete virtual relationship operation."""
4617
- db = Database.get_instance()
4618
- # Create virtual nodes and relationship
4619
- await Runner(
4620
- """
4621
- CREATE VIRTUAL (:PyDelRelUser) AS {
4622
- unwind [
4623
- {id: 1, name: 'Alice'},
4624
- {id: 2, name: 'Bob'}
4625
- ] as record
4626
- RETURN record.id as id, record.name as name
4627
- }
4628
- """
4629
- ).run()
4630
-
4631
- await Runner(
4632
- """
4633
- CREATE VIRTUAL (:PyDelRelUser)-[:PY_DEL_KNOWS]-(:PyDelRelUser) AS {
4634
- unwind [
4635
- {left_id: 1, right_id: 2}
4636
- ] as record
4637
- RETURN record.left_id as left_id, record.right_id as right_id
4638
- }
4639
- """
4640
- ).run()
4641
-
4642
- # Verify relationship exists
4643
- rel = Relationship()
4644
- rel.type = "PY_DEL_KNOWS"
4645
- assert db.get_relationship(rel) is not None
4646
-
4647
- # Delete the virtual relationship
4648
- del_runner = Runner("DELETE VIRTUAL (:PyDelRelUser)-[:PY_DEL_KNOWS]-(:PyDelRelUser)")
4649
- await del_runner.run()
4650
- assert len(del_runner.results) == 0
4651
- assert db.get_relationship(rel) is None
4652
-
4653
- @pytest.mark.asyncio
4654
- async def test_delete_virtual_node_leaves_other_nodes_intact(self):
4655
- """Test that deleting one virtual node leaves others intact."""
4656
- db = Database.get_instance()
4657
- # Create two virtual node types
4658
- await Runner(
4659
- """
4660
- CREATE VIRTUAL (:PyKeepNode) AS {
4661
- unwind [{id: 1, name: 'Keep'}] as record
4662
- RETURN record.id as id, record.name as name
4663
- }
4664
- """
4665
- ).run()
4666
-
4667
- await Runner(
4668
- """
4669
- CREATE VIRTUAL (:PyRemoveNode) AS {
4670
- unwind [{id: 2, name: 'Remove'}] as record
4671
- RETURN record.id as id, record.name as name
4672
- }
4673
- """
4674
- ).run()
4675
-
4676
- assert db.get_node(Node(None, "PyKeepNode")) is not None
4677
- assert db.get_node(Node(None, "PyRemoveNode")) is not None
4678
-
4679
- # Delete only one
4680
- await Runner("DELETE VIRTUAL (:PyRemoveNode)").run()
4681
-
4682
- # The other should still exist
4683
- assert db.get_node(Node(None, "PyKeepNode")) is not None
4684
- assert db.get_node(Node(None, "PyRemoveNode")) is None
4685
-
4686
- # The remaining node can still be matched
4687
- match = Runner("MATCH (n:PyKeepNode) RETURN n")
4688
- await match.run()
4689
- assert len(match.results) == 1
4690
- assert match.results[0]["n"]["name"] == "Keep"
4691
-
4692
- @pytest.mark.asyncio
4693
- async def test_return_alias_shadowing_graph_variable(self):
4694
- """Test that RETURN alias doesn't shadow graph variable in same clause.
4695
-
4696
- When RETURN mentor.displayName AS mentor is followed by mentor.jobTitle,
4697
- the alias 'mentor' should not overwrite the graph node variable before
4698
- subsequent expressions are parsed.
4699
- """
4700
- await Runner(
4701
- """
4702
- CREATE VIRTUAL (:PyMentorUser) AS {
4703
- UNWIND [
4704
- {id: 1, displayName: 'Alice Smith', jobTitle: 'Senior Engineer', department: 'Engineering'},
4705
- {id: 2, displayName: 'Bob Jones', jobTitle: 'Staff Engineer', department: 'Engineering'},
4706
- {id: 3, displayName: 'Chloe Dubois', jobTitle: 'Junior Engineer', department: 'Engineering'}
4707
- ] AS record
4708
- RETURN record.id AS id, record.displayName AS displayName, record.jobTitle AS jobTitle, record.department AS department
4709
- }
4710
- """
4711
- ).run()
4712
-
4713
- await Runner(
4714
- """
4715
- CREATE VIRTUAL (:PyMentorUser)-[:PY_MENTORS]-(:PyMentorUser) AS {
4716
- UNWIND [
4717
- {left_id: 1, right_id: 3},
4718
- {left_id: 2, right_id: 3}
4719
- ] AS record
4720
- RETURN record.left_id AS left_id, record.right_id AS right_id
4721
- }
4722
- """
4723
- ).run()
4724
-
4725
- runner = Runner(
4726
- """
4727
- MATCH (mentor:PyMentorUser)-[:PY_MENTORS]->(mentee:PyMentorUser)
4728
- WHERE mentee.displayName = "Chloe Dubois"
4729
- RETURN mentor.displayName AS mentor, mentor.jobTitle AS mentorJobTitle, mentor.department AS mentorDepartment
4730
- """
4731
- )
4732
- await runner.run()
4733
- results = runner.results
4734
-
4735
- assert len(results) == 2
4736
- assert results[0] == {
4737
- "mentor": "Alice Smith",
4738
- "mentorJobTitle": "Senior Engineer",
4739
- "mentorDepartment": "Engineering",
4740
- }
4741
- assert results[1] == {
4742
- "mentor": "Bob Jones",
4743
- "mentorJobTitle": "Staff Engineer",
4744
- "mentorDepartment": "Engineering",
4745
- }
4746
-
4747
- @pytest.mark.asyncio
4748
- async def test_chained_optional_match_with_null_intermediate_node(self):
4749
- """Test chained OPTIONAL MATCH where intermediate node is null doesn't crash."""
4750
- # Chain: Alice -> Bob -> Charlie (no outgoing)
4751
- await Runner(
4752
- """
4753
- CREATE VIRTUAL (:ChainEmp) AS {
4754
- unwind [
4755
- {id: 1, name: 'Alice'},
4756
- {id: 2, name: 'Bob'},
4757
- {id: 3, name: 'Charlie'}
4758
- ] as record
4759
- RETURN record.id as id, record.name as name
4760
- }
4761
- """
4762
- ).run()
4763
- await Runner(
4764
- """
4765
- CREATE VIRTUAL (:ChainEmp)-[:REPORTS_TO]-(:ChainEmp) AS {
4766
- unwind [
4767
- {left_id: 1, right_id: 2},
4768
- {left_id: 2, right_id: 3}
4769
- ] as record
4770
- RETURN record.left_id as left_id, record.right_id as right_id
4771
- }
4772
- """
4773
- ).run()
4774
-
4775
- # Alice -> Bob -> Charlie -> null -> null
4776
- runner = Runner(
4777
- """
4778
- MATCH (u:ChainEmp)
4779
- WHERE u.name = "Alice"
4780
- OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:ChainEmp)
4781
- OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:ChainEmp)
4782
- OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:ChainEmp)
4783
- OPTIONAL MATCH (m3)-[:REPORTS_TO]->(m4:ChainEmp)
4784
- RETURN
4785
- u.name AS user,
4786
- m1.name AS manager1,
4787
- m2.name AS manager2,
4788
- m3.name AS manager3,
4789
- m4.name AS manager4
4790
- """
4791
- )
4792
- await runner.run()
4793
- results = runner.results
4794
-
4795
- assert len(results) == 1
4796
- assert results[0]["user"] == "Alice"
4797
- assert results[0]["manager1"] == "Bob"
4798
- assert results[0]["manager2"] == "Charlie"
4799
- assert results[0]["manager3"] is None
4800
- assert results[0]["manager4"] is None
4801
-
4802
- @pytest.mark.asyncio
4803
- async def test_chained_optional_match_all_null_from_first(self):
4804
- """Test chained OPTIONAL MATCH where first optional returns null propagates nulls."""
4805
- await Runner(
4806
- """
4807
- CREATE VIRTUAL (:ChainWorker) AS {
4808
- unwind [
4809
- {id: 1, name: 'Solo'}
4810
- ] as record
4811
- RETURN record.id as id, record.name as name
4812
- }
4813
- """
4814
- ).run()
4815
- await Runner(
4816
- """
4817
- CREATE VIRTUAL (:ChainWorker)-[:MANAGES]-(:ChainWorker) AS {
4818
- unwind [] as record
4819
- RETURN record.left_id as left_id, record.right_id as right_id
4820
- }
4821
- """
4822
- ).run()
4823
-
4824
- # Solo has no MANAGES relationship
4825
- runner = Runner(
4826
- """
4827
- MATCH (u:ChainWorker)
4828
- OPTIONAL MATCH (u)-[:MANAGES]->(m1:ChainWorker)
4829
- OPTIONAL MATCH (m1)-[:MANAGES]->(m2:ChainWorker)
4830
- OPTIONAL MATCH (m2)-[:MANAGES]->(m3:ChainWorker)
4831
- RETURN
4832
- u.name AS user,
4833
- m1.name AS mgr1,
4834
- m2.name AS mgr2,
4835
- m3.name AS mgr3
4836
- """
4837
- )
4838
- await runner.run()
4839
- results = runner.results
4840
-
4841
- assert len(results) == 1
4842
- assert results[0]["user"] == "Solo"
4843
- assert results[0]["mgr1"] is None
4844
- assert results[0]["mgr2"] is None
4845
- assert results[0]["mgr3"] is None
4846
-
4847
- @pytest.mark.asyncio
4848
- async def test_chained_optional_match_mixed_null_and_non_null(self):
4849
- """Test chained OPTIONAL MATCH with multiple start nodes having different chain depths."""
4850
- await Runner(
4851
- """
4852
- CREATE VIRTUAL (:ChainStaff) AS {
4853
- unwind [
4854
- {id: 1, name: 'Dev'},
4855
- {id: 2, name: 'Lead'},
4856
- {id: 3, name: 'Director'},
4857
- {id: 4, name: 'Intern'}
4858
- ] as record
4859
- RETURN record.id as id, record.name as name
4860
- }
4861
- """
4862
- ).run()
4863
- await Runner(
4864
- """
4865
- CREATE VIRTUAL (:ChainStaff)-[:REPORTS_TO]-(:ChainStaff) AS {
4866
- unwind [
4867
- {left_id: 1, right_id: 2},
4868
- {left_id: 2, right_id: 3}
4869
- ] as record
4870
- RETURN record.left_id as left_id, record.right_id as right_id
4871
- }
4872
- """
4873
- ).run()
4874
-
4875
- # Dev -> Lead -> Director -> null
4876
- # Intern -> null -> null -> null
4877
- runner = Runner(
4878
- """
4879
- MATCH (u:ChainStaff)
4880
- WHERE u.name = "Dev" OR u.name = "Intern"
4881
- OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:ChainStaff)
4882
- OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:ChainStaff)
4883
- OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:ChainStaff)
4884
- RETURN
4885
- u.name AS user,
4886
- m1.name AS mgr1,
4887
- m2.name AS mgr2,
4888
- m3.name AS mgr3
4889
- """
4890
- )
4891
- await runner.run()
4892
- results = runner.results
4893
-
4894
- assert len(results) == 2
4895
- dev = next(r for r in results if r["user"] == "Dev")
4896
- assert dev["mgr1"] == "Lead"
4897
- assert dev["mgr2"] == "Director"
4898
- assert dev["mgr3"] is None
4899
- intern = next(r for r in results if r["user"] == "Intern")
4900
- assert intern["mgr1"] is None
4901
- assert intern["mgr2"] is None
4902
- assert intern["mgr3"] is None