diff options
5 files changed, 165 insertions, 246 deletions
diff --git a/ydb/public/lib/ydb_cli/common/pretty_table.cpp b/ydb/public/lib/ydb_cli/common/pretty_table.cpp index 96c2d39a20f..5a23be0c372 100644 --- a/ydb/public/lib/ydb_cli/common/pretty_table.cpp +++ b/ydb/public/lib/ydb_cli/common/pretty_table.cpp @@ -1,13 +1,15 @@ #include "pretty_table.h" -#include "common.h" #include <library/cpp/colorizer/colors.h> #include <util/generic/algorithm.h> #include <util/generic/xrange.h> #include <util/stream/format.h> +#include <util/charset/utf8.h> +#include <contrib/restricted/patched/replxx/src/utf8string.hxx> #include <ydb/public/lib/ydb_cli/common/interactive.h> + namespace NYdb { namespace NConsoleClient { @@ -16,260 +18,130 @@ TPrettyTable::TRow::TRow(size_t nColumns) { } +enum { + COLOR_BEGIN = '\033', + COLOR_END = 'm', +}; + size_t TPrettyTable::TRow::ColumnWidth(size_t columnIndex) const { Y_ABORT_UNLESS(columnIndex < Columns.size()); - enum { - TEXT, - COLOR, - UTF8, - } state = TEXT; size_t width = 0; - TStringBuf data; - for (const auto& line : Columns.at(columnIndex)) { - data = line; - // flag of first symbol in color - bool first = false; - // flag of enfing of color - bool endcolor = false; - int utf8Len = 0; - // count of visible chars - size_t curLen = 0; - for (char ch : data) { - switch (state) { - case TEXT: - // begin of color - if (ch == '\033') { - state = COLOR; - first = true; - endcolor = false; - // begin utf8 - } else if ((ch & 0x80) != 0) { - curLen++; - utf8Len = 0; - // if the first bit of the character is not 0, we met a multibyte - // counting the number of single bits at the beginning of a byte - while ((ch & 0x80) != 0) { - utf8Len++; - ch <<= 1; - } - state = UTF8; - // common text - } else { - curLen++; - } - break; - case UTF8: - // skip n chars - utf8Len -= 1; - if (utf8Len == 0) { - curLen++; - while ((ch & 0x80) != 0) { - utf8Len++; - ch <<= 1; - } - if (utf8Len != 0) { - state = UTF8; - } else { - state = TEXT; - } - } - break; - case COLOR: - // first symbol must be [ - if (first) { - if (ch != '[') { - state = TEXT; - } - first = false; - // at the end of color can be digits, m and ; - } else if (endcolor) { - if (ch != ';' && !isdigit(ch) && ch != 'm' ) { - curLen++; - state = TEXT; - } - // ending after ; - } else { - if (ch == ';') { - endcolor = true; - } - } - break; + for (const auto& column: Columns.at(columnIndex)) { + size_t printableSymbols = 0; + + for (size_t i = 0; i < column.size();) { + if (column[i] == COLOR_BEGIN) { + while (i < column.size() && column[i] != COLOR_END) { + ++i; + } + continue; + } + + + ++i; + while (i < column.size() && IsUTF8ContinuationByte(column[i])) { + ++i; } + ++printableSymbols; } - width = Max(width, curLen); + width = Max(width, printableSymbols); } return width; } -size_t TPrettyTable::TRow::PrintColumns(IOutputStream& o, const TVector<size_t>& widths, size_t offset, TString& oldColor) const { - bool next = false; - size_t firstColLen = 0; - NColorizer::TColors colors = NColorizer::AutoColors(Cout); - - for (size_t columnIndex : xrange(Columns.size())) { - enum { - TEXT, - COLOR, - UTF8, - } state = TEXT; - o << colors.Default(); - if (columnIndex == 0) { - o << "│ "; - o << oldColor; - } else { - o << " │ "; +class TColumnLinesPrinter { +public: + TColumnLinesPrinter( + IOutputStream& o, + const TVector<TVector<TString>>& columns, + const TVector<size_t>& widths + ) + : Output_(o) + , Columns_(columns) + , Widths_(widths) + , PrintedIndexByColumnIndex_(columns.size()) + {} + + bool HasNext() { + bool allColumnsPrinted = true; + + for (size_t i = 0; i < PrintedIndexByColumnIndex_.size(); ++i) { + if (!Columns_[i].empty() && Columns_[i][0].size() > PrintedIndexByColumnIndex_[i]) { + allColumnsPrinted = false; + } } + + return !allColumnsPrinted; + } + + void Print() { + NColorizer::TColors colors = NColorizer::AutoColors(Cout); - if (size_t width = widths.at(columnIndex)) { - const auto& column = Columns.at(columnIndex); + Output_ << colors.Default(); + Output_ << "│ "; - TStringBuf data; - // count of visible chars - size_t curLen = 0; - // count of chars - size_t absCurLen = 0; - // flag of first symbol in color - bool first = false; - // flag of enfing of color - bool endcolor = false; + for (size_t columnIndex : xrange(Columns_.size())) { + Output_ << colors.Default(); + if (columnIndex != 0) { + Output_ << " │ "; + } - absCurLen = 0; - for (const auto& line : column) { - data = line; - TString s = ""; - - data.Skip(offset); - // len of utf8 symbol - int utf8Len = 0; - - for (char ch : data) { - if (next && columnIndex == 0) { - break; - } - switch (state) { - case TEXT: - // begin of color - if (ch == '\033') { - - state = COLOR; - first = true; - endcolor = false; - o << s; - s = ch; - // begin utf8 - } else if ((ch & 0x80) != 0) { - o << s; - s = ""; - utf8Len = 0; - curLen++; - // if the first bit of the character is not 0, we met a multibyte - // counting the number of single bits at the beginning of a byte - while ((ch & 0x80) != 0) { - utf8Len++; - ch <<= 1; - } - if (curLen + utf8Len > width) { - next = true; - curLen -= 1; - break; - } - o << data.SubStr(absCurLen, utf8Len); - state = UTF8; - // common text - } else { - curLen++; - if (curLen > width) { - next = true; - curLen -= 1; - break; - } - s += ch; - } - break; - case UTF8: - // skip n chars - utf8Len -= 1; - if (utf8Len == 0) { - while ((ch & 0x80) != 0) { - utf8Len++; - ch <<= 1; - } - if (curLen + utf8Len > width) { - next = true; - curLen -= 1; - break; - } - curLen++; - if (utf8Len != 0) { - o << data.SubStr(absCurLen, utf8Len); - state = UTF8; - } else { - s = ch; - state = TEXT; - } - } - break; - case COLOR: - // first symbol must be [ - if (first) { - if (ch != '[') { - o << s; - s = ch; - state = TEXT; - } else { - s += ch; - } - first = false; - // at the end of color can be digits, m and ; - } else if (endcolor) { - if (ch != ';' && !isdigit(ch) && ch != 'm' ) { - o << s; - oldColor = s; - curLen++; - if (curLen > width) { - next = true; - curLen -= 1; - break; - } - s = ch; - state = TEXT; - } else { - s += ch; - } - // ending after ; - } else { - if (ch == ';') { - endcolor = true; - } - s += ch; - } - - break; - } - absCurLen++; - } - - if (s != "") { - o << s; + size_t printedSymbols = PrintColumnLine(columnIndex); + Output_ << TString(Widths_[columnIndex] - printedSymbols, ' '); + } + + Output_ << colors.Default(); + Output_ << " │" << Endl; + } + +private: + /* return's printed symbols cnt */ + size_t PrintColumnLine(size_t columnIndex) { + if (Columns_[columnIndex].empty()) { + return 0; + } + + size_t printedSymbols = 0; + const auto& column = Columns_[columnIndex][0]; + + size_t i = PrintedIndexByColumnIndex_[columnIndex]; + + for (; i < column.size() && printedSymbols < Widths_[columnIndex];) { + if (column[i] == COLOR_BEGIN) { + while (i < column.size() && column[i] != COLOR_END) { + Output_ << column[i++]; } + continue; } - o << TString(width - curLen, ' '); - if (columnIndex == 0) { - firstColLen = absCurLen - 1; + + + Output_ << column[i++]; + while (i < column.size() && IsUTF8ContinuationByte(column[i])) { + Output_ << column[i++]; } + ++printedSymbols; } + + PrintedIndexByColumnIndex_[columnIndex] = i; + return printedSymbols; } - - o << colors.Default(); - o << " │" << Endl; - if (next) { - return firstColLen; - } else { - return 0; + +private: + IOutputStream& Output_; + const TVector<TVector<TString>>& Columns_; + const TVector<size_t>& Widths_; + TVector<size_t> PrintedIndexByColumnIndex_; +}; + +void TPrettyTable::TRow::PrintColumns(IOutputStream& o, const TVector<size_t>& widths) const { + TColumnLinesPrinter printer(o, Columns, widths); + + while (printer.HasNext()) { + printer.Print(); } } @@ -324,14 +196,7 @@ void TPrettyTable::Print(IOutputStream& o) const { PrintDelim(o, widths, "┌", "┬", "┐"); for (auto i : xrange(Rows.size())) { const auto& row = Rows.at(i); - - size_t res = 1; - size_t offset = 0; - TString oldColor = ""; - while (res != 0) { - res = row.PrintColumns(o, widths, offset, oldColor); - offset += res; - } + row.PrintColumns(o, widths); if (row.HasFreeText()) { PrintDelim(o, widths, "├", "┴", "┤", true); diff --git a/ydb/public/lib/ydb_cli/common/pretty_table.h b/ydb/public/lib/ydb_cli/common/pretty_table.h index 36de1df6da4..290aeb2d855 100644 --- a/ydb/public/lib/ydb_cli/common/pretty_table.h +++ b/ydb/public/lib/ydb_cli/common/pretty_table.h @@ -76,7 +76,7 @@ public: private: size_t ColumnWidth(size_t columnIndex) const; size_t ExtraBytes(TStringBuf data) const; - size_t PrintColumns(IOutputStream& o, const TVector<size_t>& widths, size_t offset, TString& oldColor) const; + void PrintColumns(IOutputStream& o, const TVector<size_t>& widths) const; bool HasFreeText() const; void PrintFreeText(IOutputStream& o, size_t width) const; diff --git a/ydb/tests/functional/ydb_cli/canondata/result.json b/ydb/tests/functional/ydb_cli/canondata/result.json index 9653de68f85..5dacdfc4b34 100644 --- a/ydb/tests/functional/ydb_cli/canondata/result.json +++ b/ydb/tests/functional/ydb_cli/canondata/result.json @@ -227,6 +227,9 @@ "test_ydb_impex.TestImpex.test_stdin[tsv-additional_args3-row]": { "uri": "file://test_ydb_impex.TestImpex.test_stdin_tsv-additional_args3-row_/result.output" }, + "test_ydb_scripting.TestExecuteScriptFromStdinWithWideOutput.test_wide_table": { + "uri": "file://test_ydb_scripting.TestExecuteScriptFromStdinWithWideOutput.test_wide_table/result.output" + }, "test_ydb_scripting.TestExecuteScriptWithFormats.test_stream_yql_script_json_base64": { "uri": "file://test_ydb_scripting.TestExecuteScriptWithFormats.test_stream_yql_script_json_base64/result.output" }, diff --git a/ydb/tests/functional/ydb_cli/canondata/test_ydb_scripting.TestExecuteScriptFromStdinWithWideOutput.test_wide_table/result.output b/ydb/tests/functional/ydb_cli/canondata/test_ydb_scripting.TestExecuteScriptFromStdinWithWideOutput.test_wide_table/result.output new file mode 100644 index 00000000000..36bbdf13246 --- /dev/null +++ b/ydb/tests/functional/ydb_cli/canondata/test_ydb_scripting.TestExecuteScriptFromStdinWithWideOutput.test_wide_table/result.output @@ -0,0 +1,4 @@ +┌─────┬──────────────┬─────────┬──────┬───────────┬───────────────┬─────────────┬─────────┬───────────┬─────┬─────────────────────┬────────────────────┬────────────┬──────────┬─────┬─────────────┬─────────────┬───────────┬─────────┬─────────┬──────────┐ +│ box │ container_id │ context │ host │ log_level │ log_level_int │ logger_name │ message │ node_fqdn │ pod │ pod_persistent_fqdn │ pod_transient_fqdn │ request_id │ saved_at │ seq │ stack_trace │ thread_name │ timestamp │ user_id │ version │ workload │ +├─────┼──────────────┼─────────┼──────┼───────────┼───────────────┼─────────────┼─────────┼───────────┼─────┼─────────────────────┼────────────────────┼────────────┼──────────┼─────┼─────────────┼─────────────┼───────────┼─────────┼─────────┼──────────┤ +└─────┴──────────────┴─────────┴──────┴───────────┴───────────────┴─────────────┴─────────┴───────────┴─────┴─────────────────────┴────────────────────┴────────────┴──────────┴─────┴─────────────┴─────────────┴───────────┴─────────┴─────────┴──────────┘ diff --git a/ydb/tests/functional/ydb_cli/test_ydb_scripting.py b/ydb/tests/functional/ydb_cli/test_ydb_scripting.py index d9c1efcee55..2750034e2b1 100644 --- a/ydb/tests/functional/ydb_cli/test_ydb_scripting.py +++ b/ydb/tests/functional/ydb_cli/test_ydb_scripting.py @@ -49,8 +49,8 @@ def create_table_with_data(session, path): class BaseTestScriptingService(object): @classmethod - def execute_ydb_cli_command(cls, args, stdin=None): - execution = yatest_common.execute([ydb_bin()] + args, stdin=stdin) + def execute_ydb_cli_command(cls, args, stdin=None, env=None): + execution = yatest_common.execute([ydb_bin()] + args, stdin=stdin, env=env) result = execution.std_out logger.debug("std_out:\n" + result.decode('utf-8')) return result @@ -81,13 +81,13 @@ class BaseTestScriptingServiceWithDatabase(BaseTestScriptingService): cls.cluster.stop() @classmethod - def execute_ydb_cli_command_with_db(cls, args, stdin=None): + def execute_ydb_cli_command_with_db(cls, args, stdin=None, env=None): return cls.execute_ydb_cli_command( [ "--endpoint", "grpc://localhost:%d" % cls.cluster.nodes[1].grpc_port, "--database", cls.root_dir ] + - args, stdin + args, stdin, env=env ) @@ -849,3 +849,50 @@ class TestExecuteScriptWithParamsFromStdin(BaseTestScriptingServiceWithDatabase) def test_skip_rows_tsv(self, command): return self.skip_rows(self.get_command(command), "tsv") + + +def create_wide_table_with_data(session, path): + session.create_table( + path, + ydb.TableDescription() + .with_column(ydb.Column("timestamp", ydb.PrimitiveType.Timestamp)) + .with_column(ydb.Column("pod", ydb.PrimitiveType.Utf8)) + .with_column(ydb.Column("seq", ydb.PrimitiveType.Uint64)) + .with_column(ydb.Column("container_id", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("host", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("box", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("workload", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("logger_name", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("user_id", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("request_id", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("message", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("log_level", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("log_level_int", ydb.OptionalType(ydb.PrimitiveType.Int64))) + .with_column(ydb.Column("stack_trace", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("thread_name", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("pod_transient_fqdn", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("pod_persistent_fqdn", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("node_fqdn", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("context", ydb.OptionalType(ydb.PrimitiveType.JsonDocument))) + .with_column(ydb.Column("version", ydb.OptionalType(ydb.PrimitiveType.Int32))) + .with_column(ydb.Column("saved_at", ydb.OptionalType(ydb.PrimitiveType.Timestamp))) + .with_primary_keys("timestamp", "pod", "seq") + ) + +class TestExecuteScriptFromStdinWithWideOutput(BaseTestScriptingServiceWithDatabase): + @classmethod + def setup_class(cls): + BaseTestScriptingServiceWithDatabase.setup_class() + cls.session = cls.driver.table_client.session().create() + + @pytest.fixture(autouse=True, scope='function') + def init_test(self, tmp_path): + self.tmp_path = tmp_path + self.table_path = self.root_dir + "/" + self.tmp_path.name + create_wide_table_with_data(self.session, self.table_path) + + def test_wide_table(self): + script = "SELECT * FROM `{}`;".format(self.table_path) + output = self.execute_ydb_cli_command_with_db(["yql", "-s", script]) + return self.canonical_result(output, self.tmp_path) +
\ No newline at end of file |
