Skip to content

Commit f785779

Browse files
committed
Merge pull request #8 from neo4j/add-profile-explain
Added profile, explain, notifications into result summary
2 parents f369903 + 54d1c60 commit f785779

File tree

2 files changed

+157
-10
lines changed

2 files changed

+157
-10
lines changed

neo4j/session.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ def on_footer(self, metadata):
139139
"""
140140
self.complete = True
141141
self.summary = ResultSummary(self.statement, self.parameters,
142-
metadata.get("type"), metadata.get("stats"))
142+
metadata.get("type"), metadata.get("stats"),
143+
metadata.get("plan"), metadata.get("profile"),
144+
metadata.get("notifications", []))
143145
if self.bench_test:
144146
self.bench_test.end_recv = perf_counter()
145147

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

184-
def __init__(self, statement, parameters, statement_type, statistics):
186+
#: A :class:`.Plan` instance
187+
plan = None
188+
189+
#: A :class:`.ProfiledPlan` instance
190+
profile = None
191+
192+
#: Notifications provide extra information for a user executing a statement.
193+
#: They can be warnings about problematic queries or other valuable information that can be presented in a client.
194+
#: Unlike failures or errors, notifications do not affect the execution of a statement.
195+
notifications = None
196+
197+
def __init__(self, statement, parameters, statement_type, statistics, plan, profile, notifications):
185198
self.statement = statement
186199
self.parameters = parameters
187200
self.statement_type = statement_type
188201
self.statistics = StatementStatistics(statistics or {})
202+
if plan is not None:
203+
self.plan = Plan(plan)
204+
if profile is not None:
205+
self.profile = ProfiledPlan(profile)
206+
self.plan = self.profile
207+
self.notifications = list(map(Notification, notifications))
189208

190209

191210
class StatementStatistics(object):
@@ -237,6 +256,81 @@ def __repr__(self):
237256
return repr(vars(self))
238257

239258

259+
class Plan(object):
260+
""" This describes how the database will execute your statement.
261+
"""
262+
263+
#: The operation name performed by the plan
264+
operator_type = None
265+
266+
#: A list of identifiers used by this plan
267+
identifiers = None
268+
269+
#: A map of arguments used in the specific operation performed by the plan
270+
arguments = None
271+
272+
#: A list of sub plans
273+
children = None
274+
275+
def __init__(self, plan):
276+
self.operator_type = plan["operatorType"]
277+
self.identifiers = plan.get("identifiers", [])
278+
self.arguments = plan.get("args", [])
279+
self.children = [Plan(child) for child in plan.get("children", [])]
280+
281+
282+
class ProfiledPlan(Plan):
283+
""" This describes how the database excuted your statement.
284+
"""
285+
286+
#: The number of times this part of the plan touched the underlying data stores
287+
db_hits = 0
288+
289+
#: The number of records this part of the plan produced
290+
rows = 0
291+
292+
def __init__(self, profile):
293+
self.db_hits = profile.get("dbHits", 0)
294+
self.rows = profile.get("rows", 0)
295+
super(ProfiledPlan, self).__init__(profile)
296+
297+
298+
class Notification(object):
299+
""" Representation for notifications found when executing a statement.
300+
A notification can be visualized in a client pinpointing problems or other information about the statement.
301+
"""
302+
303+
#: A notification code for the discovered issue.
304+
code = None
305+
306+
#: A short summary of the notification
307+
title = None
308+
309+
#: A long description of the notification
310+
description = None
311+
312+
#: The position in the statement where this notification points to, if relevant. This is a namedtuple
313+
#: consisting of offset, line and column:
314+
#:
315+
#: - offset - the character offset referred to by this position; offset numbers start at 0
316+
#:
317+
#: - line - the line number referred to by the position; line numbers start at 1
318+
#:
319+
#: - column - the column number referred to by the position; column numbers start at 1
320+
position = None
321+
322+
def __init__(self, notification):
323+
self.code = notification["code"]
324+
self.title = notification["title"]
325+
self.description = notification["description"]
326+
position = notification.get("position")
327+
if position is not None:
328+
self.position = Position(position["offset"], position["line"], position["column"])
329+
330+
331+
Position = namedtuple('Position', ['offset', 'line', 'column'])
332+
333+
240334
class Session(object):
241335
""" Logical session carried out over an established TCP connection.
242336
Sessions should generally be constructed using the :meth:`.Driver.session`

test/session_test.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525

2626
class RunTestCase(TestCase):
27-
2827
def test_must_use_valid_url_scheme(self):
2928
try:
3029
GraphDatabase.driver("x://xxx")
@@ -168,9 +167,65 @@ def test_can_obtain_summary_info(self):
168167
assert summary.statement_type == "rw"
169168
assert summary.statistics.nodes_created == 1
170169

170+
def test_no_plan_info(self):
171+
with GraphDatabase.driver("bolt://localhost").session() as session:
172+
result = session.run("CREATE (n) RETURN n")
173+
assert result.summarize().plan is None
174+
assert result.summarize().profile is None
175+
176+
def test_can_obtain_plan_info(self):
177+
with GraphDatabase.driver("bolt://localhost").session() as session:
178+
result = session.run("EXPLAIN CREATE (n) RETURN n")
179+
plan = result.summarize().plan
180+
assert plan.operator_type == "ProduceResults"
181+
assert plan.identifiers == ["n"]
182+
assert plan.arguments == {"planner": "COST", "EstimatedRows": 1.0, "version": "CYPHER 3.0",
183+
"KeyNames": "n", "runtime-impl": "INTERPRETED", "planner-impl": "IDP",
184+
"runtime": "INTERPRETED"}
185+
assert len(plan.children) == 1
186+
187+
def test_can_obtain_profile_info(self):
188+
with GraphDatabase.driver("bolt://localhost").session() as session:
189+
result = session.run("PROFILE CREATE (n) RETURN n")
190+
profile = result.summarize().profile
191+
assert profile.db_hits == 0
192+
assert profile.rows == 1
193+
assert profile.operator_type == "ProduceResults"
194+
assert profile.identifiers == ["n"]
195+
assert profile.arguments == {"planner": "COST", "EstimatedRows": 1.0, "version": "CYPHER 3.0",
196+
"KeyNames": "n", "runtime-impl": "INTERPRETED", "planner-impl": "IDP",
197+
"runtime": "INTERPRETED", "Rows": 1, "DbHits": 0}
198+
assert len(profile.children) == 1
199+
200+
def test_no_notification_info(self):
201+
with GraphDatabase.driver("bolt://localhost").session() as session:
202+
result = session.run("CREATE (n) RETURN n")
203+
notifications = result.summarize().notifications
204+
assert notifications == []
205+
206+
def test_can_obtain_notification_info(self):
207+
with GraphDatabase.driver("bolt://localhost").session() as session:
208+
result = session.run("EXPLAIN MATCH (n), (m) RETURN n, m")
209+
notifications = result.summarize().notifications
210+
211+
assert len(notifications) == 1
212+
notification = notifications[0]
213+
assert notification.code == "Neo.ClientNotification.Statement.CartesianProduct"
214+
assert notification.title == "This query builds a cartesian product between disconnected patterns."
215+
assert notification.description == \
216+
"If a part of a query contains multiple disconnected patterns, " \
217+
"this will build a cartesian product between all those parts. " \
218+
"This may produce a large amount of data and slow down query processing. " \
219+
"While occasionally intended, it may often be possible to reformulate the query " \
220+
"that avoids the use of this cross product, perhaps by adding a relationship between " \
221+
"the different parts or by using OPTIONAL MATCH (identifier is: (m))"
222+
position = notification.position
223+
assert position.offset == 0
224+
assert position.line == 1
225+
assert position.column == 1
171226

172-
class TransactionTestCase(TestCase):
173227

228+
class TransactionTestCase(TestCase):
174229
def test_can_commit_transaction(self):
175230
with GraphDatabase.driver("bolt://localhost").session() as session:
176231
tx = session.new_transaction()
@@ -188,7 +243,7 @@ def test_can_commit_transaction(self):
188243

189244
# Check the property value
190245
result = session.run("MATCH (a) WHERE id(a) = {n} "
191-
"RETURN a.foo", {"n": node_id})
246+
"RETURN a.foo", {"n": node_id})
192247
foo = result[0][0]
193248
assert foo == "bar"
194249

@@ -209,13 +264,12 @@ def test_can_rollback_transaction(self):
209264

210265
# Check the property value
211266
result = session.run("MATCH (a) WHERE id(a) = {n} "
212-
"RETURN a.foo", {"n": node_id})
267+
"RETURN a.foo", {"n": node_id})
213268
assert len(result) == 0
214269

215270
def test_can_commit_transaction_using_with_block(self):
216271
with GraphDatabase.driver("bolt://localhost").session() as session:
217272
with session.new_transaction() as tx:
218-
219273
# Create a node
220274
result = tx.run("CREATE (a) RETURN id(a)")
221275
node_id = result[0][0]
@@ -229,14 +283,13 @@ def test_can_commit_transaction_using_with_block(self):
229283

230284
# Check the property value
231285
result = session.run("MATCH (a) WHERE id(a) = {n} "
232-
"RETURN a.foo", {"n": node_id})
286+
"RETURN a.foo", {"n": node_id})
233287
foo = result[0][0]
234288
assert foo == "bar"
235289

236290
def test_can_rollback_transaction_using_with_block(self):
237291
with GraphDatabase.driver("bolt://localhost").session() as session:
238292
with session.new_transaction() as tx:
239-
240293
# Create a node
241294
result = tx.run("CREATE (a) RETURN id(a)")
242295
node_id = result[0][0]
@@ -248,7 +301,7 @@ def test_can_rollback_transaction_using_with_block(self):
248301

249302
# Check the property value
250303
result = session.run("MATCH (a) WHERE id(a) = {n} "
251-
"RETURN a.foo", {"n": node_id})
304+
"RETURN a.foo", {"n": node_id})
252305
assert len(result) == 0
253306

254307

0 commit comments

Comments
 (0)