Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions neo4j/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ def on_footer(self, metadata):
"""
self.complete = True
self.summary = ResultSummary(self.statement, self.parameters,
metadata.get("type"), metadata.get("stats"))
metadata.get("type"), metadata.get("stats"),
metadata.get("plan"), metadata.get("profile"),
metadata.get("notifications", []))
if self.bench_test:
self.bench_test.end_recv = perf_counter()

Expand Down Expand Up @@ -181,11 +183,28 @@ class ResultSummary(object):
#: A set of statistical information held in a :class:`.StatementStatistics` instance.
statistics = None

def __init__(self, statement, parameters, statement_type, statistics):
#: A :class:`.Plan` instance
plan = None

#: A :class:`.ProfiledPlan` instance
profile = None

#: Notifications provide extra information for a user executing a statement.
#: They can be warnings about problematic queries or other valuable information that can be presented in a client.
#: Unlike failures or errors, notifications do not affect the execution of a statement.
notifications = None

def __init__(self, statement, parameters, statement_type, statistics, plan, profile, notifications):
self.statement = statement
self.parameters = parameters
self.statement_type = statement_type
self.statistics = StatementStatistics(statistics or {})
if plan is not None:
self.plan = Plan(plan)
if profile is not None:
self.profile = ProfiledPlan(profile)
self.plan = self.profile
self.notifications = list(map(Notification, notifications))


class StatementStatistics(object):
Expand Down Expand Up @@ -237,6 +256,81 @@ def __repr__(self):
return repr(vars(self))


class Plan(object):
""" This describes how the database will execute your statement.
"""

#: The operation name performed by the plan
operator_type = None

#: A list of identifiers used by this plan
identifiers = None

#: A map of arguments used in the specific operation performed by the plan
arguments = None

#: A list of sub plans
children = None

def __init__(self, plan):
self.operator_type = plan["operatorType"]
self.identifiers = plan.get("identifiers", [])
self.arguments = plan.get("args", [])
self.children = [Plan(child) for child in plan.get("children", [])]


class ProfiledPlan(Plan):
""" This describes how the database excuted your statement.
"""

#: The number of times this part of the plan touched the underlying data stores
db_hits = 0

#: The number of records this part of the plan produced
rows = 0

def __init__(self, profile):
self.db_hits = profile.get("dbHits", 0)
self.rows = profile.get("rows", 0)
super(ProfiledPlan, self).__init__(profile)


class Notification(object):
""" Representation for notifications found when executing a statement.
A notification can be visualized in a client pinpointing problems or other information about the statement.
"""

#: A notification code for the discovered issue.
code = None

#: A short summary of the notification
title = None

#: A long description of the notification
description = None

#: The position in the statement where this notification points to, if relevant. This is a namedtuple
#: consisting of offset, line and column:
#:
#: - offset - the character offset referred to by this position; offset numbers start at 0
#:
#: - line - the line number referred to by the position; line numbers start at 1
#:
#: - column - the column number referred to by the position; column numbers start at 1
position = None

def __init__(self, notification):
self.code = notification["code"]
self.title = notification["title"]
self.description = notification["description"]
position = notification.get("position")
if position is not None:
self.position = Position(position["offset"], position["line"], position["column"])


Position = namedtuple('Position', ['offset', 'line', 'column'])


class Session(object):
""" Logical session carried out over an established TCP connection.
Sessions should generally be constructed using the :meth:`.Driver.session`
Expand Down
69 changes: 61 additions & 8 deletions test/session_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@


class RunTestCase(TestCase):

def test_must_use_valid_url_scheme(self):
try:
GraphDatabase.driver("x://xxx")
Expand Down Expand Up @@ -169,9 +168,65 @@ def test_can_obtain_summary_info(self):
assert summary.statement_type == "rw"
assert summary.statistics.nodes_created == 1

def test_no_plan_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
result = session.run("CREATE (n) RETURN n")
assert result.summarize().plan is None
assert result.summarize().profile is None

def test_can_obtain_plan_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
result = session.run("EXPLAIN CREATE (n) RETURN n")
plan = result.summarize().plan
assert plan.operator_type == "ProduceResults"
assert plan.identifiers == ["n"]
assert plan.arguments == {"planner": "COST", "EstimatedRows": 1.0, "version": "CYPHER 3.0",
"KeyNames": "n", "runtime-impl": "INTERPRETED", "planner-impl": "IDP",
"runtime": "INTERPRETED"}
assert len(plan.children) == 1

def test_can_obtain_profile_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
result = session.run("PROFILE CREATE (n) RETURN n")
profile = result.summarize().profile
assert profile.db_hits == 0
assert profile.rows == 1
assert profile.operator_type == "ProduceResults"
assert profile.identifiers == ["n"]
assert profile.arguments == {"planner": "COST", "EstimatedRows": 1.0, "version": "CYPHER 3.0",
"KeyNames": "n", "runtime-impl": "INTERPRETED", "planner-impl": "IDP",
"runtime": "INTERPRETED", "Rows": 1, "DbHits": 0}
assert len(profile.children) == 1

def test_no_notification_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
result = session.run("CREATE (n) RETURN n")
notifications = result.summarize().notifications
assert notifications == []

def test_can_obtain_notification_info(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
result = session.run("EXPLAIN MATCH (n), (m) RETURN n, m")
notifications = result.summarize().notifications

assert len(notifications) == 1
notification = notifications[0]
assert notification.code == "Neo.ClientNotification.Statement.CartesianProduct"
assert notification.title == "This query builds a cartesian product between disconnected patterns."
assert notification.description == \
"If a part of a query contains multiple disconnected patterns, " \
"this will build a cartesian product between all those parts. " \
"This may produce a large amount of data and slow down query processing. " \
"While occasionally intended, it may often be possible to reformulate the query " \
"that avoids the use of this cross product, perhaps by adding a relationship between " \
"the different parts or by using OPTIONAL MATCH (identifier is: (m))"
position = notification.position
assert position.offset == 0
assert position.line == 1
assert position.column == 1

class TransactionTestCase(TestCase):

class TransactionTestCase(TestCase):
def test_can_commit_transaction(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
tx = session.new_transaction()
Expand All @@ -189,7 +244,7 @@ def test_can_commit_transaction(self):

# Check the property value
result = session.run("MATCH (a) WHERE id(a) = {n} "
"RETURN a.foo", {"n": node_id})
"RETURN a.foo", {"n": node_id})
foo = result[0][0]
assert foo == "bar"

Expand All @@ -210,13 +265,12 @@ def test_can_rollback_transaction(self):

# Check the property value
result = session.run("MATCH (a) WHERE id(a) = {n} "
"RETURN a.foo", {"n": node_id})
"RETURN a.foo", {"n": node_id})
assert len(result) == 0

def test_can_commit_transaction_using_with_block(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
with session.new_transaction() as tx:

# Create a node
result = tx.run("CREATE (a) RETURN id(a)")
node_id = result[0][0]
Expand All @@ -230,14 +284,13 @@ def test_can_commit_transaction_using_with_block(self):

# Check the property value
result = session.run("MATCH (a) WHERE id(a) = {n} "
"RETURN a.foo", {"n": node_id})
"RETURN a.foo", {"n": node_id})
foo = result[0][0]
assert foo == "bar"

def test_can_rollback_transaction_using_with_block(self):
with GraphDatabase.driver("bolt://localhost").session() as session:
with session.new_transaction() as tx:

# Create a node
result = tx.run("CREATE (a) RETURN id(a)")
node_id = result[0][0]
Expand All @@ -249,7 +302,7 @@ def test_can_rollback_transaction_using_with_block(self):

# Check the property value
result = session.run("MATCH (a) WHERE id(a) = {n} "
"RETURN a.foo", {"n": node_id})
"RETURN a.foo", {"n": node_id})
assert len(result) == 0


Expand Down