From 42a169b5bd9dcf880e9db3791b68cf92e77ba534 Mon Sep 17 00:00:00 2001 From: Zhen Date: Mon, 26 Oct 2015 14:57:49 +0100 Subject: [PATCH 1/2] Added profile and explain --- neo4j/session.py | 55 ++++++++++++++++++++++++++++++++++++++++++-- test/session_test.py | 42 ++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/neo4j/session.py b/neo4j/session.py index 789dcb4b3..0c2aa8b7e 100644 --- a/neo4j/session.py +++ b/neo4j/session.py @@ -139,7 +139,8 @@ 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")) if self.bench_test: self.bench_test.end_recv = perf_counter() @@ -181,11 +182,22 @@ 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 + + def __init__(self, statement, parameters, statement_type, statistics, plan, profile): 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 class StatementStatistics(object): @@ -237,6 +249,45 @@ 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 Session(object): """ Logical session carried out over an established TCP connection. Sessions should generally be constructed using the :meth:`.Driver.session` diff --git a/test/session_test.py b/test/session_test.py index abfac8354..9e842d5aa 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -25,7 +25,6 @@ class RunTestCase(TestCase): - def test_must_use_valid_url_scheme(self): try: GraphDatabase.driver("x://xxx") @@ -169,9 +168,38 @@ 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 -class TransactionTestCase(TestCase): +class TransactionTestCase(TestCase): def test_can_commit_transaction(self): with GraphDatabase.driver("bolt://localhost").session() as session: tx = session.new_transaction() @@ -189,7 +217,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" @@ -210,13 +238,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] @@ -230,14 +257,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] @@ -249,7 +275,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 From 54d1c609582ded048b900ad0f6bfe065d05581bf Mon Sep 17 00:00:00 2001 From: Zhen Date: Mon, 2 Nov 2015 14:09:40 +0100 Subject: [PATCH 2/2] Added notifications into result summary --- neo4j/session.py | 51 ++++++++++++++++++++++++++++++++++++++++---- test/session_test.py | 27 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/neo4j/session.py b/neo4j/session.py index 0c2aa8b7e..1d6f570ed 100644 --- a/neo4j/session.py +++ b/neo4j/session.py @@ -140,7 +140,8 @@ def on_footer(self, metadata): self.complete = True self.summary = ResultSummary(self.statement, self.parameters, metadata.get("type"), metadata.get("stats"), - metadata.get("plan"), metadata.get("profile")) + metadata.get("plan"), metadata.get("profile"), + metadata.get("notifications", [])) if self.bench_test: self.bench_test.end_recv = perf_counter() @@ -188,7 +189,12 @@ class ResultSummary(object): #: A :class:`.ProfiledPlan` instance profile = None - def __init__(self, statement, parameters, statement_type, statistics, plan, profile): + #: 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 @@ -198,6 +204,7 @@ def __init__(self, statement, parameters, statement_type, statistics, plan, prof if profile is not None: self.profile = ProfiledPlan(profile) self.plan = self.profile + self.notifications = list(map(Notification, notifications)) class StatementStatistics(object): @@ -283,11 +290,47 @@ class ProfiledPlan(Plan): rows = 0 def __init__(self, profile): - self.db_hits = profile.get("dbHits", 0); - self.rows = profile.get("rows", 0); + 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` diff --git a/test/session_test.py b/test/session_test.py index 9e842d5aa..2195703ef 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -198,6 +198,33 @@ def test_can_obtain_profile_info(self): "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): def test_can_commit_transaction(self):