From c813bbbddc5caffc486d014e333156dbc34a767a Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sat, 1 Oct 2022 23:00:23 +0200 Subject: [PATCH 01/96] Add a test --- .../0_stateless/02456_progress_tty.expect | 45 +++++++++++++++++++ .../0_stateless/02456_progress_tty.reference | 0 2 files changed, 45 insertions(+) create mode 100755 tests/queries/0_stateless/02456_progress_tty.expect create mode 100644 tests/queries/0_stateless/02456_progress_tty.reference diff --git a/tests/queries/0_stateless/02456_progress_tty.expect b/tests/queries/0_stateless/02456_progress_tty.expect new file mode 100755 index 00000000000..32ed8fd9094 --- /dev/null +++ b/tests/queries/0_stateless/02456_progress_tty.expect @@ -0,0 +1,45 @@ +#!/usr/bin/expect -f + +set basedir [file dirname $argv0] +set basename [file tail $argv0] +#exp_internal -f $env(CLICKHOUSE_TMP)/$basename.debuglog 0 + +log_user 0 +set timeout 60 +match_max 100000 +set stty_init "rows 25 cols 80" + +expect_after { + # Do not ignore eof from expect + eof { exp_continue } + # A default timeout action is to do nothing, change it to fail + timeout { exit 1 } +} + +# Progress is displayed by default +spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null" +expect "Progress: " +expect "█" + +# It is true even if we redirect both stdout and stderr to /dev/null +spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1" +expect "Progress: " +expect "█" + +# The option --progress has implicit value of true +spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --progress --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1" +expect "Progress: " +expect "█" + +# But we can set it to false +spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --progress false --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' 2>/dev/null" +expect -exact "3\n" + +# As well as to 0 for the same effect +spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --progress 0 --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' 2>/dev/null" +expect -exact "3\n" + +# If we set it to 1, the progress will be displayed as well +spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --progress 1 --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1" +expect "Progress: " +expect "█" diff --git a/tests/queries/0_stateless/02456_progress_tty.reference b/tests/queries/0_stateless/02456_progress_tty.reference new file mode 100644 index 00000000000..e69de29bb2d From 39633120ab969ce2e710dd8d530e472325e0e5a1 Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sat, 1 Oct 2022 23:17:49 +0200 Subject: [PATCH 02/96] Add a test --- .../0_stateless/02456_progress_tty.expect | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/tests/queries/0_stateless/02456_progress_tty.expect b/tests/queries/0_stateless/02456_progress_tty.expect index 32ed8fd9094..6a96c88c05c 100755 --- a/tests/queries/0_stateless/02456_progress_tty.expect +++ b/tests/queries/0_stateless/02456_progress_tty.expect @@ -4,42 +4,52 @@ set basedir [file dirname $argv0] set basename [file tail $argv0] #exp_internal -f $env(CLICKHOUSE_TMP)/$basename.debuglog 0 -log_user 0 +log_user 1 set timeout 60 match_max 100000 -set stty_init "rows 25 cols 80" +set stty_init "rows 25 cols 120" expect_after { - # Do not ignore eof from expect eof { exp_continue } - # A default timeout action is to do nothing, change it to fail timeout { exit 1 } } +spawn bash +send "source $basedir/../shell_config.sh\r" + # Progress is displayed by default -spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null" +send "\$CLICKHOUSE_LOCAL --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null\r" expect "Progress: " expect "█" +send "\3" # It is true even if we redirect both stdout and stderr to /dev/null -spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1" +send "\$CLICKHOUSE_LOCAL --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1\r" expect "Progress: " expect "█" +send "\3" # The option --progress has implicit value of true -spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --progress --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1" +send "\$CLICKHOUSE_LOCAL --progress --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1\r" expect "Progress: " expect "█" +send "\3" # But we can set it to false -spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --progress false --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' 2>/dev/null" -expect -exact "3\n" +send "\$CLICKHOUSE_LOCAL --progress false --query 'SELECT sleep(1), \$\$Hello\$\$ FROM numbers(3) SETTINGS max_block_size = 1' 2>/dev/null\r" +expect -exact "0\tHello\r\n" +send "\3" # As well as to 0 for the same effect -spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --progress 0 --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' 2>/dev/null" -expect -exact "3\n" +send "\$CLICKHOUSE_LOCAL --progress 0 --query 'SELECT sleep(1), \$\$Hello\$\$ FROM numbers(3) SETTINGS max_block_size = 1' 2>/dev/null\r" +expect -exact "0\tHello\r\n" +send "\3" # If we set it to 1, the progress will be displayed as well -spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_LOCAL --progress 1 --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1" +send "\$CLICKHOUSE_LOCAL --progress 1 --query 'SELECT sum(sleep(1) = 0) FROM numbers(3) SETTINGS max_block_size = 1' >/dev/null 2>&1\r" expect "Progress: " expect "█" +send "\3" + +send "exit\r" +expect eof From 9a54af7df936a6f50c1458f878a228c050625ded Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sat, 1 Oct 2022 23:18:54 +0200 Subject: [PATCH 03/96] Add a test --- tests/queries/0_stateless/02456_progress_tty.expect | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/queries/0_stateless/02456_progress_tty.expect b/tests/queries/0_stateless/02456_progress_tty.expect index 6a96c88c05c..90be3b1076e 100755 --- a/tests/queries/0_stateless/02456_progress_tty.expect +++ b/tests/queries/0_stateless/02456_progress_tty.expect @@ -4,7 +4,7 @@ set basedir [file dirname $argv0] set basename [file tail $argv0] #exp_internal -f $env(CLICKHOUSE_TMP)/$basename.debuglog 0 -log_user 1 +log_user 0 set timeout 60 match_max 100000 set stty_init "rows 25 cols 120" From b65f674dd488cd4da91e0a50b20744c0b74cbbdf Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sat, 1 Oct 2022 23:19:02 +0200 Subject: [PATCH 04/96] Add a test --- tests/queries/0_stateless/02456_progress_tty.expect | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/queries/0_stateless/02456_progress_tty.expect b/tests/queries/0_stateless/02456_progress_tty.expect index 90be3b1076e..9daa6caa3fa 100755 --- a/tests/queries/0_stateless/02456_progress_tty.expect +++ b/tests/queries/0_stateless/02456_progress_tty.expect @@ -2,7 +2,7 @@ set basedir [file dirname $argv0] set basename [file tail $argv0] -#exp_internal -f $env(CLICKHOUSE_TMP)/$basename.debuglog 0 +exp_internal -f $env(CLICKHOUSE_TMP)/$basename.debuglog 0 log_user 0 set timeout 60 From 5d710e21f108b736028afd3341d9d8029bb7c024 Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sat, 1 Oct 2022 23:19:36 +0200 Subject: [PATCH 05/96] Render progress directly in tty --- programs/client/Client.cpp | 2 +- programs/local/LocalServer.cpp | 2 +- src/Client/ClientBase.cpp | 66 ++++++++++++++++++++++--------- src/Client/ClientBase.h | 6 ++- src/Common/ProgressIndication.cpp | 32 +++++++-------- src/Common/ProgressIndication.h | 14 ++++--- 6 files changed, 76 insertions(+), 46 deletions(-) diff --git a/programs/client/Client.cpp b/programs/client/Client.cpp index 303c8c2ce4f..cc3bbbb63ca 100644 --- a/programs/client/Client.cpp +++ b/programs/client/Client.cpp @@ -1025,7 +1025,7 @@ void Client::processConfig() } else { - need_render_progress = config().getBool("progress", false); + need_render_progress = config().getBool("progress", true); echo_queries = config().getBool("echo", false); ignore_error = config().getBool("ignore-error", false); diff --git a/programs/local/LocalServer.cpp b/programs/local/LocalServer.cpp index 2b9d819f5eb..26d5acafbec 100644 --- a/programs/local/LocalServer.cpp +++ b/programs/local/LocalServer.cpp @@ -488,7 +488,7 @@ void LocalServer::processConfig() } else { - need_render_progress = config().getBool("progress", false); + need_render_progress = config().getBool("progress", true); echo_queries = config().hasOption("echo") || config().hasOption("verbose"); ignore_error = config().getBool("ignore-error", false); is_multiquery = true; diff --git a/src/Client/ClientBase.cpp b/src/Client/ClientBase.cpp index f407fab68f1..566ee49fba3 100644 --- a/src/Client/ClientBase.cpp +++ b/src/Client/ClientBase.cpp @@ -415,7 +415,7 @@ void ClientBase::onData(Block & block, ASTPtr parsed_query) /// If results are written INTO OUTFILE, we can avoid clearing progress to avoid flicker. if (need_render_progress && (stdout_is_a_tty || is_interactive) && (!select_into_file || select_into_file_and_stdout)) - progress_indication.clearProgressOutput(); + progress_indication.clearProgressOutput(*tty_buf); try { @@ -436,7 +436,7 @@ void ClientBase::onData(Block & block, ASTPtr parsed_query) { if (select_into_file && !select_into_file_and_stdout) std::cerr << "\r"; - progress_indication.writeProgress(); + progress_indication.writeProgress(*tty_buf); } } @@ -444,7 +444,8 @@ void ClientBase::onData(Block & block, ASTPtr parsed_query) void ClientBase::onLogData(Block & block) { initLogsOutputStream(); - progress_indication.clearProgressOutput(); + if (need_render_progress) + progress_indication.clearProgressOutput(*tty_buf); logs_out_stream->writeLogs(block); logs_out_stream->flush(); } @@ -640,6 +641,33 @@ void ClientBase::initLogsOutputStream() } } +void ClientBase::initTtyBuffer() +{ + if (!tty_buf) + { + static constexpr auto tty_file_name = "/dev/tty"; + + /// Output all progress bar commands to stderr at once to avoid flicker. + /// This size is usually greater than the window size. + static constexpr size_t buf_size = 1024; + + std::error_code ec; + std::filesystem::file_status tty = std::filesystem::status(tty_file_name, ec); + + if (!ec && exists(tty) && is_character_file(tty) + && (tty.permissions() & std::filesystem::perms::others_write) != std::filesystem::perms::none) + { + tty_buf = std::make_unique(tty_file_name, buf_size); + } + else if (stderr_is_a_tty) + { + tty_buf = std::make_unique(STDERR_FILENO, buf_size); + } + else + need_render_progress = false; + } +} + void ClientBase::updateSuggest(const ASTPtr & ast) { std::vector new_words; @@ -939,13 +967,14 @@ void ClientBase::onProgress(const Progress & value) output_format->onProgress(value); if (need_render_progress) - progress_indication.writeProgress(); + progress_indication.writeProgress(*tty_buf); } void ClientBase::onEndOfStream() { - progress_indication.clearProgressOutput(); + if (need_render_progress) + progress_indication.clearProgressOutput(*tty_buf); if (output_format) output_format->finalize(); @@ -953,10 +982,7 @@ void ClientBase::onEndOfStream() resetOutput(); if (is_interactive && !written_first_block) - { - progress_indication.clearProgressOutput(); std::cout << "Ok." << std::endl; - } } @@ -1000,14 +1026,15 @@ void ClientBase::onProfileEvents(Block & block) progress_indication.updateThreadEventData(thread_times); if (need_render_progress) - progress_indication.writeProgress(); + progress_indication.writeProgress(*tty_buf); if (profile_events.print) { if (profile_events.watch.elapsedMilliseconds() >= profile_events.delay_ms) { initLogsOutputStream(); - progress_indication.clearProgressOutput(); + if (need_render_progress) + progress_indication.clearProgressOutput(*tty_buf); logs_out_stream->writeProfileEvents(block); logs_out_stream->flush(); @@ -1181,7 +1208,7 @@ void ClientBase::sendData(Block & sample, const ColumnsDescription & columns_des progress_indication.updateProgress(Progress(file_progress)); /// Set callback to be called on file progress. - progress_indication.setFileProgressCallback(global_context, true); + progress_indication.setFileProgressCallback(global_context, *tty_buf); } /// If data fetched from file (maybe compressed file) @@ -1433,12 +1460,10 @@ bool ClientBase::receiveEndOfQuery() void ClientBase::cancelQuery() { connection->sendCancel(); - if (is_interactive) - { - progress_indication.clearProgressOutput(); - std::cout << "Cancelling query." << std::endl; + if (need_render_progress) + progress_indication.clearProgressOutput(*tty_buf); - } + std::cout << "Cancelling query." << std::endl; cancelled = true; } @@ -1556,7 +1581,8 @@ void ClientBase::processParsedSingleQuery(const String & full_query, const Strin if (profile_events.last_block) { initLogsOutputStream(); - progress_indication.clearProgressOutput(); + if (need_render_progress) + progress_indication.clearProgressOutput(*tty_buf); logs_out_stream->writeProfileEvents(profile_events.last_block); logs_out_stream->flush(); @@ -2216,6 +2242,8 @@ void ClientBase::init(int argc, char ** argv) stdout_is_a_tty = isatty(STDOUT_FILENO); stderr_is_a_tty = isatty(STDERR_FILENO); terminal_width = getTerminalWidth(); + if (need_render_progress) + initTtyBuffer(); Arguments common_arguments{""}; /// 0th argument is ignored. std::vector external_tables_arguments; @@ -2243,7 +2271,7 @@ void ClientBase::init(int argc, char ** argv) ("stage", po::value()->default_value("complete"), "Request query processing up to specified stage: complete,fetch_columns,with_mergeable_state,with_mergeable_state_after_aggregation,with_mergeable_state_after_aggregation_and_limit") ("query_kind", po::value()->default_value("initial_query"), "One of initial_query/secondary_query/no_query") ("query_id", po::value(), "query_id") - ("progress", "print progress of queries execution") + ("progress", po::value()->implicit_value(true), "print progress of queries execution") ("disable_suggestion,A", "Disable loading suggestion data. Note that suggestion data is loaded asynchronously through a second connection to ClickHouse server. Also it is reasonable to disable suggestion if you want to paste a query with TAB characters. Shorthand option -A is for those who get used to mysql client.") ("time,t", "print query execution time to stderr in non-interactive mode (for benchmarks)") @@ -2348,7 +2376,7 @@ void ClientBase::init(int argc, char ** argv) if (options.count("profile-events-delay-ms")) config().setInt("profile-events-delay-ms", options["profile-events-delay-ms"].as()); if (options.count("progress")) - config().setBool("progress", true); + config().setBool("progress", options["progress"].as()); if (options.count("echo")) config().setBool("echo", true); if (options.count("disable_suggestion")) diff --git a/src/Client/ClientBase.h b/src/Client/ClientBase.h index 3a6e623dc3f..86471405d00 100644 --- a/src/Client/ClientBase.h +++ b/src/Client/ClientBase.h @@ -143,11 +143,11 @@ private: void initOutputFormat(const Block & block, ASTPtr parsed_query); void initLogsOutputStream(); + void initTtyBuffer(); String prompt() const; void resetOutput(); - void outputQueryInfo(bool echo_query_); void parseAndCheckOptions(OptionsDescription & options_description, po::variables_map & options, Arguments & arguments); void updateSuggest(const ASTPtr & ast); @@ -219,6 +219,10 @@ protected: String server_logs_file; std::unique_ptr logs_out_stream; + /// /dev/tty if accessible or std::cerr - for progress bar. + /// We prefer to output progress bar directly to tty to allow user to redirect stdout and stderr and still get the progress indication. + std::unique_ptr tty_buf; + String home_path; String history_file; /// Path to a file containing command history. diff --git a/src/Common/ProgressIndication.cpp b/src/Common/ProgressIndication.cpp index ab4ecf5c25f..7828dc18413 100644 --- a/src/Common/ProgressIndication.cpp +++ b/src/Common/ProgressIndication.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -44,15 +45,6 @@ bool ProgressIndication::updateProgress(const Progress & value) return progress.incrementPiecewiseAtomically(value); } -void ProgressIndication::clearProgressOutput() -{ - if (written_progress_chars) - { - written_progress_chars = 0; - std::cerr << "\r" CLEAR_TO_END_OF_LINE; - } -} - void ProgressIndication::resetProgress() { watch.restart(); @@ -67,15 +59,12 @@ void ProgressIndication::resetProgress() } } -void ProgressIndication::setFileProgressCallback(ContextMutablePtr context, bool write_progress_on_update_) +void ProgressIndication::setFileProgressCallback(ContextMutablePtr context, WriteBuffer & message) { - write_progress_on_update = write_progress_on_update_; context->setFileProgressCallback([&](const FileProgress & file_progress) { progress.incrementPiecewiseAtomically(Progress(file_progress)); - - if (write_progress_on_update) - writeProgress(); + writeProgress(message); }); } @@ -153,13 +142,10 @@ void ProgressIndication::writeFinalProgress() std::cout << ". "; } -void ProgressIndication::writeProgress() +void ProgressIndication::writeProgress(WriteBuffer & message) { std::lock_guard lock(progress_mutex); - /// Output all progress bar commands to stderr at once to avoid flicker. - WriteBufferFromFileDescriptor message(STDERR_FILENO, 1024); - static size_t increment = 0; static const char * indicators[8] = { "\033[1;30m→\033[0m", @@ -318,4 +304,14 @@ void ProgressIndication::writeProgress() message.next(); } +void ProgressIndication::clearProgressOutput(WriteBuffer & message) +{ + if (written_progress_chars) + { + written_progress_chars = 0; + message << "\r" CLEAR_TO_END_OF_LINE; + message.next(); + } +} + } diff --git a/src/Common/ProgressIndication.h b/src/Common/ProgressIndication.h index 4f05f41b9d0..1262a4dc968 100644 --- a/src/Common/ProgressIndication.h +++ b/src/Common/ProgressIndication.h @@ -12,9 +12,12 @@ /// http://en.wikipedia.org/wiki/ANSI_escape_code #define CLEAR_TO_END_OF_LINE "\033[K" + namespace DB { +class WriteBuffer; + struct ThreadEventData { UInt64 time() const noexcept { return user_ms + system_ms; } @@ -30,14 +33,13 @@ using HostToThreadTimesMap = std::unordered_map; class ProgressIndication { public: - /// Write progress to stderr. - void writeProgress(); + /// Write progress bar. + void writeProgress(WriteBuffer & message); + void clearProgressOutput(WriteBuffer & message); + /// Write summary. void writeFinalProgress(); - /// Clear stderr output. - void clearProgressOutput(); - /// Reset progress values. void resetProgress(); @@ -52,7 +54,7 @@ public: /// In some cases there is a need to update progress value, when there is no access to progress_inidcation object. /// In this case it is added via context. /// `write_progress_on_update` is needed to write progress for loading files data via pipe in non-interactive mode. - void setFileProgressCallback(ContextMutablePtr context, bool write_progress_on_update = false); + void setFileProgressCallback(ContextMutablePtr context, WriteBuffer & message); /// How much seconds passed since query execution start. double elapsedSeconds() const { return getElapsedNanoseconds() / 1e9; } From 8b84788d089cbbf46e0a14f69db1f31b9e79e21d Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sun, 2 Oct 2022 00:38:41 +0200 Subject: [PATCH 06/96] Fix error --- src/Client/ClientBase.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Client/ClientBase.cpp b/src/Client/ClientBase.cpp index 566ee49fba3..936456c4c34 100644 --- a/src/Client/ClientBase.cpp +++ b/src/Client/ClientBase.cpp @@ -104,6 +104,7 @@ namespace ErrorCodes extern const int CANNOT_SET_SIGNAL_HANDLER; extern const int UNRECOGNIZED_ARGUMENTS; extern const int LOGICAL_ERROR; + extern const int CANNOT_OPEN_FILE; } } @@ -657,9 +658,22 @@ void ClientBase::initTtyBuffer() if (!ec && exists(tty) && is_character_file(tty) && (tty.permissions() & std::filesystem::perms::others_write) != std::filesystem::perms::none) { - tty_buf = std::make_unique(tty_file_name, buf_size); + try + { + tty_buf = std::make_unique(tty_file_name, buf_size); + return; + } + catch (const Exception & e) + { + if (e.code() != ErrorCodes::CANNOT_OPEN_FILE) + throw; + + /// It is normal if file exists, indicated as writeable but still cannot be opened. + /// Fallback to other options. + } } - else if (stderr_is_a_tty) + + if (stderr_is_a_tty) { tty_buf = std::make_unique(STDERR_FILENO, buf_size); } From 5be163b6467ea482dbfd1f1cfff40b1bae646220 Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sun, 2 Oct 2022 01:24:19 +0200 Subject: [PATCH 07/96] Fix comment --- src/Client/ClientBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client/ClientBase.cpp b/src/Client/ClientBase.cpp index 936456c4c34..320afb5e0f8 100644 --- a/src/Client/ClientBase.cpp +++ b/src/Client/ClientBase.cpp @@ -648,7 +648,7 @@ void ClientBase::initTtyBuffer() { static constexpr auto tty_file_name = "/dev/tty"; - /// Output all progress bar commands to stderr at once to avoid flicker. + /// Output all progress bar commands to terminal at once to avoid flicker. /// This size is usually greater than the window size. static constexpr size_t buf_size = 1024; From bc473f4ec0d57ce779bfaf669d57e6376225be28 Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sun, 2 Oct 2022 16:08:33 +0200 Subject: [PATCH 08/96] Something --- src/Client/ClientBase.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Client/ClientBase.cpp b/src/Client/ClientBase.cpp index 320afb5e0f8..3385a865550 100644 --- a/src/Client/ClientBase.cpp +++ b/src/Client/ClientBase.cpp @@ -661,10 +661,19 @@ void ClientBase::initTtyBuffer() try { tty_buf = std::make_unique(tty_file_name, buf_size); + + /// It is possible that the terminal file has writeable permissions + /// but we cannot write anything there. Check it with invisible character. + tty_buf->write('\0'); + tty_buf->next(); + return; } catch (const Exception & e) { + if (tty_buf) + tty_buf.reset(); + if (e.code() != ErrorCodes::CANNOT_OPEN_FILE) throw; From eef40ce88407e3315c1de0a1348927be9918f621 Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Wed, 5 Oct 2022 02:29:52 +0200 Subject: [PATCH 09/96] Some attempt --- src/Client/ClientBase.cpp | 3 +-- src/Client/ClientBase.h | 4 +++- src/Common/ProgressIndication.cpp | 11 +++++++---- src/Common/ProgressIndication.h | 11 ++++------- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Client/ClientBase.cpp b/src/Client/ClientBase.cpp index 3385a865550..52c2294eb7c 100644 --- a/src/Client/ClientBase.cpp +++ b/src/Client/ClientBase.cpp @@ -1,13 +1,11 @@ #include #include -#include #include #include #include #include -#include #include #include #include @@ -66,6 +64,7 @@ #include #include #include +#include #include #include #include diff --git a/src/Client/ClientBase.h b/src/Client/ClientBase.h index 86471405d00..9cc318c2f2b 100644 --- a/src/Client/ClientBase.h +++ b/src/Client/ClientBase.h @@ -15,6 +15,7 @@ #include #include + namespace po = boost::program_options; @@ -38,6 +39,7 @@ enum MultiQueryProcessingStage void interruptSignalHandler(int signum); class InternalTextLogs; +class WriteBufferFromFileDescriptor; class ClientBase : public Poco::Util::Application, public IHints<2, ClientBase> { @@ -221,7 +223,7 @@ protected: /// /dev/tty if accessible or std::cerr - for progress bar. /// We prefer to output progress bar directly to tty to allow user to redirect stdout and stderr and still get the progress indication. - std::unique_ptr tty_buf; + std::unique_ptr tty_buf; String home_path; String history_file; /// Path to a file containing command history. diff --git a/src/Common/ProgressIndication.cpp b/src/Common/ProgressIndication.cpp index 7828dc18413..30604d7808a 100644 --- a/src/Common/ProgressIndication.cpp +++ b/src/Common/ProgressIndication.cpp @@ -12,6 +12,9 @@ #include "IO/WriteBufferFromString.h" #include +/// http://en.wikipedia.org/wiki/ANSI_escape_code +#define CLEAR_TO_END_OF_LINE "\033[K" + namespace { @@ -59,7 +62,7 @@ void ProgressIndication::resetProgress() } } -void ProgressIndication::setFileProgressCallback(ContextMutablePtr context, WriteBuffer & message) +void ProgressIndication::setFileProgressCallback(ContextMutablePtr context, WriteBufferFromFileDescriptor & message) { context->setFileProgressCallback([&](const FileProgress & file_progress) { @@ -142,7 +145,7 @@ void ProgressIndication::writeFinalProgress() std::cout << ". "; } -void ProgressIndication::writeProgress(WriteBuffer & message) +void ProgressIndication::writeProgress(WriteBufferFromFileDescriptor & message) { std::lock_guard lock(progress_mutex); @@ -160,7 +163,7 @@ void ProgressIndication::writeProgress(WriteBuffer & message) const char * indicator = indicators[increment % 8]; - size_t terminal_width = getTerminalWidth(); + size_t terminal_width = getTerminalWidth(message.getFD()); if (!written_progress_chars) { @@ -304,7 +307,7 @@ void ProgressIndication::writeProgress(WriteBuffer & message) message.next(); } -void ProgressIndication::clearProgressOutput(WriteBuffer & message) +void ProgressIndication::clearProgressOutput(WriteBufferFromFileDescriptor & message) { if (written_progress_chars) { diff --git a/src/Common/ProgressIndication.h b/src/Common/ProgressIndication.h index 1262a4dc968..33c77f4fc34 100644 --- a/src/Common/ProgressIndication.h +++ b/src/Common/ProgressIndication.h @@ -9,14 +9,11 @@ #include #include -/// http://en.wikipedia.org/wiki/ANSI_escape_code -#define CLEAR_TO_END_OF_LINE "\033[K" - namespace DB { -class WriteBuffer; +class WriteBufferFromFileDescriptor; struct ThreadEventData { @@ -34,8 +31,8 @@ class ProgressIndication { public: /// Write progress bar. - void writeProgress(WriteBuffer & message); - void clearProgressOutput(WriteBuffer & message); + void writeProgress(WriteBufferFromFileDescriptor & message); + void clearProgressOutput(WriteBufferFromFileDescriptor & message); /// Write summary. void writeFinalProgress(); @@ -54,7 +51,7 @@ public: /// In some cases there is a need to update progress value, when there is no access to progress_inidcation object. /// In this case it is added via context. /// `write_progress_on_update` is needed to write progress for loading files data via pipe in non-interactive mode. - void setFileProgressCallback(ContextMutablePtr context, WriteBuffer & message); + void setFileProgressCallback(ContextMutablePtr context, WriteBufferFromFileDescriptor & message); /// How much seconds passed since query execution start. double elapsedSeconds() const { return getElapsedNanoseconds() / 1e9; } From 9320ae1abf34ec596161b41f1852abf9deb240e4 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Mon, 17 Oct 2022 13:10:31 +0000 Subject: [PATCH 10/96] update sqlancer docker files --- docker/test/sqlancer/Dockerfile | 2 +- docker/test/sqlancer/run.sh | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docker/test/sqlancer/Dockerfile b/docker/test/sqlancer/Dockerfile index 0821d516e23..2ebc61e35a9 100644 --- a/docker/test/sqlancer/Dockerfile +++ b/docker/test/sqlancer/Dockerfile @@ -1,5 +1,5 @@ # docker build -t clickhouse/sqlancer-test . -FROM ubuntu:20.04 +FROM ubuntu:22.04 # ARG for quick switch to a given ubuntu mirror ARG apt_archive="http://archive.ubuntu.com" diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index a1891569d34..3f08568159b 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -1,13 +1,10 @@ #!/bin/bash +set -exu +trap "exit" INT TERM -set -e -x +/clickhouse server -P /clickhouse-server.pid -L /clickhouse-server.log -E /clickhouse-server.log.err --daemon -dpkg -i package_folder/clickhouse-common-static_*.deb -dpkg -i package_folder/clickhouse-common-static-dbg_*.deb -dpkg -i package_folder/clickhouse-server_*.deb -dpkg -i package_folder/clickhouse-client_*.deb - -service clickhouse-server start && sleep 5 +for i in `seq 1 30`; do if [[ `wget -q 'localhost:8123' -O-` == 'Ok.' ]]; then break ; else sleep 1; fi ; done cd /sqlancer/sqlancer-master @@ -21,7 +18,10 @@ export NUM_QUERIES=1000 ( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /test_output/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPDistinct.err ( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /test_output/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPAggregate.err -service clickhouse stop +pkill -F ./clickhouse-server.pid + +for i in `seq 1 30`; do if [[ `wget -q 'localhost:8123' -O-` == 'Ok.' ]]; then sleep 1 ; else break; fi ; done + ls /var/log/clickhouse-server/ tar czf /test_output/logs.tar.gz -C /var/log/clickhouse-server/ . From 6a82d54314721844d4289201dc9bfc60be4c6a86 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:46:50 +0000 Subject: [PATCH 11/96] add CI files --- .github/workflows/master.yml | 38 ++++++ .github/workflows/pull_request.yml | 39 +++++- docker/test/sqlancer/run.sh | 42 +++++-- tests/ci/ci_config.py | 3 + tests/ci/sqlancer_check.py | 183 +++++++++++++++++++++++++++++ 5 files changed, 296 insertions(+), 9 deletions(-) create mode 100644 tests/ci/sqlancer_check.py diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 3d22cb984dd..b373a3cc210 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -2992,6 +2992,43 @@ jobs: docker ps --quiet | xargs --no-run-if-empty docker kill ||: docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: sudo rm -fr "$TEMP_PATH" +############################################################################################## +###################################### SQLANCER FUZZERS ###################################### +############################################################################################## + SQLancerTestRelease: + needs: [BuilderDebRelease] + runs-on: [self-hosted, fuzzer-unit-tester] + steps: + - name: Set envs + run: | + cat >> "$GITHUB_ENV" << 'EOF' + TEMP_PATH=${{runner.temp}}/sqlancer_release + REPORTS_PATH=${{runner.temp}}/reports_dir + CHECK_NAME=SQLancer (release) + REPO_COPY=${{runner.temp}}/sqlancer_release/ClickHouse + EOF + - name: Download json reports + uses: actions/download-artifact@v2 + with: + path: ${{ env.REPORTS_PATH }} + - name: Clear repository + run: | + sudo rm -fr "$GITHUB_WORKSPACE" && mkdir "$GITHUB_WORKSPACE" + - name: Check out repository code + uses: actions/checkout@v2 + - name: SQLancer + run: | + sudo rm -fr "$TEMP_PATH" + mkdir -p "$TEMP_PATH" + cp -r "$GITHUB_WORKSPACE" "$TEMP_PATH" + cd "$REPO_COPY/tests/ci" + python3 sqlancer_check.py "$CHECK_NAME" + - name: Cleanup + if: always() + run: | + docker ps --quiet | xargs --no-run-if-empty docker kill ||: + docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: + sudo rm -fr "$TEMP_PATH" FinishCheck: needs: - DockerHubPush @@ -3051,6 +3088,7 @@ jobs: - UnitTestsUBsan - UnitTestsReleaseClang - SharedBuildSmokeTest + - SQLancerTestRelease runs-on: [self-hosted, style-checker] steps: - name: Clear repository diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2795dc62d6d..5b1011019f6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3500,7 +3500,43 @@ jobs: if: contains(github.event.pull_request.labels.*.name, 'jepsen-test') needs: [BuilderBinRelease] uses: ./.github/workflows/jepsen.yml - +############################################################################################## +###################################### SQLANCER FUZZERS ###################################### +############################################################################################## + SQLancerTestRelease: + needs: [BuilderDebRelease] + runs-on: [self-hosted, fuzzer-unit-tester] + steps: + - name: Set envs + run: | + cat >> "$GITHUB_ENV" << 'EOF' + TEMP_PATH=${{runner.temp}}/sqlancer_release + REPORTS_PATH=${{runner.temp}}/reports_dir + CHECK_NAME=SQLancer (release) + REPO_COPY=${{runner.temp}}/sqlancer_release/ClickHouse + EOF + - name: Download json reports + uses: actions/download-artifact@v2 + with: + path: ${{ env.REPORTS_PATH }} + - name: Clear repository + run: | + sudo rm -fr "$GITHUB_WORKSPACE" && mkdir "$GITHUB_WORKSPACE" + - name: Check out repository code + uses: actions/checkout@v2 + - name: SQLancer + run: | + sudo rm -fr "$TEMP_PATH" + mkdir -p "$TEMP_PATH" + cp -r "$GITHUB_WORKSPACE" "$TEMP_PATH" + cd "$REPO_COPY/tests/ci" + python3 sqlancer_check.py "$CHECK_NAME" + - name: Cleanup + if: always() + run: | + docker ps --quiet | xargs --no-run-if-empty docker kill ||: + docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: + sudo rm -fr "$TEMP_PATH" FinishCheck: needs: - StyleCheck @@ -3575,6 +3611,7 @@ jobs: - SharedBuildSmokeTest - CompatibilityCheck - IntegrationTestsFlakyCheck + - SQLancerTestRelease runs-on: [self-hosted, style-checker] steps: - name: Clear repository diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 3f08568159b..7e35b6073ae 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -2,6 +2,33 @@ set -exu trap "exit" INT TERM +function wget_with_retry +{ + for _ in 1 2 3 4; do + if wget -nv -nd -c "$1";then + return 0 + else + sleep 0.5 + fi + done + return 1 +} + +if [ -z ${BINARY_URL_TO_DOWNLOAD+x} ] +then + echo "No BINARY_URL_TO_DOWNLOAD provided." +else + wget_with_retry "$BINARY_URL_TO_DOWNLOAD" + chmod +x /clickhouse +fi + +if [[ -f "/clickhouse" ]]; then + echo "/clickhouse exists" +else + exit 1 +fi + + /clickhouse server -P /clickhouse-server.pid -L /clickhouse-server.log -E /clickhouse-server.log.err --daemon for i in `seq 1 30`; do if [[ `wget -q 'localhost:8123' -O-` == 'Ok.' ]]; then break ; else sleep 1; fi ; done @@ -18,16 +45,15 @@ export NUM_QUERIES=1000 ( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /test_output/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPDistinct.err ( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /test_output/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPAggregate.err -pkill -F ./clickhouse-server.pid +pkill -F /clickhouse-server.pid for i in `seq 1 30`; do if [[ `wget -q 'localhost:8123' -O-` == 'Ok.' ]]; then sleep 1 ; else break; fi ; done - ls /var/log/clickhouse-server/ -tar czf /test_output/logs.tar.gz -C /var/log/clickhouse-server/ . -tail -n 1000 /var/log/clickhouse-server/stderr.log > /test_output/stderr.log -tail -n 1000 /var/log/clickhouse-server/stdout.log > /test_output/stdout.log -tail -n 1000 /var/log/clickhouse-server/clickhouse-server.log > /test_output/clickhouse-server.log +tar czf /workplace/logs.tar.gz -C /var/log/clickhouse-server/ . +tail -n 1000 /var/log/clickhouse-server/stderr.log > /workplace/stderr.log +tail -n 1000 /var/log/clickhouse-server/stdout.log > /workplace/stdout.log +tail -n 1000 /var/log/clickhouse-server/clickhouse-server.log > /workplace/clickhouse-server.log -/process_sqlancer_result.py || echo -e "failure\tCannot parse results" > /test_output/check_status.tsv -ls /test_output +/process_sqlancer_result.py || echo -e "failure\tCannot parse results" > /workplace/check_status.tsv +ls /workplace diff --git a/tests/ci/ci_config.py b/tests/ci/ci_config.py index 5e69046915e..4b49a088a37 100644 --- a/tests/ci/ci_config.py +++ b/tests/ci/ci_config.py @@ -355,6 +355,9 @@ CI_CONFIG = { "required_build": "package_aarch64", "test_grep_exclude_filter": "", }, + "SQLancer (debug)": { + "required_build": "package_release", + }, }, } # type: dict diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py new file mode 100644 index 00000000000..5e6a8629a45 --- /dev/null +++ b/tests/ci/sqlancer_check.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +import logging +import subprocess +import os +import sys + +from github import Github + +from env_helper import ( + GITHUB_REPOSITORY, + GITHUB_RUN_URL, + REPORTS_PATH, + REPO_COPY, + TEMP_PATH, +) +from s3_helper import S3Helper +from get_robot_token import get_best_robot_token +from pr_info import PRInfo +from build_download_helper import get_build_name_for_check, read_build_urls +from docker_pull_helper import get_image_with_version +from commit_status_helper import post_commit_status +from clickhouse_helper import ClickHouseHelper, prepare_tests_results_for_clickhouse +from stopwatch import Stopwatch +from rerun_helper import RerunHelper + +IMAGE_NAME = "clickhouse/sqlancer-test" + + +def get_run_command(download_url, workspace_path, image): + return ( + f"docker run " + # For sysctl + "--privileged " + "--network=host " + f"--volume={workspace_path}:/workspace " + "--cap-add syslog --cap-add sys_admin --cap-add=SYS_PTRACE " + f'-e BINARY_URL_TO_DOWNLOAD="{download_url}" ' + f"{image}" + ) + + +def get_commit(gh, commit_sha): + repo = gh.get_repo(GITHUB_REPOSITORY) + commit = repo.get_commit(commit_sha) + return commit + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + + stopwatch = Stopwatch() + + temp_path = TEMP_PATH + repo_path = REPO_COPY + reports_path = REPORTS_PATH + + check_name = sys.argv[1] + + if not os.path.exists(temp_path): + os.makedirs(temp_path) + + pr_info = PRInfo() + + gh = Github(get_best_robot_token(), per_page=100) + + rerun_helper = RerunHelper(gh, pr_info, check_name) + if rerun_helper.is_already_finished_by_status(): + logging.info("Check is already finished according to github status, exiting") + sys.exit(0) + + docker_image = get_image_with_version(temp_path, IMAGE_NAME) + + build_name = get_build_name_for_check(check_name) + print(build_name) + urls = read_build_urls(build_name, reports_path) + if not urls: + raise Exception("No build URLs found") + + for url in urls: + if url.endswith("/clickhouse"): + build_url = url + break + else: + raise Exception("Cannot find binary clickhouse among build results") + + logging.info("Got build url %s", build_url) + + workspace_path = os.path.join(temp_path, "workspace") + if not os.path.exists(workspace_path): + os.makedirs(workspace_path) + + run_command = get_run_command(build_url, workspace_path, docker_image) + logging.info("Going to run %s", run_command) + + run_log_path = os.path.join(temp_path, "runlog.log") + with open(run_log_path, "w", encoding="utf-8") as log: + with subprocess.Popen( + run_command, shell=True, stderr=log, stdout=log + ) as process: + retcode = process.wait() + if retcode == 0: + logging.info("Run successfully") + else: + logging.info("Run failed") + + subprocess.check_call(f"sudo chown -R ubuntu:ubuntu {temp_path}", shell=True) + + check_name_lower = ( + check_name.lower().replace("(", "").replace(")", "").replace(" ", "") + ) + s3_prefix = f"{pr_info.number}/{pr_info.sha}/{check_name_lower}/" + paths = { + "runlog.log": run_log_path, + "TLPWhere": os.path.join(workspace_path, "TLPWhere.log"), + "TLPGroupBy": os.path.join(workspace_path, "TLPGroupBy.log"), + "TLPHaving": os.path.join(workspace_path, "TLPHaving.log"), + "TLPWhereGroupBy": os.path.join(workspace_path, "TLPWhereGroupBy.log"), + "TLPDistinct": os.path.join(workspace_path, "TLPDistinct.log"), + "TLPAggregate": os.path.join(workspace_path, "TLPAggregate.log"), + "stderr.log": os.path.join(workspace_path, "stderr.log"), + "stdout.log": os.path.join(workspace_path, "stdout.log"), + "clickhouse-server.log": os.path.join(workspace_path, "clickhouse-server.log"), + "check_status.tsv": os.path.join(workspace_path, "check_status.tsv"), + "report.html": os.path.join(workspace_path, "report.html"), + "logs.tar.gz": os.path.join(workspace_path, "logs.tar.gz"), + } + + s3_helper = S3Helper() + for f in paths: + try: + paths[f] = s3_helper.upload_test_report_to_s3(paths[f], s3_prefix + "/" + f) + except Exception as ex: + logging.info("Exception uploading file %s text %s", f, ex) + paths[f] = "" + + report_url = GITHUB_RUN_URL + if paths["stderr.log"]: + report_url = paths["stderr.log"] + if paths["stdout.log"]: + report_url = paths["stdout.log"] + if paths["clickhouse-server.log"]: + report_url = paths["clickhouse-server.log"] + if paths["report.html"]: + report_url = paths["report.html"] + + # # Try to get status message saved by the SQLancer + # try: + # with open( + # os.path.join(workspace_path, "status.txt"), "r", encoding="utf-8" + # ) as status_f: + # status = status_f.readline().rstrip("\n") + + # with open( + # os.path.join(workspace_path, "description.txt"), "r", encoding="utf-8" + # ) as desc_f: + # description = desc_f.readline().rstrip("\n")[:140] + # except: + # status = "failure" + # description = "Task failed: $?=" + str(retcode) + + # if "fail" in status: + # test_result = [(description, "FAIL")] + # else: + test_result = [(check_name, "OK")] + + ch_helper = ClickHouseHelper() + + prepared_events = prepare_tests_results_for_clickhouse( + pr_info, + test_result, + status, + stopwatch.duration_seconds, + stopwatch.start_time_str, + report_url, + check_name, + ) + + ch_helper.insert_events_into(db="default", table="checks", events=prepared_events) + + logging.info("Result: '%s', '%s', '%s'", status, description, report_url) + print(f"::notice ::Report url: {report_url}") + post_commit_status(gh, pr_info.sha, check_name, description, status, report_url) From aa432fe95a9e204dda836c5449223e196c784cb4 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:20:20 +0000 Subject: [PATCH 12/96] staging --- .github/workflows/pull_request.yml | 20 ++++++++++---------- docker/test/sqlancer/run.sh | 4 ++-- tests/ci/sqlancer_check.py | 2 ++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5b1011019f6..6335ccc9a35 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3490,16 +3490,6 @@ jobs: docker ps --quiet | xargs --no-run-if-empty docker kill ||: docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: sudo rm -fr "$TEMP_PATH" -############################################################################################# -###################################### JEPSEN TESTS ######################################### -############################################################################################# - Jepsen: - # This is special test NOT INCLUDED in FinishCheck - # When it's skipped, all dependent tasks will be skipped too. - # DO NOT add it there - if: contains(github.event.pull_request.labels.*.name, 'jepsen-test') - needs: [BuilderBinRelease] - uses: ./.github/workflows/jepsen.yml ############################################################################################## ###################################### SQLANCER FUZZERS ###################################### ############################################################################################## @@ -3537,6 +3527,16 @@ jobs: docker ps --quiet | xargs --no-run-if-empty docker kill ||: docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: sudo rm -fr "$TEMP_PATH" +############################################################################################# +###################################### JEPSEN TESTS ######################################### +############################################################################################# + Jepsen: + # This is special test NOT INCLUDED in FinishCheck + # When it's skipped, all dependent tasks will be skipped too. + # DO NOT add it there + if: contains(github.event.pull_request.labels.*.name, 'jepsen-test') + needs: [BuilderBinRelease] + uses: ./.github/workflows/jepsen.yml FinishCheck: needs: - StyleCheck diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 7e35b6073ae..3a8c43cdfd4 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -31,7 +31,7 @@ fi /clickhouse server -P /clickhouse-server.pid -L /clickhouse-server.log -E /clickhouse-server.log.err --daemon -for i in `seq 1 30`; do if [[ `wget -q 'localhost:8123' -O-` == 'Ok.' ]]; then break ; else sleep 1; fi ; done +for i in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then break ; else sleep 1; fi ; done cd /sqlancer/sqlancer-master @@ -47,7 +47,7 @@ export NUM_QUERIES=1000 pkill -F /clickhouse-server.pid -for i in `seq 1 30`; do if [[ `wget -q 'localhost:8123' -O-` == 'Ok.' ]]; then sleep 1 ; else break; fi ; done +for i in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then sleep 1 ; else break; fi ; done ls /var/log/clickhouse-server/ tar czf /workplace/logs.tar.gz -C /var/log/clickhouse-server/ . diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 5e6a8629a45..55660e4877b 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -162,6 +162,8 @@ if __name__ == "__main__": # if "fail" in status: # test_result = [(description, "FAIL")] # else: + status = "success" + description = "SQLancer runs in always green mode for now" test_result = [(check_name, "OK")] ch_helper = ClickHouseHelper() From 7a74792c1b48dc7a3c3843e79335c815df053dc9 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:30:36 +0000 Subject: [PATCH 13/96] shellcheck --- docker/test/sqlancer/process_sqlancer_result.py | 6 +++--- docker/test/sqlancer/run.sh | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docker/test/sqlancer/process_sqlancer_result.py b/docker/test/sqlancer/process_sqlancer_result.py index 37b8f465498..5721349b6e5 100755 --- a/docker/test/sqlancer/process_sqlancer_result.py +++ b/docker/test/sqlancer/process_sqlancer_result.py @@ -72,9 +72,9 @@ if __name__ == "__main__": parser = argparse.ArgumentParser( description="ClickHouse script for parsing results of sqlancer test" ) - parser.add_argument("--in-results-dir", default="/test_output/") - parser.add_argument("--out-results-file", default="/test_output/test_results.tsv") - parser.add_argument("--out-status-file", default="/test_output/check_status.tsv") + parser.add_argument("--in-results-dir", default="/workspace/") + parser.add_argument("--out-results-file", default="/workspace/test_results.tsv") + parser.add_argument("--out-status-file", default="/workspace/check_status.tsv") args = parser.parse_args() state, description, test_results, logs = process_result(args.in_results_dir) diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 3a8c43cdfd4..6a479db8fdd 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -31,19 +31,19 @@ fi /clickhouse server -P /clickhouse-server.pid -L /clickhouse-server.log -E /clickhouse-server.log.err --daemon -for i in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then break ; else sleep 1; fi ; done +for _ in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then break ; else sleep 1; fi ; done cd /sqlancer/sqlancer-master export TIMEOUT=300 export NUM_QUERIES=1000 -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere | tee /test_output/TLPWhere.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPWhere.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPGroupBy | tee /test_output/TLPGroupBy.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPGroupBy.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPHaving | tee /test_output/TLPHaving.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPHaving.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere --oracle TLPGroupBy | tee /test_output/TLPWhereGroupBy.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPWhereGroupBy.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /test_output/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPDistinct.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /test_output/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /test_output/TLPAggregate.err +( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere | tee /workspace/TLPWhere.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhere.err +( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPGroupBy | tee /workspace/TLPGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPGroupBy.err +( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPHaving | tee /workspace/TLPHaving.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPHaving.err +( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere --oracle TLPGroupBy | tee /workspace/TLPWhereGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhereGroupBy.err +( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /workspace/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPDistinct.err +( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /workspace/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPAggregate.err pkill -F /clickhouse-server.pid @@ -55,5 +55,6 @@ tail -n 1000 /var/log/clickhouse-server/stderr.log > /workplace/stderr.log tail -n 1000 /var/log/clickhouse-server/stdout.log > /workplace/stdout.log tail -n 1000 /var/log/clickhouse-server/clickhouse-server.log > /workplace/clickhouse-server.log +for f in $(ls /workspace/) /process_sqlancer_result.py || echo -e "failure\tCannot parse results" > /workplace/check_status.tsv ls /workplace From 6f26816d4fc7439fc7acadbc7eb7f836347e047b Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:31:19 +0000 Subject: [PATCH 14/96] fix --- docker/test/sqlancer/run.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 6a479db8fdd..ca0178c42b2 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -55,6 +55,5 @@ tail -n 1000 /var/log/clickhouse-server/stderr.log > /workplace/stderr.log tail -n 1000 /var/log/clickhouse-server/stdout.log > /workplace/stdout.log tail -n 1000 /var/log/clickhouse-server/clickhouse-server.log > /workplace/clickhouse-server.log -for f in $(ls /workspace/) /process_sqlancer_result.py || echo -e "failure\tCannot parse results" > /workplace/check_status.tsv ls /workplace From b8193a0eb3934d291e4f6abbd569073008db5bf4 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:42:18 +0000 Subject: [PATCH 15/96] one more time --- docker/test/sqlancer/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index ca0178c42b2..63b2aee1d09 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -47,7 +47,7 @@ export NUM_QUERIES=1000 pkill -F /clickhouse-server.pid -for i in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then sleep 1 ; else break; fi ; done +for _ in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then sleep 1 ; else break; fi ; done ls /var/log/clickhouse-server/ tar czf /workplace/logs.tar.gz -C /var/log/clickhouse-server/ . From 8b3dc2d551e2ce2824c4ab7f9ab841282ac0a5df Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Tue, 18 Oct 2022 07:52:11 +0000 Subject: [PATCH 16/96] typo --- tests/ci/ci_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci/ci_config.py b/tests/ci/ci_config.py index 4b49a088a37..7c5a5a56e42 100644 --- a/tests/ci/ci_config.py +++ b/tests/ci/ci_config.py @@ -355,7 +355,7 @@ CI_CONFIG = { "required_build": "package_aarch64", "test_grep_exclude_filter": "", }, - "SQLancer (debug)": { + "SQLancer (release)": { "required_build": "package_release", }, }, From 757959d087e1abb881246327c6e39acb8e1ad198 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Tue, 18 Oct 2022 15:14:42 +0000 Subject: [PATCH 17/96] fix bug --- tests/ci/ast_fuzzer_check.py | 2 +- tests/ci/sqlancer_check.py | 38 +++++++++++++++++------------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/tests/ci/ast_fuzzer_check.py b/tests/ci/ast_fuzzer_check.py index 8f94ef4a915..d6be5648d67 100644 --- a/tests/ci/ast_fuzzer_check.py +++ b/tests/ci/ast_fuzzer_check.py @@ -69,7 +69,7 @@ if __name__ == "__main__": logging.info("Check is already finished according to github status, exiting") sys.exit(0) - docker_image = get_image_with_version(temp_path, IMAGE_NAME) + docker_image = get_image_with_version(get_image_with_version, IMAGE_NAME) build_name = get_build_name_for_check(check_name) print(build_name) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 55660e4877b..66511aebe6d 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -69,7 +69,7 @@ if __name__ == "__main__": logging.info("Check is already finished according to github status, exiting") sys.exit(0) - docker_image = get_image_with_version(temp_path, IMAGE_NAME) + docker_image = get_image_with_version(reports_path, IMAGE_NAME) build_name = get_build_name_for_check(check_name) print(build_name) @@ -144,27 +144,25 @@ if __name__ == "__main__": if paths["report.html"]: report_url = paths["report.html"] - # # Try to get status message saved by the SQLancer - # try: - # with open( - # os.path.join(workspace_path, "status.txt"), "r", encoding="utf-8" - # ) as status_f: - # status = status_f.readline().rstrip("\n") + # Try to get status message saved by the SQLancer + try: + with open( + os.path.join(workspace_path, "status.txt"), "r", encoding="utf-8" + ) as status_f: + status = status_f.readline().rstrip("\n") - # with open( - # os.path.join(workspace_path, "description.txt"), "r", encoding="utf-8" - # ) as desc_f: - # description = desc_f.readline().rstrip("\n")[:140] - # except: - # status = "failure" - # description = "Task failed: $?=" + str(retcode) + with open( + os.path.join(workspace_path, "description.txt"), "r", encoding="utf-8" + ) as desc_f: + description = desc_f.readline().rstrip("\n")[:140] + except: + status = "failure" + description = "Task failed: $?=" + str(retcode) - # if "fail" in status: - # test_result = [(description, "FAIL")] - # else: - status = "success" - description = "SQLancer runs in always green mode for now" - test_result = [(check_name, "OK")] + if "fail" in status: + test_result = [(description, "FAIL")] + else: + status = "success" ch_helper = ClickHouseHelper() From 5121e9b09632f2a4a78c0be225936d68b471349e Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Wed, 19 Oct 2022 16:30:01 +0000 Subject: [PATCH 18/96] update docker scripts --- .../test/sqlancer/process_sqlancer_result.py | 5 ---- docker/test/sqlancer/run.sh | 30 ++++++++----------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/docker/test/sqlancer/process_sqlancer_result.py b/docker/test/sqlancer/process_sqlancer_result.py index 5721349b6e5..9e40f01c885 100755 --- a/docker/test/sqlancer/process_sqlancer_result.py +++ b/docker/test/sqlancer/process_sqlancer_result.py @@ -37,11 +37,6 @@ def process_result(result_folder): else: summary.append((test, "OK")) - logs_path = "{}/logs.tar.gz".format(result_folder) - if not os.path.exists(logs_path): - logging.info("No logs tar on path %s", logs_path) - else: - paths.append(logs_path) stdout_path = "{}/stdout.log".format(result_folder) if not os.path.exists(stdout_path): logging.info("No stdout log on path %s", stdout_path) diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 63b2aee1d09..8476afaefb9 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -28,8 +28,8 @@ else exit 1 fi - -/clickhouse server -P /clickhouse-server.pid -L /clickhouse-server.log -E /clickhouse-server.log.err --daemon +cd /workspace +/clickhouse server -P /workspace/clickhouse-server.pid -L /workspace/clickhouse-server.log -E /workspace/clickhouse-server.log.err --daemon for _ in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then break ; else sleep 1; fi ; done @@ -37,23 +37,19 @@ cd /sqlancer/sqlancer-master export TIMEOUT=300 export NUM_QUERIES=1000 +export NUM_THREADS=30 -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere | tee /workspace/TLPWhere.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhere.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPGroupBy | tee /workspace/TLPGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPGroupBy.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPHaving | tee /workspace/TLPHaving.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPHaving.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere --oracle TLPGroupBy | tee /workspace/TLPWhereGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhereGroupBy.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /workspace/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPDistinct.err -( java -jar target/sqlancer-*.jar --num-threads 10 --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /workspace/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPAggregate.err +( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere | tee /workspace/TLPWhere.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhere.err +( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPGroupBy | tee /workspace/TLPGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPGroupBy.err +( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPHaving | tee /workspace/TLPHaving.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPHaving.err +( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere --oracle TLPGroupBy | tee /workspace/TLPWhereGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhereGroupBy.err +( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /workspace/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPDistinct.err +( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /workspace/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPAggregate.err -pkill -F /clickhouse-server.pid +ls /workspace +pkill -F /workspace/clickhouse-server.pid for _ in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then sleep 1 ; else break; fi ; done -ls /var/log/clickhouse-server/ -tar czf /workplace/logs.tar.gz -C /var/log/clickhouse-server/ . -tail -n 1000 /var/log/clickhouse-server/stderr.log > /workplace/stderr.log -tail -n 1000 /var/log/clickhouse-server/stdout.log > /workplace/stdout.log -tail -n 1000 /var/log/clickhouse-server/clickhouse-server.log > /workplace/clickhouse-server.log - -/process_sqlancer_result.py || echo -e "failure\tCannot parse results" > /workplace/check_status.tsv -ls /workplace +/process_sqlancer_result.py || echo -e "failure\tCannot parse results" > /workspace/check_status.tsv +ls /workspace From 7d90d54af8b394312456abf909e9f9a7169c1756 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Wed, 19 Oct 2022 17:02:49 +0000 Subject: [PATCH 19/96] better reporting --- .../test/sqlancer/process_sqlancer_result.py | 25 ++++++----- tests/ci/sqlancer_check.py | 44 +++++++++---------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/docker/test/sqlancer/process_sqlancer_result.py b/docker/test/sqlancer/process_sqlancer_result.py index 9e40f01c885..dda52b782c8 100755 --- a/docker/test/sqlancer/process_sqlancer_result.py +++ b/docker/test/sqlancer/process_sqlancer_result.py @@ -19,6 +19,8 @@ def process_result(result_folder): "TLPAggregate", ] + failed_tests= [] + for test in tests: err_path = "{}/{}.err".format(result_folder, test) out_path = "{}/{}.out".format(result_folder, test) @@ -33,6 +35,7 @@ def process_result(result_folder): with open(err_path, "r") as f: if "AssertionError" in f.read(): summary.append((test, "FAIL")) + failed_tests.append(test) status = "failure" else: summary.append((test, "OK")) @@ -48,19 +51,21 @@ def process_result(result_folder): else: paths.append(stderr_path) - description = "SQLancer test run. See report" + description = "SQLancer run successfully" + if status == "failure": + description = f"Failed oracles: {failed_tests}" return status, description, summary, paths -def write_results(results_file, status_file, results, status): +def write_results(results_file, status_file, description_file, results, status, description): with open(results_file, "w") as f: out = csv.writer(f, delimiter="\t") out.writerows(results) with open(status_file, "w") as f: - out = csv.writer(f, delimiter="\t") - out.writerow(status) - + f.write(status + '\n') + with open(description_file, "w") as f: + f.write(description + '\n') if __name__ == "__main__": logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") @@ -68,12 +73,12 @@ if __name__ == "__main__": description="ClickHouse script for parsing results of sqlancer test" ) parser.add_argument("--in-results-dir", default="/workspace/") - parser.add_argument("--out-results-file", default="/workspace/test_results.tsv") - parser.add_argument("--out-status-file", default="/workspace/check_status.tsv") + parser.add_argument("--out-results-file", default="/workspace/summary.tsv") + parser.add_argument("--out-description-file", default="/workspace/description.txt") + parser.add_argument("--out-status-file", default="/workspace/status.txt") args = parser.parse_args() - state, description, test_results, logs = process_result(args.in_results_dir) + status, description, summary, logs = process_result(args.in_results_dir) logging.info("Result parsed") - status = (state, description) - write_results(args.out_results_file, args.out_status_file, test_results, status) + write_results(args.out_results_file, args.out_status_file, args.out_description_file, summary, status, description) logging.info("Result written") diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 66511aebe6d..cf71b9199f8 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -113,17 +113,23 @@ if __name__ == "__main__": paths = { "runlog.log": run_log_path, "TLPWhere": os.path.join(workspace_path, "TLPWhere.log"), - "TLPGroupBy": os.path.join(workspace_path, "TLPGroupBy.log"), - "TLPHaving": os.path.join(workspace_path, "TLPHaving.log"), - "TLPWhereGroupBy": os.path.join(workspace_path, "TLPWhereGroupBy.log"), - "TLPDistinct": os.path.join(workspace_path, "TLPDistinct.log"), - "TLPAggregate": os.path.join(workspace_path, "TLPAggregate.log"), + "TLPAggregate.err": os.path.join(workspace_path, "TLPAggregate.err"), + "TLPAggregate.out": os.path.join(workspace_path, "TLPAggregate.out"), + "TLPDistinct.err": os.path.join(workspace_path, "TLPDistinct.err"), + "TLPDistinct.out": os.path.join(workspace_path, "TLPDistinct.out"), + "TLPGroupBy.err": os.path.join(workspace_path, "TLPGroupBy.err"), + "TLPGroupBy.out": os.path.join(workspace_path, "TLPGroupBy.out"), + "TLPHaving.err": os.path.join(workspace_path, "TLPHaving.err"), + "TLPHaving.out": os.path.join(workspace_path, "TLPHaving.out"), + "TLPWhere.err": os.path.join(workspace_path, "TLPWhere.err"), + "TLPWhere.out": os.path.join(workspace_path, "TLPWhere.out"), + "TLPWhereGroupBy.err": os.path.join(workspace_path, "TLPWhereGroupBy.err"), + "TLPWhereGroupBy.out": os.path.join(workspace_path, "TLPWhereGroupBy.out"), + "clickhouse-server.log": os.path.join(workspace_path, "clickhouse-server"), + "clickhouse-server.log.err": os.path.join(workspace_path, "clickhouse-server"), "stderr.log": os.path.join(workspace_path, "stderr.log"), "stdout.log": os.path.join(workspace_path, "stdout.log"), - "clickhouse-server.log": os.path.join(workspace_path, "clickhouse-server.log"), - "check_status.tsv": os.path.join(workspace_path, "check_status.tsv"), - "report.html": os.path.join(workspace_path, "report.html"), - "logs.tar.gz": os.path.join(workspace_path, "logs.tar.gz"), + "test_results.tsv": os.path.join(workspace_path, "test_results.tsv"), } s3_helper = S3Helper() @@ -135,14 +141,6 @@ if __name__ == "__main__": paths[f] = "" report_url = GITHUB_RUN_URL - if paths["stderr.log"]: - report_url = paths["stderr.log"] - if paths["stdout.log"]: - report_url = paths["stdout.log"] - if paths["clickhouse-server.log"]: - report_url = paths["clickhouse-server.log"] - if paths["report.html"]: - report_url = paths["report.html"] # Try to get status message saved by the SQLancer try: @@ -151,6 +149,13 @@ if __name__ == "__main__": ) as status_f: status = status_f.readline().rstrip("\n") + with open( + os.path.join(workspace_path, "summary.tsv"), "r", encoding="utf-8" + ) as summary_f: + for line in summary_f: + l=line.split('\t') + test_result.append((l[0],l[1])) + with open( os.path.join(workspace_path, "description.txt"), "r", encoding="utf-8" ) as desc_f: @@ -159,11 +164,6 @@ if __name__ == "__main__": status = "failure" description = "Task failed: $?=" + str(retcode) - if "fail" in status: - test_result = [(description, "FAIL")] - else: - status = "success" - ch_helper = ClickHouseHelper() prepared_events = prepare_tests_results_for_clickhouse( From ab41c8e8ce456cf219351c79445757f4c8c32331 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Wed, 19 Oct 2022 17:16:41 +0000 Subject: [PATCH 20/96] black --- .../test/sqlancer/process_sqlancer_result.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docker/test/sqlancer/process_sqlancer_result.py b/docker/test/sqlancer/process_sqlancer_result.py index dda52b782c8..d0e41643467 100755 --- a/docker/test/sqlancer/process_sqlancer_result.py +++ b/docker/test/sqlancer/process_sqlancer_result.py @@ -19,7 +19,7 @@ def process_result(result_folder): "TLPAggregate", ] - failed_tests= [] + failed_tests = [] for test in tests: err_path = "{}/{}.err".format(result_folder, test) @@ -58,14 +58,17 @@ def process_result(result_folder): return status, description, summary, paths -def write_results(results_file, status_file, description_file, results, status, description): +def write_results( + results_file, status_file, description_file, results, status, description +): with open(results_file, "w") as f: out = csv.writer(f, delimiter="\t") out.writerows(results) with open(status_file, "w") as f: - f.write(status + '\n') + f.write(status + "\n") with open(description_file, "w") as f: - f.write(description + '\n') + f.write(description + "\n") + if __name__ == "__main__": logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") @@ -80,5 +83,12 @@ if __name__ == "__main__": status, description, summary, logs = process_result(args.in_results_dir) logging.info("Result parsed") - write_results(args.out_results_file, args.out_status_file, args.out_description_file, summary, status, description) + write_results( + args.out_results_file, + args.out_status_file, + args.out_description_file, + summary, + status, + description, + ) logging.info("Result written") From 8ef8def9abddd0e81514f475d23f36b28c26420c Mon Sep 17 00:00:00 2001 From: Ilya Yatsishin <2159081+qoega@users.noreply.github.com> Date: Wed, 19 Oct 2022 21:00:05 +0200 Subject: [PATCH 21/96] Update tests/ci/sqlancer_check.py --- tests/ci/sqlancer_check.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index cf71b9199f8..ec70951e033 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -153,8 +153,8 @@ if __name__ == "__main__": os.path.join(workspace_path, "summary.tsv"), "r", encoding="utf-8" ) as summary_f: for line in summary_f: - l=line.split('\t') - test_result.append((l[0],l[1])) + l = line.split('\t') + test_result.append((l[0], l[1])) with open( os.path.join(workspace_path, "description.txt"), "r", encoding="utf-8" From b768925256e52b3d0ab0e4254dcdb7d221ebd149 Mon Sep 17 00:00:00 2001 From: Ilya Yatsishin <2159081+qoega@users.noreply.github.com> Date: Thu, 20 Oct 2022 00:09:09 +0200 Subject: [PATCH 22/96] Update tests/ci/sqlancer_check.py --- tests/ci/sqlancer_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index ec70951e033..3cf4b42c873 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -153,7 +153,7 @@ if __name__ == "__main__": os.path.join(workspace_path, "summary.tsv"), "r", encoding="utf-8" ) as summary_f: for line in summary_f: - l = line.split('\t') + l = line.split("\t") test_result.append((l[0], l[1])) with open( From 105ca72955dbf3f082c34833f33c66291fddf957 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Thu, 20 Oct 2022 10:51:47 +0000 Subject: [PATCH 23/96] fix --- tests/ci/sqlancer_check.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 3cf4b42c873..5367c302626 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -152,6 +152,7 @@ if __name__ == "__main__": with open( os.path.join(workspace_path, "summary.tsv"), "r", encoding="utf-8" ) as summary_f: + test_result = [] for line in summary_f: l = line.split("\t") test_result.append((l[0], l[1])) From 548ff1d2aaf348cd7f097892a26d13391543eb39 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Thu, 20 Oct 2022 12:33:56 +0000 Subject: [PATCH 24/96] fix --- tests/ci/ast_fuzzer_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci/ast_fuzzer_check.py b/tests/ci/ast_fuzzer_check.py index d6be5648d67..04dbe78adc4 100644 --- a/tests/ci/ast_fuzzer_check.py +++ b/tests/ci/ast_fuzzer_check.py @@ -69,7 +69,7 @@ if __name__ == "__main__": logging.info("Check is already finished according to github status, exiting") sys.exit(0) - docker_image = get_image_with_version(get_image_with_version, IMAGE_NAME) + docker_image = get_image_with_version(reports_path, IMAGE_NAME) build_name = get_build_name_for_check(check_name) print(build_name) From 13f506f7b12a5d8246211440df9a20d8481368c9 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Fri, 21 Oct 2022 11:48:58 +0000 Subject: [PATCH 25/96] minor --- tests/ci/ci_config.py | 3 +++ tests/ci/sqlancer_check.py | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/ci/ci_config.py b/tests/ci/ci_config.py index 7c5a5a56e42..93322b69669 100644 --- a/tests/ci/ci_config.py +++ b/tests/ci/ci_config.py @@ -358,6 +358,9 @@ CI_CONFIG = { "SQLancer (release)": { "required_build": "package_release", }, + "SQLancer (debug)": { + "required_build": "package_debug", + }, }, } # type: dict diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 5367c302626..2f320fbbf71 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -129,7 +129,6 @@ if __name__ == "__main__": "clickhouse-server.log.err": os.path.join(workspace_path, "clickhouse-server"), "stderr.log": os.path.join(workspace_path, "stderr.log"), "stdout.log": os.path.join(workspace_path, "stdout.log"), - "test_results.tsv": os.path.join(workspace_path, "test_results.tsv"), } s3_helper = S3Helper() @@ -152,10 +151,10 @@ if __name__ == "__main__": with open( os.path.join(workspace_path, "summary.tsv"), "r", encoding="utf-8" ) as summary_f: - test_result = [] + test_results = [] for line in summary_f: l = line.split("\t") - test_result.append((l[0], l[1])) + test_results.append((l[0], l[1])) with open( os.path.join(workspace_path, "description.txt"), "r", encoding="utf-8" @@ -165,6 +164,20 @@ if __name__ == "__main__": status = "failure" description = "Task failed: $?=" + str(retcode) + report_url = upload_results( + s3_helper, + pr_info.number, + pr_info.sha, + test_results, + paths, + check_name, + False, + ) + + post_commit_status(gh, pr_info.sha, check_name, description, status, report_url) + + print(f"::notice:: {check_name} Report url: {report_url}") + ch_helper = ClickHouseHelper() prepared_events = prepare_tests_results_for_clickhouse( From 52ca29140e739f1a67caccb450ccda7c97f349b6 Mon Sep 17 00:00:00 2001 From: DanRoscigno Date: Fri, 21 Oct 2022 12:27:20 -0400 Subject: [PATCH 26/96] WIP: add Superset deploy instructions --- .../example-datasets/cell-towers.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/en/getting-started/example-datasets/cell-towers.md b/docs/en/getting-started/example-datasets/cell-towers.md index 3d993c3e224..989ae6f0a7e 100644 --- a/docs/en/getting-started/example-datasets/cell-towers.md +++ b/docs/en/getting-started/example-datasets/cell-towers.md @@ -10,6 +10,7 @@ import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import ActionsMenu from '@site/docs/en/_snippets/_service_actions_menu.md'; import SQLConsoleDetail from '@site/docs/en/_snippets/_launch_sql_console.md'; +import SupersetDocker from '@site/docs/en/_snippets/_add_superset_detail.md'; This dataset is from [OpenCellid](https://www.opencellid.org/) - The world's largest Open Database of Cell Towers. @@ -224,6 +225,29 @@ WHERE pointInPolygon((lon, lat), (SELECT * FROM moscow)) 1 rows in set. Elapsed: 0.067 sec. Processed 43.28 million rows, 692.42 MB (645.83 million rows/s., 10.33 GB/s.) ``` +## Review of the schema + +This dataset primarily provides the location (Longitude and Latitude) and radio types at mobile cellular towers worldwide. The column descriptions can be found in the [community forum](https://community.opencellid.org/t/documenting-the-columns-in-the-downloadable-cells-database-csv/186). The columns used in the visualizations that will be built are described below + +Here is a description of the columns taken from the OpenCellID forum: + +| Column | Description | +|--------------|--------------------------------------------------------| +| radio | Technology generation: CDMA, GSM, UMTS, 5G NR | +| mcc | Mobile Country Code: `204` is The Netherlands | +| lon | Longitude: With Latitude, approximate tower location | +| lat | Latitude: With Longitude, approximate tower location | + +:::tip mcc +To find your MCC check [Mobile network codes](https://en.wikipedia.org/wiki/Mobile_country_code), and use the first three digits in the `MCC / MNC` column. +::: + +## Build visualizations with Apache Superset + +Superset is easy to run from Docker. If you already have Superset running, all you need to do is add ClickHouse Connect with `pip install clickhouse-connect`. If you need to install Superset open the **Launch Apache Superset in Docker** directly below. + + + The data is also available for interactive queries in the [Playground](https://play.clickhouse.com/play?user=play), [example](https://play.clickhouse.com/play?user=play#U0VMRUNUIG1jYywgY291bnQoKSBGUk9NIGNlbGxfdG93ZXJzIEdST1VQIEJZIG1jYyBPUkRFUiBCWSBjb3VudCgpIERFU0M=). Although you cannot create temporary tables there. From 6f8f7150ffc2e70cb979659a0c6524d648101532 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Fri, 21 Oct 2022 16:42:45 +0000 Subject: [PATCH 27/96] style --- tests/ci/sqlancer_check.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 2f320fbbf71..f41ae14b2d3 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -21,6 +21,7 @@ from build_download_helper import get_build_name_for_check, read_build_urls from docker_pull_helper import get_image_with_version from commit_status_helper import post_commit_status from clickhouse_helper import ClickHouseHelper, prepare_tests_results_for_clickhouse +from upload_result_helper import upload_results from stopwatch import Stopwatch from rerun_helper import RerunHelper @@ -141,6 +142,8 @@ if __name__ == "__main__": report_url = GITHUB_RUN_URL + status = "succeess" + test_results = [] # Try to get status message saved by the SQLancer try: with open( @@ -151,7 +154,6 @@ if __name__ == "__main__": with open( os.path.join(workspace_path, "summary.tsv"), "r", encoding="utf-8" ) as summary_f: - test_results = [] for line in summary_f: l = line.split("\t") test_results.append((l[0], l[1])) From db2f3baf39cc752dd2d80001091f86abae0de51e Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Fri, 21 Oct 2022 19:43:06 +0000 Subject: [PATCH 28/96] fix --- .github/workflows/master.yml | 1 + .github/workflows/pull_request.yml | 1 + tests/ci/sqlancer_check.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index b373a3cc210..9de7891f1d7 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -3089,6 +3089,7 @@ jobs: - UnitTestsReleaseClang - SharedBuildSmokeTest - SQLancerTestRelease + - SQLancerTestDebug runs-on: [self-hosted, style-checker] steps: - name: Clear repository diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6335ccc9a35..29fff46c7ce 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3612,6 +3612,7 @@ jobs: - CompatibilityCheck - IntegrationTestsFlakyCheck - SQLancerTestRelease + - SQLancerTestDebug runs-on: [self-hosted, style-checker] steps: - name: Clear repository diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index f41ae14b2d3..eced80db9b7 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -184,7 +184,7 @@ if __name__ == "__main__": prepared_events = prepare_tests_results_for_clickhouse( pr_info, - test_result, + test_results, status, stopwatch.duration_seconds, stopwatch.start_time_str, From 409fb28d0cd9cfcad87d8eb1e073d77f892260bf Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sun, 23 Oct 2022 01:52:30 +0200 Subject: [PATCH 29/96] Fix build --- src/Common/ProgressIndication.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/ProgressIndication.cpp b/src/Common/ProgressIndication.cpp index f1abd20a6c8..b049edcdcf7 100644 --- a/src/Common/ProgressIndication.cpp +++ b/src/Common/ProgressIndication.cpp @@ -152,7 +152,7 @@ void ProgressIndication::writeProgress(WriteBufferFromFileDescriptor & message) const char * indicator = indicators[increment % 8]; - size_t terminal_width = getTerminalWidth(message.getFD()); + size_t terminal_width = getTerminalWidth(); if (!written_progress_chars) { From c386898268f580336aa6710e91dc43cbf5607383 Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sun, 23 Oct 2022 04:46:07 +0200 Subject: [PATCH 30/96] Fix error --- src/Client/ClientBase.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Client/ClientBase.cpp b/src/Client/ClientBase.cpp index fd8aec6973f..1c0251e7671 100644 --- a/src/Client/ClientBase.cpp +++ b/src/Client/ClientBase.cpp @@ -415,7 +415,7 @@ void ClientBase::onData(Block & block, ASTPtr parsed_query) return; /// If results are written INTO OUTFILE, we can avoid clearing progress to avoid flicker. - if (need_render_progress && (stdout_is_a_tty || is_interactive) && (!select_into_file || select_into_file_and_stdout)) + if (need_render_progress && tty_buf && (!select_into_file || select_into_file_and_stdout)) progress_indication.clearProgressOutput(*tty_buf); try @@ -433,7 +433,7 @@ void ClientBase::onData(Block & block, ASTPtr parsed_query) output_format->flush(); /// Restore progress bar after data block. - if (need_render_progress && (stdout_is_a_tty || is_interactive)) + if (need_render_progress && tty_buf) { if (select_into_file && !select_into_file_and_stdout) std::cerr << "\r"; @@ -445,7 +445,7 @@ void ClientBase::onData(Block & block, ASTPtr parsed_query) void ClientBase::onLogData(Block & block) { initLogsOutputStream(); - if (need_render_progress) + if (need_render_progress && tty_buf) progress_indication.clearProgressOutput(*tty_buf); logs_out_stream->writeLogs(block); logs_out_stream->flush(); @@ -989,14 +989,14 @@ void ClientBase::onProgress(const Progress & value) if (output_format) output_format->onProgress(value); - if (need_render_progress) + if (need_render_progress && tty_buf) progress_indication.writeProgress(*tty_buf); } void ClientBase::onEndOfStream() { - if (need_render_progress) + if (need_render_progress && tty_buf) progress_indication.clearProgressOutput(*tty_buf); if (output_format) @@ -1048,7 +1048,7 @@ void ClientBase::onProfileEvents(Block & block) } progress_indication.updateThreadEventData(thread_times); - if (need_render_progress) + if (need_render_progress && tty_buf) progress_indication.writeProgress(*tty_buf); if (profile_events.print) @@ -1056,7 +1056,7 @@ void ClientBase::onProfileEvents(Block & block) if (profile_events.watch.elapsedMilliseconds() >= profile_events.delay_ms) { initLogsOutputStream(); - if (need_render_progress) + if (need_render_progress && tty_buf) progress_indication.clearProgressOutput(*tty_buf); logs_out_stream->writeProfileEvents(block); logs_out_stream->flush(); @@ -1231,7 +1231,8 @@ void ClientBase::sendData(Block & sample, const ColumnsDescription & columns_des progress_indication.updateProgress(Progress(file_progress)); /// Set callback to be called on file progress. - progress_indication.setFileProgressCallback(global_context, *tty_buf); + if (tty_buf) + progress_indication.setFileProgressCallback(global_context, *tty_buf); } /// If data fetched from file (maybe compressed file) @@ -1483,7 +1484,7 @@ bool ClientBase::receiveEndOfQuery() void ClientBase::cancelQuery() { connection->sendCancel(); - if (need_render_progress) + if (need_render_progress && tty_buf) progress_indication.clearProgressOutput(*tty_buf); std::cout << "Cancelling query." << std::endl; @@ -1606,7 +1607,7 @@ void ClientBase::processParsedSingleQuery(const String & full_query, const Strin if (profile_events.last_block) { initLogsOutputStream(); - if (need_render_progress) + if (need_render_progress && tty_buf) progress_indication.clearProgressOutput(*tty_buf); logs_out_stream->writeProfileEvents(profile_events.last_block); logs_out_stream->flush(); From 8eb751fbe551aeb8a5af94c0c7e7b4960865645a Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Sun, 23 Oct 2022 05:28:28 +0200 Subject: [PATCH 31/96] Fix error --- src/Client/ClientBase.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Client/ClientBase.cpp b/src/Client/ClientBase.cpp index 1c0251e7671..ba553b07e73 100644 --- a/src/Client/ClientBase.cpp +++ b/src/Client/ClientBase.cpp @@ -1487,7 +1487,9 @@ void ClientBase::cancelQuery() if (need_render_progress && tty_buf) progress_indication.clearProgressOutput(*tty_buf); - std::cout << "Cancelling query." << std::endl; + if (is_interactive) + std::cout << "Cancelling query." << std::endl; + cancelled = true; } From c8a4dc088f3842dd886475954b832a7b709e1439 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Mon, 24 Oct 2022 08:25:21 +0000 Subject: [PATCH 32/96] fix workflows --- .github/workflows/master.yml | 34 ++++++++++++++++++++++++++++++ .github/workflows/pull_request.yml | 34 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 9de7891f1d7..f80efd98f54 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -3029,6 +3029,40 @@ jobs: docker ps --quiet | xargs --no-run-if-empty docker kill ||: docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: sudo rm -fr "$TEMP_PATH" + SQLancerTestDebug: + needs: [BuilderDebDebug] + runs-on: [self-hosted, fuzzer-unit-tester] + steps: + - name: Set envs + run: | + cat >> "$GITHUB_ENV" << 'EOF' + TEMP_PATH=${{runner.temp}}/sqlancer_debug + REPORTS_PATH=${{runner.temp}}/reports_dir + CHECK_NAME=SQLancer (release) + REPO_COPY=${{runner.temp}}/sqlancer_debug/ClickHouse + EOF + - name: Download json reports + uses: actions/download-artifact@v2 + with: + path: ${{ env.REPORTS_PATH }} + - name: Clear repository + run: | + sudo rm -fr "$GITHUB_WORKSPACE" && mkdir "$GITHUB_WORKSPACE" + - name: Check out repository code + uses: actions/checkout@v2 + - name: SQLancer + run: | + sudo rm -fr "$TEMP_PATH" + mkdir -p "$TEMP_PATH" + cp -r "$GITHUB_WORKSPACE" "$TEMP_PATH" + cd "$REPO_COPY/tests/ci" + python3 sqlancer_check.py "$CHECK_NAME" + - name: Cleanup + if: always() + run: | + docker ps --quiet | xargs --no-run-if-empty docker kill ||: + docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: + sudo rm -fr "$TEMP_PATH" FinishCheck: needs: - DockerHubPush diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 29fff46c7ce..cf6e32e136f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3527,6 +3527,40 @@ jobs: docker ps --quiet | xargs --no-run-if-empty docker kill ||: docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: sudo rm -fr "$TEMP_PATH" + SQLancerTestDebug: + needs: [BuilderDebDebug] + runs-on: [self-hosted, fuzzer-unit-tester] + steps: + - name: Set envs + run: | + cat >> "$GITHUB_ENV" << 'EOF' + TEMP_PATH=${{runner.temp}}/sqlancer_debug + REPORTS_PATH=${{runner.temp}}/reports_dir + CHECK_NAME=SQLancer (release) + REPO_COPY=${{runner.temp}}/sqlancer_debug/ClickHouse + EOF + - name: Download json reports + uses: actions/download-artifact@v2 + with: + path: ${{ env.REPORTS_PATH }} + - name: Clear repository + run: | + sudo rm -fr "$GITHUB_WORKSPACE" && mkdir "$GITHUB_WORKSPACE" + - name: Check out repository code + uses: actions/checkout@v2 + - name: SQLancer + run: | + sudo rm -fr "$TEMP_PATH" + mkdir -p "$TEMP_PATH" + cp -r "$GITHUB_WORKSPACE" "$TEMP_PATH" + cd "$REPO_COPY/tests/ci" + python3 sqlancer_check.py "$CHECK_NAME" + - name: Cleanup + if: always() + run: | + docker ps --quiet | xargs --no-run-if-empty docker kill ||: + docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||: + sudo rm -fr "$TEMP_PATH" ############################################################################################# ###################################### JEPSEN TESTS ######################################### ############################################################################################# From 8da389faf41dad9630decc6de0ca56e036bb50bb Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Mon, 24 Oct 2022 11:28:43 +0000 Subject: [PATCH 33/96] fix name --- .github/workflows/master.yml | 2 +- .github/workflows/pull_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index f80efd98f54..33d4cb4f2cc 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -3038,7 +3038,7 @@ jobs: cat >> "$GITHUB_ENV" << 'EOF' TEMP_PATH=${{runner.temp}}/sqlancer_debug REPORTS_PATH=${{runner.temp}}/reports_dir - CHECK_NAME=SQLancer (release) + CHECK_NAME=SQLancer (debug) REPO_COPY=${{runner.temp}}/sqlancer_debug/ClickHouse EOF - name: Download json reports diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cf6e32e136f..210dc1dce4d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3536,7 +3536,7 @@ jobs: cat >> "$GITHUB_ENV" << 'EOF' TEMP_PATH=${{runner.temp}}/sqlancer_debug REPORTS_PATH=${{runner.temp}}/reports_dir - CHECK_NAME=SQLancer (release) + CHECK_NAME=SQLancer (debug) REPO_COPY=${{runner.temp}}/sqlancer_debug/ClickHouse EOF - name: Download json reports From c89c14a05d855c85d0ae318a6937dc33a6e9bae8 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:34:55 +0000 Subject: [PATCH 34/96] add NoREC --- .../test/sqlancer/process_sqlancer_result.py | 8 ++--- docker/test/sqlancer/run.sh | 13 ++++---- tests/ci/sqlancer_check.py | 30 ++++++++++--------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/docker/test/sqlancer/process_sqlancer_result.py b/docker/test/sqlancer/process_sqlancer_result.py index d0e41643467..3bed4578565 100755 --- a/docker/test/sqlancer/process_sqlancer_result.py +++ b/docker/test/sqlancer/process_sqlancer_result.py @@ -11,14 +11,14 @@ def process_result(result_folder): summary = [] paths = [] tests = [ - "TLPWhere", + "TLPAggregate", + "TLPDistinct", "TLPGroupBy", "TLPHaving", + "TLPWhere", "TLPWhereGroupBy", - "TLPDistinct", - "TLPAggregate", + "NoREC", ] - failed_tests = [] for test in tests: diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 8476afaefb9..4f73ce0359e 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -39,12 +39,13 @@ export TIMEOUT=300 export NUM_QUERIES=1000 export NUM_THREADS=30 -( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere | tee /workspace/TLPWhere.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhere.err -( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPGroupBy | tee /workspace/TLPGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPGroupBy.err -( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPHaving | tee /workspace/TLPHaving.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPHaving.err -( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere --oracle TLPGroupBy | tee /workspace/TLPWhereGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhereGroupBy.err -( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /workspace/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPDistinct.err -( java -jar target/sqlancer-*.jar --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /workspace/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPAggregate.err +( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere | tee /workspace/TLPWhere.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhere.err +( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPGroupBy | tee /workspace/TLPGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPGroupBy.err +( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPHaving | tee /workspace/TLPHaving.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPHaving.err +( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere --oracle TLPGroupBy | tee /workspace/TLPWhereGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhereGroupBy.err +( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /workspace/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPDistinct.err +( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /workspace/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPAggregate.err +( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /workspace/NoREC.out ) 3>&1 1>&2 2>&3 | tee /workspace/NoREC.err ls /workspace pkill -F /workspace/clickhouse-server.pid diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index eced80db9b7..3ea00d60b44 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -111,26 +111,28 @@ if __name__ == "__main__": check_name.lower().replace("(", "").replace(")", "").replace(" ", "") ) s3_prefix = f"{pr_info.number}/{pr_info.sha}/{check_name_lower}/" + + tests = [ + "TLPAggregate", + "TLPDistinct", + "TLPGroupBy", + "TLPHaving", + "TLPWhere", + "TLPWhereGroupBy", + "NoREC", + ] + paths = { "runlog.log": run_log_path, - "TLPWhere": os.path.join(workspace_path, "TLPWhere.log"), - "TLPAggregate.err": os.path.join(workspace_path, "TLPAggregate.err"), - "TLPAggregate.out": os.path.join(workspace_path, "TLPAggregate.out"), - "TLPDistinct.err": os.path.join(workspace_path, "TLPDistinct.err"), - "TLPDistinct.out": os.path.join(workspace_path, "TLPDistinct.out"), - "TLPGroupBy.err": os.path.join(workspace_path, "TLPGroupBy.err"), - "TLPGroupBy.out": os.path.join(workspace_path, "TLPGroupBy.out"), - "TLPHaving.err": os.path.join(workspace_path, "TLPHaving.err"), - "TLPHaving.out": os.path.join(workspace_path, "TLPHaving.out"), - "TLPWhere.err": os.path.join(workspace_path, "TLPWhere.err"), - "TLPWhere.out": os.path.join(workspace_path, "TLPWhere.out"), - "TLPWhereGroupBy.err": os.path.join(workspace_path, "TLPWhereGroupBy.err"), - "TLPWhereGroupBy.out": os.path.join(workspace_path, "TLPWhereGroupBy.out"), "clickhouse-server.log": os.path.join(workspace_path, "clickhouse-server"), - "clickhouse-server.log.err": os.path.join(workspace_path, "clickhouse-server"), "stderr.log": os.path.join(workspace_path, "stderr.log"), "stdout.log": os.path.join(workspace_path, "stdout.log"), } + for t in tests: + err_name = f"{t}.err" + log_name = f"{t}.err" + paths[err_name] = os.path.join(workspace_path, err_name) + paths[log_name] = os.path.join(workspace_path, log_name) s3_helper = S3Helper() for f in paths: From 611c2e2bd75614e8eeb9d10933d76d6c5cd9f89b Mon Sep 17 00:00:00 2001 From: Antonio Andelic Date: Tue, 25 Oct 2022 13:34:34 +0000 Subject: [PATCH 35/96] Support for optimizing old parts for entire partition only --- .../MergeTree/MergeTreeDataMergerMutator.cpp | 27 ++++++++++++++++++- src/Storages/MergeTree/MergeTreeSettings.h | 1 + .../test.py | 24 ++++++++++++----- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/Storages/MergeTree/MergeTreeDataMergerMutator.cpp b/src/Storages/MergeTree/MergeTreeDataMergerMutator.cpp index b0ef1522685..27000796343 100644 --- a/src/Storages/MergeTree/MergeTreeDataMergerMutator.cpp +++ b/src/Storages/MergeTree/MergeTreeDataMergerMutator.cpp @@ -214,6 +214,14 @@ SelectPartsDecision MergeTreeDataMergerMutator::selectPartsToMerge( /// Previous part only in boundaries of partition frame const MergeTreeData::DataPartPtr * prev_part = nullptr; + /// collect min_age for each partition while iterating parts + struct PartitionInfo + { + time_t min_age{std::numeric_limits::max()}; + }; + + std::unordered_map partitions_info; + size_t parts_selected_precondition = 0; for (const MergeTreeData::DataPartPtr & part : data_parts) { @@ -277,6 +285,9 @@ SelectPartsDecision MergeTreeDataMergerMutator::selectPartsToMerge( part_info.compression_codec_desc = part->default_codec->getFullCodecDesc(); part_info.shall_participate_in_merges = has_volumes_with_disabled_merges ? part->shallParticipateInMerges(storage_policy) : true; + auto & partition_info = partitions_info[partition_id]; + partition_info.min_age = std::min(partition_info.min_age, part_info.age); + ++parts_selected_precondition; parts_ranges.back().emplace_back(part_info); @@ -333,7 +344,8 @@ SelectPartsDecision MergeTreeDataMergerMutator::selectPartsToMerge( SimpleMergeSelector::Settings merge_settings; /// Override value from table settings merge_settings.max_parts_to_merge_at_once = data_settings->max_parts_to_merge_at_once; - merge_settings.min_age_to_force_merge = data_settings->min_age_to_force_merge_seconds; + if (!data_settings->min_age_to_force_merge_on_partition_only) + merge_settings.min_age_to_force_merge = data_settings->min_age_to_force_merge_seconds; if (aggressive) merge_settings.base = 1; @@ -347,6 +359,19 @@ SelectPartsDecision MergeTreeDataMergerMutator::selectPartsToMerge( if (parts_to_merge.empty()) { + if (data_settings->min_age_to_force_merge_on_partition_only && data_settings->min_age_to_force_merge_seconds) + { + auto best_partition_it = std::max_element( + partitions_info.begin(), + partitions_info.end(), + [](const auto & e1, const auto & e2) { return e1.second.min_age > e2.second.min_age; }); + + if (best_partition_it != partitions_info.end() + && static_cast(best_partition_it->second.min_age) >= data_settings->min_age_to_force_merge_seconds) + return selectAllPartsToMergeWithinPartition( + future_part, can_merge_callback, best_partition_it->first, true, metadata_snapshot, txn, out_disable_reason); + } + if (out_disable_reason) *out_disable_reason = "There is no need to merge parts according to merge selector algorithm"; return SelectPartsDecision::CANNOT_SELECT; diff --git a/src/Storages/MergeTree/MergeTreeSettings.h b/src/Storages/MergeTree/MergeTreeSettings.h index 3fecb85f484..844c1ddbfe5 100644 --- a/src/Storages/MergeTree/MergeTreeSettings.h +++ b/src/Storages/MergeTree/MergeTreeSettings.h @@ -63,6 +63,7 @@ struct Settings; M(UInt64, merge_tree_clear_old_parts_interval_seconds, 1, "The period of executing the clear old parts operation in background.", 0) \ M(UInt64, merge_tree_clear_old_broken_detached_parts_ttl_timeout_seconds, 1ULL * 3600 * 24 * 30, "Remove old broken detached parts in the background if they remained intouched for a specified by this setting period of time.", 0) \ M(UInt64, min_age_to_force_merge_seconds, 0, "If all parts in a certain range are older than this value, range will be always eligible for merging. Set to 0 to disable.", 0) \ + M(Bool, min_age_to_force_merge_on_partition_only, false, "Whether min_age_to_force_merge_seconds should be applied only on the entire partition and not on subset.", false) \ M(UInt64, merge_tree_enable_clear_old_broken_detached, false, "Enable clearing old broken detached parts operation in background.", 0) \ M(Bool, remove_rolled_back_parts_immediately, 1, "Setting for an incomplete experimental feature.", 0) \ \ diff --git a/tests/integration/test_merge_tree_optimize_old_parts/test.py b/tests/integration/test_merge_tree_optimize_old_parts/test.py index 7b386eba2c4..87e0ecd8108 100644 --- a/tests/integration/test_merge_tree_optimize_old_parts/test.py +++ b/tests/integration/test_merge_tree_optimize_old_parts/test.py @@ -13,7 +13,7 @@ node = cluster.add_instance( @pytest.fixture(scope="module") -def start_cluster(): +def started_cluster(): try: cluster.start() @@ -42,7 +42,7 @@ def check_expected_part_number(seconds, table_name, expected): assert ok -def test_without_force_merge_old_parts(start_cluster): +def test_without_force_merge_old_parts(started_cluster): node.query( "CREATE TABLE test_without_merge (i Int64) ENGINE = MergeTree ORDER BY i;" ) @@ -60,13 +60,18 @@ def test_without_force_merge_old_parts(start_cluster): node.query("DROP TABLE test_without_merge;") -def test_force_merge_old_parts(start_cluster): +@pytest.mark.parametrize("partition_only", ["True", "False"]) +def test_force_merge_old_parts(started_cluster, partition_only): node.query( - "CREATE TABLE test_with_merge (i Int64) ENGINE = MergeTree ORDER BY i SETTINGS min_age_to_force_merge_seconds=5;" + "CREATE TABLE test_with_merge (i Int64) " + "ENGINE = MergeTree " + "ORDER BY i " + f"SETTINGS min_age_to_force_merge_seconds=5, min_age_to_force_merge_on_partition_only={partition_only};" ) node.query("INSERT INTO test_with_merge SELECT 1") node.query("INSERT INTO test_with_merge SELECT 2") node.query("INSERT INTO test_with_merge SELECT 3") + assert get_part_number("test_with_merge") == TSV("""3\n""") expected = TSV("""1\n""") check_expected_part_number(10, "test_with_merge", expected) @@ -74,15 +79,20 @@ def test_force_merge_old_parts(start_cluster): node.query("DROP TABLE test_with_merge;") -def test_force_merge_old_parts_replicated_merge_tree(start_cluster): +@pytest.mark.parametrize("partition_only", ["True", "False"]) +def test_force_merge_old_parts_replicated_merge_tree(started_cluster, partition_only): node.query( - "CREATE TABLE test_replicated (i Int64) ENGINE = ReplicatedMergeTree('/clickhouse/testing/test', 'node') ORDER BY i SETTINGS min_age_to_force_merge_seconds=5;" + "CREATE TABLE test_replicated (i Int64) " + "ENGINE = ReplicatedMergeTree('/clickhouse/testing/test', 'node') " + "ORDER BY i " + f"SETTINGS min_age_to_force_merge_seconds=5, min_age_to_force_merge_on_partition_only={partition_only};" ) node.query("INSERT INTO test_replicated SELECT 1") node.query("INSERT INTO test_replicated SELECT 2") node.query("INSERT INTO test_replicated SELECT 3") + assert get_part_number("test_replicated") == TSV("""3\n""") expected = TSV("""1\n""") check_expected_part_number(10, "test_replicated", expected) - node.query("DROP TABLE test_replicated;") + node.query("DROP TABLE test_replicated SYNC;") From 302df3af122533a8ddd683100395fdcbd71b55ba Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Tue, 25 Oct 2022 16:34:12 +0000 Subject: [PATCH 36/96] fix? --- tests/ci/sqlancer_check.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 3ea00d60b44..c06760d3627 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -94,7 +94,7 @@ if __name__ == "__main__": run_command = get_run_command(build_url, workspace_path, docker_image) logging.info("Going to run %s", run_command) - run_log_path = os.path.join(temp_path, "runlog.log") + run_log_path = os.path.join(workspace_path, "runlog.log") with open(run_log_path, "w", encoding="utf-8") as log: with subprocess.Popen( run_command, shell=True, stderr=log, stdout=log @@ -123,7 +123,7 @@ if __name__ == "__main__": ] paths = { - "runlog.log": run_log_path, + # "runlog.log": run_log_path, "clickhouse-server.log": os.path.join(workspace_path, "clickhouse-server"), "stderr.log": os.path.join(workspace_path, "stderr.log"), "stdout.log": os.path.join(workspace_path, "stdout.log"), From b096edc995f95f0d017a0721c7beece521262c47 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Wed, 26 Oct 2022 08:30:20 +0000 Subject: [PATCH 37/96] do not upload files manually --- tests/ci/sqlancer_check.py | 23 ++++++++--------------- tests/ci/upload_result_helper.py | 2 ++ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index c06760d3627..1564dfc4fc4 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -122,26 +122,19 @@ if __name__ == "__main__": "NoREC", ] - paths = { - # "runlog.log": run_log_path, - "clickhouse-server.log": os.path.join(workspace_path, "clickhouse-server"), - "stderr.log": os.path.join(workspace_path, "stderr.log"), - "stdout.log": os.path.join(workspace_path, "stdout.log"), - } + paths = [ + run_log_path, + os.path.join(workspace_path, "clickhouse-server"), + os.path.join(workspace_path, "stderr.log"), + os.path.join(workspace_path, "stdout.log"), + ] for t in tests: err_name = f"{t}.err" log_name = f"{t}.err" - paths[err_name] = os.path.join(workspace_path, err_name) - paths[log_name] = os.path.join(workspace_path, log_name) + paths.append(os.path.join(workspace_path, err_name)) + paths.append(os.path.join(workspace_path, log_name)) s3_helper = S3Helper() - for f in paths: - try: - paths[f] = s3_helper.upload_test_report_to_s3(paths[f], s3_prefix + "/" + f) - except Exception as ex: - logging.info("Exception uploading file %s text %s", f, ex) - paths[f] = "" - report_url = GITHUB_RUN_URL status = "succeess" diff --git a/tests/ci/upload_result_helper.py b/tests/ci/upload_result_helper.py index 0fde4408176..6fb8982027c 100644 --- a/tests/ci/upload_result_helper.py +++ b/tests/ci/upload_result_helper.py @@ -14,6 +14,8 @@ from report import ReportColorTheme, create_test_html_report def process_logs( s3_client, additional_logs, s3_path_prefix, test_results, with_raw_logs ): + logging.info(f"Upload files to s3 {additional_logs}") + processed_logs = {} # Firstly convert paths of logs from test_results to urls to s3. for test_result in test_results: From 3794d2108943b5e7f98d9b06800308de3de6ab88 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Wed, 26 Oct 2022 08:30:57 +0000 Subject: [PATCH 38/96] ignore tests tmp files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index dd632eba85d..af4615a8e6c 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,6 @@ website/package-lock.json /programs/server/metadata /programs/server/store +# temporary test files +tests/queries/0_stateless/test_* +tests/queries/0_stateless/*.binary From 97aaebfa1808e9b90258a78de2927c97bae05feb Mon Sep 17 00:00:00 2001 From: Antonio Andelic Date: Wed, 26 Oct 2022 10:06:56 +0000 Subject: [PATCH 39/96] Address PR comments --- .../MergeTree/MergeTreeDataMergerMutator.cpp | 5 +- .../__init__.py | 0 .../configs/zookeeper_config.xml | 8 -- .../test.py | 98 ------------------- .../02473_optimize_old_parts.reference | 10 ++ .../0_stateless/02473_optimize_old_parts.sql | 67 +++++++++++++ 6 files changed, 80 insertions(+), 108 deletions(-) delete mode 100644 tests/integration/test_merge_tree_optimize_old_parts/__init__.py delete mode 100644 tests/integration/test_merge_tree_optimize_old_parts/configs/zookeeper_config.xml delete mode 100644 tests/integration/test_merge_tree_optimize_old_parts/test.py create mode 100644 tests/queries/0_stateless/02473_optimize_old_parts.reference create mode 100644 tests/queries/0_stateless/02473_optimize_old_parts.sql diff --git a/src/Storages/MergeTree/MergeTreeDataMergerMutator.cpp b/src/Storages/MergeTree/MergeTreeDataMergerMutator.cpp index 27000796343..0f44d1a7da3 100644 --- a/src/Storages/MergeTree/MergeTreeDataMergerMutator.cpp +++ b/src/Storages/MergeTree/MergeTreeDataMergerMutator.cpp @@ -366,8 +366,9 @@ SelectPartsDecision MergeTreeDataMergerMutator::selectPartsToMerge( partitions_info.end(), [](const auto & e1, const auto & e2) { return e1.second.min_age > e2.second.min_age; }); - if (best_partition_it != partitions_info.end() - && static_cast(best_partition_it->second.min_age) >= data_settings->min_age_to_force_merge_seconds) + assert(best_partition_it != partitions_info.end()); + + if (static_cast(best_partition_it->second.min_age) >= data_settings->min_age_to_force_merge_seconds) return selectAllPartsToMergeWithinPartition( future_part, can_merge_callback, best_partition_it->first, true, metadata_snapshot, txn, out_disable_reason); } diff --git a/tests/integration/test_merge_tree_optimize_old_parts/__init__.py b/tests/integration/test_merge_tree_optimize_old_parts/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/integration/test_merge_tree_optimize_old_parts/configs/zookeeper_config.xml b/tests/integration/test_merge_tree_optimize_old_parts/configs/zookeeper_config.xml deleted file mode 100644 index 18412349228..00000000000 --- a/tests/integration/test_merge_tree_optimize_old_parts/configs/zookeeper_config.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - zoo1 - 2181 - - - diff --git a/tests/integration/test_merge_tree_optimize_old_parts/test.py b/tests/integration/test_merge_tree_optimize_old_parts/test.py deleted file mode 100644 index 87e0ecd8108..00000000000 --- a/tests/integration/test_merge_tree_optimize_old_parts/test.py +++ /dev/null @@ -1,98 +0,0 @@ -import pytest -import time -from helpers.client import QueryRuntimeException -from helpers.cluster import ClickHouseCluster -from helpers.test_tools import TSV - -cluster = ClickHouseCluster(__file__) -node = cluster.add_instance( - "node", - main_configs=["configs/zookeeper_config.xml"], - with_zookeeper=True, -) - - -@pytest.fixture(scope="module") -def started_cluster(): - try: - cluster.start() - - yield cluster - finally: - cluster.shutdown() - - -def get_part_number(table_name): - return TSV( - node.query( - f"SELECT count(*) FROM system.parts where table='{table_name}' and active=1" - ) - ) - - -def check_expected_part_number(seconds, table_name, expected): - ok = False - for i in range(int(seconds) * 2): - result = get_part_number(table_name) - if result == expected: - ok = True - break - else: - time.sleep(1) - assert ok - - -def test_without_force_merge_old_parts(started_cluster): - node.query( - "CREATE TABLE test_without_merge (i Int64) ENGINE = MergeTree ORDER BY i;" - ) - node.query("INSERT INTO test_without_merge SELECT 1") - node.query("INSERT INTO test_without_merge SELECT 2") - node.query("INSERT INTO test_without_merge SELECT 3") - - expected = TSV("""3\n""") - # verify that the parts don't get merged - for i in range(10): - if get_part_number("test_without_merge") != expected: - assert False - time.sleep(1) - - node.query("DROP TABLE test_without_merge;") - - -@pytest.mark.parametrize("partition_only", ["True", "False"]) -def test_force_merge_old_parts(started_cluster, partition_only): - node.query( - "CREATE TABLE test_with_merge (i Int64) " - "ENGINE = MergeTree " - "ORDER BY i " - f"SETTINGS min_age_to_force_merge_seconds=5, min_age_to_force_merge_on_partition_only={partition_only};" - ) - node.query("INSERT INTO test_with_merge SELECT 1") - node.query("INSERT INTO test_with_merge SELECT 2") - node.query("INSERT INTO test_with_merge SELECT 3") - assert get_part_number("test_with_merge") == TSV("""3\n""") - - expected = TSV("""1\n""") - check_expected_part_number(10, "test_with_merge", expected) - - node.query("DROP TABLE test_with_merge;") - - -@pytest.mark.parametrize("partition_only", ["True", "False"]) -def test_force_merge_old_parts_replicated_merge_tree(started_cluster, partition_only): - node.query( - "CREATE TABLE test_replicated (i Int64) " - "ENGINE = ReplicatedMergeTree('/clickhouse/testing/test', 'node') " - "ORDER BY i " - f"SETTINGS min_age_to_force_merge_seconds=5, min_age_to_force_merge_on_partition_only={partition_only};" - ) - node.query("INSERT INTO test_replicated SELECT 1") - node.query("INSERT INTO test_replicated SELECT 2") - node.query("INSERT INTO test_replicated SELECT 3") - assert get_part_number("test_replicated") == TSV("""3\n""") - - expected = TSV("""1\n""") - check_expected_part_number(10, "test_replicated", expected) - - node.query("DROP TABLE test_replicated SYNC;") diff --git a/tests/queries/0_stateless/02473_optimize_old_parts.reference b/tests/queries/0_stateless/02473_optimize_old_parts.reference new file mode 100644 index 00000000000..6767887ba86 --- /dev/null +++ b/tests/queries/0_stateless/02473_optimize_old_parts.reference @@ -0,0 +1,10 @@ +Without merge +6 +With merge any part range +1 +With merge partition only +1 +With merge replicated any part range +1 +With merge replicated partition only +1 diff --git a/tests/queries/0_stateless/02473_optimize_old_parts.sql b/tests/queries/0_stateless/02473_optimize_old_parts.sql new file mode 100644 index 00000000000..545bd58dddc --- /dev/null +++ b/tests/queries/0_stateless/02473_optimize_old_parts.sql @@ -0,0 +1,67 @@ +DROP TABLE IF EXISTS test_without_merge; +DROP TABLE IF EXISTS test_with_merge; +DROP TABLE IF EXISTS test_replicated; + +SELECT 'Without merge'; + +CREATE TABLE test_without_merge (i Int64) ENGINE = MergeTree ORDER BY i; +INSERT INTO test_without_merge SELECT 1; +INSERT INTO test_without_merge SELECT 2; +INSERT INTO test_without_merge SELECT 3; + +SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT count(*) FROM system.parts where table='test_without_merge' and active=1; + +DROP TABLE test_without_merge; + +SELECT 'With merge any part range'; + +CREATE TABLE test_with_merge (i Int64) ENGINE = MergeTree ORDER BY i +SETTINGS min_age_to_force_merge_seconds=3, min_age_to_force_merge_on_partition_only=false; +INSERT INTO test_with_merge SELECT 1; +INSERT INTO test_with_merge SELECT 2; +INSERT INTO test_with_merge SELECT 3; + +SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT count(*) FROM system.parts where table='test_with_merge' and active=1; + +DROP TABLE test_with_merge; + +SELECT 'With merge partition only'; + +CREATE TABLE test_with_merge (i Int64) ENGINE = MergeTree ORDER BY i +SETTINGS min_age_to_force_merge_seconds=3, min_age_to_force_merge_on_partition_only=true; +INSERT INTO test_with_merge SELECT 1; +INSERT INTO test_with_merge SELECT 2; +INSERT INTO test_with_merge SELECT 3; + +SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT count(*) FROM system.parts where table='test_with_merge' and active=1; + +DROP TABLE test_with_merge; + +SELECT 'With merge replicated any part range'; + +CREATE TABLE test_replicated (i Int64) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{database}/test02473', 'node') ORDER BY i +SETTINGS min_age_to_force_merge_seconds=3, min_age_to_force_merge_on_partition_only=false; +INSERT INTO test_replicated SELECT 1; +INSERT INTO test_replicated SELECT 2; +INSERT INTO test_replicated SELECT 3; + +SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT count(*) FROM system.parts where table='test_replicated' and active=1; + +DROP TABLE test_replicated; + +SELECT 'With merge replicated partition only'; + +CREATE TABLE test_replicated (i Int64) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{database}/test02473_partition_only', 'node') ORDER BY i +SETTINGS min_age_to_force_merge_seconds=3, min_age_to_force_merge_on_partition_only=true; +INSERT INTO test_replicated SELECT 1; +INSERT INTO test_replicated SELECT 2; +INSERT INTO test_replicated SELECT 3; + +SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT count(*) FROM system.parts where table='test_replicated' and active=1; + +DROP TABLE test_replicated; From acfea0e2df1c9a41ee58f054c462ca9b70ab3cd0 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Wed, 26 Oct 2022 11:21:33 +0000 Subject: [PATCH 40/96] style --- tests/ci/upload_result_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci/upload_result_helper.py b/tests/ci/upload_result_helper.py index 6fb8982027c..f47d3a68ee8 100644 --- a/tests/ci/upload_result_helper.py +++ b/tests/ci/upload_result_helper.py @@ -14,7 +14,7 @@ from report import ReportColorTheme, create_test_html_report def process_logs( s3_client, additional_logs, s3_path_prefix, test_results, with_raw_logs ): - logging.info(f"Upload files to s3 {additional_logs}") + logging.info("Upload files to s3 %s", additional_logs") processed_logs = {} # Firstly convert paths of logs from test_results to urls to s3. From fa402d8967a3be21541ac1f679b24092ea9b8b03 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Wed, 26 Oct 2022 12:01:12 +0000 Subject: [PATCH 41/96] fix --- tests/ci/upload_result_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci/upload_result_helper.py b/tests/ci/upload_result_helper.py index f47d3a68ee8..e145df02f80 100644 --- a/tests/ci/upload_result_helper.py +++ b/tests/ci/upload_result_helper.py @@ -14,7 +14,7 @@ from report import ReportColorTheme, create_test_html_report def process_logs( s3_client, additional_logs, s3_path_prefix, test_results, with_raw_logs ): - logging.info("Upload files to s3 %s", additional_logs") + logging.info("Upload files to s3 %s", additional_logs) processed_logs = {} # Firstly convert paths of logs from test_results to urls to s3. From 9dee90cd429fa60da2a5e5bab9daf83a16fc9206 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Wed, 26 Oct 2022 15:06:01 +0000 Subject: [PATCH 42/96] fix path --- tests/ci/sqlancer_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 1564dfc4fc4..e8768982417 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -124,7 +124,7 @@ if __name__ == "__main__": paths = [ run_log_path, - os.path.join(workspace_path, "clickhouse-server"), + os.path.join(workspace_path, "clickhouse-server.log"), os.path.join(workspace_path, "stderr.log"), os.path.join(workspace_path, "stdout.log"), ] From 598b45f1ec2194183ec20418d7b7caf7192429be Mon Sep 17 00:00:00 2001 From: Antonio Andelic Date: Thu, 27 Oct 2022 08:06:39 +0000 Subject: [PATCH 43/96] Add test for partition only and new parts --- .../02473_optimize_old_parts.reference | 2 ++ .../0_stateless/02473_optimize_old_parts.sql | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/queries/0_stateless/02473_optimize_old_parts.reference b/tests/queries/0_stateless/02473_optimize_old_parts.reference index e80812bddcd..9002d73ff27 100644 --- a/tests/queries/0_stateless/02473_optimize_old_parts.reference +++ b/tests/queries/0_stateless/02473_optimize_old_parts.reference @@ -8,3 +8,5 @@ With merge replicated any part range 1 With merge replicated partition only 1 +With merge partition only and new parts +3 diff --git a/tests/queries/0_stateless/02473_optimize_old_parts.sql b/tests/queries/0_stateless/02473_optimize_old_parts.sql index d673ef22f67..76c1ba73097 100644 --- a/tests/queries/0_stateless/02473_optimize_old_parts.sql +++ b/tests/queries/0_stateless/02473_optimize_old_parts.sql @@ -65,3 +65,21 @@ SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; SELECT count(*) FROM system.parts WHERE database = currentDatabase() AND table='test_replicated' AND active; DROP TABLE test_replicated; + +SELECT 'With merge partition only and new parts'; + +CREATE TABLE test_with_merge (i Int64) ENGINE = MergeTree ORDER BY i +SETTINGS min_age_to_force_merge_seconds=3, min_age_to_force_merge_on_partition_only=true; +SYSTEM STOP MERGES test_with_merge; +-- These three parts will have min_age=6 at the time of merge +INSERT INTO test_with_merge SELECT 1; +INSERT INTO test_with_merge SELECT 2; +SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +-- These three parts will have min_age=0 at the time of merge +-- and so, nothing will be merged. +INSERT INTO test_with_merge SELECT 3; +SYSTEM START MERGES test_with_merge; + +SELECT count(*) FROM system.parts WHERE database = currentDatabase() AND table='test_with_merge' AND active; + +DROP TABLE test_with_merge; From d8e8dd006f956ef32aa496f21f4fc638c94e99a1 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Thu, 27 Oct 2022 09:06:11 +0000 Subject: [PATCH 44/96] support server fail --- docker/test/sqlancer/run.sh | 32 +++++++++++++++++++------------- tests/ci/sqlancer_check.py | 15 ++++++++------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 4f73ce0359e..8c4d16b4f71 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -31,26 +31,32 @@ fi cd /workspace /clickhouse server -P /workspace/clickhouse-server.pid -L /workspace/clickhouse-server.log -E /workspace/clickhouse-server.log.err --daemon -for _ in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then break ; else sleep 1; fi ; done +for _ in $(seq 1 60); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then break ; else sleep 1; fi ; done cd /sqlancer/sqlancer-master -export TIMEOUT=300 -export NUM_QUERIES=1000 -export NUM_THREADS=30 +TIMEOUT=300 +NUM_QUERIES=1000 +NUM_THREADS=10 +TESTS=( "TLPGroupBy" "TLPHaving" "TLPWhere" "TLPDistinct" "TLPAggregate" "NoREC" ) +echo ${TESTS[@]} -( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere | tee /workspace/TLPWhere.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhere.err -( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPGroupBy | tee /workspace/TLPGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPGroupBy.err -( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPHaving | tee /workspace/TLPHaving.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPHaving.err -( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPWhere --oracle TLPGroupBy | tee /workspace/TLPWhereGroupBy.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPWhereGroupBy.err -( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPDistinct | tee /workspace/TLPDistinct.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPDistinct.err -( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /workspace/TLPAggregate.out ) 3>&1 1>&2 2>&3 | tee /workspace/TLPAggregate.err -( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle TLPAggregate | tee /workspace/NoREC.out ) 3>&1 1>&2 2>&3 | tee /workspace/NoREC.err +for TEST in ${TESTS[@]}; do + echo $TEST + if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]] + then + echo "Server is OK" + ( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle $TEST | tee /workspace/$TEST.out ) 3>&1 1>&2 2>&3 | tee /workspace/$TEST.err + else + touch /workspace/$TEST.err /workspace/$TEST.out + echo "Server is not responding" | tee /workspace/server_crashed.log + fi +done ls /workspace -pkill -F /workspace/clickhouse-server.pid +pkill -F /workspace/clickhouse-server.pid || true -for _ in $(seq 1 30); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then sleep 1 ; else break; fi ; done +for _ in $(seq 1 60); do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]]; then sleep 1 ; else break; fi ; done /process_sqlancer_result.py || echo -e "failure\tCannot parse results" > /workspace/check_status.tsv ls /workspace diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index e8768982417..bdf4b534d46 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -130,7 +130,7 @@ if __name__ == "__main__": ] for t in tests: err_name = f"{t}.err" - log_name = f"{t}.err" + log_name = f"{t}.out" paths.append(os.path.join(workspace_path, err_name)) paths.append(os.path.join(workspace_path, log_name)) @@ -141,11 +141,12 @@ if __name__ == "__main__": test_results = [] # Try to get status message saved by the SQLancer try: - with open( - os.path.join(workspace_path, "status.txt"), "r", encoding="utf-8" - ) as status_f: - status = status_f.readline().rstrip("\n") - + # with open( + # os.path.join(workspace_path, "status.txt"), "r", encoding="utf-8" + # ) as status_f: + # status = status_f.readline().rstrip("\n") + if os.path.exists(os.path.join, "server_crashed.log"): + test_results.append("Server crashed", "FAIL") with open( os.path.join(workspace_path, "summary.tsv"), "r", encoding="utf-8" ) as summary_f: @@ -158,7 +159,7 @@ if __name__ == "__main__": ) as desc_f: description = desc_f.readline().rstrip("\n")[:140] except: - status = "failure" + # status = "failure" description = "Task failed: $?=" + str(retcode) report_url = upload_results( From 8a5ef517e3a2d3c2ad5ff3f2734722faa0cdbfe5 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Thu, 27 Oct 2022 13:30:35 +0000 Subject: [PATCH 45/96] style --- docker/test/sqlancer/run.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 8c4d16b4f71..8816afd6992 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -39,16 +39,16 @@ TIMEOUT=300 NUM_QUERIES=1000 NUM_THREADS=10 TESTS=( "TLPGroupBy" "TLPHaving" "TLPWhere" "TLPDistinct" "TLPAggregate" "NoREC" ) -echo ${TESTS[@]} +echo "${TESTS[@]}" -for TEST in ${TESTS[@]}; do - echo $TEST +for TEST in "${TESTS[@]}"; do + echo "$TEST" if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]] then echo "Server is OK" - ( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle $TEST | tee /workspace/$TEST.out ) 3>&1 1>&2 2>&3 | tee /workspace/$TEST.err + ( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle $TEST | tee "/workspace/$TEST.out" ) 3>&1 1>&2 2>&3 | tee "/workspace/$TEST.err" else - touch /workspace/$TEST.err /workspace/$TEST.out + touch "/workspace/$TEST.err" "/workspace/$TEST.out" echo "Server is not responding" | tee /workspace/server_crashed.log fi done From 3a19752d54ddee5e3c81aa28281eea45f9b02d2b Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:08:23 +0000 Subject: [PATCH 46/96] style --- docker/test/sqlancer/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/test/sqlancer/run.sh b/docker/test/sqlancer/run.sh index 8816afd6992..4a0f0f6a512 100755 --- a/docker/test/sqlancer/run.sh +++ b/docker/test/sqlancer/run.sh @@ -46,7 +46,7 @@ for TEST in "${TESTS[@]}"; do if [[ $(wget -q 'localhost:8123' -O-) == 'Ok.' ]] then echo "Server is OK" - ( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads $NUM_THREADS --timeout-seconds $TIMEOUT --num-queries $NUM_QUERIES --username default --password "" clickhouse --oracle $TEST | tee "/workspace/$TEST.out" ) 3>&1 1>&2 2>&3 | tee "/workspace/$TEST.err" + ( java -jar target/sqlancer-*.jar --log-each-select true --print-failed false --num-threads "$NUM_THREADS" --timeout-seconds "$TIMEOUT" --num-queries "$NUM_QUERIES" --username default --password "" clickhouse --oracle "$TEST" | tee "/workspace/$TEST.out" ) 3>&1 1>&2 2>&3 | tee "/workspace/$TEST.err" else touch "/workspace/$TEST.err" "/workspace/$TEST.out" echo "Server is not responding" | tee /workspace/server_crashed.log From a4626c64af523e3efa4ad67cda5f01feb9e03b56 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Thu, 27 Oct 2022 17:56:08 +0000 Subject: [PATCH 47/96] fix --- tests/ci/sqlancer_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index bdf4b534d46..415e3f2f2f7 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -145,7 +145,7 @@ if __name__ == "__main__": # os.path.join(workspace_path, "status.txt"), "r", encoding="utf-8" # ) as status_f: # status = status_f.readline().rstrip("\n") - if os.path.exists(os.path.join, "server_crashed.log"): + if os.path.exists(os.path.join(workspace_path, "server_crashed.log")): test_results.append("Server crashed", "FAIL") with open( os.path.join(workspace_path, "summary.tsv"), "r", encoding="utf-8" From 0fe0aa44d0ec318d3e9c35aa5f5af964fa28dc5e Mon Sep 17 00:00:00 2001 From: Antonio Andelic Date: Fri, 28 Oct 2022 07:38:57 +0000 Subject: [PATCH 48/96] Increase wait time --- .../queries/0_stateless/02473_optimize_old_parts.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/queries/0_stateless/02473_optimize_old_parts.sql b/tests/queries/0_stateless/02473_optimize_old_parts.sql index 76c1ba73097..106175ab6f5 100644 --- a/tests/queries/0_stateless/02473_optimize_old_parts.sql +++ b/tests/queries/0_stateless/02473_optimize_old_parts.sql @@ -9,7 +9,7 @@ INSERT INTO test_without_merge SELECT 1; INSERT INTO test_without_merge SELECT 2; INSERT INTO test_without_merge SELECT 3; -SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT sleepEachRow(1) FROM numbers(9) FORMAT Null; SELECT count(*) FROM system.parts WHERE database = currentDatabase() AND table='test_without_merge' AND active; DROP TABLE test_without_merge; @@ -22,7 +22,7 @@ INSERT INTO test_with_merge SELECT 1; INSERT INTO test_with_merge SELECT 2; INSERT INTO test_with_merge SELECT 3; -SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT sleepEachRow(1) FROM numbers(9) FORMAT Null; SELECT count(*) FROM system.parts WHERE database = currentDatabase() AND table='test_with_merge' AND active; DROP TABLE test_with_merge; @@ -35,7 +35,7 @@ INSERT INTO test_with_merge SELECT 1; INSERT INTO test_with_merge SELECT 2; INSERT INTO test_with_merge SELECT 3; -SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT sleepEachRow(1) FROM numbers(9) FORMAT Null; SELECT count(*) FROM system.parts WHERE database = currentDatabase() AND table='test_with_merge' AND active; DROP TABLE test_with_merge; @@ -48,7 +48,7 @@ INSERT INTO test_replicated SELECT 1; INSERT INTO test_replicated SELECT 2; INSERT INTO test_replicated SELECT 3; -SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT sleepEachRow(1) FROM numbers(9) FORMAT Null; SELECT count(*) FROM system.parts WHERE database = currentDatabase() AND table='test_replicated' AND active; DROP TABLE test_replicated; @@ -61,7 +61,7 @@ INSERT INTO test_replicated SELECT 1; INSERT INTO test_replicated SELECT 2; INSERT INTO test_replicated SELECT 3; -SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT sleepEachRow(1) FROM numbers(9) FORMAT Null; SELECT count(*) FROM system.parts WHERE database = currentDatabase() AND table='test_replicated' AND active; DROP TABLE test_replicated; @@ -74,7 +74,7 @@ SYSTEM STOP MERGES test_with_merge; -- These three parts will have min_age=6 at the time of merge INSERT INTO test_with_merge SELECT 1; INSERT INTO test_with_merge SELECT 2; -SELECT sleepEachRow(1) FROM numbers(6) FORMAT Null; +SELECT sleepEachRow(1) FROM numbers(9) FORMAT Null; -- These three parts will have min_age=0 at the time of merge -- and so, nothing will be merged. INSERT INTO test_with_merge SELECT 3; From aa75515219579abe6db266eb26d6112811ff44a6 Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Fri, 28 Oct 2022 09:47:16 +0000 Subject: [PATCH 49/96] fix --- tests/ci/sqlancer_check.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 415e3f2f2f7..6c3ebe4d7f8 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -113,12 +113,11 @@ if __name__ == "__main__": s3_prefix = f"{pr_info.number}/{pr_info.sha}/{check_name_lower}/" tests = [ - "TLPAggregate", - "TLPDistinct", "TLPGroupBy", "TLPHaving", "TLPWhere", - "TLPWhereGroupBy", + "TLPDistinct", + "TLPAggregate", "NoREC", ] From 10873dfc9f1990ae2e8fb414a0422a1afe22baba Mon Sep 17 00:00:00 2001 From: Yatsishin Ilya <2159081+qoega@users.noreply.github.com> Date: Fri, 28 Oct 2022 11:28:20 +0000 Subject: [PATCH 50/96] succeess --- tests/ci/sqlancer_check.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/ci/sqlancer_check.py b/tests/ci/sqlancer_check.py index 6c3ebe4d7f8..51c95e50746 100644 --- a/tests/ci/sqlancer_check.py +++ b/tests/ci/sqlancer_check.py @@ -136,7 +136,7 @@ if __name__ == "__main__": s3_helper = S3Helper() report_url = GITHUB_RUN_URL - status = "succeess" + status = "success" test_results = [] # Try to get status message saved by the SQLancer try: @@ -189,6 +189,5 @@ if __name__ == "__main__": ch_helper.insert_events_into(db="default", table="checks", events=prepared_events) - logging.info("Result: '%s', '%s', '%s'", status, description, report_url) - print(f"::notice ::Report url: {report_url}") + print(f"::notice Result: '{status}', '{description}', '{report_url}'") post_commit_status(gh, pr_info.sha, check_name, description, status, report_url) From e4786a611ffc8fc5426c409c4f1e8749af446a34 Mon Sep 17 00:00:00 2001 From: Antonio Andelic Date: Fri, 28 Oct 2022 11:45:18 +0000 Subject: [PATCH 51/96] Add long tag --- tests/queries/0_stateless/02473_optimize_old_parts.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/queries/0_stateless/02473_optimize_old_parts.sql b/tests/queries/0_stateless/02473_optimize_old_parts.sql index 106175ab6f5..c2bd37033c1 100644 --- a/tests/queries/0_stateless/02473_optimize_old_parts.sql +++ b/tests/queries/0_stateless/02473_optimize_old_parts.sql @@ -1,3 +1,5 @@ +-- Tags: long + DROP TABLE IF EXISTS test_without_merge; DROP TABLE IF EXISTS test_with_merge; DROP TABLE IF EXISTS test_replicated; From 1ec10ade5bf5df2271f897bafb2ffabcb11d754a Mon Sep 17 00:00:00 2001 From: DanRoscigno Date: Fri, 28 Oct 2022 09:56:16 -0400 Subject: [PATCH 52/96] add link to geo fn, fix index --- .../example-datasets/cell-towers.md | 4 +- docs/en/sql-reference/functions/geo/index.md | 113 +++++++++--------- 2 files changed, 58 insertions(+), 59 deletions(-) diff --git a/docs/en/getting-started/example-datasets/cell-towers.md b/docs/en/getting-started/example-datasets/cell-towers.md index 989ae6f0a7e..3258f186b08 100644 --- a/docs/en/getting-started/example-datasets/cell-towers.md +++ b/docs/en/getting-started/example-datasets/cell-towers.md @@ -128,13 +128,13 @@ SELECT mcc, count() FROM cell_towers GROUP BY mcc ORDER BY count() DESC LIMIT 10 10 rows in set. Elapsed: 0.019 sec. Processed 43.28 million rows, 86.55 MB (2.33 billion rows/s., 4.65 GB/s.) ``` -So, the top countries are: the USA, Germany, and Russia. +Based on the above query and the [MCC list](https://en.wikipedia.org/wiki/Mobile_country_code), the countries with the most cell towers are: the USA, Germany, and Russia. You may want to create an [External Dictionary](../../sql-reference/dictionaries/external-dictionaries/external-dicts.md) in ClickHouse to decode these values. ## Use case: Incorporate geo data {#use-case} -Using `pointInPolygon` function. +Using the [`pointInPolygon`](/docs/en/sql-reference/functions/geo/coordinates.md/#pointinpolygon) function. 1. Create a table where we will store polygons: diff --git a/docs/en/sql-reference/functions/geo/index.md b/docs/en/sql-reference/functions/geo/index.md index 64e23094105..8d659236d4c 100644 --- a/docs/en/sql-reference/functions/geo/index.md +++ b/docs/en/sql-reference/functions/geo/index.md @@ -8,70 +8,69 @@ title: "Geo Functions" ## Geographical Coordinates Functions -- [greatCircleDistance](./coordinates.md#greatCircleDistance) -- [geoDistance](./coordinates.md#geoDistance) -- [greatCircleAngle](./coordinates.md#greatCircleAngle) -- [pointInEllipses](./coordinates.md#pointInEllipses) -- [pointInPolygon](./coordinates.md#pointInPolygon) +- [greatCircleDistance](./coordinates.md#greatcircledistance) +- [geoDistance](./coordinates.md#geodistance) +- [greatCircleAngle](./coordinates.md#greatcircleangle) +- [pointInEllipses](./coordinates.md#pointinellipses) +- [pointInPolygon](./coordinates.md#pointinpolygon) ## Geohash Functions -- [geohashEncode](./geohash.md#geohashEncode) -- [geohashDecode](./geohash.md#geohashDecode) -- [geohashesInBox](./geohash.md#geohashesInBox) +- [geohashEncode](./geohash.md#geohashencode) +- [geohashDecode](./geohash.md#geohashdecode) +- [geohashesInBox](./geohash.md#geohashesinbox) ## H3 Indexes Functions -- [h3IsValid](./h3.md#h3IsValid) -- [h3GetResolution](./h3.md#h3GetResolution) -- [h3EdgeAngle](./h3.md#h3EdgeAngle) -- [h3EdgeLengthM​](./h3.md#h3EdgeLengthM​) -- [h3EdgeLengthKm](./h3.md#h3EdgeLengthKm) -- [geoToH3](./h3.md#geoToH3) -- [h3ToGeo](./h3.md#h3ToGeo) -- [h3ToGeoBoundary](./h3.md#h3ToGeoBoundary) -- [h3kRing](./h3.md#h3kRing) -- [h3GetBaseCell](./h3.md#h3GetBaseCell) -- [h3HexAreaM2](./h3.md#h3HexAreaM2) -- [h3HexAreaKm2](./h3.md#h3HexAreaKm2) -- [h3IndexesAreNeighbors](./h3.md#h3IndexesAreNeighbors) -- [h3ToChildren](./h3.md#h3ToChildren) -- [h3ToParent](./h3.md#h3ToParent) -- [h3ToString](./h3.md#h3ToString) -- [stringToH3](./h3.md#stringToH3) -- [h3GetResolution](./h3.md#h3GetResolution) -- [h3IsResClassIII](./h3.md#h3IsResClassIII) -- [h3IsPentagon](./h3.md#h3IsPentagon) -- [h3GetFaces](./h3.md#h3GetFaces) -- [h3CellAreaM2](./h3.md#h3CellAreaM2) -- [h3CellAreaRads2](./h3.md#h3CellAreaRads2) -- [h3ToCenterChild](./h3.md#h3ToCenterChild) -- [h3ExactEdgeLengthM](./h3.md#h3ExactEdgeLengthM) -- [h3ExactEdgeLengthKm](./h3.md#h3ExactEdgeLengthKm) -- [h3ExactEdgeLengthRads](./h3.md#h3ExactEdgeLengthRads) -- [h3NumHexagons](./h3.md#h3NumHexagons) -- [h3Line](./h3.md#h3Line) -- [h3Distance](./h3.md#h3Distance) -- [h3HexRing](./h3.md#h3HexRing) -- [h3GetUnidirectionalEdge](./h3.md#h3GetUnidirectionalEdge) -- [h3UnidirectionalEdgeIsValid](./h3.md#h3UnidirectionalEdgeIsValid) -- [h3GetOriginIndexFromUnidirectionalEdge](./h3.md#h3GetOriginIndexFromUnidirectionalEdge) -- [h3GetDestinationIndexFromUnidirectionalEdge](./h3.md#h3GetDestinationIndexFromUnidirectionalEdge) -- [h3GetIndexesFromUnidirectionalEdge](./h3.md#h3GetIndexesFromUnidirectionalEdge) -- [h3GetUnidirectionalEdgesFromHexagon](./h3.md#h3GetUnidirectionalEdgesFromHexagon) -- [h3GetUnidirectionalEdgeBoundary](./h3.md#h3GetUnidirectionalEdgeBoundary) +- [h3IsValid](./h3.md#h3isvalid) +- [h3GetResolution](./h3.md#h3getresolution) +- [h3EdgeAngle](./h3.md#h3edgeangle) +- [h3EdgeLengthM](./h3.md#h3edgelengthm) +- [h3EdgeLengthKm](./h3.md#h3edgelengthkm) +- [geoToH3](./h3.md#geotoh3) +- [h3ToGeo](./h3.md#h3togeo) +- [h3ToGeoBoundary](./h3.md#h3togeoboundary) +- [h3kRing](./h3.md#h3kring) +- [h3GetBaseCell](./h3.md#h3getbasecell) +- [h3HexAreaM2](./h3.md#h3hexaream2) +- [h3HexAreaKm2](./h3.md#h3hexareakm2) +- [h3IndexesAreNeighbors](./h3.md#h3indexesareneighbors) +- [h3ToChildren](./h3.md#h3tochildren) +- [h3ToParent](./h3.md#h3toparent) +- [h3ToString](./h3.md#h3tostring) +- [stringToH3](./h3.md#stringtoh3) +- [h3GetResolution](./h3.md#h3getresolution) +- [h3IsResClassIII](./h3.md#h3isresclassiii) +- [h3IsPentagon](./h3.md#h3ispentagon) +- [h3GetFaces](./h3.md#h3getfaces) +- [h3CellAreaM2](./h3.md#h3cellaream2) +- [h3CellAreaRads2](./h3.md#h3cellarearads2) +- [h3ToCenterChild](./h3.md#h3tocenterchild) +- [h3ExactEdgeLengthM](./h3.md#h3exactedgelengthm) +- [h3ExactEdgeLengthKm](./h3.md#h3exactedgelengthkm) +- [h3ExactEdgeLengthRads](./h3.md#h3exactedgelengthrads) +- [h3NumHexagons](./h3.md#h3numhexagons) +- [h3Line](./h3.md#h3line) +- [h3Distance](./h3.md#h3distance) +- [h3HexRing](./h3.md#h3hexring) +- [h3GetUnidirectionalEdge](./h3.md#h3getunidirectionaledge) +- [h3UnidirectionalEdgeIsValid](./h3.md#h3unidirectionaledgeisvalid) +- [h3GetOriginIndexFromUnidirectionalEdge](./h3.md#h3getoriginindexfromunidirectionaledge) +- [h3GetDestinationIndexFromUnidirectionalEdge](./h3.md#h3getdestinationindexfromunidirectionaledge) +- [h3GetIndexesFromUnidirectionalEdge](./h3.md#h3getindexesfromunidirectionaledge) +- [h3GetUnidirectionalEdgesFromHexagon](./h3.md#h3getunidirectionaledgesfromhexagon) +- [h3GetUnidirectionalEdgeBoundary](./h3.md#h3getunidirectionaledgeboundary) ## S2 Index Functions -- [geoToS2](./s2.md#geoToS2) -- [s2ToGeo](./s2.md#s2ToGeo) -- [s2GetNeighbors](./s2.md#s2GetNeighbors) -- [s2CellsIntersect](./s2.md#s2CellsIntersect) -- [s2CapContains](./s2.md#s2CapContains) -- [s2CapUnion](./s2.md#s2CapUnion) -- [s2RectAdd](./s2.md#s2RectAdd) -- [s2RectContains](./s2.md#s2RectContains) -- [s2RectUinion](./s2.md#s2RectUinion) -- [s2RectIntersection](./s2.md#s2RectIntersection) +- [geoToS2](./s2.md#geotos2) +- [s2ToGeo](./s2.md#s2togeo) +- [s2GetNeighbors](./s2.md#s2getneighbors) +- [s2CellsIntersect](./s2.md#s2cellsintersect) +- [s2CapContains](./s2.md#s2capcontains) +- [s2CapUnion](./s2.md#s2capunion) +- [s2RectAdd](./s2.md#s2rectadd) +- [s2RectContains](./s2.md#s2rectcontains) +- [s2RectUnion](./s2.md#s2rectunion) +- [s2RectIntersection](./s2.md#s2rectintersection) -[Original article](https://clickhouse.com/docs/en/sql-reference/functions/geo/) From 418539cd514fbf34c3ce5436e76bc9d81bbe1298 Mon Sep 17 00:00:00 2001 From: DanRoscigno Date: Fri, 28 Oct 2022 11:54:44 -0400 Subject: [PATCH 53/96] add images --- .../example-datasets/cell-towers.md | 48 ++++++++++++++++-- .../images/superset-cell-tower-dashboard.png | Bin 0 -> 486355 bytes .../images/superset-connect-a-database.png | Bin 0 -> 74606 bytes .../images/superset-create-map.png | Bin 0 -> 294614 bytes .../images/superset-lon-lat.png | Bin 0 -> 38436 bytes .../images/superset-mcc-204.png | Bin 0 -> 12467 bytes .../images/superset-radio-umts.png | Bin 0 -> 12698 bytes .../images/superset-umts-netherlands.png | Bin 0 -> 46590 bytes 8 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 docs/en/getting-started/example-datasets/images/superset-cell-tower-dashboard.png create mode 100644 docs/en/getting-started/example-datasets/images/superset-connect-a-database.png create mode 100644 docs/en/getting-started/example-datasets/images/superset-create-map.png create mode 100644 docs/en/getting-started/example-datasets/images/superset-lon-lat.png create mode 100644 docs/en/getting-started/example-datasets/images/superset-mcc-204.png create mode 100644 docs/en/getting-started/example-datasets/images/superset-radio-umts.png create mode 100644 docs/en/getting-started/example-datasets/images/superset-umts-netherlands.png diff --git a/docs/en/getting-started/example-datasets/cell-towers.md b/docs/en/getting-started/example-datasets/cell-towers.md index 3258f186b08..c66b49d4751 100644 --- a/docs/en/getting-started/example-datasets/cell-towers.md +++ b/docs/en/getting-started/example-datasets/cell-towers.md @@ -12,15 +12,27 @@ import ActionsMenu from '@site/docs/en/_snippets/_service_actions_menu.md'; import SQLConsoleDetail from '@site/docs/en/_snippets/_launch_sql_console.md'; import SupersetDocker from '@site/docs/en/_snippets/_add_superset_detail.md'; +## Goal + +In this guide you will learn how to: +- load the OpenCellID data in Clickhouse +- connect Apache Superset to ClickHouse +- build a dashboard based on data available in the dataset +- how to choose data types for your table fields +- how to choose a primary key + +Here is a preview of the dashboard created in this guide: + +![Dashboard of cell towers by radio type in mcc 204](@site/docs/en/getting-started/example-datasets/images/superset-cell-tower-dashboard.png) + +## Get the Dataset {#get-the-dataset} + This dataset is from [OpenCellid](https://www.opencellid.org/) - The world's largest Open Database of Cell Towers. As of 2021, it contains more than 40 million records about cell towers (GSM, LTE, UMTS, etc.) around the world with their geographical coordinates and metadata (country code, network, etc). OpenCelliD Project is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License, and we redistribute a snapshot of this dataset under the terms of the same license. The up-to-date version of the dataset is available to download after sign in. - -## Get the Dataset {#get-the-dataset} - @@ -248,6 +260,36 @@ Superset is easy to run from Docker. If you already have Superset running, all +## Build a dashboard + +1. foo + +![Dashboard of cell towers by radio type in mcc 204](@site/docs/en/getting-started/example-datasets/images/superset-cell-tower-dashboard.png) + +1. foo + +![Add ClickHouse as a Superset datasource](@site/docs/en/getting-started/example-datasets/images/superset-connect-a-database.png) + +1. foo + +![Create a map in Superset](@site/docs/en/getting-started/example-datasets/images/superset-create-map.png) + +1. foo + +![Specify longitude and latitude fields](@site/docs/en/getting-started/example-datasets/images/superset-lon-lat.png) + +1. foo + +![Filter on MCC 204](@site/docs/en/getting-started/example-datasets/images/superset-mcc-204.png) + +1. foo + +![Filter on radio = UMTS](@site/docs/en/getting-started/example-datasets/images/superset-radio-umts.png) + +1. foo + +![Chart for UMTS radios in MCC 204](@site/docs/en/getting-started/example-datasets/images/superset-umts-netherlands.png) + The data is also available for interactive queries in the [Playground](https://play.clickhouse.com/play?user=play), [example](https://play.clickhouse.com/play?user=play#U0VMRUNUIG1jYywgY291bnQoKSBGUk9NIGNlbGxfdG93ZXJzIEdST1VQIEJZIG1jYyBPUkRFUiBCWSBjb3VudCgpIERFU0M=). Although you cannot create temporary tables there. diff --git a/docs/en/getting-started/example-datasets/images/superset-cell-tower-dashboard.png b/docs/en/getting-started/example-datasets/images/superset-cell-tower-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..8197ea223c278f314fd9b7b17b95d15e9373a92f GIT binary patch literal 486355 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