diff --git a/doc/source/advanced_query_operations.rst b/doc/source/advanced_query_operations.rst index 6a2b5466..e602d479 100644 --- a/doc/source/advanced_query_operations.rst +++ b/doc/source/advanced_query_operations.rst @@ -67,7 +67,8 @@ The `subquery` method allows you to perform a `Cypher subquery (student_parents:Student) + MATCH (student)-[r4:`LIVES_IN`]->(building_lives_in:Building) + OPTIONAL MATCH (student)<-[r2:`HAS_PARENT`]-(student_children:Student)-[r3:`HAS_PREFERRED_COURSE`]->(course_children__preferred_course:Course) + WITH * + # building_lives_in_name_1 = "Eiffel Tower" + # student_name_1 = "(?i)m.*" + WHERE building_lives_in.name = $building_lives_in_name_1 AND student.name =~ $student_name_1 + CALL { + WITH student + MATCH (student)-[r1:`HAS_COURSE`]->(course_courses:Course) + WITH r1 AS rel + ORDER BY toInteger(split(rel.level, '.')[0]),toInteger(split(rel.level, '.')[1]),rel.end_date,rel.start_date + RETURN last(collect(rel)) AS latest_course + } + RETURN latest_course, student, student_parents, r1, student_children, r2, course_children__preferred_course, r3, building_lives_in, r4, collect(DISTINCT student_children) AS children, collect(DISTINCT course_children__preferred_course) AS children_preferred_course + ORDER BY student.name + """ \ No newline at end of file diff --git a/doc/source/traversal.rst b/doc/source/traversal.rst index e4d94b34..c6347ad0 100644 --- a/doc/source/traversal.rst +++ b/doc/source/traversal.rst @@ -6,6 +6,8 @@ Path traversal Neo4j is about traversing the graph, which means leveraging nodes and relations between them. This section will show you how to traverse the graph using neomodel. +We will cover two methods : `traverse_relations` and `fetch_relations`. Those two methods are *mutually exclusive*, so you cannot chain them. + For the examples in this section, we will be using the following model:: class Country(StructuredNode): diff --git a/test/async_/test_match_api.py b/test/async_/test_match_api.py index bed4f877..56d5f37f 100644 --- a/test/async_/test_match_api.py +++ b/test/async_/test_match_api.py @@ -11,6 +11,7 @@ AsyncRelationshipTo, AsyncStructuredNode, AsyncStructuredRel, + AsyncZeroOrOne, DateTimeProperty, IntegerProperty, Q, @@ -108,11 +109,16 @@ class Building(AsyncStructuredNode): class Student(AsyncStructuredNode): name = StringProperty() - parents = AsyncRelationshipTo("Student", "HAS_PARENT") - children = AsyncRelationshipFrom("Student", "HAS_PARENT") - lives_in = AsyncRelationshipTo(Building, "LIVES_IN") - has_course = AsyncRelationshipTo(Course, "HAS_COURSE", model=HasCourseRel) - has_latest_course = AsyncRelationshipTo(Course, "HAS_COURSE") + parents = AsyncRelationshipTo("Student", "HAS_PARENT", model=AsyncStructuredRel) + children = AsyncRelationshipFrom("Student", "HAS_PARENT", model=AsyncStructuredRel) + lives_in = AsyncRelationshipTo(Building, "LIVES_IN", model=AsyncStructuredRel) + courses = AsyncRelationshipTo(Course, "HAS_COURSE", model=HasCourseRel) + preferred_course = AsyncRelationshipTo( + Course, + "HAS_PREFERRED_COURSE", + model=AsyncStructuredRel, + cardinality=AsyncZeroOrOne, + ) @mark_async_test @@ -1013,25 +1019,26 @@ async def test_mix_functions(): await mimoun.lives_in.connect(eiffel_tower) await mimoun.parents.connect(mireille) await mimoun.children.connect(mimoun_jr) - course = await Course(name="Math").save() - await mimoun.has_course.connect( - course, + math = await Course(name="Math").save() + dessin = await Course(name="Dessin").save() + await mimoun.courses.connect( + math, { "level": "1.2", "start_date": datetime(2020, 6, 2), "end_date": datetime(2020, 12, 31), }, ) - await mimoun.has_course.connect( - course, + await mimoun.courses.connect( + math, { "level": "1.1", "start_date": datetime(2020, 1, 1), "end_date": datetime(2020, 6, 1), }, ) - await mimoun_jr.has_course.connect( - course, + await mimoun_jr.courses.connect( + math, { "level": "1.1", "start_date": datetime(2020, 1, 1), @@ -1039,21 +1046,19 @@ async def test_mix_functions(): }, ) - filtered_nodeset = Student.nodes.filter( - name__istartswith="m", lives_in__name="Eiffel Tower" - ) + await mimoun_jr.preferred_course.connect(dessin) + full_nodeset = ( - await filtered_nodeset.order_by("name") - .traverse_relations( - "parents", - ) + await Student.nodes.filter(name__istartswith="m", lives_in__name="Eiffel Tower") + .order_by("name") .fetch_relations( - Optional("children__has_latest_course"), + "parents", + Optional("children__preferred_course"), ) .subquery( - Student.nodes.fetch_relations("has_course") + Student.nodes.fetch_relations("courses") .intermediate_transform( - {"rel": RelationNameResolver("has_course")}, + {"rel": RelationNameResolver("courses")}, ordering=[ RawCypher("toInteger(split(rel.level, '.')[0])"), RawCypher("toInteger(split(rel.level, '.')[1])"), @@ -1069,11 +1074,20 @@ async def test_mix_functions(): ) subgraph = await full_nodeset.annotate( - Collect(NodeNameResolver("children"), distinct=True), - Collect(NodeNameResolver("children__has_latest_course"), distinct=True), + children=Collect(NodeNameResolver("children"), distinct=True), + children_preferred_course=Collect( + NodeNameResolver("children__preferred_course"), distinct=True + ), ).resolve_subgraph() - print(subgraph) + assert len(subgraph) == 2 + assert subgraph[0] == mimoun + assert subgraph[1] == mimoun_jr + mimoun_returned_rels = subgraph[0]._relations + assert mimoun_returned_rels["children"] == mimoun_jr + assert mimoun_returned_rels["children"]._relations["preferred_course"] == dessin + assert mimoun_returned_rels["parents"] == mireille + assert mimoun_returned_rels["latest_course_relationship"].level == "1.2" @mark_async_test diff --git a/test/sync_/test_match_api.py b/test/sync_/test_match_api.py index 0ba00b38..60b48554 100644 --- a/test/sync_/test_match_api.py +++ b/test/sync_/test_match_api.py @@ -16,6 +16,7 @@ StructuredNode, StructuredRel, UniqueIdProperty, + ZeroOrOne, db, ) from neomodel._async_compat.util import Util @@ -106,11 +107,16 @@ class Building(StructuredNode): class Student(StructuredNode): name = StringProperty() - parents = RelationshipTo("Student", "HAS_PARENT") - children = RelationshipFrom("Student", "HAS_PARENT") - lives_in = RelationshipTo(Building, "LIVES_IN") - has_course = RelationshipTo(Course, "HAS_COURSE", model=HasCourseRel) - has_latest_course = RelationshipTo(Course, "HAS_COURSE") + parents = RelationshipTo("Student", "HAS_PARENT", model=StructuredRel) + children = RelationshipFrom("Student", "HAS_PARENT", model=StructuredRel) + lives_in = RelationshipTo(Building, "LIVES_IN", model=StructuredRel) + courses = RelationshipTo(Course, "HAS_COURSE", model=HasCourseRel) + preferred_course = RelationshipTo( + Course, + "HAS_PREFERRED_COURSE", + model=StructuredRel, + cardinality=ZeroOrOne, + ) @mark_sync_test @@ -999,25 +1005,26 @@ def test_mix_functions(): mimoun.lives_in.connect(eiffel_tower) mimoun.parents.connect(mireille) mimoun.children.connect(mimoun_jr) - course = Course(name="Math").save() - mimoun.has_course.connect( - course, + math = Course(name="Math").save() + dessin = Course(name="Dessin").save() + mimoun.courses.connect( + math, { "level": "1.2", "start_date": datetime(2020, 6, 2), "end_date": datetime(2020, 12, 31), }, ) - mimoun.has_course.connect( - course, + mimoun.courses.connect( + math, { "level": "1.1", "start_date": datetime(2020, 1, 1), "end_date": datetime(2020, 6, 1), }, ) - mimoun_jr.has_course.connect( - course, + mimoun_jr.courses.connect( + math, { "level": "1.1", "start_date": datetime(2020, 1, 1), @@ -1025,21 +1032,19 @@ def test_mix_functions(): }, ) - filtered_nodeset = Student.nodes.filter( - name__istartswith="m", lives_in__name="Eiffel Tower" - ) + mimoun_jr.preferred_course.connect(dessin) + full_nodeset = ( - filtered_nodeset.order_by("name") - .traverse_relations( - "parents", - ) + Student.nodes.filter(name__istartswith="m", lives_in__name="Eiffel Tower") + .order_by("name") .fetch_relations( - Optional("children__has_latest_course"), + "parents", + Optional("children__preferred_course"), ) .subquery( - Student.nodes.fetch_relations("has_course") + Student.nodes.fetch_relations("courses") .intermediate_transform( - {"rel": RelationNameResolver("has_course")}, + {"rel": RelationNameResolver("courses")}, ordering=[ RawCypher("toInteger(split(rel.level, '.')[0])"), RawCypher("toInteger(split(rel.level, '.')[1])"), @@ -1055,11 +1060,20 @@ def test_mix_functions(): ) subgraph = full_nodeset.annotate( - Collect(NodeNameResolver("children"), distinct=True), - Collect(NodeNameResolver("children__has_latest_course"), distinct=True), + children=Collect(NodeNameResolver("children"), distinct=True), + children_preferred_course=Collect( + NodeNameResolver("children__preferred_course"), distinct=True + ), ).resolve_subgraph() - print(subgraph) + assert len(subgraph) == 2 + assert subgraph[0] == mimoun + assert subgraph[1] == mimoun_jr + mimoun_returned_rels = subgraph[0]._relations + assert mimoun_returned_rels["children"] == mimoun_jr + assert mimoun_returned_rels["children"]._relations["preferred_course"] == dessin + assert mimoun_returned_rels["parents"] == mireille + assert mimoun_returned_rels["latest_course_relationship"].level == "1.2" @mark_sync_test