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.
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/.editorconfig +0 -21
- package/.gitattributes +0 -3
- package/.github/workflows/npm-publish.yml +0 -32
- package/.github/workflows/python-publish.yml +0 -143
- package/.github/workflows/release.yml +0 -107
- package/.husky/pre-commit +0 -28
- package/.prettierrc +0 -22
- package/CODE_OF_CONDUCT.md +0 -10
- package/FlowQueryLogoIcon.png +0 -0
- package/SECURITY.md +0 -14
- package/SUPPORT.md +0 -13
- package/docs/flowquery.min.js +0 -1
- package/docs/index.html +0 -105
- package/flowquery-py/CONTRIBUTING.md +0 -127
- package/flowquery-py/README.md +0 -67
- package/flowquery-py/misc/data/test.json +0 -10
- package/flowquery-py/misc/data/users.json +0 -242
- package/flowquery-py/notebooks/TestFlowQuery.ipynb +0 -440
- package/flowquery-py/pyproject.toml +0 -121
- package/flowquery-py/setup_env.ps1 +0 -92
- package/flowquery-py/setup_env.sh +0 -87
- package/flowquery-py/src/__init__.py +0 -38
- package/flowquery-py/src/__main__.py +0 -10
- package/flowquery-py/src/compute/__init__.py +0 -6
- package/flowquery-py/src/compute/flowquery.py +0 -68
- package/flowquery-py/src/compute/runner.py +0 -64
- package/flowquery-py/src/extensibility.py +0 -52
- package/flowquery-py/src/graph/__init__.py +0 -31
- package/flowquery-py/src/graph/data.py +0 -136
- package/flowquery-py/src/graph/database.py +0 -141
- package/flowquery-py/src/graph/hops.py +0 -43
- package/flowquery-py/src/graph/node.py +0 -143
- package/flowquery-py/src/graph/node_data.py +0 -26
- package/flowquery-py/src/graph/node_reference.py +0 -50
- package/flowquery-py/src/graph/pattern.py +0 -115
- package/flowquery-py/src/graph/pattern_expression.py +0 -67
- package/flowquery-py/src/graph/patterns.py +0 -42
- package/flowquery-py/src/graph/physical_node.py +0 -41
- package/flowquery-py/src/graph/physical_relationship.py +0 -36
- package/flowquery-py/src/graph/relationship.py +0 -193
- package/flowquery-py/src/graph/relationship_data.py +0 -36
- package/flowquery-py/src/graph/relationship_match_collector.py +0 -85
- package/flowquery-py/src/graph/relationship_reference.py +0 -21
- package/flowquery-py/src/io/__init__.py +0 -5
- package/flowquery-py/src/io/command_line.py +0 -108
- package/flowquery-py/src/parsing/__init__.py +0 -17
- package/flowquery-py/src/parsing/alias.py +0 -20
- package/flowquery-py/src/parsing/alias_option.py +0 -11
- package/flowquery-py/src/parsing/ast_node.py +0 -147
- package/flowquery-py/src/parsing/base_parser.py +0 -84
- package/flowquery-py/src/parsing/components/__init__.py +0 -19
- package/flowquery-py/src/parsing/components/csv.py +0 -8
- package/flowquery-py/src/parsing/components/from_.py +0 -12
- package/flowquery-py/src/parsing/components/headers.py +0 -12
- package/flowquery-py/src/parsing/components/json.py +0 -8
- package/flowquery-py/src/parsing/components/null.py +0 -10
- package/flowquery-py/src/parsing/components/post.py +0 -8
- package/flowquery-py/src/parsing/components/text.py +0 -8
- package/flowquery-py/src/parsing/context.py +0 -50
- package/flowquery-py/src/parsing/data_structures/__init__.py +0 -15
- package/flowquery-py/src/parsing/data_structures/associative_array.py +0 -41
- package/flowquery-py/src/parsing/data_structures/json_array.py +0 -30
- package/flowquery-py/src/parsing/data_structures/key_value_pair.py +0 -38
- package/flowquery-py/src/parsing/data_structures/lookup.py +0 -51
- package/flowquery-py/src/parsing/data_structures/range_lookup.py +0 -42
- package/flowquery-py/src/parsing/expressions/__init__.py +0 -61
- package/flowquery-py/src/parsing/expressions/boolean.py +0 -20
- package/flowquery-py/src/parsing/expressions/expression.py +0 -141
- package/flowquery-py/src/parsing/expressions/expression_map.py +0 -26
- package/flowquery-py/src/parsing/expressions/f_string.py +0 -27
- package/flowquery-py/src/parsing/expressions/identifier.py +0 -21
- package/flowquery-py/src/parsing/expressions/number.py +0 -32
- package/flowquery-py/src/parsing/expressions/operator.py +0 -271
- package/flowquery-py/src/parsing/expressions/reference.py +0 -47
- package/flowquery-py/src/parsing/expressions/string.py +0 -27
- package/flowquery-py/src/parsing/functions/__init__.py +0 -127
- package/flowquery-py/src/parsing/functions/aggregate_function.py +0 -60
- package/flowquery-py/src/parsing/functions/async_function.py +0 -65
- package/flowquery-py/src/parsing/functions/avg.py +0 -55
- package/flowquery-py/src/parsing/functions/coalesce.py +0 -43
- package/flowquery-py/src/parsing/functions/collect.py +0 -75
- package/flowquery-py/src/parsing/functions/count.py +0 -79
- package/flowquery-py/src/parsing/functions/date_.py +0 -61
- package/flowquery-py/src/parsing/functions/datetime_.py +0 -62
- package/flowquery-py/src/parsing/functions/duration.py +0 -159
- package/flowquery-py/src/parsing/functions/element_id.py +0 -50
- package/flowquery-py/src/parsing/functions/function.py +0 -68
- package/flowquery-py/src/parsing/functions/function_factory.py +0 -170
- package/flowquery-py/src/parsing/functions/function_metadata.py +0 -148
- package/flowquery-py/src/parsing/functions/functions.py +0 -67
- package/flowquery-py/src/parsing/functions/head.py +0 -39
- package/flowquery-py/src/parsing/functions/id_.py +0 -49
- package/flowquery-py/src/parsing/functions/join.py +0 -49
- package/flowquery-py/src/parsing/functions/keys.py +0 -34
- package/flowquery-py/src/parsing/functions/last.py +0 -39
- package/flowquery-py/src/parsing/functions/localdatetime.py +0 -60
- package/flowquery-py/src/parsing/functions/localtime.py +0 -57
- package/flowquery-py/src/parsing/functions/max_.py +0 -49
- package/flowquery-py/src/parsing/functions/min_.py +0 -49
- package/flowquery-py/src/parsing/functions/nodes.py +0 -48
- package/flowquery-py/src/parsing/functions/predicate_function.py +0 -47
- package/flowquery-py/src/parsing/functions/predicate_sum.py +0 -49
- package/flowquery-py/src/parsing/functions/properties.py +0 -50
- package/flowquery-py/src/parsing/functions/rand.py +0 -28
- package/flowquery-py/src/parsing/functions/range_.py +0 -41
- package/flowquery-py/src/parsing/functions/reducer_element.py +0 -15
- package/flowquery-py/src/parsing/functions/relationships.py +0 -46
- package/flowquery-py/src/parsing/functions/replace.py +0 -39
- package/flowquery-py/src/parsing/functions/round_.py +0 -34
- package/flowquery-py/src/parsing/functions/schema.py +0 -40
- package/flowquery-py/src/parsing/functions/size.py +0 -34
- package/flowquery-py/src/parsing/functions/split.py +0 -54
- package/flowquery-py/src/parsing/functions/string_distance.py +0 -92
- package/flowquery-py/src/parsing/functions/stringify.py +0 -49
- package/flowquery-py/src/parsing/functions/substring.py +0 -76
- package/flowquery-py/src/parsing/functions/sum.py +0 -51
- package/flowquery-py/src/parsing/functions/tail.py +0 -37
- package/flowquery-py/src/parsing/functions/temporal_utils.py +0 -186
- package/flowquery-py/src/parsing/functions/time_.py +0 -57
- package/flowquery-py/src/parsing/functions/timestamp.py +0 -37
- package/flowquery-py/src/parsing/functions/to_float.py +0 -46
- package/flowquery-py/src/parsing/functions/to_integer.py +0 -46
- package/flowquery-py/src/parsing/functions/to_json.py +0 -35
- package/flowquery-py/src/parsing/functions/to_lower.py +0 -37
- package/flowquery-py/src/parsing/functions/to_string.py +0 -41
- package/flowquery-py/src/parsing/functions/trim.py +0 -37
- package/flowquery-py/src/parsing/functions/type_.py +0 -47
- package/flowquery-py/src/parsing/functions/value_holder.py +0 -24
- package/flowquery-py/src/parsing/logic/__init__.py +0 -15
- package/flowquery-py/src/parsing/logic/case.py +0 -28
- package/flowquery-py/src/parsing/logic/else_.py +0 -12
- package/flowquery-py/src/parsing/logic/end.py +0 -8
- package/flowquery-py/src/parsing/logic/then.py +0 -12
- package/flowquery-py/src/parsing/logic/when.py +0 -12
- package/flowquery-py/src/parsing/operations/__init__.py +0 -46
- package/flowquery-py/src/parsing/operations/aggregated_return.py +0 -25
- package/flowquery-py/src/parsing/operations/aggregated_with.py +0 -22
- package/flowquery-py/src/parsing/operations/call.py +0 -73
- package/flowquery-py/src/parsing/operations/create_node.py +0 -35
- package/flowquery-py/src/parsing/operations/create_relationship.py +0 -35
- package/flowquery-py/src/parsing/operations/delete_node.py +0 -29
- package/flowquery-py/src/parsing/operations/delete_relationship.py +0 -29
- package/flowquery-py/src/parsing/operations/group_by.py +0 -148
- package/flowquery-py/src/parsing/operations/limit.py +0 -33
- package/flowquery-py/src/parsing/operations/load.py +0 -148
- package/flowquery-py/src/parsing/operations/match.py +0 -52
- package/flowquery-py/src/parsing/operations/operation.py +0 -69
- package/flowquery-py/src/parsing/operations/order_by.py +0 -114
- package/flowquery-py/src/parsing/operations/projection.py +0 -21
- package/flowquery-py/src/parsing/operations/return_op.py +0 -88
- package/flowquery-py/src/parsing/operations/union.py +0 -115
- package/flowquery-py/src/parsing/operations/union_all.py +0 -17
- package/flowquery-py/src/parsing/operations/unwind.py +0 -42
- package/flowquery-py/src/parsing/operations/where.py +0 -43
- package/flowquery-py/src/parsing/operations/with_op.py +0 -18
- package/flowquery-py/src/parsing/parser.py +0 -1384
- package/flowquery-py/src/parsing/parser_state.py +0 -26
- package/flowquery-py/src/parsing/token_to_node.py +0 -109
- package/flowquery-py/src/tokenization/__init__.py +0 -23
- package/flowquery-py/src/tokenization/keyword.py +0 -54
- package/flowquery-py/src/tokenization/operator.py +0 -29
- package/flowquery-py/src/tokenization/string_walker.py +0 -158
- package/flowquery-py/src/tokenization/symbol.py +0 -19
- package/flowquery-py/src/tokenization/token.py +0 -693
- package/flowquery-py/src/tokenization/token_mapper.py +0 -53
- package/flowquery-py/src/tokenization/token_type.py +0 -21
- package/flowquery-py/src/tokenization/tokenizer.py +0 -214
- package/flowquery-py/src/tokenization/trie.py +0 -125
- package/flowquery-py/src/utils/__init__.py +0 -6
- package/flowquery-py/src/utils/object_utils.py +0 -20
- package/flowquery-py/src/utils/string_utils.py +0 -113
- package/flowquery-py/tests/__init__.py +0 -1
- package/flowquery-py/tests/compute/__init__.py +0 -1
- package/flowquery-py/tests/compute/test_runner.py +0 -4902
- package/flowquery-py/tests/graph/__init__.py +0 -1
- package/flowquery-py/tests/graph/test_create.py +0 -56
- package/flowquery-py/tests/graph/test_data.py +0 -73
- package/flowquery-py/tests/graph/test_match.py +0 -40
- package/flowquery-py/tests/parsing/__init__.py +0 -1
- package/flowquery-py/tests/parsing/test_context.py +0 -34
- package/flowquery-py/tests/parsing/test_expression.py +0 -248
- package/flowquery-py/tests/parsing/test_parser.py +0 -1237
- package/flowquery-py/tests/test_extensibility.py +0 -611
- package/flowquery-py/tests/tokenization/__init__.py +0 -1
- package/flowquery-py/tests/tokenization/test_token_mapper.py +0 -60
- package/flowquery-py/tests/tokenization/test_tokenizer.py +0 -198
- package/flowquery-py/tests/tokenization/test_trie.py +0 -30
- package/flowquery-vscode/.vscode-test.mjs +0 -5
- package/flowquery-vscode/.vscodeignore +0 -13
- package/flowquery-vscode/LICENSE +0 -21
- package/flowquery-vscode/README.md +0 -11
- package/flowquery-vscode/demo/FlowQueryVSCodeDemo.gif +0 -0
- package/flowquery-vscode/eslint.config.mjs +0 -25
- package/flowquery-vscode/extension.js +0 -508
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +0 -1
- package/flowquery-vscode/flowquery-worker.js +0 -66
- package/flowquery-vscode/images/FlowQueryLogoIcon.png +0 -0
- package/flowquery-vscode/jsconfig.json +0 -13
- package/flowquery-vscode/libs/page.css +0 -53
- package/flowquery-vscode/libs/table.css +0 -13
- package/flowquery-vscode/libs/tabs.css +0 -66
- package/flowquery-vscode/package-lock.json +0 -2917
- package/flowquery-vscode/package.json +0 -51
- package/flowquery-vscode/test/extension.test.js +0 -196
- package/flowquery-vscode/test/worker.test.js +0 -25
- package/flowquery-vscode/vsc-extension-quickstart.md +0 -42
- package/jest.config.js +0 -14
- package/misc/apps/RAG/README.md +0 -29
- package/misc/apps/RAG/data/chats.json +0 -302
- package/misc/apps/RAG/data/emails.json +0 -182
- package/misc/apps/RAG/data/events.json +0 -226
- package/misc/apps/RAG/data/files.json +0 -172
- package/misc/apps/RAG/data/users.json +0 -158
- package/misc/apps/RAG/jest.config.js +0 -21
- package/misc/apps/RAG/package.json +0 -48
- package/misc/apps/RAG/public/index.html +0 -18
- package/misc/apps/RAG/src/App.css +0 -42
- package/misc/apps/RAG/src/App.tsx +0 -50
- package/misc/apps/RAG/src/components/AdaptiveCardRenderer.css +0 -172
- package/misc/apps/RAG/src/components/AdaptiveCardRenderer.tsx +0 -380
- package/misc/apps/RAG/src/components/ApiKeySettings.tsx +0 -245
- package/misc/apps/RAG/src/components/ChatContainer.css +0 -67
- package/misc/apps/RAG/src/components/ChatContainer.tsx +0 -242
- package/misc/apps/RAG/src/components/ChatInput.css +0 -23
- package/misc/apps/RAG/src/components/ChatInput.tsx +0 -76
- package/misc/apps/RAG/src/components/ChatMessage.css +0 -160
- package/misc/apps/RAG/src/components/ChatMessage.tsx +0 -286
- package/misc/apps/RAG/src/components/FlowQueryAgent.ts +0 -708
- package/misc/apps/RAG/src/components/FlowQueryRunner.css +0 -113
- package/misc/apps/RAG/src/components/FlowQueryRunner.tsx +0 -371
- package/misc/apps/RAG/src/components/index.ts +0 -28
- package/misc/apps/RAG/src/graph/index.ts +0 -19
- package/misc/apps/RAG/src/graph/initializeGraph.ts +0 -254
- package/misc/apps/RAG/src/index.tsx +0 -29
- package/misc/apps/RAG/src/prompts/FlowQuerySystemPrompt.ts +0 -327
- package/misc/apps/RAG/src/prompts/index.ts +0 -10
- package/misc/apps/RAG/src/tests/graph.test.ts +0 -35
- package/misc/apps/RAG/src/utils/FlowQueryExecutor.ts +0 -130
- package/misc/apps/RAG/src/utils/FlowQueryExtractor.ts +0 -208
- package/misc/apps/RAG/src/utils/Llm.ts +0 -248
- package/misc/apps/RAG/src/utils/index.ts +0 -12
- package/misc/apps/RAG/tsconfig.json +0 -22
- package/misc/apps/RAG/webpack.config.js +0 -43
- package/misc/apps/README.md +0 -1
- package/misc/queries/analyze_catfacts.cql +0 -75
- package/misc/queries/azure_openai_completions.cql +0 -13
- package/misc/queries/azure_openai_models.cql +0 -9
- package/misc/queries/mock_pipeline.cql +0 -84
- package/misc/queries/openai_completions.cql +0 -15
- package/misc/queries/openai_models.cql +0 -13
- package/misc/queries/test.cql +0 -6
- package/misc/queries/tool_inference.cql +0 -24
- package/misc/queries/wisdom.cql +0 -6
- package/misc/queries/wisdom_letter_histogram.cql +0 -8
- package/src/compute/flowquery.ts +0 -46
- package/src/compute/runner.ts +0 -66
- package/src/extensibility.ts +0 -45
- package/src/graph/data.ts +0 -130
- package/src/graph/database.ts +0 -143
- package/src/graph/hops.ts +0 -22
- package/src/graph/node.ts +0 -122
- package/src/graph/node_data.ts +0 -18
- package/src/graph/node_reference.ts +0 -38
- package/src/graph/pattern.ts +0 -110
- package/src/graph/pattern_expression.ts +0 -48
- package/src/graph/patterns.ts +0 -36
- package/src/graph/physical_node.ts +0 -23
- package/src/graph/physical_relationship.ts +0 -23
- package/src/graph/relationship.ts +0 -167
- package/src/graph/relationship_data.ts +0 -31
- package/src/graph/relationship_match_collector.ts +0 -64
- package/src/graph/relationship_reference.ts +0 -25
- package/src/index.browser.ts +0 -46
- package/src/index.node.ts +0 -55
- package/src/index.ts +0 -12
- package/src/io/command_line.ts +0 -74
- package/src/parsing/alias.ts +0 -23
- package/src/parsing/alias_option.ts +0 -5
- package/src/parsing/ast_node.ts +0 -153
- package/src/parsing/base_parser.ts +0 -98
- package/src/parsing/components/csv.ts +0 -9
- package/src/parsing/components/from.ts +0 -12
- package/src/parsing/components/headers.ts +0 -12
- package/src/parsing/components/json.ts +0 -9
- package/src/parsing/components/null.ts +0 -9
- package/src/parsing/components/post.ts +0 -9
- package/src/parsing/components/text.ts +0 -9
- package/src/parsing/context.ts +0 -54
- package/src/parsing/data_structures/associative_array.ts +0 -43
- package/src/parsing/data_structures/json_array.ts +0 -31
- package/src/parsing/data_structures/key_value_pair.ts +0 -37
- package/src/parsing/data_structures/lookup.ts +0 -44
- package/src/parsing/data_structures/range_lookup.ts +0 -36
- package/src/parsing/expressions/boolean.ts +0 -21
- package/src/parsing/expressions/expression.ts +0 -150
- package/src/parsing/expressions/expression_map.ts +0 -22
- package/src/parsing/expressions/f_string.ts +0 -26
- package/src/parsing/expressions/identifier.ts +0 -22
- package/src/parsing/expressions/number.ts +0 -40
- package/src/parsing/expressions/operator.ts +0 -354
- package/src/parsing/expressions/reference.ts +0 -45
- package/src/parsing/expressions/string.ts +0 -34
- package/src/parsing/functions/aggregate_function.ts +0 -58
- package/src/parsing/functions/async_function.ts +0 -64
- package/src/parsing/functions/avg.ts +0 -47
- package/src/parsing/functions/coalesce.ts +0 -49
- package/src/parsing/functions/collect.ts +0 -54
- package/src/parsing/functions/count.ts +0 -54
- package/src/parsing/functions/date.ts +0 -63
- package/src/parsing/functions/datetime.ts +0 -63
- package/src/parsing/functions/duration.ts +0 -143
- package/src/parsing/functions/element_id.ts +0 -51
- package/src/parsing/functions/function.ts +0 -60
- package/src/parsing/functions/function_factory.ts +0 -195
- package/src/parsing/functions/function_metadata.ts +0 -217
- package/src/parsing/functions/functions.ts +0 -70
- package/src/parsing/functions/head.ts +0 -42
- package/src/parsing/functions/id.ts +0 -51
- package/src/parsing/functions/join.ts +0 -40
- package/src/parsing/functions/keys.ts +0 -29
- package/src/parsing/functions/last.ts +0 -42
- package/src/parsing/functions/localdatetime.ts +0 -63
- package/src/parsing/functions/localtime.ts +0 -58
- package/src/parsing/functions/max.ts +0 -37
- package/src/parsing/functions/min.ts +0 -37
- package/src/parsing/functions/nodes.ts +0 -54
- package/src/parsing/functions/predicate_function.ts +0 -48
- package/src/parsing/functions/predicate_sum.ts +0 -47
- package/src/parsing/functions/properties.ts +0 -56
- package/src/parsing/functions/rand.ts +0 -21
- package/src/parsing/functions/range.ts +0 -37
- package/src/parsing/functions/reducer_element.ts +0 -10
- package/src/parsing/functions/relationships.ts +0 -52
- package/src/parsing/functions/replace.ts +0 -38
- package/src/parsing/functions/round.ts +0 -28
- package/src/parsing/functions/schema.ts +0 -39
- package/src/parsing/functions/size.ts +0 -28
- package/src/parsing/functions/split.ts +0 -45
- package/src/parsing/functions/string_distance.ts +0 -83
- package/src/parsing/functions/stringify.ts +0 -37
- package/src/parsing/functions/substring.ts +0 -68
- package/src/parsing/functions/sum.ts +0 -41
- package/src/parsing/functions/tail.ts +0 -39
- package/src/parsing/functions/temporal_utils.ts +0 -180
- package/src/parsing/functions/time.ts +0 -58
- package/src/parsing/functions/timestamp.ts +0 -37
- package/src/parsing/functions/to_float.ts +0 -50
- package/src/parsing/functions/to_integer.ts +0 -50
- package/src/parsing/functions/to_json.ts +0 -28
- package/src/parsing/functions/to_lower.ts +0 -28
- package/src/parsing/functions/to_string.ts +0 -32
- package/src/parsing/functions/trim.ts +0 -28
- package/src/parsing/functions/type.ts +0 -39
- package/src/parsing/functions/value_holder.ts +0 -13
- package/src/parsing/logic/case.ts +0 -26
- package/src/parsing/logic/else.ts +0 -12
- package/src/parsing/logic/end.ts +0 -9
- package/src/parsing/logic/then.ts +0 -12
- package/src/parsing/logic/when.ts +0 -12
- package/src/parsing/operations/aggregated_return.ts +0 -22
- package/src/parsing/operations/aggregated_with.ts +0 -18
- package/src/parsing/operations/call.ts +0 -69
- package/src/parsing/operations/create_node.ts +0 -39
- package/src/parsing/operations/create_relationship.ts +0 -38
- package/src/parsing/operations/delete_node.ts +0 -33
- package/src/parsing/operations/delete_relationship.ts +0 -32
- package/src/parsing/operations/group_by.ts +0 -137
- package/src/parsing/operations/limit.ts +0 -31
- package/src/parsing/operations/load.ts +0 -146
- package/src/parsing/operations/match.ts +0 -54
- package/src/parsing/operations/operation.ts +0 -69
- package/src/parsing/operations/order_by.ts +0 -126
- package/src/parsing/operations/projection.ts +0 -18
- package/src/parsing/operations/return.ts +0 -76
- package/src/parsing/operations/union.ts +0 -114
- package/src/parsing/operations/union_all.ts +0 -16
- package/src/parsing/operations/unwind.ts +0 -36
- package/src/parsing/operations/where.ts +0 -42
- package/src/parsing/operations/with.ts +0 -20
- package/src/parsing/parser.ts +0 -1641
- package/src/parsing/parser_state.ts +0 -25
- package/src/parsing/token_to_node.ts +0 -114
- package/src/tokenization/keyword.ts +0 -50
- package/src/tokenization/operator.ts +0 -25
- package/src/tokenization/string_walker.ts +0 -197
- package/src/tokenization/symbol.ts +0 -15
- package/src/tokenization/token.ts +0 -764
- package/src/tokenization/token_mapper.ts +0 -53
- package/src/tokenization/token_type.ts +0 -16
- package/src/tokenization/tokenizer.ts +0 -250
- package/src/tokenization/trie.ts +0 -117
- package/src/utils/object_utils.ts +0 -17
- package/src/utils/string_utils.ts +0 -114
- package/tests/compute/runner.test.ts +0 -4559
- package/tests/extensibility.test.ts +0 -643
- package/tests/graph/create.test.ts +0 -36
- package/tests/graph/data.test.ts +0 -58
- package/tests/graph/match.test.ts +0 -29
- package/tests/parsing/context.test.ts +0 -27
- package/tests/parsing/expression.test.ts +0 -303
- package/tests/parsing/parser.test.ts +0 -1327
- package/tests/tokenization/token_mapper.test.ts +0 -47
- package/tests/tokenization/tokenizer.test.ts +0 -191
- package/tests/tokenization/trie.test.ts +0 -20
- package/tsconfig.json +0 -19
- package/typedoc.json +0 -16
- package/vscode-settings.json.recommended +0 -16
- 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
|