From 39e9eed7e73cbd635013ff327a8325b433394f18 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> Date: Tue, 26 Aug 2025 20:48:21 -0400 Subject: [PATCH 1/8] new: `second_order_edge_collection_name` --- arango_rdf/main.py | 56 ++++++++++++++++++++++++++++++++++++----- tests/test_main.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/arango_rdf/main.py b/arango_rdf/main.py index 270d317..b29fdbf 100644 --- a/arango_rdf/main.py +++ b/arango_rdf/main.py @@ -1345,6 +1345,9 @@ def migrate_edges_to_attributes( sort_clause: Optional[str] = None, return_clause: Optional[str] = None, filter_clause: Optional[str] = None, + with_collections: Optional[List[str]] = None, + second_order_edge_collection_name: Optional[str] = None, + second_order_depth: Optional[int] = None, ) -> int: """RDF --> ArangoDB (PGT): Migrate all edges in the specified edge collection to attributes. This method is useful when combined with the @@ -1378,9 +1381,23 @@ def migrate_edges_to_attributes( :param filter_clause: A FILTER statement to filter the traversed edges & target vertices. Defaults to None. :type filter_clause: Optional[str] + :param with_collections: A list of collections to include in the WITH clause. + Defaults to the target edge collection's + **to_vertex_collections** property based off its edge definition. + :type with_collections: Optional[List[str]] + :param second_order_edge_collection_name: In addition to the **edge_collection_name**, + it is possible to traverse the edges of the second order edge collection to apply + the same attribute to the original target verticies. A common use case is to + set **edge_collection_name** to **"type"** and **second_order_edge_collection_name** + to **"subClassOf"** for inferring the **_type** attribute. Defaults to None. + :type second_order_edge_collection_name: Optional[str] + :param second_order_depth: The depth of the second order traversal. Defaults to 1. + This parameter is only used if **second_order_edge_collection_name** is set. + :type second_order_depth: Optional[int] :return: The number of documents updated. :rtype: int """ + bind_vars = {"@e_col": edge_collection_name} if not self.db.has_graph(graph_name): raise ValueError(f"Graph '{graph_name}' does not exist") @@ -1409,27 +1426,54 @@ def migrate_edges_to_attributes( if not return_clause: return_clause = f"v.{self.__rdf_label_attr}" - with_cols = set(target_e_d["to_vertex_collections"]) - with_cols_str = "WITH " + ", ".join(with_cols) + with_collections_set = ( + set(with_collections) + if with_collections + else set(target_e_d["to_vertex_collections"]) + ) + + with_cols_str = "WITH " + ", ".join(with_collections_set) + + second_order_labels_query = "[]" + if second_order_edge_collection_name is not None: + second_order_depth = ( + second_order_depth if isinstance(second_order_depth, int) else 1 + ) + + second_order_labels_query = f""" + ( + FOR start IN 1..1 {edge_direction} doc @@e_col + FOR v, e IN 1..{second_order_depth} {edge_direction} start @@second_order_e_col + {f"FILTER {filter_clause}" if filter_clause else ""} + {f"SORT {sort_clause}" if sort_clause else ""} + RETURN {return_clause} + ) + """ + + bind_vars["@second_order_e_col"] = second_order_edge_collection_name count = 0 for v_col in target_e_d["from_vertex_collections"]: query = f""" {with_cols_str} FOR doc IN @@v_col - LET labels = ( + LET first_order_labels = ( FOR v, e IN 1 {edge_direction} doc @@e_col {f"FILTER {filter_clause}" if filter_clause else ""} {f"SORT {sort_clause}" if sort_clause else ""} RETURN {return_clause} ) + LET second_order_labels = {second_order_labels_query} + + LET labels = UNION_DISTINCT(first_order_labels, second_order_labels) + UPDATE doc WITH {{{attribute_name}: labels}} IN @@v_col """ - self.db.aql.execute( - query, bind_vars={"@v_col": v_col, "@e_col": edge_collection_name} - ) + bind_vars["@v_col"] = v_col + + self.db.aql.execute(query, bind_vars=bind_vars) count += self.db.collection(v_col).count() diff --git a/tests/test_main.py b/tests/test_main.py index 32684fc..2ff079a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5655,3 +5655,66 @@ def test_lpg_case_12_1(name: str, rdf_graph: RDFGraph) -> None: assert edge["_to"].split("/")[0] in {"Class", "Node"} db.delete_graph("Test", drop_collections=True) + + +def test_pgt_second_order_edge_collection_name() -> None: + db.delete_graph("Test", drop_collections=True, ignore_missing=True) + + g = RDFGraph() + g.parse( + data=""" + @prefix ex: . + @prefix rdfs: . + + ex:Alice a ex:Human . + + ex:Bob a ex:Person . + + ex:Charlie a ex:Animal . + + ex:Dana a ex:Entity . + + ex:Eve a ex:Human . + ex:Eve a ex:Person . + + ex:Fred a ex:Human . + ex:Fred a ex:Individual . + + ex:Human rdfs:subClassOf ex:Animal . + ex:Person rdfs:subClassOf ex:Individual . + ex:Animal rdfs:subClassOf ex:Entity . + ex:Individual rdfs:subClassOf ex:Entity . + """, + format="turtle", + ) + + adbrdf.rdf_to_arangodb_by_pgt("Test", g, resource_collection_name="Node") + + assert db.collection("subClassOf").count() == 4 + + adbrdf.migrate_edges_to_attributes( + "Test", + edge_collection_name="type", + second_order_edge_collection_name="subClassOf", + second_order_depth=10, + ) + + alice = db.collection("Node").get(adbrdf.hash("http://example.com/Alice")) + assert set(alice["_type"]) == {"Human", "Animal", "Entity"} + + bob = db.collection("Node").get(adbrdf.hash("http://example.com/Bob")) + assert set(bob["_type"]) == {"Person", "Individual", "Entity"} + + charlie = db.collection("Node").get(adbrdf.hash("http://example.com/Charlie")) + assert set(charlie["_type"]) == {"Animal", "Entity"} + + dana = db.collection("Node").get(adbrdf.hash("http://example.com/Dana")) + assert set(dana["_type"]) == {"Entity"} + + eve = db.collection("Node").get(adbrdf.hash("http://example.com/Eve")) + assert set(eve["_type"]) == {"Human", "Person", "Animal", "Individual", "Entity"} + + fred = db.collection("Node").get(adbrdf.hash("http://example.com/Fred")) + assert set(fred["_type"]) == {"Human", "Individual", "Entity", "Animal"} + + db.delete_graph("Test", drop_collections=True) From b33d26e05a6da160f96b69773135b448b35264f7 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna Date: Tue, 26 Aug 2025 21:02:59 -0400 Subject: [PATCH 2/8] new: filter clause, sort clause --- arango_rdf/main.py | 35 +++++++++++++++++++++++++---------- tests/test_main.py | 8 ++++---- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/arango_rdf/main.py b/arango_rdf/main.py index b29fdbf..dc9d6dd 100644 --- a/arango_rdf/main.py +++ b/arango_rdf/main.py @@ -1348,6 +1348,8 @@ def migrate_edges_to_attributes( with_collections: Optional[List[str]] = None, second_order_edge_collection_name: Optional[str] = None, second_order_depth: Optional[int] = None, + second_order_filter_clause: Optional[str] = None, + second_order_sort_clause: Optional[str] = None, ) -> int: """RDF --> ArangoDB (PGT): Migrate all edges in the specified edge collection to attributes. This method is useful when combined with the @@ -1385,15 +1387,25 @@ def migrate_edges_to_attributes( Defaults to the target edge collection's **to_vertex_collections** property based off its edge definition. :type with_collections: Optional[List[str]] - :param second_order_edge_collection_name: In addition to the **edge_collection_name**, - it is possible to traverse the edges of the second order edge collection to apply - the same attribute to the original target verticies. A common use case is to - set **edge_collection_name** to **"type"** and **second_order_edge_collection_name** - to **"subClassOf"** for inferring the **_type** attribute. Defaults to None. + :param second_order_edge_collection_name: In addition to the + **edge_collection_name**, it is possible to traverse the edges of the + second order edge collection to apply the same attribute to the original + target verticies. A common use case is to set **edge_collection_name** to + **"type"** and **second_order_edge_collection_name** to **"subClassOf"** + for inferring the **_type** attribute. Defaults to None. :type second_order_edge_collection_name: Optional[str] - :param second_order_depth: The depth of the second order traversal. Defaults to 1. - This parameter is only used if **second_order_edge_collection_name** is set. + :param second_order_depth: The depth of the second order traversal. + Defaults to 1. This parameter is only used if + **second_order_edge_collection_name** is set. :type second_order_depth: Optional[int] + :param second_order_filter_clause: A FILTER statement to filter the second order + traversed edges & target vertices. Defaults to None. This parameter is only + used if **second_order_edge_collection_name** is set. + :type second_order_filter_clause: Optional[str] + :param second_order_sort_clause: A SORT statement to order the second order + traversed vertices. Defaults to None. This parameter is only used if + **second_order_edge_collection_name** is set. + :type second_order_sort_clause: Optional[str] :return: The number of documents updated. :rtype: int """ @@ -1443,9 +1455,12 @@ def migrate_edges_to_attributes( second_order_labels_query = f""" ( FOR start IN 1..1 {edge_direction} doc @@e_col - FOR v, e IN 1..{second_order_depth} {edge_direction} start @@second_order_e_col - {f"FILTER {filter_clause}" if filter_clause else ""} - {f"SORT {sort_clause}" if sort_clause else ""} + FOR v, e IN 1..{second_order_depth} {edge_direction} + start @@second_order_e_col + {f"FILTER {second_order_filter_clause}" + if second_order_filter_clause else ""} + {f"SORT {second_order_sort_clause}" + if second_order_sort_clause else ""} RETURN {return_clause} ) """ diff --git a/tests/test_main.py b/tests/test_main.py index 2ff079a..bbdc149 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5667,13 +5667,13 @@ def test_pgt_second_order_edge_collection_name() -> None: @prefix rdfs: . ex:Alice a ex:Human . - + ex:Bob a ex:Person . - + ex:Charlie a ex:Animal . - + ex:Dana a ex:Entity . - + ex:Eve a ex:Human . ex:Eve a ex:Person . From 7590e0dd4e8a468106bda4a829c710230984a311 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna Date: Tue, 26 Aug 2025 21:03:04 -0400 Subject: [PATCH 3/8] update docs --- docs/rdf_to_arangodb_lpg.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/rdf_to_arangodb_lpg.rst b/docs/rdf_to_arangodb_lpg.rst index b7d0e4d..329be3f 100644 --- a/docs/rdf_to_arangodb_lpg.rst +++ b/docs/rdf_to_arangodb_lpg.rst @@ -21,6 +21,7 @@ Consider the following RDF graph: .. code-block:: turtle @prefix ex: . + @prefix rdfs: . ex:Alice a ex:Person ; ex:name "Alice" ; @@ -32,6 +33,8 @@ Consider the following RDF graph: ex:Alice ex:friend ex:Bob . + ex:Person rdfs:subClassOf ex:Human . + Running the LPG transformation produces a graph with: * **2 vertices** in the ``Node`` collection (``ex:Alice`` & ``ex:Bob``) @@ -80,6 +83,28 @@ After the migration each vertex has an ``_type`` array property – ``["Person"]`` in this example – and the original ``rdf:type`` edges remain untouched. Delete them if you do not need them any more. +In addition to the **edge_collection_name** parameter, it is possible to traverse the vertices of the 2nd Order edge collection to apply +the same attribute (but at the 2nd Order) to the original target verticies. In PGT, a common use case is to +set **edge_collection_name** to **"type"** and **second_order_edge_collection_name** +to **"subClassOf"** for inferring the **_type** attribute. + +In LPG, this can be done with ``second_order_filter_clause``: + +.. code-block:: python + + adbrdf.migrate_edges_to_attributes( + graph_name="DemoGraph", + edge_collection_name="Edge", + attribute_name="_type", + filter_clause="e._label == 'type'", + second_order_edge_collection_name="Edge", + second_order_filter_clause="e._label == 'subClassOf'" + second_order_depth=10, + ) + +After this migration, the ``_type`` attribute of ``ex:Alice`` and ``ex:Bob`` will be adjusted to ``["Person", "Human"]``. + + LPG Collection Mapping Process ============================== From 419319b35fb86b2dc7711d4cd5cba188b2657f22 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna Date: Tue, 26 Aug 2025 21:04:52 -0400 Subject: [PATCH 4/8] fix: lint --- arango_rdf/main.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/arango_rdf/main.py b/arango_rdf/main.py index dc9d6dd..72a8e78 100644 --- a/arango_rdf/main.py +++ b/arango_rdf/main.py @@ -1452,15 +1452,23 @@ def migrate_edges_to_attributes( second_order_depth if isinstance(second_order_depth, int) else 1 ) + second_order_filter_clause = ( + f"FILTER {second_order_filter_clause}" + if second_order_filter_clause + else "" + ) + + second_order_sort_clause = ( + f"SORT {second_order_sort_clause}" if second_order_sort_clause else "" + ) + second_order_labels_query = f""" ( FOR start IN 1..1 {edge_direction} doc @@e_col FOR v, e IN 1..{second_order_depth} {edge_direction} start @@second_order_e_col - {f"FILTER {second_order_filter_clause}" - if second_order_filter_clause else ""} - {f"SORT {second_order_sort_clause}" - if second_order_sort_clause else ""} + {second_order_filter_clause} + {second_order_sort_clause} RETURN {return_clause} ) """ From 9be61835e114e028d47d17c569b6cfb416b591da Mon Sep 17 00:00:00 2001 From: Anthony Mahanna Date: Tue, 26 Aug 2025 21:06:20 -0400 Subject: [PATCH 5/8] fix: apply filter clause --- arango_rdf/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arango_rdf/main.py b/arango_rdf/main.py index 72a8e78..df0bbc3 100644 --- a/arango_rdf/main.py +++ b/arango_rdf/main.py @@ -1465,6 +1465,8 @@ def migrate_edges_to_attributes( second_order_labels_query = f""" ( FOR start IN 1..1 {edge_direction} doc @@e_col + {f"FILTER {filter_clause}" if filter_clause else ""} + {f"SORT {sort_clause}" if sort_clause else ""} FOR v, e IN 1..{second_order_depth} {edge_direction} start @@second_order_e_col {second_order_filter_clause} From 8200d5275b18b3bd994c990bf95383a462f674ad Mon Sep 17 00:00:00 2001 From: Anthony Mahanna Date: Thu, 8 Jan 2026 12:12:29 -0500 Subject: [PATCH 6/8] refactor: `migrate_edges_to_attributes` --- arango_rdf/main.py | 137 +++++++++++++++------------------------------ tests/test_main.py | 87 +++++++++++++++++++++------- 2 files changed, 113 insertions(+), 111 deletions(-) diff --git a/arango_rdf/main.py b/arango_rdf/main.py index df0bbc3..8feeef3 100644 --- a/arango_rdf/main.py +++ b/arango_rdf/main.py @@ -1339,38 +1339,41 @@ def migrate_unknown_resources( def migrate_edges_to_attributes( self, graph_name: str, - edge_collection_name: str, + edge_path: list[str], attribute_name: Optional[str] = None, edge_direction: str = "OUTBOUND", + max_depth: int = 1, sort_clause: Optional[str] = None, return_clause: Optional[str] = None, filter_clause: Optional[str] = None, - with_collections: Optional[List[str]] = None, - second_order_edge_collection_name: Optional[str] = None, - second_order_depth: Optional[int] = None, - second_order_filter_clause: Optional[str] = None, - second_order_sort_clause: Optional[str] = None, + traversal_options: Optional[dict[str, Any]] = None, ) -> int: """RDF --> ArangoDB (PGT): Migrate all edges in the specified edge collection to attributes. This method is useful when combined with the **resource_collection_name** parameter of the :func:`rdf_to_arangodb_by_pgt` method. - NOTE: It is recommended to run this method with **edge_collection_name** set - to **"type"** after :func:`rdf_to_arangodb_by_pgt` if the user has set the + NOTE: It is recommended to run this method with **edge_path** set + to **["type"]** after :func:`rdf_to_arangodb_by_pgt` if the user has set the **resource_collection_name** parameter. :param graph_name: The name of the graph to migrate the edges from. :type graph_name: str - :param edge_collection_name: The name of the edge collection to migrate. - :type edge_collection_name: str + :param edge_path: The path of the edges to migrate. The first element is the + starting edge collection, the last element is the ending edge collection. + Can also include edge direction traversal + (e.g ["OUTBOUND type", "OUTBOUND subClassOf"]). + :type edge_path: list[str] + :param edge_direction: The default traversal direction of the edges to migrate. + Defaults to **OUTBOUND**. + :type edge_direction: str + :param max_depth: The maximum depth of the edge path to migrate. + Defaults to 1. + :type max_depth: int :param attribute_name: The name of the attribute to migrate the edges to. - Defaults to **edge_collection_name**, prefixed with the + Defaults to **edge_path[0]**, prefixed with the **rdf_attribute_prefix** parameter set in the constructor. :type attribute_name: Optional[str] - :param edge_direction: The direction of the edges to migrate. - Defaults to **OUTBOUND**. - :type edge_direction: str :param sort_clause: A SORT statement to order the traversed vertices. Defaults to f"v.{self.__rdf_attribute_prefix}label". If set to None, the vertex values will be ordered based on their traversal order. @@ -1383,33 +1386,9 @@ def migrate_edges_to_attributes( :param filter_clause: A FILTER statement to filter the traversed edges & target vertices. Defaults to None. :type filter_clause: Optional[str] - :param with_collections: A list of collections to include in the WITH clause. - Defaults to the target edge collection's - **to_vertex_collections** property based off its edge definition. - :type with_collections: Optional[List[str]] - :param second_order_edge_collection_name: In addition to the - **edge_collection_name**, it is possible to traverse the edges of the - second order edge collection to apply the same attribute to the original - target verticies. A common use case is to set **edge_collection_name** to - **"type"** and **second_order_edge_collection_name** to **"subClassOf"** - for inferring the **_type** attribute. Defaults to None. - :type second_order_edge_collection_name: Optional[str] - :param second_order_depth: The depth of the second order traversal. - Defaults to 1. This parameter is only used if - **second_order_edge_collection_name** is set. - :type second_order_depth: Optional[int] - :param second_order_filter_clause: A FILTER statement to filter the second order - traversed edges & target vertices. Defaults to None. This parameter is only - used if **second_order_edge_collection_name** is set. - :type second_order_filter_clause: Optional[str] - :param second_order_sort_clause: A SORT statement to order the second order - traversed vertices. Defaults to None. This parameter is only used if - **second_order_edge_collection_name** is set. - :type second_order_sort_clause: Optional[str] :return: The number of documents updated. :rtype: int """ - bind_vars = {"@e_col": edge_collection_name} if not self.db.has_graph(graph_name): raise ValueError(f"Graph '{graph_name}' does not exist") @@ -1419,18 +1398,26 @@ def migrate_edges_to_attributes( graph = self.db.graph(graph_name) - target_e_d = {} + # Remove potential INBOUND/OUTBOUND/ANY prefix + # (e.g ["OUTBOUND type", "OUTBOUND subClassOf"]) + edge_path_cleaned = [e_col.split(" ")[-1] for e_col in edge_path] + start_edge_collection = edge_path_cleaned[0] + + start_node_collections = [] + all_e_ds = [] for e_d in graph.edge_definitions(): - if e_d["edge_collection"] == edge_collection_name: - target_e_d = e_d - break + if e_d["edge_collection"] == start_edge_collection: + start_node_collections = e_d["from_vertex_collections"] + + if e_d["edge_collection"] in edge_path_cleaned: + all_e_ds.append(e_d) - if not target_e_d: - m = f"No edge definition found for '{edge_collection_name}' in graph '{graph_name}'. Cannot migrate edges to attributes." # noqa: E501 + if not all_e_ds: + m = f"No edge definitions found for '{edge_path}' in graph '{graph_name}'. Cannot migrate edges to attributes." # noqa: E501 raise ValueError(m) if not attribute_name: - attribute_name = f"{self.__rdf_attribute_prefix}{edge_collection_name}" + attribute_name = f"{self.__rdf_attribute_prefix}{start_edge_collection}" if not sort_clause: sort_clause = f"v.{self.__rdf_label_attr}" @@ -1438,67 +1425,33 @@ def migrate_edges_to_attributes( if not return_clause: return_clause = f"v.{self.__rdf_label_attr}" - with_collections_set = ( - set(with_collections) - if with_collections - else set(target_e_d["to_vertex_collections"]) - ) - - with_cols_str = "WITH " + ", ".join(with_collections_set) - - second_order_labels_query = "[]" - if second_order_edge_collection_name is not None: - second_order_depth = ( - second_order_depth if isinstance(second_order_depth, int) else 1 - ) - - second_order_filter_clause = ( - f"FILTER {second_order_filter_clause}" - if second_order_filter_clause - else "" - ) - - second_order_sort_clause = ( - f"SORT {second_order_sort_clause}" if second_order_sort_clause else "" - ) - - second_order_labels_query = f""" - ( - FOR start IN 1..1 {edge_direction} doc @@e_col - {f"FILTER {filter_clause}" if filter_clause else ""} - {f"SORT {sort_clause}" if sort_clause else ""} - FOR v, e IN 1..{second_order_depth} {edge_direction} - start @@second_order_e_col - {second_order_filter_clause} - {second_order_sort_clause} - RETURN {return_clause} - ) - """ + if traversal_options is None: + traversal_options = { + "uniqueVertices": "path", + "uniqueEdges": "path", + } - bind_vars["@second_order_e_col"] = second_order_edge_collection_name + with_cols = {col for e_d in all_e_ds for col in e_d["to_vertex_collections"]} + with_cols_str = "WITH " + ", ".join(with_cols) + e_cols = ", ".join(edge_path_cleaned) count = 0 - for v_col in target_e_d["from_vertex_collections"]: + for v_col in start_node_collections: query = f""" {with_cols_str} FOR doc IN @@v_col - LET first_order_labels = ( - FOR v, e IN 1 {edge_direction} doc @@e_col + LET labels = ( + FOR v, e IN 1..{max_depth} {edge_direction} doc {e_cols} + OPTIONS {json.dumps(traversal_options)} {f"FILTER {filter_clause}" if filter_clause else ""} {f"SORT {sort_clause}" if sort_clause else ""} RETURN {return_clause} ) - LET second_order_labels = {second_order_labels_query} - - LET labels = UNION_DISTINCT(first_order_labels, second_order_labels) - UPDATE doc WITH {{{attribute_name}: labels}} IN @@v_col """ - bind_vars["@v_col"] = v_col - - self.db.aql.execute(query, bind_vars=bind_vars) + self.db.aql.execute(query, bind_vars={"@v_col": v_col}) count += self.db.collection(v_col).count() diff --git a/tests/test_main.py b/tests/test_main.py index bbdc149..a8de38b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5441,7 +5441,7 @@ def test_pgt_resource_collection_name_and_set_types_attribute() -> None: for node in db.collection("Node"): assert "_type" not in node - count = adbrdf.migrate_edges_to_attributes("Test", "type") + count = adbrdf.migrate_edges_to_attributes("Test", ["type"]) node_col = db.collection("Node") assert set(node_col.get(adbrdf.hash("http://example.com/Alice"))["_type"]) == { @@ -5474,7 +5474,7 @@ def test_pgt_resource_collection_name_and_set_types_attribute() -> None: for v in db.collection("Company"): assert "_type" not in v - count = adbrdf.migrate_edges_to_attributes("Test", "type", "foo") + count = adbrdf.migrate_edges_to_attributes("Test", ["type"], "foo") assert count == 3 for v in db.collection("Human"): @@ -5483,9 +5483,7 @@ def test_pgt_resource_collection_name_and_set_types_attribute() -> None: for v in db.collection("Company"): assert set(v["foo"]) == {"Organization", "Company"} - count = adbrdf.migrate_edges_to_attributes( - graph_name="Test", edge_collection_name="friend" - ) + count = adbrdf.migrate_edges_to_attributes(graph_name="Test", edge_path=["friend"]) alice = db.collection("Human").get(adbrdf.hash("http://example.com/Alice")) assert alice["_friend"] == ["Bob"] @@ -5496,7 +5494,7 @@ def test_pgt_resource_collection_name_and_set_types_attribute() -> None: assert count == 2 count = adbrdf.migrate_edges_to_attributes( - graph_name="Test", edge_collection_name="friend", edge_direction="ANY" + graph_name="Test", edge_path=["friend"], edge_direction="ANY" ) assert count == 2 @@ -5509,23 +5507,19 @@ def test_pgt_resource_collection_name_and_set_types_attribute() -> None: with pytest.raises(ValueError) as e: adbrdf.migrate_edges_to_attributes( - graph_name="Test", edge_collection_name="friend", edge_direction="INVALID" + graph_name="Test", edge_path=["friend"], edge_direction="INVALID" ) assert "Invalid edge direction: INVALID" in str(e.value) with pytest.raises(ValueError) as e: - adbrdf.migrate_edges_to_attributes( - graph_name="Test", edge_collection_name="INVALID" - ) + adbrdf.migrate_edges_to_attributes(graph_name="Test", edge_path=["INVALID"]) - m = "No edge definition found for 'INVALID' in graph 'Test'. Cannot migrate edges to attributes." # noqa: E501 + m = "No edge definitions found for '['INVALID']' in graph 'Test'. Cannot migrate edges to attributes." # noqa: E501 assert m in str(e.value) with pytest.raises(ValueError) as e: - adbrdf.migrate_edges_to_attributes( - graph_name="INVALID", edge_collection_name="friend" - ) + adbrdf.migrate_edges_to_attributes(graph_name="INVALID", edge_path=["friend"]) assert "Graph 'INVALID' does not exist" in str(e.value) @@ -5626,7 +5620,7 @@ def test_lpg() -> None: assert "_type" not in node adbrdf.migrate_edges_to_attributes( - "Test", "Edge", "_type", filter_clause="e._label == 'type'" + "Test", ["Edge"], "_type", filter_clause="e._label == 'type'" ) for node in db.collection("Node"): @@ -5693,10 +5687,65 @@ def test_pgt_second_order_edge_collection_name() -> None: assert db.collection("subClassOf").count() == 4 adbrdf.migrate_edges_to_attributes( - "Test", - edge_collection_name="type", - second_order_edge_collection_name="subClassOf", - second_order_depth=10, + graph_name="Test", + edge_path=["type", "subClassOf"], + max_depth=1, + ) + + alice = db.collection("Node").get(adbrdf.hash("http://example.com/Alice")) + assert set(alice["_type"]) == {"Human"} + + bob = db.collection("Node").get(adbrdf.hash("http://example.com/Bob")) + assert set(bob["_type"]) == {"Person"} + + charlie = db.collection("Node").get(adbrdf.hash("http://example.com/Charlie")) + assert set(charlie["_type"]) == {"Animal"} + + dana = db.collection("Node").get(adbrdf.hash("http://example.com/Dana")) + assert set(dana["_type"]) == {"Entity"} + + eve = db.collection("Node").get(adbrdf.hash("http://example.com/Eve")) + assert set(eve["_type"]) == {"Human", "Person"} + + fred = db.collection("Node").get(adbrdf.hash("http://example.com/Fred")) + assert set(fred["_type"]) == {"Human", "Individual"} + + db.delete_graph("Test", drop_collections=True) + + adbrdf.rdf_to_arangodb_by_pgt("Test", g, resource_collection_name="Node") + + adbrdf.migrate_edges_to_attributes( + graph_name="Test", + edge_path=["type", "subClassOf"], + max_depth=2, + ) + + alice = db.collection("Node").get(adbrdf.hash("http://example.com/Alice")) + assert set(alice["_type"]) == {"Human", "Animal"} + + bob = db.collection("Node").get(adbrdf.hash("http://example.com/Bob")) + assert set(bob["_type"]) == {"Person", "Individual"} + + charlie = db.collection("Node").get(adbrdf.hash("http://example.com/Charlie")) + assert set(charlie["_type"]) == {"Animal", "Entity"} + + dana = db.collection("Node").get(adbrdf.hash("http://example.com/Dana")) + assert set(dana["_type"]) == {"Entity"} + + eve = db.collection("Node").get(adbrdf.hash("http://example.com/Eve")) + assert set(eve["_type"]) == {"Human", "Person", "Animal", "Individual"} + + fred = db.collection("Node").get(adbrdf.hash("http://example.com/Fred")) + assert set(fred["_type"]) == {"Human", "Individual", "Animal", "Entity"} + + db.delete_graph("Test", drop_collections=True) + + adbrdf.rdf_to_arangodb_by_pgt("Test", g, resource_collection_name="Node") + + adbrdf.migrate_edges_to_attributes( + graph_name="Test", + edge_path=["type", "subClassOf"], + max_depth=3, ) alice = db.collection("Node").get(adbrdf.hash("http://example.com/Alice")) From 02b0dafebd1858b800327313710e5d7dae87164f Mon Sep 17 00:00:00 2001 From: Anthony Mahanna Date: Thu, 8 Jan 2026 12:17:29 -0500 Subject: [PATCH 7/8] update docstring --- arango_rdf/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arango_rdf/main.py b/arango_rdf/main.py index baf1a39..a7e11c3 100644 --- a/arango_rdf/main.py +++ b/arango_rdf/main.py @@ -1503,6 +1503,9 @@ def migrate_edges_to_attributes( :param filter_clause: A FILTER statement to filter the traversed edges & target vertices. Defaults to None. :type filter_clause: Optional[str] + :param traversal_options: A dictionary of traversal options to pass to the + AQL query. Defaults to None. + :type traversal_options: Optional[dict[str, Any]] :return: The number of documents updated. :rtype: int """ From 367d4ba54251650c379aaf5be819892906f4eff3 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna Date: Thu, 8 Jan 2026 12:21:33 -0500 Subject: [PATCH 8/8] None check --- arango_rdf/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arango_rdf/main.py b/arango_rdf/main.py index a7e11c3..dcae8b6 100644 --- a/arango_rdf/main.py +++ b/arango_rdf/main.py @@ -1536,13 +1536,13 @@ def migrate_edges_to_attributes( m = f"No edge definitions found for '{edge_path}' in graph '{graph_name}'. Cannot migrate edges to attributes." # noqa: E501 raise ValueError(m) - if not attribute_name: + if attribute_name is None: attribute_name = f"{self.__rdf_attribute_prefix}{start_edge_collection}" - if not sort_clause: + if sort_clause is None: sort_clause = f"v.{self.__rdf_label_attr}" - if not return_clause: + if return_clause is None: return_clause = f"v.{self.__rdf_label_attr}" if traversal_options is None: