From 57bdc61cdddb718743e344168a7daa2d8ed714d2 Mon Sep 17 00:00:00 2001 From: Nicolas Drebenstedt Date: Thu, 31 Oct 2024 13:13:55 +0100 Subject: [PATCH 1/2] improve logs and queries --- CHANGELOG.md | 3 + mex/backend/graph/connector.py | 7 +- .../cypher/fetch_extracted_or_rule_items.cql | 9 +- .../graph/cypher/fetch_merged_items.cql | 9 +- mex/backend/graph/cypher/merge_edges.cql | 6 +- mex/backend/graph/cypher/merge_item.cql | 2 +- mex/backend/merged/helpers.py | 82 +++++++++++-------- pdm.lock | 2 +- pyproject.toml | 4 +- tests/graph/test_query.py | 52 ++++++------ tests/rules/test_main.py | 4 +- 11 files changed, 99 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b48ef9c..3f5823d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - upgrade mex-common and mex-model dependencies to metadata model v3 - apply additional linters in prep for `all` ruff linters +- mute warnings about labels used in queries but missing in graph +- split up search_merged_items_in_graph for better readability +- update cypher queries to use `CALL` clauses with correct variable scope ### Deprecated diff --git a/mex/backend/graph/connector.py b/mex/backend/graph/connector.py index 59569f9..54a2454 100644 --- a/mex/backend/graph/connector.py +++ b/mex/backend/graph/connector.py @@ -3,7 +3,7 @@ from string import Template from typing import Annotated, Any, Literal, cast -from neo4j import Driver, GraphDatabase, NotificationMinimumSeverity +from neo4j import Driver, GraphDatabase, NotificationDisabledCategory from pydantic import Field from mex.backend.fields import ( @@ -84,7 +84,10 @@ def _init_driver(self) -> Driver: settings.graph_password.get_secret_value(), ), database=settings.graph_db, - warn_notification_severity=NotificationMinimumSeverity.OFF, + notifications_disabled_categories=[ + # mute warnings about labels used in queries but missing in graph + NotificationDisabledCategory.UNRECOGNIZED, + ], ) def _check_connectivity_and_authentication(self) -> Result: diff --git a/mex/backend/graph/cypher/fetch_extracted_or_rule_items.cql b/mex/backend/graph/cypher/fetch_extracted_or_rule_items.cql index cb13b43..0087e6f 100644 --- a/mex/backend/graph/cypher/fetch_extracted_or_rule_items.cql +++ b/mex/backend/graph/cypher/fetch_extracted_or_rule_items.cql @@ -16,7 +16,7 @@ Returns: contains the values of nested objects as well as the identifiers of referenced items -#> -CALL { +CALL () { <%- block match_clause -%> <%- if filter_by_query_string %> CALL db.index.fulltext.queryNodes("search_index", $query_string) @@ -38,10 +38,10 @@ CALL { <%- endblock %> RETURN COUNT(n) AS total } -CALL { +CALL () { <<-self.match_clause()>> - CALL { - WITH n + WITH n + CALL (n) { OPTIONAL MATCH (n)-[r]->(referenced:<>) RETURN CASE WHEN referenced IS NOT NULL THEN { label: type(r), @@ -49,7 +49,6 @@ CALL { value: referenced.identifier } ELSE NULL END as ref UNION - WITH n OPTIONAL MATCH (n)-[r]->(nested:<>) RETURN CASE WHEN nested IS NOT NULL THEN { label: type(r), diff --git a/mex/backend/graph/cypher/fetch_merged_items.cql b/mex/backend/graph/cypher/fetch_merged_items.cql index a96447b..86bb137 100644 --- a/mex/backend/graph/cypher/fetch_merged_items.cql +++ b/mex/backend/graph/cypher/fetch_merged_items.cql @@ -19,7 +19,7 @@ Returns: merged item. Each component has an extra attribute `_refs` that contains the values of nested objects as well as the identifiers of referenced items. -#> -CALL { +CALL () { <%- block match_clause -%> <%- if filter_by_query_string %> CALL db.index.fulltext.queryNodes("search_index", $query_string) @@ -39,11 +39,11 @@ CALL { <%- endblock %> RETURN COUNT(merged) AS total } -CALL { +CALL () { <<-self.match_clause()>> OPTIONAL MATCH (n)-[:stableTargetId]->(merged) - CALL { - WITH n + WITH n, merged + CALL (n) { OPTIONAL MATCH (n)-[r]->(referenced:<>) RETURN CASE WHEN referenced IS NOT NULL THEN { label: type(r), @@ -51,7 +51,6 @@ CALL { value: referenced.identifier } ELSE NULL END as ref UNION - WITH n OPTIONAL MATCH (n)-[r]->(nested:<>) RETURN CASE WHEN nested IS NOT NULL THEN { label: type(r), diff --git a/mex/backend/graph/cypher/merge_edges.cql b/mex/backend/graph/cypher/merge_edges.cql index 7b1a82a..58a84a6 100644 --- a/mex/backend/graph/cypher/merge_edges.cql +++ b/mex/backend/graph/cypher/merge_edges.cql @@ -19,8 +19,8 @@ Returns: pruned: Number of pruned edges edges: List of the merged edge objects -#> -MATCH (source:<> {<>})-[stableTargetId:stableTargetId]->({identifier: $stable_target_id}) -CALL { +MATCH (source:<> {<>})-[:stableTargetId]->({identifier: $stable_target_id}) +CALL (source) { <%- if ref_labels %> <%- set union = joiner("UNION\n ") %> <%- for ref_label in ref_labels %> @@ -35,7 +35,7 @@ CALL { <%- endif %> } WITH source, count(edge) as merged, collect(edge) as edges -CALL { +CALL (source, edges) { WITH source, edges MATCH (source)-[outdated_edge]->(:<>) WHERE NOT outdated_edge IN edges diff --git a/mex/backend/graph/cypher/merge_item.cql b/mex/backend/graph/cypher/merge_item.cql index d4f534d..ea93003 100644 --- a/mex/backend/graph/cypher/merge_item.cql +++ b/mex/backend/graph/cypher/merge_item.cql @@ -40,7 +40,7 @@ ON MATCH SET value_<> += $nested_values[<>] WITH current, [<>] as edges, [<>] as values -CALL { +CALL (current, values) { WITH current, values MATCH (current)-[]->(outdated_node:<>) WHERE NOT outdated_node IN values diff --git a/mex/backend/merged/helpers.py b/mex/backend/merged/helpers.py index aec920c..96653c8 100644 --- a/mex/backend/merged/helpers.py +++ b/mex/backend/merged/helpers.py @@ -25,6 +25,10 @@ from mex.common.transform import ensure_prefix from mex.common.types import Identifier +EXTRACTED_MODEL_ADAPTER: TypeAdapter[AnyExtractedModel] = TypeAdapter( + Annotated[AnyExtractedModel, Field(discriminator="entityType")] +) + def _merge_extracted_items_and_apply_preventive_rule( merged_dict: dict[str, Any], @@ -132,6 +136,43 @@ def create_merged_item( return cast(AnyMergedModel, merged_item) # mypy, get a grip! +def merge_search_result_item(item: dict[str, Any]) -> AnyMergedModel: + """Merge a single search result into a merged item. + + Args: + item: Raw merged search result item from the graph response + + Raises: + InconsistentGraphError: When the graph response item has inconsistencies + + Returns: + AnyMergedModel instance + """ + extracted_items = [ + EXTRACTED_MODEL_ADAPTER.validate_python(component) + for component in item["components"] + if component["entityType"] in EXTRACTED_MODEL_CLASSES_BY_NAME + ] + rules_raw = [ + component + for component in item["components"] + if component["entityType"] in RULE_MODEL_CLASSES_BY_NAME + ] + if len(rules_raw) == NUMBER_OF_RULE_TYPES: + rule_set_response = transform_raw_rules_to_rule_set_response(rules_raw) + elif len(rules_raw) == 0: + rule_set_response = None + else: + msg = f"Unexpected number of rules found in graph: {len(rules_raw)}" + raise InconsistentGraphError(msg) + + return create_merged_item( + identifier=item["identifier"], + extracted_items=extracted_items, + rule_set=rule_set_response, + ) + + def search_merged_items_in_graph( query_string: str | None = None, stable_target_id: str | None = None, @@ -149,7 +190,7 @@ def search_merged_items_in_graph( limit: How many items to return at most Raises: - InconsistentGraphError: When the graph response cannot be parsed + InconsistentGraphError: When the graph response has inconsistencies Returns: MergedItemSearch instance @@ -162,39 +203,16 @@ def search_merged_items_in_graph( skip=skip, limit=limit, ) - extracted_model_adapter: TypeAdapter[AnyExtractedModel] = TypeAdapter( - Annotated[AnyExtractedModel, Field(discriminator="entityType")] - ) - items: list[AnyMergedModel] = [] total: int = result["total"] - - for item in result["items"]: - extracted_items = [ - extracted_model_adapter.validate_python(component) - for component in item["components"] - if component["entityType"] in EXTRACTED_MODEL_CLASSES_BY_NAME - ] - - rules_raw = [ - component - for component in item["components"] - if component["entityType"] in RULE_MODEL_CLASSES_BY_NAME - ] - if len(rules_raw) == NUMBER_OF_RULE_TYPES: - rule_set_response = transform_raw_rules_to_rule_set_response(rules_raw) - elif len(rules_raw) == 0: - rule_set_response = None - else: - msg = f"Unexpected number of rules found in graph: {len(rules_raw)}" - raise MExError(msg) - - items.append( - create_merged_item( - identifier=item["identifier"], - extracted_items=extracted_items, - rule_set=rule_set_response, - ) + items: list[AnyMergedModel] = [ + reraising( + ValidationError, + InconsistentGraphError, + merge_search_result_item, + item, ) + for item in result["items"] + ] return reraising( ValidationError, InconsistentGraphError, diff --git a/pdm.lock b/pdm.lock index 1706693..0f14abf 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:955352b2e90470cfd0c69cc2077533d857fc618f58ad1e959359943e1d0a0801" +content_hash = "sha256:9c596095290ba137ed62016d05b715b9cbbe1195ee19f4cf3b33a927ff19cc26" [[metadata.targets]] requires_python = "==3.11.*" diff --git a/pyproject.toml b/pyproject.toml index 1a01b27..7df0cb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,10 +12,10 @@ dependencies = [ "httpx>=0.27.2,<1", "jinja2>=3.1.4,<4", "mex-common @ git+https://github.com/robert-koch-institut/mex-common.git@0.40.0", - "neo4j>=5.24.0,<6", + "neo4j>=5.25.0,<6", "pydantic>=2.9.1,<3", "starlette>=0.41.2,<1", - "uvicorn[standard]>=0.30.6,<1", + "uvicorn[standard]>=0.32.0,<1", ] optional-dependencies.dev = [ "black>=24.8.0,<25", diff --git a/tests/graph/test_query.py b/tests/graph/test_query.py index 670f554..0a6c71d 100644 --- a/tests/graph/test_query.py +++ b/tests/graph/test_query.py @@ -74,7 +74,7 @@ def test_fetch_database_status(query_builder: QueryBuilder) -> None: True, True, """\ -CALL { +CALL () { CALL db.index.fulltext.queryNodes("search_index", $query_string) YIELD node AS hit, score OPTIONAL MATCH (n:AdditiveThis|AdditiveThat|AdditiveOther|ExtractedThis|ExtractedThat|ExtractedOther)-[:stableTargetId]->(merged:MergedThis|MergedThat|MergedOther) @@ -84,7 +84,7 @@ def test_fetch_database_status(query_builder: QueryBuilder) -> None: AND ANY(label IN labels(n) WHERE label IN $labels) RETURN COUNT(n) AS total } -CALL { +CALL () { CALL db.index.fulltext.queryNodes("search_index", $query_string) YIELD node AS hit, score OPTIONAL MATCH (n:AdditiveThis|AdditiveThat|AdditiveOther|ExtractedThis|ExtractedThat|ExtractedOther)-[:stableTargetId]->(merged:MergedThis|MergedThat|MergedOther) @@ -92,8 +92,8 @@ def test_fetch_database_status(query_builder: QueryBuilder) -> None: elementId(hit) = elementId(n) AND merged.identifier = $stable_target_id AND ANY(label IN labels(n) WHERE label IN $labels) - CALL { - WITH n + WITH n + CALL (n) { OPTIONAL MATCH (n)-[r]->(referenced:MergedThis|MergedThat|MergedOther) RETURN CASE WHEN referenced IS NOT NULL THEN { label: type(r), @@ -101,7 +101,6 @@ def test_fetch_database_status(query_builder: QueryBuilder) -> None: value: referenced.identifier } ELSE NULL END as ref UNION - WITH n OPTIONAL MATCH (n)-[r]->(nested:Link|Text|Location) RETURN CASE WHEN nested IS NOT NULL THEN { label: type(r), @@ -121,18 +120,18 @@ def test_fetch_database_status(query_builder: QueryBuilder) -> None: False, False, """\ -CALL { +CALL () { OPTIONAL MATCH (n:AdditiveThis|AdditiveThat|AdditiveOther|ExtractedThis|ExtractedThat|ExtractedOther) WHERE ANY(label IN labels(n) WHERE label IN $labels) RETURN COUNT(n) AS total } -CALL { +CALL () { OPTIONAL MATCH (n:AdditiveThis|AdditiveThat|AdditiveOther|ExtractedThis|ExtractedThat|ExtractedOther) WHERE ANY(label IN labels(n) WHERE label IN $labels) - CALL { - WITH n + WITH n + CALL (n) { OPTIONAL MATCH (n)-[r]->(referenced:MergedThis|MergedThat|MergedOther) RETURN CASE WHEN referenced IS NOT NULL THEN { label: type(r), @@ -140,7 +139,6 @@ def test_fetch_database_status(query_builder: QueryBuilder) -> None: value: referenced.identifier } ELSE NULL END as ref UNION - WITH n OPTIONAL MATCH (n)-[r]->(nested:Link|Text|Location) RETURN CASE WHEN nested IS NOT NULL THEN { label: type(r), @@ -183,7 +181,7 @@ def test_fetch_extracted_items( True, True, """\ -CALL { +CALL () { CALL db.index.fulltext.queryNodes("search_index", $query_string) YIELD node AS hit, score OPTIONAL MATCH (n:AdditiveThis|AdditiveThat|AdditiveOther|ExtractedThis|ExtractedThat|ExtractedOther)-[:stableTargetId]->(merged:MergedThis|MergedThat|MergedOther) @@ -194,7 +192,7 @@ def test_fetch_extracted_items( WITH DISTINCT merged as merged RETURN COUNT(merged) AS total } -CALL { +CALL () { CALL db.index.fulltext.queryNodes("search_index", $query_string) YIELD node AS hit, score OPTIONAL MATCH (n:AdditiveThis|AdditiveThat|AdditiveOther|ExtractedThis|ExtractedThat|ExtractedOther)-[:stableTargetId]->(merged:MergedThis|MergedThat|MergedOther) @@ -204,8 +202,8 @@ def test_fetch_extracted_items( AND ANY(label IN labels(merged) WHERE label IN $labels) WITH DISTINCT merged as merged OPTIONAL MATCH (n)-[:stableTargetId]->(merged) - CALL { - WITH n + WITH n, merged + CALL (n) { OPTIONAL MATCH (n)-[r]->(referenced:MergedThis|MergedThat|MergedOther) RETURN CASE WHEN referenced IS NOT NULL THEN { label: type(r), @@ -213,7 +211,6 @@ def test_fetch_extracted_items( value: referenced.identifier } ELSE NULL END as ref UNION - WITH n OPTIONAL MATCH (n)-[r]->(nested:Link|Text|Location) RETURN CASE WHEN nested IS NOT NULL THEN { label: type(r), @@ -234,21 +231,21 @@ def test_fetch_extracted_items( False, False, """\ -CALL { +CALL () { OPTIONAL MATCH (n:AdditiveThis|AdditiveThat|AdditiveOther|ExtractedThis|ExtractedThat|ExtractedOther)-[:stableTargetId]->(merged:MergedThis|MergedThat|MergedOther) WHERE ANY(label IN labels(merged) WHERE label IN $labels) WITH DISTINCT merged as merged RETURN COUNT(merged) AS total } -CALL { +CALL () { OPTIONAL MATCH (n:AdditiveThis|AdditiveThat|AdditiveOther|ExtractedThis|ExtractedThat|ExtractedOther)-[:stableTargetId]->(merged:MergedThis|MergedThat|MergedOther) WHERE ANY(label IN labels(merged) WHERE label IN $labels) WITH DISTINCT merged as merged OPTIONAL MATCH (n)-[:stableTargetId]->(merged) - CALL { - WITH n + WITH n, merged + CALL (n) { OPTIONAL MATCH (n)-[r]->(referenced:MergedThis|MergedThat|MergedOther) RETURN CASE WHEN referenced IS NOT NULL THEN { label: type(r), @@ -256,7 +253,6 @@ def test_fetch_extracted_items( value: referenced.identifier } ELSE NULL END as ref UNION - WITH n OPTIONAL MATCH (n)-[r]->(nested:Link|Text|Location) RETURN CASE WHEN nested IS NOT NULL THEN { label: type(r), @@ -375,8 +371,8 @@ def test_fetch_identities( ( ["personInCharge", "meetingScheduledBy", "agendaSignedOff"], """\ -MATCH (source:ExtractedThat {identifier: $identifier})-[stableTargetId:stableTargetId]->({identifier: $stable_target_id}) -CALL { +MATCH (source:ExtractedThat {identifier: $identifier})-[:stableTargetId]->({identifier: $stable_target_id}) +CALL (source) { WITH source MATCH (target_0 {identifier: $ref_identifiers[0]}) MERGE (source)-[edge:personInCharge {position: $ref_positions[0]}]->(target_0) @@ -393,7 +389,7 @@ def test_fetch_identities( RETURN edge } WITH source, count(edge) as merged, collect(edge) as edges -CALL { +CALL (source, edges) { WITH source, edges MATCH (source)-[outdated_edge]->(:MergedThis|MergedThat|MergedOther) WHERE NOT outdated_edge IN edges @@ -405,12 +401,12 @@ def test_fetch_identities( ( [], """\ -MATCH (source:ExtractedThat {identifier: $identifier})-[stableTargetId:stableTargetId]->({identifier: $stable_target_id}) -CALL { +MATCH (source:ExtractedThat {identifier: $identifier})-[:stableTargetId]->({identifier: $stable_target_id}) +CALL (source) { RETURN null as edge } WITH source, count(edge) as merged, collect(edge) as edges -CALL { +CALL (source, edges) { WITH source, edges MATCH (source)-[outdated_edge]->(:MergedThis|MergedThat|MergedOther) WHERE NOT outdated_edge IN edges @@ -456,7 +452,7 @@ def test_merge_edges( WITH current, [edge_0, edge_1, edge_2] as edges, [value_0, value_1, value_2] as values -CALL { +CALL (current, values) { WITH current, values MATCH (current)-[]->(outdated_node:Link|Text|Location) WHERE NOT outdated_node IN values @@ -476,7 +472,7 @@ def test_merge_edges( WITH current, [] as edges, [] as values -CALL { +CALL (current, values) { WITH current, values MATCH (current)-[]->(outdated_node:Link|Text|Location) WHERE NOT outdated_node IN values diff --git a/tests/rules/test_main.py b/tests/rules/test_main.py index fbab97b..62c3998 100644 --- a/tests/rules/test_main.py +++ b/tests/rules/test_main.py @@ -13,13 +13,13 @@ def get_graph() -> list[dict[str, Any]]: connector = GraphConnector.get() graph = connector.commit( """ -CALL { +CALL () { MATCH (n) RETURN collect(n{ .*, label: head(labels(n)) }) as nodes } -CALL { +CALL () { MATCH ()-[r]->() RETURN collect({ label: type(r), position: r.position, From a83d6c78b152bf862470cd1bf95084576c935abc Mon Sep 17 00:00:00 2001 From: Nicolas Drebenstedt Date: Mon, 4 Nov 2024 10:12:55 +0100 Subject: [PATCH 2/2] apply code review suggestions --- .github/workflows/testing.yml | 2 +- CHANGELOG.md | 1 + README.md | 8 ++++++++ compose.yaml | 2 +- mex/backend/graph/cypher/merge_edges.cql | 1 - mex/backend/graph/cypher/merge_item.cql | 1 - tests/graph/test_query.py | 4 ---- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f5f9e50..7fa1f6f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -27,7 +27,7 @@ jobs: services: neo4j: - image: neo4j:5.24-community + image: neo4j:5.25-community env: NEO4J_AUTH: neo4j/password ports: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f5823d..dcf7c54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - mute warnings about labels used in queries but missing in graph - split up search_merged_items_in_graph for better readability - update cypher queries to use `CALL` clauses with correct variable scope +- BREAKING: drop support for neo4j server version 5.6 and lower ### Deprecated diff --git a/README.md b/README.md index a6750fb..d922883 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,14 @@ components of the MEx project are open-sourced under the same license as well. - switch version `pyenv global 3.11` - run `.\mex.bat install` +### database + +- for local development, neo4j desktop edition is recommended + - make sure you download and run the same version as in `testing.yml` + - also make sure db name, user and password match the `settings.py` +- for production deployments, a container runtime is recommended + - for a configuration example, see `compose.yaml` + ### linting and testing - run all linters with `pdm lint` diff --git a/compose.yaml b/compose.yaml index 4d8396d..41a124b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -17,7 +17,7 @@ services: timeout: 5s retries: 5 neo4j: - image: neo4j:5.19-community + image: neo4j:5.25-community environment: - NEO4J_AUTH=neo4j/password expose: diff --git a/mex/backend/graph/cypher/merge_edges.cql b/mex/backend/graph/cypher/merge_edges.cql index 58a84a6..d4af8d1 100644 --- a/mex/backend/graph/cypher/merge_edges.cql +++ b/mex/backend/graph/cypher/merge_edges.cql @@ -36,7 +36,6 @@ CALL (source) { } WITH source, count(edge) as merged, collect(edge) as edges CALL (source, edges) { - WITH source, edges MATCH (source)-[outdated_edge]->(:<>) WHERE NOT outdated_edge IN edges DELETE outdated_edge diff --git a/mex/backend/graph/cypher/merge_item.cql b/mex/backend/graph/cypher/merge_item.cql index ea93003..99dd315 100644 --- a/mex/backend/graph/cypher/merge_item.cql +++ b/mex/backend/graph/cypher/merge_item.cql @@ -41,7 +41,6 @@ WITH current, [<>] as edges, [<>] as values CALL (current, values) { - WITH current, values MATCH (current)-[]->(outdated_node:<>) WHERE NOT outdated_node IN values DETACH DELETE outdated_node diff --git a/tests/graph/test_query.py b/tests/graph/test_query.py index 0a6c71d..e121170 100644 --- a/tests/graph/test_query.py +++ b/tests/graph/test_query.py @@ -390,7 +390,6 @@ def test_fetch_identities( } WITH source, count(edge) as merged, collect(edge) as edges CALL (source, edges) { - WITH source, edges MATCH (source)-[outdated_edge]->(:MergedThis|MergedThat|MergedOther) WHERE NOT outdated_edge IN edges DELETE outdated_edge @@ -407,7 +406,6 @@ def test_fetch_identities( } WITH source, count(edge) as merged, collect(edge) as edges CALL (source, edges) { - WITH source, edges MATCH (source)-[outdated_edge]->(:MergedThis|MergedThat|MergedOther) WHERE NOT outdated_edge IN edges DELETE outdated_edge @@ -453,7 +451,6 @@ def test_merge_edges( [edge_0, edge_1, edge_2] as edges, [value_0, value_1, value_2] as values CALL (current, values) { - WITH current, values MATCH (current)-[]->(outdated_node:Link|Text|Location) WHERE NOT outdated_node IN values DETACH DELETE outdated_node @@ -473,7 +470,6 @@ def test_merge_edges( [] as edges, [] as values CALL (current, values) { - WITH current, values MATCH (current)-[]->(outdated_node:Link|Text|Location) WHERE NOT outdated_node IN values DETACH DELETE outdated_node