diff --git a/src/graph_notebook/magics/graph_magic.py b/src/graph_notebook/magics/graph_magic.py index 5901a650..00a793b5 100644 --- a/src/graph_notebook/magics/graph_magic.py +++ b/src/graph_notebook/magics/graph_magic.py @@ -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: diff --git a/src/graph_notebook/magics/metadata.py b/src/graph_notebook/magics/metadata.py index 0ebe94fa..bf9f9b09 100644 --- a/src/graph_notebook/magics/metadata.py +++ b/src/graph_notebook/magics/metadata.py @@ -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 diff --git a/src/graph_notebook/neptune/client.py b/src/graph_notebook/neptune/client.py index 3f7bf59b..dc17aed0 100644 --- a/src/graph_notebook/neptune/client.py +++ b/src/graph_notebook/neptune/client.py @@ -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) diff --git a/src/graph_notebook/visualization/templates/gremlin_explain_profile.html b/src/graph_notebook/visualization/templates/gremlin_explain_profile.html new file mode 100644 index 00000000..6a642f77 --- /dev/null +++ b/src/graph_notebook/visualization/templates/gremlin_explain_profile.html @@ -0,0 +1,16 @@ + + +
diff --git a/src/graph_notebook/visualization/templates/opencypher_explain.html b/src/graph_notebook/visualization/templates/opencypher_explain.html new file mode 100644 index 00000000..f345832b --- /dev/null +++ b/src/graph_notebook/visualization/templates/opencypher_explain.html @@ -0,0 +1,46 @@ + +