Skip to content

Added support for OC Explain plans #265

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 18, 2022
Merged
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
79 changes: 57 additions & 22 deletions src/graph_notebook/magics/graph_magic.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
from __future__ import print_function # Python 2/3 compatibility

import argparse
import base64
import logging
import json
import time
@@ -50,6 +51,8 @@
sparql_construct_template = retrieve_template("sparql_construct.html")
gremlin_table_template = retrieve_template("gremlin_table.html")
opencypher_table_template = retrieve_template("opencypher_table.html")
opencypher_explain_template = retrieve_template("opencypher_explain.html")
gremlin_explain_profile_template = retrieve_template("gremlin_explain_profile.html")
pre_container_template = retrieve_template("pre_container.html")
loading_wheel_template = retrieve_template("loading_wheel.html")
error_template = retrieve_template("error.html")
@@ -322,7 +325,9 @@ def sparql(self, line='', cell='', local_ns: dict = None):
if not args.silent:
sparql_metadata = build_sparql_metadata_from_query(query_type='explain', res=res)
titles.append('Explain')
first_tab_html = sparql_explain_template.render(table=explain)
explain_bytes = explain.encode('ascii')
base64_str = base64.b64encode(explain_bytes).decode('ascii')
first_tab_html = sparql_explain_template.render(table=explain, link=f"data:text/html;base64,{base64_str}")
else:
query_type = get_query_type(cell)
headers = {} if query_type not in ['SELECT', 'CONSTRUCT', 'DESCRIBE'] else {
@@ -538,7 +543,9 @@ def gremlin(self, line, cell, local_ns: dict = None):
gremlin_metadata = build_gremlin_metadata_from_query(query_type='explain', results=query_res, res=res)
titles.append('Explain')
if 'Neptune Gremlin Explain' in query_res:
first_tab_html = pre_container_template.render(content=query_res)
explain_bytes = query_res.encode('ascii')
base64_str = base64.b64encode(explain_bytes).decode('ascii')
first_tab_html = gremlin_explain_profile_template.render(content=query_res, link=f"data:text/html;base64,{base64_str}")
else:
first_tab_html = pre_container_template.render(content='No explain found')
elif mode == QueryMode.PROFILE:
@@ -561,7 +568,9 @@ def gremlin(self, line, cell, local_ns: dict = None):
gremlin_metadata = build_gremlin_metadata_from_query(query_type='profile', results=query_res, res=res)
titles.append('Profile')
if 'Neptune Gremlin Profile' in query_res:
first_tab_html = pre_container_template.render(content=query_res)
explain_bytes = query_res.encode('ascii')
base64_str = base64.b64encode(explain_bytes).decode('ascii')
first_tab_html = gremlin_explain_profile_template.render(content=query_res, link=f"data:text/html;base64,{base64_str}")
else:
first_tab_html = pre_container_template.render(content='No profile found')
else:
@@ -1666,14 +1675,17 @@ def handle_opencypher_query(self, line, cell, local_ns):
This method in its own handler so that the magics %%opencypher and %%oc can both call it
"""
parser = argparse.ArgumentParser()
parser.add_argument('--explain-type', default='dynamic',
help='explain mode to use when using the explain query mode',
choices=['dynamic', 'static', 'details', 'debug'])
parser.add_argument('-g', '--group-by', type=str, default='~labels',
help='Property used to group nodes (e.g. code, ~id) default is ~labels')
parser.add_argument('-gd', '--group-by-depth', action='store_true', default=False,
help="Group nodes based on path hierarchy")
parser.add_argument('-gr', '--group-by-raw', action='store_true', default=False,
help="Group nodes by the raw result")
parser.add_argument('mode', nargs='?', default='query', help='query mode [query|bolt]',
choices=['query', 'bolt'])
parser.add_argument('mode', nargs='?', default='query', help='query mode [query|bolt|explain]',
choices=['query', 'bolt', 'explain'])
parser.add_argument('-d', '--display-property', type=str, default='~labels',
help='Property to display the value of on each node, default is ~labels')
parser.add_argument('-de', '--edge-display-property', type=str, default='~labels',
@@ -1713,8 +1725,22 @@ def handle_opencypher_query(self, line, cell, local_ns):
titles = []
children = []
force_graph_output = None
explain_html = ""

if args.mode == 'query':
if args.mode == 'explain':
query_start = time.time() * 1000 # time.time() returns time in seconds w/high precision; x1000 to get in ms
res = self.client.opencypher_http(cell, explain=args.explain_type)
query_time = time.time() * 1000 - query_start
explain = res.content.decode("utf-8")
res.raise_for_status()
##store_to_ns(args.store_to, explain, local_ns)
if not args.silent:
oc_metadata = build_opencypher_metadata_from_query(query_type='explain', results=None, res=res,
query_time=query_time)
explain_bytes = explain.encode('utf-8')
base64_str = base64.b64encode(explain_bytes).decode('utf-8')
explain_html = opencypher_explain_template.render(table=explain, link=f"data:text/html;base64,{base64_str}")
elif args.mode == 'query':
query_start = time.time() * 1000 # time.time() returns time in seconds w/high precision; x1000 to get in ms
oc_http = self.client.opencypher_http(cell)
query_time = time.time() * 1000 - query_start
@@ -1753,35 +1779,44 @@ def handle_opencypher_query(self, line, cell, local_ns):
# Need to eventually add code to parse and display a network for the bolt format here

if not args.silent:
rows_and_columns = opencypher_get_rows_and_columns(res, True if args.mode == 'bolt' else False)
rows_and_columns = None
if args.mode != "explain":
rows_and_columns = opencypher_get_rows_and_columns(res, True if args.mode == 'bolt' else False)

display(tab)
table_output = widgets.Output(layout=oc_layout)
first_tab_output = widgets.Output(layout=oc_layout)
# Assign an empty value so we can always display to table output.
table_html = ""

# Display Console Tab
# some issues with displaying a datatable when not wrapped in an hbox and displayed last
hbox = widgets.HBox([table_output], layout=oc_layout)
hbox = widgets.HBox([first_tab_output], layout=oc_layout)
children.append(hbox)
titles.append('Console')
if rows_and_columns is not None:
titles.append('Console')
table_id = f"table-{str(uuid.uuid4())[:8]}"
visible_results = results_per_page_check(args.results_per_page)
table_html = opencypher_table_template.render(columns=rows_and_columns['columns'],
rows=rows_and_columns['rows'], guid=table_id,
amount=visible_results)
rows=rows_and_columns['rows'], guid=table_id,
amount=visible_results)

# Display Graph Tab (if exists)
if force_graph_output:
titles.append('Graph')
children.append(force_graph_output)
if explain_html != "":
titles.append('Explain')
with first_tab_output:
display(HTML(explain_html))
else:
# Display Graph Tab (if exists)
if force_graph_output:
titles.append('Graph')
children.append(force_graph_output)

# Display JSON tab
json_output = widgets.Output(layout=oc_layout)
with json_output:
print(json.dumps(res, indent=2))
children.append(json_output)
titles.append('JSON')
# Display JSON tab
json_output = widgets.Output(layout=oc_layout)
with json_output:
print(json.dumps(res, indent=2))
children.append(json_output)
titles.append('JSON')

# Display Query Metadata Tab
metadata_output = widgets.Output(layout=oc_layout)
@@ -1793,7 +1828,7 @@ def handle_opencypher_query(self, line, cell, local_ns):
tab.set_title(i, titles[i])

if table_html != "":
with table_output:
with first_tab_output:
display(HTML(table_html))

with metadata_output:
7 changes: 4 additions & 3 deletions src/graph_notebook/magics/metadata.py
Original file line number Diff line number Diff line change
@@ -218,7 +218,7 @@ def build_gremlin_metadata_from_query(query_type: str, results: any, res: Respon


def build_opencypher_metadata_from_query(query_type: str, results: any, res: Response = None, query_time: float = None) -> Metadata:
if query_type == 'bolt':
if query_type in ['bolt', 'explain']:
res_final = results
else:
res_final = results['results']
@@ -230,6 +230,7 @@ def build_opencypher_metadata_from_query(query_type: str, results: any, res: Res
def build_propertygraph_metadata_from_default_query(results: any, query_type: str = 'query', query_time: float = None) -> Metadata:
propertygraph_metadata = create_propertygraph_metadata_obj(query_type)
propertygraph_metadata.set_metric_value('request_time', query_time)
propertygraph_metadata.set_metric_value('resp_size', sys.getsizeof(results))
propertygraph_metadata.set_metric_value('results', len(results))
if query_type != 'explain':
propertygraph_metadata.set_metric_value('resp_size', sys.getsizeof(results))
propertygraph_metadata.set_metric_value('results', len(results))
return propertygraph_metadata
5 changes: 4 additions & 1 deletion src/graph_notebook/neptune/client.py
Original file line number Diff line number Diff line change
@@ -236,7 +236,7 @@ def _gremlin_query_plan(self, query: str, plan_type: str, args: dict, ) -> reque
res = self._http_session.send(req)
return res

def opencypher_http(self, query: str, headers: dict = None) -> requests.Response:
def opencypher_http(self, query: str, headers: dict = None, explain: str = None) -> requests.Response:
if headers is None:
headers = {}

@@ -247,6 +247,9 @@ def opencypher_http(self, query: str, headers: dict = None) -> requests.Response
data = {
'query': query
}
if explain:
data['explain'] = explain
headers['Accept'] = "text/html"

req = self._prepare_request('POST', url, data=data, headers=headers)
res = self._http_session.send(req)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@


<div>
{% block style %}
<style>
#results-pre{
overflow: hidden !important;
font-size: 1em !important;
font-family: "Courier New", Courier, monospace !important;
}
</style>
{% endblock %}
<a download="explain.txt" href={{link}} style="float:right">Download</a>
<br />
<pre id="results-pre">{{content|e}}</pre>
</div>
46 changes: 46 additions & 0 deletions src/graph_notebook/visualization/templates/opencypher_explain.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<style>
.oc_explain {
overflow: hidden;
}

.oc_explain form {
margin-bottom: 5px;
}

.oc_explain form a {
float: right;
}

.oc_explain table {
font-family: "Courier New", Courier, monospace !important;
border-collapse: collapse;
width: 100%;
display: block;
table-layout: auto;
margin: 0 auto;
overflow-x: scroll;
}

.oc_explain table td, .oc_explain table th {
border: 1px solid #ddd;
padding: 8px;
word-wrap: break-word;
text-align: center;
}

.oc_explain table tr:nth-child(even){background-color: white;}

.oc_explain table tr:hover {background-color: #ddd;}

.oc_explain table th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
}
</style>
<div class="oc_explain">
<form method="get" action="file.doc">
<a download="explain.html" href={{link}}>Download</a>
</form>
{{ table }}
</div>
34 changes: 27 additions & 7 deletions src/graph_notebook/visualization/templates/sparql_explain.html
Original file line number Diff line number Diff line change
@@ -1,28 +1,48 @@
<div>
{% block style %}
<style>
table {
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
.sparql_explain {
overflow: hidden;
}

.sparql_explain form {
margin-bottom: 5px;
}

.sparql_explain form a {
float: right;
}

.sparql_explain table {
font-family: "Courier New", Courier, monospace !important;
border-collapse: collapse;
width: 100%;
display: block;
table-layout: auto;
margin: 0 auto;
overflow-x: scroll;
}

table td, table th {
.sparql_explain table td, .sparql_explain table th {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}

table tr:nth-child(even){background-color: #f2f2f2;}
.sparql_explain table tr:nth-child(even){background-color: white;}

table tr:hover {background-color: #ddd;}
.sparql_explain table tr:hover {background-color: #ddd;}

table th {
.sparql_explain table th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
}
</style>
{% endblock %}
<div class="sparql_explain">
<form method="get" action="file.doc">
<a download="explain.html" href={{link}}>Download</a>
</form>

{{ table }}
</div>