mysqlshdk/shellcore/shell_resultset_dumper.cc (907 lines of code) (raw):
/*
* Copyright (c) 2014, 2024, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms,
* as designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define __STDC_FORMAT_MACROS 1
#include "shellcore/shell_resultset_dumper.h"
#include <algorithm>
#include <cinttypes>
#include <deque>
#include "ext/linenoise-ng/include/linenoise.h"
#include "mysqlshdk/include/shellcore/base_shell.h"
#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/libs/db/column.h"
#include "mysqlshdk/libs/db/row_copy.h"
#include "mysqlshdk/libs/utils/dtoa.h"
#include "mysqlshdk/libs/utils/strformat.h"
#include "mysqlshdk/libs/utils/utils_encoding.h" // base64 encoding utilities
#include "mysqlshdk/libs/utils/utils_json.h"
#include "shellcore/interrupt_handler.h"
#include "utils/utils_general.h"
#include "utils/utils_string.h"
#define MAX_DISPLAY_LENGTH 1024
// max # of rows to pre-fetch when dumping resultsets with table formatting,
// in order to calculate column widths
static constexpr const int k_pre_fetch_result_rows = 1000;
namespace mysqlsh {
/* Calculates the required buffer size and display size considering:
* - Some single byte characters may require injection of escaped sequence \\
* - Some multibyte characters are displayed in the space of a single character
* - Some multibyte characters are displayed in the space of two characters
*
* For the reasons above, the real buffer required to store the formatted text
* might be bigger than the original length and the space required on screen
* might be either lower (mb chars) or bigger (injected escapes) than the
* original length.
*
* This function returns a tuple containing:
* - Real display character count
* - Real byte size
*/
std::tuple<size_t, size_t> get_utf8_sizes(const char *text, size_t length,
Print_flags flags) {
size_t char_count = 0;
size_t byte_count = 0;
const char *index = text;
const char *end = index + length;
#ifdef _WIN32
// By default, we assume no multibyte content on the string and
// no escaped characters.
bool is_multibyte = false;
byte_count = length;
char_count = length;
if (length) {
// Calculates the required size for the buffer
int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text,
length, nullptr, 0);
// Required would be 0 in case of error, possible errors include:
// ERROR_INSUFFICIENT_BUFFER. A supplied buffer size was not large enough,
// or it was incorrectly set to NULL. ERROR_INVALID_FLAGS. The values
// supplied for flags were not valid. ERROR_INVALID_PARAMETER. Any of the
// parameter values was invalid. ERROR_NO_UNICODE_TRANSLATION. Invalid
// Unicode was found in a string.
//
// We know ERROR_INSUFFICIENT_BUFFER is not since we were calculating the
// buffer size We assume ERROR_INVALID_FLAGS and ERROR_INVALID_PARAMETER are
// not, since the function succeeds in most of the cases. So only posibility
// is ERROR_NO_UNICODE_TRANSLATION which would be the case i.e. for binary
// data. In such case the function simply exits returning the original
// lengths.
if (required > 0) {
std::wstring wstr;
wstr = std::wstring(required, 0);
required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text,
length, &wstr[0], required);
// If the final number of characters is different from the input
// length, it means there are characters that are multibyte
if (required != length) {
is_multibyte = true;
// Character count will be totally calculated on the loop
// not only escaped characters
char_count = 0;
}
auto character = wstr.begin();
// This loop is used to calculates two things
// 1) The "screen spaces" to be required by the string
// - Some are injected spaces
// - For multibyte strings, it includes width calculation
// 2) The injected bytes due to escape handling
while (character != wstr.end()) {
if (*character == '\0') {
if (flags.is_set(Print_flag::PRINT_0_AS_SPC)) {
// If it's multibyte, length is being calculated so we need
// to add 1 on this case, otherwise it is already considered
if (is_multibyte) char_count++;
} else if (flags.is_set(Print_flag::PRINT_0_AS_ESC)) {
char_count += is_multibyte ? 2 : 1;
byte_count++;
} else {
// If not multibyte, we need to remove one char since unescaped \0
// should not occupy space
if (!is_multibyte) char_count--;
}
} else if (flags.is_set(Print_flag::PRINT_CTRL) &&
(*character == '\t' || *character == '\n' ||
*character == '\\')) {
char_count += is_multibyte ? 2 : 1;
byte_count++;
} else if (is_multibyte) {
// Get the character width
int size = getWcwidth(*character);
// We ignore control characters which may return -1
// since no character will really remove screen space
if (size > 0) char_count += size;
}
character++;
}
}
}
#else
std::mblen(NULL, 0);
while (index < end) {
int width = std::mblen(index, end - index);
// handles single byte characters
if (width == 1) {
// Controls characters to be printed add one extra char to the output
if (flags.is_set(Print_flag::PRINT_CTRL) &&
(*index == '\t' || *index == '\n' || *index == '\\')) {
char_count++;
byte_count++;
}
// The character itself
char_count++;
byte_count++;
index++;
} else if (width == 0) {
// handles the \0 character
index += 1;
// Printed as a space
if (flags.is_set(Print_flag::PRINT_0_AS_SPC)) {
char_count += 1;
byte_count += 1;
// Escape injection to be printed as \\0
} else if (flags.is_set(Print_flag::PRINT_0_AS_ESC)) {
char_count += 2;
byte_count += 2;
} else {
// No char_count but byte is needed
byte_count++;
}
} else if (width == -1) {
// If a so weird character was found, then it makes no sense to continue
// processing since the final measure will not be accurate anyway, so we
// return the original data
// This could be the case on any binary data column.
char_count = length;
byte_count = length;
break;
} else {
// Multibyte characters handling
// We need to find out whether they are printed in single or double space
wchar_t mbchar;
int size = 0;
if (std::mbtowc(&mbchar, index, width) > 0) {
size = getWcwidth(mbchar);
// We ignore control characters which may return -1
// since no character will really remove screen space
if (size < 0) size = 0;
} else {
size = 1;
}
char_count += size;
byte_count += width;
index += width;
}
}
#endif
std::tuple<size_t, size_t> ret_val{char_count, byte_count};
return ret_val;
}
namespace {
enum class ResultFormat { VERTICAL, TABBED, TABLE };
class Field_formatter {
public:
Field_formatter(ResultFormat format, const mysqlshdk::db::Column &column)
: m_allocated(0),
m_max_display_length(0),
m_max_buffer_length(0),
m_max_mb_holes(0),
m_format(format),
m_type(column.get_type()),
m_is_numeric(column.is_numeric()) {
m_zerofill = column.is_zerofill() ? column.get_length() : 0;
switch (m_format) {
case ResultFormat::TABBED:
m_flags = Print_flags(Print_flag::PRINT_0_AS_ESC);
m_flags.set(Print_flag::PRINT_CTRL);
m_align_right = false;
break;
case ResultFormat::VERTICAL:
m_flags = Print_flags(Print_flag::PRINT_0_AS_SPC);
m_align_right = false;
break;
case ResultFormat::TABLE:
m_flags = Print_flags(Print_flag::PRINT_0_AS_SPC);
m_align_right = column.is_zerofill() || column.is_numeric();
// Gets the column name display/buffer sizes
auto col_sizes =
get_utf8_sizes(column.get_column_label().c_str(),
column.get_column_label().length(),
Print_flags(Print_flag::PRINT_0_AS_ESC));
m_max_mb_holes = std::get<1>(col_sizes) - std::get<0>(col_sizes);
m_max_display_length = std::max(std::get<0>(col_sizes), m_zerofill);
m_max_buffer_length = std::get<1>(col_sizes);
break;
}
}
void process(const mysqlshdk::db::IRow *row, size_t index) {
// This function is meant to be called only for tables
assert(m_format == ResultFormat::TABLE);
size_t dlength{0};
size_t blength{0};
if (row->is_null(index)) {
dlength = blength = 4;
} else if (m_is_numeric) {
auto str = get_number_string(row, index);
dlength = blength = str.length();
} else if (m_type == mysqlshdk::db::Type::Bit) {
dlength = blength =
shcore::bits_to_string_hex_size(std::get<1>(row->get_bit(index))) + 2;
} else if (m_type == mysqlshdk::db::Type::Bytes) {
// TODO (anyone): Implement support for --skip-binary-as-hex
auto data = row->get_string_data(index);
dlength = blength = 2 + data.second * 2;
} else {
auto data = row->get_as_string(index);
auto fsizes = get_utf8_sizes(data.c_str(), data.length(), m_flags);
dlength = std::get<0>(fsizes);
blength = std::get<1>(fsizes);
}
m_max_mb_holes = std::max<size_t>(m_max_mb_holes, blength - dlength);
m_max_display_length = std::max<size_t>(m_max_display_length, dlength);
m_max_buffer_length = std::max<size_t>(m_max_buffer_length, blength);
}
~Field_formatter() = default;
std::string get_number_string(const mysqlshdk::db::IRow *row, size_t index) {
if (m_type == mysqlshdk::db::Type::Float) {
return shcore::ftoa(row->get_float(index));
} else if (m_type == mysqlshdk::db::Type::Integer) {
return std::to_string(row->get_int(index));
} else if (m_type == mysqlshdk::db::Type::UInteger) {
return std::to_string(row->get_uint(index));
} else if (m_type == mysqlshdk::db::Type::Double) {
return shcore::dtoa(row->get_double(index));
} else {
return row->get_as_string(index);
}
}
bool put(const mysqlshdk::db::IRow *row, size_t index) {
reset();
std::string tmp;
const char *data;
size_t length;
size_t display_size;
size_t buffer_size;
if (row->is_null(index)) {
data = "NULL";
display_size = buffer_size = length = 4;
} else if (m_is_numeric) {
tmp = get_number_string(row, index);
if (m_zerofill > tmp.length()) {
tmp = std::string(m_zerofill - tmp.length(), '0').append(tmp);
}
data = tmp.data();
display_size = buffer_size = length = tmp.length();
} else if (m_type == mysqlshdk::db::Type::Bit) {
auto [bit_value, bit_size] = row->get_bit(index);
tmp = shcore::bits_to_string_hex(bit_value, bit_size);
data = tmp.data();
display_size = buffer_size = length = tmp.length();
} else if (m_type == mysqlshdk::db::Type::Bytes) {
std::tie(data, length) = row->get_string_data(index);
tmp = shcore::string_to_hex({data, length});
data = tmp.data();
length = tmp.size();
std::tie(display_size, buffer_size) =
get_utf8_sizes(data, length, m_flags);
} else {
tmp = row->get_as_string(index);
data = tmp.data();
length = tmp.length();
std::tie(display_size, buffer_size) =
get_utf8_sizes(data, length, m_flags);
}
if (!append(data, length, display_size, buffer_size)) {
if (m_is_numeric || m_type == mysqlshdk::db::Type::Bit) {
// if a number is larger than expected (e.g. floating pt with lots of
// decimals)
m_buffer.assign(data, length);
} else {
return false;
}
}
return true;
}
const std::string &str() const { return m_buffer; }
size_t get_max_display_length() const { return m_max_display_length; }
size_t get_max_buffer_length() const { return m_max_buffer_length; }
private:
std::string m_buffer;
size_t m_allocated;
size_t m_zerofill;
bool m_align_right;
size_t m_max_display_length;
size_t m_max_buffer_length;
size_t m_max_mb_holes;
ResultFormat m_format;
Print_flags m_flags;
mysqlshdk::db::Type m_type;
bool m_is_numeric;
void reset() {
// sets the buffer only once
if (m_buffer.empty()) {
if (m_format == ResultFormat::TABLE) {
m_allocated = std::max<size_t>(m_max_display_length,
m_max_buffer_length + m_max_mb_holes);
if (m_allocated > MAX_DISPLAY_LENGTH) m_allocated = MAX_DISPLAY_LENGTH;
} else {
m_allocated = MAX_DISPLAY_LENGTH;
}
}
m_buffer.resize(m_allocated);
memset(&m_buffer[0], ' ', m_buffer.size());
}
bool append(const char *text, size_t length, size_t display_size,
size_t buffer_size) {
if (buffer_size > m_allocated) return false;
size_t next_index = 0;
if (m_format == ResultFormat::TABLE) {
if (m_align_right && m_max_display_length > display_size) {
next_index = m_max_display_length - display_size;
}
}
auto buffer = &m_buffer[0];
for (size_t index = 0; index < length; index++) {
if (m_flags.is_set(Print_flag::PRINT_0_AS_ESC) && text[index] == '\0') {
buffer[next_index++] = '\\';
buffer[next_index++] = '0';
} else if (m_flags.is_set(Print_flag::PRINT_0_AS_SPC) &&
text[index] == '\0') {
buffer[next_index++] = ' ';
} else if (m_flags.is_set(Print_flag::PRINT_CTRL) &&
text[index] == '\t') {
buffer[next_index++] = '\\';
buffer[next_index++] = 't';
} else if (m_flags.is_set(Print_flag::PRINT_CTRL) &&
text[index] == '\n') {
buffer[next_index++] = '\\';
buffer[next_index++] = 'n';
} else if (m_flags.is_set(Print_flag::PRINT_CTRL) &&
text[index] == '\\') {
buffer[next_index++] = '\\';
buffer[next_index++] = '\\';
} else {
buffer[next_index++] = text[index];
}
}
if (m_format == ResultFormat::TABLE) {
// If some multibyte characters were found, we need to truncate the buffer
// adding the 'lost' characters
buffer[std::min(m_allocated,
m_max_display_length + (buffer_size - display_size))] = 0;
} else {
m_buffer.resize(next_index);
}
return true;
}
};
class Console_printer : public Resultset_printer {
public:
Console_printer() : m_console(mysqlsh::current_console()) {}
void print(const std::string &s) override { m_console->print(s); }
void println(const std::string &s) override { m_console->println(s); }
void raw_print(const std::string &s) override {
m_console->raw_print(s, mysqlsh::Output_stream::STDOUT, false);
}
private:
std::shared_ptr<IConsole> m_console;
};
class String_printer : public Resultset_printer {
public:
String_printer() = default;
void print(const std::string &s) override { raw_print(s); }
void println(const std::string &s) override {
print(s);
print("\n");
}
void raw_print(const std::string &s) override { m_output += s; }
void reset() override { m_output.clear(); }
std::string data() const override { return m_output; }
private:
std::string m_output;
};
} // namespace
Resultset_dumper_base::Resultset_dumper_base(
mysqlshdk::db::IResult *target, std::unique_ptr<Resultset_printer> printer,
const std::string &wrap_json, const std::string &format,
bool show_column_type_info)
: m_result(target),
m_wrap_json(wrap_json),
m_format(format),
m_printer(std::move(printer)),
m_show_column_type_info(show_column_type_info) {
if (m_format == "ndjson") m_format = "json/raw";
}
Resultset_dumper::Resultset_dumper(mysqlshdk::db::IResult *target,
bool show_column_type_info)
: Resultset_dumper(target,
mysqlsh::current_shell_options()->get().wrap_json,
mysqlsh::current_shell_options()->get().result_format,
mysqlsh::current_shell_options()->get().show_warnings,
mysqlsh::current_shell_options()->get().interactive,
show_column_type_info) {}
Resultset_dumper::Resultset_dumper(mysqlshdk::db::IResult *target,
const std::string &wrap_json,
const std::string &format,
bool show_warnings, bool show_stats,
bool show_column_type_info)
: Resultset_dumper_base(target, std::make_unique<Console_printer>(),
wrap_json, format, show_column_type_info),
m_show_warnings(show_warnings),
m_show_stats(show_stats) {}
size_t Resultset_dumper::dump(const std::string &item_label, bool is_query,
bool is_doc_result) {
std::string output;
size_t total_count = 0;
shcore::Interrupt_handler intr([this]() {
m_cancelled = true;
return true;
});
bool first = true;
do {
if (m_wrap_json != "off") {
total_count = dump_json(item_label, is_doc_result);
} else {
size_t count = 0;
// Prints a blank line between multi-results
if (first) {
first = false;
} else {
m_printer->print("\n");
}
if (m_result->has_resultset()) {
if (show_column_type_info()) dump_metadata();
if (is_doc_result || m_format.find("json") != std::string::npos)
count = dump_documents(is_doc_result);
else if (m_format == "vertical")
count = dump_vertical();
else if (m_format == "table")
count = dump_table();
else
count = dump_tabbed();
total_count += count;
if (count)
output =
shcore::str_format("%zu %s%s in set", count, item_label.c_str(),
(count == 1 ? "" : "s"));
else
output = "Empty set";
} else if (m_show_stats && !is_query) {
// Starts only make sense on non read only operations
output = get_affected_stats(item_label);
}
// This information output is only printed in interactive mode
int warning_count = 0;
if (m_show_stats) {
warning_count = get_warning_and_execution_time_stats(&output);
m_printer->print(output);
}
if (!is_query) {
std::string info = m_result->get_info();
if (!info.empty()) {
info = "\n" + info + "\n";
m_printer->print(info);
}
}
// Prints the warnings if there were any
if (warning_count && m_show_warnings) dump_warnings();
}
} while (m_result->next_resultset() && !m_cancelled);
if (m_cancelled)
m_printer->println(
"Result printing interrupted, rows may be missing from the output.");
return total_count;
}
/**
* This utility function creates a JSON document including all the fields in a
* given row and appends it to a JSON_dumper object.
*/
void dump_json_row(shcore::JSON_dumper *dumper,
const std::vector<mysqlshdk::db::Column> &metadata,
const mysqlshdk::db::IRow *row) {
dumper->start_object();
for (size_t col_index = 0; col_index < metadata.size(); col_index++) {
auto column = metadata[col_index];
dumper->append_string(column.get_column_label());
auto type = column.get_type();
if (row->is_null(col_index)) {
dumper->append_null();
} else if (mysqlshdk::db::is_string_type(type)) {
if (type == mysqlshdk::db::Type::Json) {
dumper->append_json(row->get_string(col_index));
} else if (type == mysqlshdk::db::Type::Bytes) {
auto data = row->get_string_data(col_index);
std::string encoded;
size_t binary_limit = data.second;
if (mysqlsh::current_shell_options()->get().binary_limit > 0) {
binary_limit = std::min(
data.second,
mysqlsh::current_shell_options()->get().binary_limit + 1);
}
shcore::encode_base64(
static_cast<const unsigned char *>(
static_cast<const void *>(data.first)),
// At most binary-limit + 1 bytes shuold be sent, when the extra
// byte is sent, it will be an indicator for the consumer of the
// data that a truncation happened
binary_limit, &encoded);
dumper->append_string(encoded);
} else {
auto data = row->get_as_string(col_index);
dumper->append_string(data.c_str(), data.length());
}
} else if (type == mysqlshdk::db::Type::Integer) {
dumper->append_int64(row->get_int(col_index));
} else if (type == mysqlshdk::db::Type::UInteger) {
dumper->append_uint64(row->get_uint(col_index));
} else if (type == mysqlshdk::db::Type::Float) {
dumper->append_float(static_cast<double>(row->get_float(col_index)));
} else if (type == mysqlshdk::db::Type::Double) {
dumper->append_float(row->get_double(col_index));
} else if (type == mysqlshdk::db::Type::Decimal) {
dumper->append_float(static_cast<double>(row->get_float(col_index)));
} else if (type == mysqlshdk::db::Type::Bit) {
auto [bit_value, bit_size] = row->get_bit(col_index);
dumper->append_string(shcore::bits_to_string_hex(bit_value, bit_size));
}
}
dumper->end_object();
}
/**
* Dumps a JSON document for each row/document contained on the result
* being processed.
*/
size_t Resultset_dumper_base::dump_documents(bool is_doc_result) {
const auto &metadata = m_result->get_metadata();
auto row = m_result->fetch_one();
size_t row_count = 0;
const bool as_array = m_format == "json/array";
const bool pretty = m_format != "json/raw" && m_format != "json/array";
if (!row) return row_count;
if (as_array) m_printer->raw_print("[\n");
while (row) {
shcore::JSON_dumper dumper(
pretty, mysqlsh::current_shell_options()->get().binary_limit);
if (row_count > 0) {
if (as_array)
m_printer->raw_print(",\n");
else
m_printer->raw_print("\n");
}
if (is_doc_result)
dumper.append_json(row->get_string(0));
else
dump_json_row(&dumper, metadata, row);
m_printer->raw_print(dumper.str());
row_count++;
row = m_result->fetch_one();
}
m_printer->raw_print("\n");
if (as_array) m_printer->raw_print("]\n");
return row_count;
}
size_t Resultset_dumper_base::dump_tabbed() {
const auto &metadata = m_result->get_metadata();
auto row = m_result->fetch_one();
size_t row_index = 0;
if (!row) return row_index;
size_t index = 0;
size_t field_count = metadata.size();
std::vector<std::string> formats(field_count, "%-");
std::vector<Field_formatter> fmt;
// Prints the initial separator line and the column headers
for (index = 0; index < field_count; index++) {
auto column = metadata[index];
fmt.emplace_back(ResultFormat::TABBED, column);
m_printer->print(column.get_column_label().c_str());
m_printer->print(index < (field_count - 1) ? "\t" : "\n");
}
// Now prints the records
while (row && !m_cancelled) {
for (size_t field_index = 0; field_index < field_count; field_index++) {
auto column = metadata[field_index];
if (fmt[field_index].put(row, field_index)) {
m_printer->print(fmt[field_index].str());
} else {
mysqlshdk::db::is_string_type(column.get_type());
m_printer->print(row->get_string(field_index));
}
m_printer->print(field_index < (field_count - 1) ? "\t" : "\n");
}
row = m_result->fetch_one();
row_index++;
}
return row_index;
}
size_t Resultset_dumper_base::dump_vertical() {
return format_vertical(true, true, 0);
}
size_t Resultset_dumper_base::format_vertical(bool has_header, bool align_right,
size_t min_label_width) {
const auto &metadata = m_result->get_metadata();
const std::string star_separator(27, '*');
std::vector<Field_formatter> fmt;
// Calculate length of a longest column description, used to right align
// column descriptions
std::size_t max_col_len = min_label_width;
for (const auto &column : metadata) {
max_col_len = std::max(max_col_len, column.get_column_label().length());
fmt.emplace_back(ResultFormat::VERTICAL, column);
}
auto row = m_result->fetch_one();
size_t row_index = 0;
while (row && !m_cancelled) {
if (has_header) {
std::string row_header = star_separator + " " +
std::to_string(row_index + 1) + ". row " +
star_separator + "\n";
m_printer->print(row_header);
}
for (size_t col_index = 0; col_index < metadata.size(); col_index++) {
auto column = metadata[col_index];
std::string padding(max_col_len - column.get_column_label().size(), ' ');
std::string label = column.get_column_label() + ": ";
if (align_right) {
label = padding + label;
} else {
label += padding;
}
m_printer->print(label);
if (fmt[col_index].put(row, col_index)) {
m_printer->print(fmt[col_index].str());
} else {
assert(mysqlshdk::db::is_string_type(column.get_type()));
m_printer->print(row->get_string(col_index));
}
m_printer->raw_print("\n");
}
row = m_result->fetch_one();
row_index++;
}
return row_index;
}
/**
* Creates a JSON Document including all the information in the result being
* processd, this includes:
*
* - Metadata
* - Rows
* - Statistics
* - Warnings
*
* This function is used when JSON Wrapping is turned ON
*/
std::string Resultset_dumper_base::format_json(const std::string &item_label,
bool is_doc_result, bool pretty,
int *row_count) {
assert(row_count);
shcore::JSON_dumper dumper(
pretty, mysqlsh::current_shell_options()->get().binary_limit);
dumper.start_object();
dumper.append_string("hasData");
dumper.append_bool(m_result->has_resultset());
dumper.append_string(item_label + "s");
dumper.start_array();
*row_count = 0;
if (m_result->has_resultset()) {
if (show_column_type_info()) dump_metadata();
const auto &metadata = m_result->get_metadata();
auto row = m_result->fetch_one();
while (row) {
if (is_doc_result) {
dumper.append_json(row->get_string(0));
} else {
dump_json_row(&dumper, metadata, row);
}
(*row_count)++;
row = m_result->fetch_one();
}
}
dumper.end_array();
dumper.append_string("executionTime");
dumper.append_string(
mysqlshdk::utils::format_seconds(m_result->get_execution_time()));
if (!is_doc_result) {
dumper.append_string("affectedRowCount");
dumper.append_uint64(m_result->get_affected_row_count());
}
dumper.append_string("affectedItemsCount");
dumper.append_uint64(m_result->get_affected_row_count());
dumper.append_string("warningCount");
dumper.append_int64(m_result->get_warning_count());
dumper.append_string("warningsCount");
dumper.append_uint64(m_result->get_warning_count());
dumper.append_string("warnings");
dumper.start_array();
auto warning = m_result->fetch_one_warning();
while (warning) {
std::string level;
switch (warning->level) {
case mysqlshdk::db::Warning::Level::Note:
level = "Note";
break;
case mysqlshdk::db::Warning::Level::Warn:
level = "Warning";
break;
case mysqlshdk::db::Warning::Level::Error:
level = "Error";
break;
}
dumper.start_object();
dumper.append_string("Level", level);
dumper.append_string("Code");
dumper.append_int(warning->code);
dumper.append_string("Message", warning->msg);
dumper.end_object();
warning = m_result->fetch_one_warning();
}
dumper.end_array();
dumper.append_string("info");
dumper.append_string(m_result->get_info());
dumper.append_string("autoIncrementValue");
dumper.append_int64(m_result->get_auto_increment_value());
dumper.end_object();
return dumper.str();
}
size_t Resultset_dumper_base::dump_json(const std::string &item_label,
bool is_doc_result) {
int row_count = 0;
m_printer->raw_print(format_json(item_label, is_doc_result,
m_wrap_json == "json", &row_count));
m_printer->raw_print("\n");
return row_count;
}
size_t Resultset_dumper_base::dump_table() {
const auto &metadata = m_result->get_metadata();
std::vector<Field_formatter> fmt;
std::vector<mysqlshdk::db::Row_copy> pre_fetched_rows;
size_t num_records = 0;
const size_t field_count = metadata.size();
if (field_count == 0) return 0;
// Updates the max_length array with the maximum length between column name,
// min column length and column max length
for (size_t field_index = 0; field_index < field_count; field_index++) {
auto column = metadata[field_index];
fmt.emplace_back(ResultFormat::TABLE, column);
}
pre_fetched_rows.reserve(k_pre_fetch_result_rows);
{
auto row = m_result->fetch_one();
while (row && !m_cancelled) {
pre_fetched_rows.emplace_back(*row);
for (size_t field_index = 0; field_index < field_count; field_index++) {
fmt[field_index].process(row, field_index);
}
if (pre_fetched_rows.size() >= k_pre_fetch_result_rows) break;
row = m_result->fetch_one();
}
}
if (m_cancelled || pre_fetched_rows.empty()) return 0;
//-----------
size_t index = 0;
std::string separator("+");
for (index = 0; index < field_count; index++) {
std::string field_separator(fmt[index].get_max_display_length() + 2, '-');
field_separator.append("+");
separator.append(field_separator);
}
separator.append("\n");
// Prints the initial separator line and the column headers
m_printer->print(separator);
m_printer->print("| ");
for (index = 0; index < field_count; index++) {
std::string format = "%-";
format.append(std::to_string(fmt[index].get_max_display_length()));
format.append((index == field_count - 1) ? "s |\n" : "s | ");
auto column = metadata[index];
m_printer->print(
shcore::str_format(format.c_str(), column.get_column_label().c_str()));
}
m_printer->print(separator);
// Print pre-fetched records
for (const auto &row : pre_fetched_rows) {
++num_records;
m_printer->print("| ");
for (size_t field_index = 0; field_index < field_count; field_index++) {
if (fmt[field_index].put(&row, field_index)) {
m_printer->print(fmt[field_index].str());
} else {
assert(mysqlshdk::db::is_string_type(metadata[field_index].get_type()));
if (row.get_type(field_index) == mysqlshdk::db::Type::Bytes) {
const char *data;
size_t length;
std::tie(data, length) = row.get_string_data(field_index);
m_printer->print(shcore::string_to_hex({data, length}));
} else {
m_printer->print(row.get_as_string(field_index));
}
}
if (field_index < field_count - 1) m_printer->print(" | ");
}
m_printer->print(" |\n");
if (m_cancelled) break;
}
// Now prints the remaining records
if (!m_cancelled) {
auto row = m_result->fetch_one();
while (row && !m_cancelled) {
++num_records;
m_printer->print("| ");
for (size_t field_index = 0; field_index < field_count; field_index++) {
if (fmt[field_index].put(row, field_index)) {
m_printer->print(fmt[field_index].str());
} else {
assert(
mysqlshdk::db::is_string_type(metadata[field_index].get_type()));
m_printer->print(row->get_as_string(field_index));
}
if (field_index < field_count - 1) m_printer->print(" | ");
}
m_printer->print(" |\n");
row = m_result->fetch_one();
}
}
m_printer->print(separator);
return num_records;
}
std::string Resultset_dumper::get_affected_stats(
const std::string &item_label) {
std::string output;
// Some queries return -1 since affected rows do not apply to them
int64_t affected_items = m_result->get_affected_row_count();
if (affected_items == -1)
output = "Query OK";
else
// In case of Query OK, prints the actual number of affected rows.
output = shcore::str_format(
"Query OK, %" PRId64 " %s affected", affected_items,
(affected_items == 1 ? item_label : item_label + "s").c_str());
return output;
}
int Resultset_dumper::get_warning_and_execution_time_stats(
std::string *output_stats) {
int warning_count = 0;
if (m_show_stats) {
warning_count = m_result->get_warning_count();
if (warning_count)
output_stats->append(shcore::str_format(", %d warning%s", warning_count,
(warning_count == 1 ? "" : "s")));
output_stats->append(" ");
output_stats->append(shcore::str_format(
"(%s)", mysqlshdk::utils::format_seconds(m_result->get_execution_time())
.c_str()));
output_stats->append("\n");
}
return warning_count;
}
void Resultset_dumper_base::dump_warnings() {
auto warning = m_result->fetch_one_warning();
while (warning && !m_cancelled) {
std::string type;
switch (warning->level) {
case mysqlshdk::db::Warning::Level::Note:
type = "Note";
break;
case mysqlshdk::db::Warning::Level::Warn:
type = "Warning";
break;
case mysqlshdk::db::Warning::Level::Error:
type = "Error";
break;
}
m_printer->print((shcore::str_format("%s (code %d): %s\n", type.c_str(),
warning->code, warning->msg.c_str())));
warning = m_result->fetch_one_warning();
}
}
namespace {
void column_to_json(shcore::JSON_dumper *dumper,
const mysqlshdk::db::Column &column) {
dumper->start_object();
std::stringstream ss(to_string(column));
std::string line;
while (std::getline(ss, line)) {
auto sep = line.find(":");
assert(sep + 1 < line.length());
dumper->append_string(line.substr(0, sep));
dumper->append_string(shcore::str_strip(line.substr(sep + 1)));
}
dumper->end_object();
}
} // namespace
std::string Resultset_dumper_base::format_json_metadata(bool pretty) {
shcore::JSON_dumper dumper(
pretty, mysqlsh::current_shell_options()->get().binary_limit);
const auto &cols = m_result->get_metadata();
dumper.start_object();
for (std::size_t i = 0; i < cols.size(); i++) {
dumper.append_string(shcore::str_format("Field %zu", i + 1));
column_to_json(&dumper, cols[i]);
}
dumper.end_object();
return dumper.str();
}
void Resultset_dumper_base::dump_metadata() {
const auto &cols = m_result->get_metadata();
if (m_wrap_json != "off") {
m_printer->raw_print(format_json_metadata(m_wrap_json == "json"));
m_printer->raw_print("\n");
} else {
for (std::size_t i = 0; i < cols.size(); i++) {
m_printer->println(shcore::str_format("Field %zu", i + 1));
m_printer->println(to_string(cols[i]));
}
}
}
Gui_resultset_dumper::Gui_resultset_dumper(mysqlshdk::db::IResult *target,
const std::string &format)
: Resultset_dumper(target, "json/raw", format, true, true, true) {}
bool Gui_resultset_dumper::show_column_type_info() const {
return m_show_column_type_info && !shcore::str_beginswith(m_format, "json");
}
size_t Gui_resultset_dumper::dump_json(const std::string &item_label,
bool is_doc_result) {
int row_count = 0;
if (shcore::str_beginswith(m_format, "json")) {
auto pretty = m_format == "json/pretty";
if (mysqlsh::current_shell_options()->get().show_column_type_info) {
m_printer->print(format_json_metadata(pretty));
}
m_printer->print(
format_json(item_label, is_doc_result, pretty, &row_count));
m_printer->print("\n");
} else {
row_count = Resultset_dumper::dump_json(item_label, is_doc_result);
}
return row_count;
}
Resultset_writer::Resultset_writer(mysqlshdk::db::IResult *target)
: Resultset_writer(target, std::make_unique<String_printer>(),
mysqlsh::current_shell_options()->get().wrap_json,
mysqlsh::current_shell_options()->get().result_format) {}
Resultset_writer::Resultset_writer(mysqlshdk::db::IResult *target,
std::unique_ptr<Resultset_printer> printer,
const std::string &wrap_json,
const std::string &result_format,
bool show_column_type_info)
: Resultset_dumper_base(target, std::move(printer), wrap_json,
result_format, show_column_type_info) {}
std::string Resultset_writer::write_table() {
return write([this]() { dump_table(); });
}
std::string Resultset_writer::write_vertical() {
return write([this]() { dump_vertical(); });
}
std::string Resultset_writer::write_status() {
return write([this]() { format_vertical(false, false, 24); });
}
std::string Resultset_writer::writer_gui() {
return write([this]() {
dump_metadata();
dump_json("row", false);
});
}
std::string Resultset_writer::write(const std::function<void()> &dump) {
m_printer->reset();
while (m_result->has_resultset()) {
dump();
m_printer->raw_print("\n");
m_result->next_resultset();
}
return m_printer->data();
}
Gui_resultset_writer::Gui_resultset_writer(mysqlshdk::db::IResult *target)
: Resultset_writer(target, std::make_unique<Console_printer>(), "json/raw",
"json/raw", true) {}
size_t dump_result(mysqlshdk::db::IResult *target,
const std::string &item_label, bool is_query,
bool is_doc_result,
const std::optional<std::string> &wrap_json,
const std::optional<std::string> &opt_format,
const std::optional<bool> &show_warnings,
const std::optional<bool> &show_stats,
const std::optional<bool> &show_column_type_info) {
const auto &options = mysqlsh::current_shell_options()->get();
bool gui_mode = options.gui_mode;
std::string format = opt_format.value_or(options.result_format);
std::shared_ptr<Resultset_dumper> dumper;
// The GUI dumper is used in the cases that require specific formatting for
// proper rendering in the GUI, in the case of the table
if (gui_mode &&
(format == "table" || shcore::str_beginswith(format, "json"))) {
dumper = std::make_shared<Gui_resultset_dumper>(target, format);
} else {
dumper = std::make_shared<Resultset_dumper>(
target, wrap_json.value_or(options.wrap_json), format,
show_warnings.value_or(options.show_warnings),
show_stats.value_or(options.interactive),
show_column_type_info.value_or(options.show_column_type_info));
}
return dumper->dump(item_label, is_query, is_doc_result);
}
} // namespace mysqlsh