Skip to content

Commit

Permalink
Add full example ; fix example in test
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusconjeaud committed Nov 4, 2024
1 parent 1d927bc commit 1b26793
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 51 deletions.
8 changes: 7 additions & 1 deletion doc/source/advanced_query_operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ The `subquery` method allows you to perform a `Cypher subquery <https://neo4j.co
# This will create a CALL{} subquery
# And return a variable named supps usable in the rest of your query
Coffee.nodes.subquery(
Coffee.nodes.filter(name="Espresso")
.subquery(
Coffee.nodes.traverse_relations(suppliers="suppliers")
.intermediate_transform(
{"suppliers": "suppliers"}, ordering=["suppliers.delivery_cost"]
Expand All @@ -76,6 +77,11 @@ The `subquery` method allows you to perform a `Cypher subquery <https://neo4j.co
["supps"],
)

.. note::
Notice the subquery starts with Coffee.nodes ; neomodel will use this to know it needs to inject the source "coffee" variable generated by the outer query into the subquery. This means only Espresso coffee nodes will be considered in the subquery.

We know this is confusing to read, but have not found a better wat to do this yet. If you have any suggestions, please let us know.

Helpers
-------

Expand Down
2 changes: 2 additions & 0 deletions doc/source/filtering_ordering.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _Filtering and ordering:

======================
Filtering and ordering
======================
Expand Down
90 changes: 90 additions & 0 deletions doc/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,93 @@ Most _dunder_ methods for nodes and relationships had to be overriden to support
# Sync equivalent - __getitem__
assert len(list(Coffee.nodes[1:])) == 2


Full example
============

The example below will show you how you can mix and match query operations, as described in :ref:`Filtering and ordering`, :ref:`Path traversal`, or :ref:`Advanced query operations`::

# These are the class definitions used for the query below
class HasCourseRel(AsyncStructuredRel):
level = StringProperty()
start_date = DateTimeProperty()
end_date = DateTimeProperty()


class Course(AsyncStructuredNode):
name = StringProperty()


class Building(AsyncStructuredNode):
name = StringProperty()


class Student(AsyncStructuredNode):
name = StringProperty()

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,
)

# This is the query
full_nodeset = (
await Student.nodes.filter(name__istartswith="m", lives_in__name="Eiffel Tower") # Combine filters
.order_by("name")
.fetch_relations(
"parents",
Optional("children__preferred_course"),
) # Combine fetch_relations
.subquery(
Student.nodes.fetch_relations("courses") # Root variable student will be auto-injected here
.intermediate_transform(
{"rel": RelationNameResolver("courses")},
ordering=[
RawCypher("toInteger(split(rel.level, '.')[0])"),
RawCypher("toInteger(split(rel.level, '.')[1])"),
"rel.end_date",
"rel.start_date",
], # Intermediate ordering
)
.annotate(
latest_course=Last(Collect("rel")),
),
["latest_course"],
)
)

# Using async, we need to do 2 await
# One is for subquery, the other is for resolve_subgraph
# It only runs a single Cypher query though
subgraph = await full_nodeset.annotate(
children=Collect(NodeNameResolver("children"), distinct=True),
children_preferred_course=Collect(
NodeNameResolver("children__preferred_course"), distinct=True
),
).resolve_subgraph()

# The generated Cypher query looks like this
query = """
MATCH (student:Student)-[r1:`HAS_PARENT`]->(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
"""
2 changes: 2 additions & 0 deletions doc/source/traversal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
64 changes: 39 additions & 25 deletions test/async_/test_match_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
AsyncRelationshipTo,
AsyncStructuredNode,
AsyncStructuredRel,
AsyncZeroOrOne,
DateTimeProperty,
IntegerProperty,
Q,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1013,47 +1019,46 @@ 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),
"end_date": datetime(2020, 6, 1),
},
)

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])"),
Expand All @@ -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
Expand Down
64 changes: 39 additions & 25 deletions test/sync_/test_match_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
StructuredNode,
StructuredRel,
UniqueIdProperty,
ZeroOrOne,
db,
)
from neomodel._async_compat.util import Util
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -999,47 +1005,46 @@ 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),
"end_date": datetime(2020, 6, 1),
},
)

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])"),
Expand All @@ -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
Expand Down

0 comments on commit 1b26793

Please sign in to comment.