Skip to content

Commit 942b39f

Browse files
committed
Merge branch 'master' into test-refactoring
2 parents d75d2de + f785779 commit 942b39f

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/test_session.py

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

2626

2727
class RunTestCase(TestCase):
28-
2928
def test_must_use_valid_url_scheme(self):
3029
with self.assertRaises(ValueError):
3130
GraphDatabase.driver("x://xxx")
@@ -141,9 +140,65 @@ def test_can_obtain_summary_info(self):
141140
assert summary.statement_type == "rw"
142141
assert summary.statistics.nodes_created == 1
143142

143+
def test_no_plan_info(self):
144+
with GraphDatabase.driver("bolt://localhost").session() as session:
145+
result = session.run("CREATE (n) RETURN n")
146+
assert result.summarize().plan is None
147+
assert result.summarize().profile is None
148+
149+
def test_can_obtain_plan_info(self):
150+
with GraphDatabase.driver("bolt://localhost").session() as session:
151+
result = session.run("EXPLAIN CREATE (n) RETURN n")
152+
plan = result.summarize().plan
153+
assert plan.operator_type == "ProduceResults"
154+
assert plan.identifiers == ["n"]
155+
assert plan.arguments == {"planner": "COST", "EstimatedRows": 1.0, "version": "CYPHER 3.0",
156+
"KeyNames": "n", "runtime-impl": "INTERPRETED", "planner-impl": "IDP",
157+
"runtime": "INTERPRETED"}
158+
assert len(plan.children) == 1
159+
160+
def test_can_obtain_profile_info(self):
161+
with GraphDatabase.driver("bolt://localhost").session() as session:
162+
result = session.run("PROFILE CREATE (n) RETURN n")
163+
profile = result.summarize().profile
164+
assert profile.db_hits == 0
165+
assert profile.rows == 1
166+
assert profile.operator_type == "ProduceResults"
167+
assert profile.identifiers == ["n"]
168+
assert profile.arguments == {"planner": "COST", "EstimatedRows": 1.0, "version": "CYPHER 3.0",
169+
"KeyNames": "n", "runtime-impl": "INTERPRETED", "planner-impl": "IDP",
170+
"runtime": "INTERPRETED", "Rows": 1, "DbHits": 0}
171+
assert len(profile.children) == 1
172+
173+
def test_no_notification_info(self):
174+
with GraphDatabase.driver("bolt://localhost").session() as session:
175+
result = session.run("CREATE (n) RETURN n")
176+
notifications = result.summarize().notifications
177+
assert notifications == []
178+
179+
def test_can_obtain_notification_info(self):
180+
with GraphDatabase.driver("bolt://localhost").session() as session:
181+
result = session.run("EXPLAIN MATCH (n), (m) RETURN n, m")
182+
notifications = result.summarize().notifications
183+
184+
assert len(notifications) == 1
185+
notification = notifications[0]
186+
assert notification.code == "Neo.ClientNotification.Statement.CartesianProduct"
187+
assert notification.title == "This query builds a cartesian product between disconnected patterns."
188+
assert notification.description == \
189+
"If a part of a query contains multiple disconnected patterns, " \
190+
"this will build a cartesian product between all those parts. " \
191+
"This may produce a large amount of data and slow down query processing. " \
192+
"While occasionally intended, it may often be possible to reformulate the query " \
193+
"that avoids the use of this cross product, perhaps by adding a relationship between " \
194+
"the different parts or by using OPTIONAL MATCH (identifier is: (m))"
195+
position = notification.position
196+
assert position.offset == 0
197+
assert position.line == 1
198+
assert position.column == 1
144199

145-
class TransactionTestCase(TestCase):
146200

201+
class TransactionTestCase(TestCase):
147202
def test_can_commit_transaction(self):
148203
with GraphDatabase.driver("bolt://localhost").session() as session:
149204
tx = session.new_transaction()
@@ -161,7 +216,7 @@ def test_can_commit_transaction(self):
161216

162217
# Check the property value
163218
result = session.run("MATCH (a) WHERE id(a) = {n} "
164-
"RETURN a.foo", {"n": node_id})
219+
"RETURN a.foo", {"n": node_id})
165220
foo = result[0][0]
166221
assert foo == "bar"
167222

@@ -182,13 +237,12 @@ def test_can_rollback_transaction(self):
182237

183238
# Check the property value
184239
result = session.run("MATCH (a) WHERE id(a) = {n} "
185-
"RETURN a.foo", {"n": node_id})
240+
"RETURN a.foo", {"n": node_id})
186241
assert len(result) == 0
187242

188243
def test_can_commit_transaction_using_with_block(self):
189244
with GraphDatabase.driver("bolt://localhost").session() as session:
190245
with session.new_transaction() as tx:
191-
192246
# Create a node
193247
result = tx.run("CREATE (a) RETURN id(a)")
194248
node_id = result[0][0]
@@ -202,14 +256,13 @@ def test_can_commit_transaction_using_with_block(self):
202256

203257
# Check the property value
204258
result = session.run("MATCH (a) WHERE id(a) = {n} "
205-
"RETURN a.foo", {"n": node_id})
259+
"RETURN a.foo", {"n": node_id})
206260
foo = result[0][0]
207261
assert foo == "bar"
208262

209263
def test_can_rollback_transaction_using_with_block(self):
210264
with GraphDatabase.driver("bolt://localhost").session() as session:
211265
with session.new_transaction() as tx:
212-
213266
# Create a node
214267
result = tx.run("CREATE (a) RETURN id(a)")
215268
node_id = result[0][0]
@@ -221,5 +274,5 @@ def test_can_rollback_transaction_using_with_block(self):
221274

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

0 commit comments

Comments
 (0)