File: //snap/google-cloud-cli/396/lib/googlecloudsdk/command_lib/spanner/sql.py
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Common methods to display parts of SQL query results."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from functools import partial
from apitools.base.py import encoding
from googlecloudsdk.core.resource import resource_printer
from googlecloudsdk.core.util import text
from sqlparse import lexer
from sqlparse import tokens as T
def _GetAdditionalProperty(properties, property_key, not_found_value='Unknown'):
"""Gets the value for the given key in a list of properties.
Looks through a list of properties and tries to find the value for the given
key. If it's not found, not_found_value is returned.
Args:
properties: A dictionary of key string, value string pairs.
property_key: The key string for which we want to get the value.
not_found_value: The string value to return if the key is not found.
Returns:
A string containing the value for the given key, or `not_found_value` if
the key is not found.
"""
for prop in properties:
if prop.key == property_key:
if hasattr(prop, 'value'):
return prop.value
break
return not_found_value
def _ConvertToTree(plan_nodes):
"""Creates tree of Node objects from the plan_nodes in server response.
Args:
plan_nodes (spanner_v1_messages.PlanNode[]): The plan_nodes from the server
response. Plan nodes are topologically sorted.
Returns:
A Node, root of a tree built from `plan_nodes`.
"""
# plan_nodes is a topologically sorted list, with the root node first.
return _BuildSubTree(plan_nodes, plan_nodes[0])
def _BuildSubTree(plan_nodes, node):
"""Helper for building the subtree of a query plan node.
Args:
plan_nodes (spanner_v1_messages.PlanNode[]): The plan_nodes from the server
response. Plan nodes are topologically sorted.
node (spanner_v1_messages.PlanNode): The root node of the subtree to be
built.
Returns:
A Node object.
"""
children = None
if node.childLinks:
children = [_BuildSubTree(plan_nodes, plan_nodes[link.childIndex])
for link in node.childLinks]
return Node(node, children)
def _ConvertToStringValue(prop):
"""Converts the prop to a string if it exists.
Args:
prop (object_value): The value returned from _GetAdditionalProperty.
Returns:
A string value for the given prop, or the `not_found_value` if the prop does
not exist.
"""
return getattr(prop, 'string_value', prop)
def _DisplayNumberOfRowsModified(row_count, is_exact_count, out):
"""Prints number of rows modified by a DML statement.
Args:
row_count: Either the exact number of rows modified by statement or the
lower bound of rows modified by a Partitioned DML statement.
is_exact_count: Boolean stating whether the number is the exact count.
out: Output stream to which we print.
"""
if is_exact_count:
output_str = 'Statement modified {} {}'
else:
output_str = 'Statement modified a lower bound of {} {}'
if row_count == 1:
out.Print(output_str.format(row_count, 'row'))
else:
out.Print(output_str.format(row_count, 'rows'))
def QueryHasDml(sql):
"""Determines if the sql string contains a DML query.
Args:
sql (string): The sql string entered by the user.
Returns:
A boolean.
"""
sql = sql.lstrip().lower()
tokenized = lexer.tokenize(sql)
for token in list(tokenized):
has_dml = (
token == (T.Keyword.DML, 'insert') or
token == (T.Keyword.DML, 'update') or
token == (T.Keyword.DML, 'delete'))
if has_dml:
return True
return False
def QueryHasAggregateStats(result):
"""Checks if the given results have aggregate statistics.
Args:
result (spanner_v1_messages.ResultSetStats): The stats for a query.
Returns:
A boolean indicating whether 'results' contain aggregate statistics.
"""
return hasattr(
result, 'stats') and getattr(result.stats, 'queryStats', None) is not None
def DisplayQueryAggregateStats(query_stats, out):
"""Displays the aggregate stats for a Spanner SQL query.
Looks at the queryStats portion of the query response and prints some of
the aggregate statistics.
Args:
query_stats (spanner_v1_messages.ResultSetStats.QueryStatsValue): The query
stats taken from the server response to a query.
out: Output stream to which we print.
"""
get_prop = partial(_GetAdditionalProperty, query_stats.additionalProperties)
stats = {
'total_elapsed_time': _ConvertToStringValue(get_prop('elapsed_time')),
'cpu_time': _ConvertToStringValue(get_prop('cpu_time')),
'rows_returned': _ConvertToStringValue(get_prop('rows_returned')),
'rows_scanned': _ConvertToStringValue(get_prop('rows_scanned')),
'optimizer_version': _ConvertToStringValue(get_prop('optimizer_version')),
}
resource_printer.Print(
stats,
'table[box](total_elapsed_time, cpu_time, rows_returned, rows_scanned, optimizer_version)',
out=out)
def DisplayQueryPlan(result, out):
"""Displays a graphical query plan for a query.
Args:
result (spanner_v1_messages.ResultSet): The server response to a query.
out: Output stream to which we print.
"""
node_tree_root = _ConvertToTree(result.stats.queryPlan.planNodes)
node_tree_root.PrettyPrint(out)
def DisplayQueryResults(result, out):
"""Prints the result rows for a query.
Args:
result (spanner_v1_messages.ResultSet): The server response to a query.
out: Output stream to which we print.
"""
if hasattr(result.stats,
'rowCountExact') and result.stats.rowCountExact is not None:
_DisplayNumberOfRowsModified(result.stats.rowCountExact, True, out)
if hasattr(
result.stats,
'rowCountLowerBound') and result.stats.rowCountLowerBound is not None:
_DisplayNumberOfRowsModified(result.stats.rowCountLowerBound, False, out)
if result.metadata.rowType.fields:
# Print "(Unspecified)" for computed columns.
fields = [
field.name or '(Unspecified)'
for field in result.metadata.rowType.fields
]
# Create the format string we pass to the table layout.
table_format = ','.join('row.slice({0}).join():label="{1}"'.format(i, f)
for i, f in enumerate(fields))
rows = [{
'row': encoding.MessageToPyValue(row.entry)
} for row in result.rows]
# Can't use the PrintText method because we want special formatting.
resource_printer.Print(rows, 'table({0})'.format(table_format), out=out)
class Node(object):
"""Represents a single node in a Spanner query plan.
Attributes:
properties (spanner_v1_messages.PlanNode): The details about a given node
as returned from the server.
children: A list of children in the query plan of type Node.
"""
def __init__(self, properties, children=None):
self.children = children or []
self.properties = properties
def _DisplayKindAndName(self, out, prepend, stub):
"""Prints the kind of the node (SCALAR or RELATIONAL) and its name."""
kind_and_name = '{}{} {} {}'.format(prepend, stub, self.properties.kind,
self.properties.displayName)
out.Print(kind_and_name)
def _GetNestedStatProperty(self, prop_name, nested_prop_name):
"""Gets a nested property name on this object's executionStats.
Args:
prop_name: A string of the key name for the outer property on
executionStats.
nested_prop_name: A string of the key name of the nested property.
Returns:
The string value of the nested property, or None if the outermost
property or nested property don't exist.
"""
prop = _GetAdditionalProperty(
self.properties.executionStats.additionalProperties, prop_name, '')
if not prop:
return None
nested_prop = _GetAdditionalProperty(prop.object_value.properties,
nested_prop_name, '')
if nested_prop:
return nested_prop.string_value
return None
def _DisplayExecutionStats(self, out, prepend, beneath_stub):
"""Prints the relevant execution statistics for a node.
More specifically, print out latency information and the number of
executions. This information only exists when query is run in 'PROFILE'
mode.
Args:
out: Output stream to which we print.
prepend: String that precedes any information about this node to maintain
a visible hierarchy.
beneath_stub: String that preserves the indentation of the vertical lines.
"""
if not self.properties.executionStats:
return None
stat_props = []
num_executions = self._GetNestedStatProperty('execution_summary',
'num_executions')
if num_executions:
num_executions = int(num_executions)
executions_str = '{} {}'.format(num_executions,
text.Pluralize(num_executions,
'execution'))
stat_props.append(executions_str)
# Total latency and latency unit are always expected to be present when
# latency exists. Latency exists when the query is run in PROFILE mode.
mean_latency = self._GetNestedStatProperty('latency', 'mean')
total_latency = self._GetNestedStatProperty('latency', 'total')
unit = self._GetNestedStatProperty('latency', 'unit')
if mean_latency:
stat_props.append('{} {} average latency'.format(mean_latency, unit))
elif total_latency:
stat_props.append('{} {} total latency'.format(total_latency, unit))
if stat_props:
executions_stats_str = '{}{} ({})'.format(prepend, beneath_stub,
', '.join(stat_props))
out.Print(executions_stats_str)
def _DisplayMetadata(self, out, prepend, beneath_stub):
"""Prints the keys and values of the metadata for a node.
Args:
out: Output stream to which we print.
prepend: String that precedes any information about this node to maintain
a visible hierarchy.
beneath_stub: String that preserves the indentation of the vertical lines.
"""
if self.properties.metadata:
additional_props = []
# additionalProperties looks like: [key: {value: {string_value: str}}]
for prop in self.properties.metadata.additionalProperties:
additional_props.append(
'{}: {}'.format(prop.key, prop.value.string_value))
metadata = '{}{} {}'.format(prepend, beneath_stub,
', '.join(sorted(additional_props)))
out.Print(metadata)
def _DisplayShortRepresentation(self, out, prepend, beneath_stub):
if self.properties.shortRepresentation:
short_rep = '{}{} {}'.format(
prepend, beneath_stub,
self.properties.shortRepresentation.description)
out.Print(short_rep)
def _DisplayBreakLine(self, out, prepend, beneath_stub, is_root):
"""Displays an empty line between nodes for visual breathing room.
Keeps in tact the vertical lines connecting all immediate children of a
node to each other.
Args:
out: Output stream to which we print.
prepend: String that precedes any information about this node to maintain
a visible hierarchy.
beneath_stub: String that preserves the indentation of the vertical lines.
is_root: Boolean indicating whether this node is the root of the tree.
"""
above_child = ' ' if is_root else ''
above_child += ' |' if self.children else ''
break_line = '{}{}{}'.format(prepend, beneath_stub, above_child)
# It could be the case the beneath_stub adds spaces but above_child doesn't
# add an additional vertical line, in which case we want to remove the
# extra trailing spaces.
out.Print(break_line.rstrip())
def PrettyPrint(self, out, prepend=None, is_last=True, is_root=True):
"""Prints a string representation of this node in the tree.
Args:
out: Output stream to which we print.
prepend: String that precedes any information about this node to maintain
a visible hierarchy.
is_last: Boolean indicating whether this node is the last child of its
parent.
is_root: Boolean indicating whether this node is the root of the tree.
"""
prepend = prepend or ''
# The symbol immediately before node kind to indicate that this is a child
# of its parents. All nodes except the root get one.
stub = '' if is_root else (r'\-' if is_last else '+-')
# To list additional properties beneath the name, figure out how they should
# be indented relative to the name's stub.
beneath_stub = '' if is_root else (' ' if is_last else '| ')
self._DisplayKindAndName(out, prepend, stub)
self._DisplayExecutionStats(out, prepend, beneath_stub)
self._DisplayMetadata(out, prepend, beneath_stub)
self._DisplayShortRepresentation(out, prepend, beneath_stub)
self._DisplayBreakLine(out, prepend, beneath_stub, is_root)
for idx, child in enumerate(self.children):
is_last_child = idx == len(self.children) - 1
# The amount each subsequent level in the tree is indented.
indent = ' '
# Connect all immediate children to each other with a vertical line
# of '|'. Don't extend this line down past the last child node. It's
# cleaner.
child_prepend = prepend + (' ' if is_last else '|') + indent
child.PrettyPrint(
out, prepend=child_prepend, is_last=is_last_child, is_root=False)