mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-24 16:42:05 +00:00
Support test mode for clickhouse-local
This commit is contained in:
parent
5f8900cee6
commit
199188be08
@ -1,6 +1,5 @@
|
|||||||
set (CLICKHOUSE_CLIENT_SOURCES
|
set (CLICKHOUSE_CLIENT_SOURCES
|
||||||
Client.cpp
|
Client.cpp
|
||||||
TestTags.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set (CLICKHOUSE_CLIENT_LINK
|
set (CLICKHOUSE_CLIENT_LINK
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
#include <Common/Config/configReadClient.h>
|
#include <Common/Config/configReadClient.h>
|
||||||
|
|
||||||
#include <Core/QueryProcessingStage.h>
|
#include <Core/QueryProcessingStage.h>
|
||||||
#include <Client/TestHint.h>
|
|
||||||
#include <Columns/ColumnString.h>
|
#include <Columns/ColumnString.h>
|
||||||
#include <Poco/Util/Application.h>
|
#include <Poco/Util/Application.h>
|
||||||
|
|
||||||
@ -43,7 +42,6 @@
|
|||||||
#include <Functions/registerFunctions.h>
|
#include <Functions/registerFunctions.h>
|
||||||
#include <AggregateFunctions/registerAggregateFunctions.h>
|
#include <AggregateFunctions/registerAggregateFunctions.h>
|
||||||
#include <Formats/registerFormats.h>
|
#include <Formats/registerFormats.h>
|
||||||
#include "TestTags.h"
|
|
||||||
|
|
||||||
#ifndef __clang__
|
#ifndef __clang__
|
||||||
#pragma GCC optimize("-fno-var-tracking-assignments")
|
#pragma GCC optimize("-fno-var-tracking-assignments")
|
||||||
@ -102,212 +100,6 @@ void Client::processError(const String & query) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool Client::executeMultiQuery(const String & all_queries_text)
|
|
||||||
{
|
|
||||||
// It makes sense not to base any control flow on this, so that it is
|
|
||||||
// the same in tests and in normal usage. The only difference is that in
|
|
||||||
// normal mode we ignore the test hints.
|
|
||||||
const bool test_mode = config().has("testmode");
|
|
||||||
if (test_mode)
|
|
||||||
{
|
|
||||||
/// disable logs if expects errors
|
|
||||||
TestHint test_hint(test_mode, all_queries_text);
|
|
||||||
if (test_hint.clientError() || test_hint.serverError())
|
|
||||||
processTextAsSingleQuery("SET send_logs_level = 'fatal'");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool echo_query = echo_queries;
|
|
||||||
|
|
||||||
/// Test tags are started with "--" so they are interpreted as comments anyway.
|
|
||||||
/// But if the echo is enabled we have to remove the test tags from `all_queries_text`
|
|
||||||
/// because we don't want test tags to be echoed.
|
|
||||||
size_t test_tags_length = test_mode ? getTestTagsLength(all_queries_text) : 0;
|
|
||||||
|
|
||||||
/// Several queries separated by ';'.
|
|
||||||
/// INSERT data is ended by the end of line, not ';'.
|
|
||||||
/// An exception is VALUES format where we also support semicolon in
|
|
||||||
/// addition to end of line.
|
|
||||||
const char * this_query_begin = all_queries_text.data() + test_tags_length;
|
|
||||||
const char * this_query_end;
|
|
||||||
const char * all_queries_end = all_queries_text.data() + all_queries_text.size();
|
|
||||||
|
|
||||||
String full_query; // full_query is the query + inline INSERT data + trailing comments (the latter is our best guess for now).
|
|
||||||
String query_to_execute;
|
|
||||||
ASTPtr parsed_query;
|
|
||||||
std::optional<Exception> current_exception;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
auto stage = analyzeMultiQueryText(this_query_begin, this_query_end, all_queries_end,
|
|
||||||
query_to_execute, parsed_query, all_queries_text, current_exception);
|
|
||||||
switch (stage)
|
|
||||||
{
|
|
||||||
case MultiQueryProcessingStage::QUERIES_END:
|
|
||||||
case MultiQueryProcessingStage::PARSING_FAILED:
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MultiQueryProcessingStage::CONTINUE_PARSING:
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case MultiQueryProcessingStage::PARSING_EXCEPTION:
|
|
||||||
{
|
|
||||||
this_query_end = find_first_symbols<'\n'>(this_query_end, all_queries_end);
|
|
||||||
|
|
||||||
// Try to find test hint for syntax error. We don't know where
|
|
||||||
// the query ends because we failed to parse it, so we consume
|
|
||||||
// the entire line.
|
|
||||||
TestHint hint(test_mode, String(this_query_begin, this_query_end - this_query_begin));
|
|
||||||
if (hint.serverError())
|
|
||||||
{
|
|
||||||
// Syntax errors are considered as client errors
|
|
||||||
current_exception->addMessage("\nExpected server error '{}'.", hint.serverError());
|
|
||||||
current_exception->rethrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hint.clientError() != current_exception->code())
|
|
||||||
{
|
|
||||||
if (hint.clientError())
|
|
||||||
current_exception->addMessage("\nExpected client error: " + std::to_string(hint.clientError()));
|
|
||||||
current_exception->rethrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// It's expected syntax error, skip the line
|
|
||||||
this_query_begin = this_query_end;
|
|
||||||
current_exception.reset();
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case MultiQueryProcessingStage::EXECUTE_QUERY:
|
|
||||||
{
|
|
||||||
full_query = all_queries_text.substr(this_query_begin - all_queries_text.data(), this_query_end - this_query_begin);
|
|
||||||
if (query_fuzzer_runs)
|
|
||||||
{
|
|
||||||
if (!processWithFuzzing(full_query))
|
|
||||||
return false;
|
|
||||||
this_query_begin = this_query_end;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we know for sure where the query ends.
|
|
||||||
// Look for the hint in the text of query + insert data + trailing
|
|
||||||
// comments,
|
|
||||||
// e.g. insert into t format CSV 'a' -- { serverError 123 }.
|
|
||||||
// Use the updated query boundaries we just calculated.
|
|
||||||
TestHint test_hint(test_mode, full_query);
|
|
||||||
// Echo all queries if asked; makes for a more readable reference
|
|
||||||
// file.
|
|
||||||
echo_query = test_hint.echoQueries().value_or(echo_query);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
processParsedSingleQuery(full_query, query_to_execute, parsed_query, echo_query, false);
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
// Surprisingly, this is a client error. A server error would
|
|
||||||
// have been reported w/o throwing (see onReceiveSeverException()).
|
|
||||||
client_exception = std::make_unique<Exception>(getCurrentExceptionMessage(print_stack_trace), getCurrentExceptionCode());
|
|
||||||
have_error = true;
|
|
||||||
}
|
|
||||||
// Check whether the error (or its absence) matches the test hints
|
|
||||||
// (or their absence).
|
|
||||||
bool error_matches_hint = true;
|
|
||||||
if (have_error)
|
|
||||||
{
|
|
||||||
if (test_hint.serverError())
|
|
||||||
{
|
|
||||||
if (!server_exception)
|
|
||||||
{
|
|
||||||
error_matches_hint = false;
|
|
||||||
fmt::print(stderr, "Expected server error code '{}' but got no server error (query: {}).\n",
|
|
||||||
test_hint.serverError(), full_query);
|
|
||||||
}
|
|
||||||
else if (server_exception->code() != test_hint.serverError())
|
|
||||||
{
|
|
||||||
error_matches_hint = false;
|
|
||||||
fmt::print(stderr, "Expected server error code: {} but got: {} (query: {}).\n",
|
|
||||||
test_hint.serverError(), server_exception->code(), full_query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (test_hint.clientError())
|
|
||||||
{
|
|
||||||
if (!client_exception)
|
|
||||||
{
|
|
||||||
error_matches_hint = false;
|
|
||||||
fmt::print(stderr, "Expected client error code '{}' but got no client error (query: {}).\n",
|
|
||||||
test_hint.clientError(), full_query);
|
|
||||||
}
|
|
||||||
else if (client_exception->code() != test_hint.clientError())
|
|
||||||
{
|
|
||||||
error_matches_hint = false;
|
|
||||||
fmt::print(stderr, "Expected client error code '{}' but got '{}' (query: {}).\n",
|
|
||||||
test_hint.clientError(), client_exception->code(), full_query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!test_hint.clientError() && !test_hint.serverError())
|
|
||||||
{
|
|
||||||
// No error was expected but it still occurred. This is the
|
|
||||||
// default case w/o test hint, doesn't need additional
|
|
||||||
// diagnostics.
|
|
||||||
error_matches_hint = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (test_hint.clientError())
|
|
||||||
{
|
|
||||||
fmt::print(stderr, "The query succeeded but the client error '{}' was expected (query: {}).\n",
|
|
||||||
test_hint.clientError(), full_query);
|
|
||||||
error_matches_hint = false;
|
|
||||||
}
|
|
||||||
if (test_hint.serverError())
|
|
||||||
{
|
|
||||||
fmt::print(stderr, "The query succeeded but the server error '{}' was expected (query: {}).\n",
|
|
||||||
test_hint.serverError(), full_query);
|
|
||||||
error_matches_hint = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the error is expected, force reconnect and ignore it.
|
|
||||||
if (have_error && error_matches_hint)
|
|
||||||
{
|
|
||||||
client_exception.reset();
|
|
||||||
server_exception.reset();
|
|
||||||
have_error = false;
|
|
||||||
|
|
||||||
if (!connection->checkConnected())
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// For INSERTs with inline data: use the end of inline data as
|
|
||||||
// reported by the format parser (it is saved in sendData()).
|
|
||||||
// This allows us to handle queries like:
|
|
||||||
// insert into t values (1); select 1
|
|
||||||
// , where the inline data is delimited by semicolon and not by a
|
|
||||||
// newline.
|
|
||||||
auto * insert_ast = parsed_query->as<ASTInsertQuery>();
|
|
||||||
if (insert_ast && isSyncInsertWithData(*insert_ast, global_context))
|
|
||||||
{
|
|
||||||
this_query_end = insert_ast->end;
|
|
||||||
adjustQueryEnd(this_query_end, all_queries_end, global_context->getSettingsRef().max_parser_depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report error.
|
|
||||||
if (have_error)
|
|
||||||
processError(full_query);
|
|
||||||
|
|
||||||
// Stop processing queries if needed.
|
|
||||||
if (have_error && !ignore_error)
|
|
||||||
return is_interactive;
|
|
||||||
|
|
||||||
this_query_begin = this_query_end;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Make query to get all server warnings
|
/// Make query to get all server warnings
|
||||||
std::vector<String> Client::loadWarningMessages()
|
std::vector<String> Client::loadWarningMessages()
|
||||||
{
|
{
|
||||||
@ -1015,7 +807,6 @@ void Client::addOptions(OptionsDescription & options_description)
|
|||||||
("password", po::value<std::string>()->implicit_value("\n", ""), "password")
|
("password", po::value<std::string>()->implicit_value("\n", ""), "password")
|
||||||
("ask-password", "ask-password")
|
("ask-password", "ask-password")
|
||||||
("quota_key", po::value<std::string>(), "A string to differentiate quotas when the user have keyed quotas configured on server")
|
("quota_key", po::value<std::string>(), "A string to differentiate quotas when the user have keyed quotas configured on server")
|
||||||
("testmode,T", "enable test hints in comments")
|
|
||||||
|
|
||||||
("max_client_network_bandwidth", po::value<int>(), "the maximum speed of data exchange over the network for the client in bytes per second.")
|
("max_client_network_bandwidth", po::value<int>(), "the maximum speed of data exchange over the network for the client in bytes per second.")
|
||||||
("compression", po::value<bool>(), "enable or disable compression")
|
("compression", po::value<bool>(), "enable or disable compression")
|
||||||
@ -1151,8 +942,6 @@ void Client::processOptions(const OptionsDescription & options_description,
|
|||||||
config().setBool("ask-password", true);
|
config().setBool("ask-password", true);
|
||||||
if (options.count("quota_key"))
|
if (options.count("quota_key"))
|
||||||
config().setString("quota_key", options["quota_key"].as<std::string>());
|
config().setString("quota_key", options["quota_key"].as<std::string>());
|
||||||
if (options.count("testmode"))
|
|
||||||
config().setBool("testmode", true);
|
|
||||||
if (options.count("max_client_network_bandwidth"))
|
if (options.count("max_client_network_bandwidth"))
|
||||||
max_client_network_bandwidth = options["max_client_network_bandwidth"].as<int>();
|
max_client_network_bandwidth = options["max_client_network_bandwidth"].as<int>();
|
||||||
if (options.count("compression"))
|
if (options.count("compression"))
|
||||||
|
@ -16,20 +16,24 @@ public:
|
|||||||
int main(const std::vector<String> & /*args*/) override;
|
int main(const std::vector<String> & /*args*/) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool executeMultiQuery(const String & all_queries_text) override;
|
|
||||||
bool processWithFuzzing(const String & full_query) override;
|
bool processWithFuzzing(const String & full_query) override;
|
||||||
|
|
||||||
void connect() override;
|
void connect() override;
|
||||||
|
|
||||||
void processError(const String & query) const override;
|
void processError(const String & query) const override;
|
||||||
|
|
||||||
String getName() const override { return "client"; }
|
String getName() const override { return "client"; }
|
||||||
|
|
||||||
void printHelpMessage(const OptionsDescription & options_description) override;
|
void printHelpMessage(const OptionsDescription & options_description) override;
|
||||||
|
|
||||||
void addOptions(OptionsDescription & options_description) override;
|
void addOptions(OptionsDescription & options_description) override;
|
||||||
|
|
||||||
void processOptions(
|
void processOptions(
|
||||||
const OptionsDescription & options_description,
|
const OptionsDescription & options_description,
|
||||||
const CommandLineOptions & options,
|
const CommandLineOptions & options,
|
||||||
const std::vector<Arguments> & external_tables_arguments,
|
const std::vector<Arguments> & external_tables_arguments,
|
||||||
const std::vector<Arguments> & hosts_and_ports_arguments) override;
|
const std::vector<Arguments> & hosts_and_ports_arguments) override;
|
||||||
|
|
||||||
void processConfig() override;
|
void processConfig() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -91,92 +91,6 @@ void LocalServer::processError(const String &) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool LocalServer::executeMultiQuery(const String & all_queries_text)
|
|
||||||
{
|
|
||||||
bool echo_query = echo_queries;
|
|
||||||
|
|
||||||
/// Several queries separated by ';'.
|
|
||||||
/// INSERT data is ended by the end of line, not ';'.
|
|
||||||
/// An exception is VALUES format where we also support semicolon in
|
|
||||||
/// addition to end of line.
|
|
||||||
const char * this_query_begin = all_queries_text.data();
|
|
||||||
const char * this_query_end;
|
|
||||||
const char * all_queries_end = all_queries_text.data() + all_queries_text.size();
|
|
||||||
|
|
||||||
String full_query; // full_query is the query + inline INSERT data + trailing comments (the latter is our best guess for now).
|
|
||||||
String query_to_execute;
|
|
||||||
ASTPtr parsed_query;
|
|
||||||
std::optional<Exception> current_exception;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
auto stage = analyzeMultiQueryText(this_query_begin, this_query_end, all_queries_end,
|
|
||||||
query_to_execute, parsed_query, all_queries_text, current_exception);
|
|
||||||
switch (stage)
|
|
||||||
{
|
|
||||||
case MultiQueryProcessingStage::QUERIES_END:
|
|
||||||
case MultiQueryProcessingStage::PARSING_FAILED:
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MultiQueryProcessingStage::CONTINUE_PARSING:
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case MultiQueryProcessingStage::PARSING_EXCEPTION:
|
|
||||||
{
|
|
||||||
if (current_exception)
|
|
||||||
current_exception->rethrow();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MultiQueryProcessingStage::EXECUTE_QUERY:
|
|
||||||
{
|
|
||||||
full_query = all_queries_text.substr(this_query_begin - all_queries_text.data(), this_query_end - this_query_begin);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
processParsedSingleQuery(full_query, query_to_execute, parsed_query, echo_query, false);
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
if (!is_interactive && !ignore_error)
|
|
||||||
throw;
|
|
||||||
|
|
||||||
// Surprisingly, this is a client error. A server error would
|
|
||||||
// have been reported w/o throwing (see onReceiveSeverException()).
|
|
||||||
client_exception = std::make_unique<Exception>(getCurrentExceptionMessage(print_stack_trace), getCurrentExceptionCode());
|
|
||||||
have_error = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For INSERTs with inline data: use the end of inline data as
|
|
||||||
// reported by the format parser (it is saved in sendData()).
|
|
||||||
// This allows us to handle queries like:
|
|
||||||
// insert into t values (1); select 1
|
|
||||||
// , where the inline data is delimited by semicolon and not by a
|
|
||||||
// newline.
|
|
||||||
auto * insert_ast = parsed_query->as<ASTInsertQuery>();
|
|
||||||
if (insert_ast && insert_ast->data)
|
|
||||||
{
|
|
||||||
this_query_end = insert_ast->end;
|
|
||||||
adjustQueryEnd(this_query_end, all_queries_end, global_context->getSettingsRef().max_parser_depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report error.
|
|
||||||
if (have_error)
|
|
||||||
processError(full_query);
|
|
||||||
|
|
||||||
// Stop processing queries if needed.
|
|
||||||
if (have_error && !ignore_error)
|
|
||||||
return is_interactive;
|
|
||||||
|
|
||||||
this_query_begin = this_query_end;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void LocalServer::initialize(Poco::Util::Application & self)
|
void LocalServer::initialize(Poco::Util::Application & self)
|
||||||
{
|
{
|
||||||
Poco::Util::Application::initialize(self);
|
Poco::Util::Application::initialize(self);
|
||||||
|
@ -31,17 +31,19 @@ public:
|
|||||||
int main(const std::vector<String> & /*args*/) override;
|
int main(const std::vector<String> & /*args*/) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool executeMultiQuery(const String & all_queries_text) override;
|
|
||||||
|
|
||||||
void connect() override;
|
void connect() override;
|
||||||
|
|
||||||
void processError(const String & query) const override;
|
void processError(const String & query) const override;
|
||||||
|
|
||||||
String getName() const override { return "local"; }
|
String getName() const override { return "local"; }
|
||||||
|
|
||||||
void printHelpMessage(const OptionsDescription & options_description) override;
|
void printHelpMessage(const OptionsDescription & options_description) override;
|
||||||
|
|
||||||
void addOptions(OptionsDescription & options_description) override;
|
void addOptions(OptionsDescription & options_description) override;
|
||||||
|
|
||||||
void processOptions(const OptionsDescription & options_description, const CommandLineOptions & options,
|
void processOptions(const OptionsDescription & options_description, const CommandLineOptions & options,
|
||||||
const std::vector<Arguments> &, const std::vector<Arguments> &) override;
|
const std::vector<Arguments> &, const std::vector<Arguments> &) override;
|
||||||
|
|
||||||
void processConfig() override;
|
void processConfig() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -36,6 +36,8 @@
|
|||||||
#include <Storages/ColumnsDescription.h>
|
#include <Storages/ColumnsDescription.h>
|
||||||
|
|
||||||
#include <Client/ClientBaseHelpers.h>
|
#include <Client/ClientBaseHelpers.h>
|
||||||
|
#include <Client/TestHint.h>
|
||||||
|
#include "TestTags.h"
|
||||||
|
|
||||||
#include <Parsers/parseQuery.h>
|
#include <Parsers/parseQuery.h>
|
||||||
#include <Parsers/ParserQuery.h>
|
#include <Parsers/ParserQuery.h>
|
||||||
@ -1483,6 +1485,219 @@ MultiQueryProcessingStage ClientBase::analyzeMultiQueryText(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool ClientBase::executeMultiQuery(const String & all_queries_text)
|
||||||
|
{
|
||||||
|
// It makes sense not to base any control flow on this, so that it is
|
||||||
|
// the same in tests and in normal usage. The only difference is that in
|
||||||
|
// normal mode we ignore the test hints.
|
||||||
|
const bool test_mode = config().has("testmode");
|
||||||
|
if (test_mode)
|
||||||
|
{
|
||||||
|
/// disable logs if expects errors
|
||||||
|
TestHint test_hint(test_mode, all_queries_text);
|
||||||
|
if (test_hint.clientError() || test_hint.serverError())
|
||||||
|
processTextAsSingleQuery("SET send_logs_level = 'fatal'");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool echo_query = echo_queries;
|
||||||
|
|
||||||
|
/// Test tags are started with "--" so they are interpreted as comments anyway.
|
||||||
|
/// But if the echo is enabled we have to remove the test tags from `all_queries_text`
|
||||||
|
/// because we don't want test tags to be echoed.
|
||||||
|
size_t test_tags_length = test_mode ? getTestTagsLength(all_queries_text) : 0;
|
||||||
|
|
||||||
|
/// Several queries separated by ';'.
|
||||||
|
/// INSERT data is ended by the end of line, not ';'.
|
||||||
|
/// An exception is VALUES format where we also support semicolon in
|
||||||
|
/// addition to end of line.
|
||||||
|
const char * this_query_begin = all_queries_text.data() + test_tags_length;
|
||||||
|
const char * this_query_end;
|
||||||
|
const char * all_queries_end = all_queries_text.data() + all_queries_text.size();
|
||||||
|
|
||||||
|
String full_query; // full_query is the query + inline INSERT data + trailing comments (the latter is our best guess for now).
|
||||||
|
String query_to_execute;
|
||||||
|
ASTPtr parsed_query;
|
||||||
|
std::optional<Exception> current_exception;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
auto stage = analyzeMultiQueryText(this_query_begin, this_query_end, all_queries_end,
|
||||||
|
query_to_execute, parsed_query, all_queries_text, current_exception);
|
||||||
|
switch (stage)
|
||||||
|
{
|
||||||
|
case MultiQueryProcessingStage::QUERIES_END:
|
||||||
|
case MultiQueryProcessingStage::PARSING_FAILED:
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MultiQueryProcessingStage::CONTINUE_PARSING:
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case MultiQueryProcessingStage::PARSING_EXCEPTION:
|
||||||
|
{
|
||||||
|
this_query_end = find_first_symbols<'\n'>(this_query_end, all_queries_end);
|
||||||
|
|
||||||
|
// Try to find test hint for syntax error. We don't know where
|
||||||
|
// the query ends because we failed to parse it, so we consume
|
||||||
|
// the entire line.
|
||||||
|
TestHint hint(test_mode, String(this_query_begin, this_query_end - this_query_begin));
|
||||||
|
if (hint.serverError())
|
||||||
|
{
|
||||||
|
// Syntax errors are considered as client errors
|
||||||
|
current_exception->addMessage("\nExpected server error '{}'.", hint.serverError());
|
||||||
|
current_exception->rethrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hint.clientError() != current_exception->code())
|
||||||
|
{
|
||||||
|
if (hint.clientError())
|
||||||
|
current_exception->addMessage("\nExpected client error: " + std::to_string(hint.clientError()));
|
||||||
|
|
||||||
|
current_exception->rethrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// It's expected syntax error, skip the line
|
||||||
|
this_query_begin = this_query_end;
|
||||||
|
current_exception.reset();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case MultiQueryProcessingStage::EXECUTE_QUERY:
|
||||||
|
{
|
||||||
|
full_query = all_queries_text.substr(this_query_begin - all_queries_text.data(), this_query_end - this_query_begin);
|
||||||
|
if (query_fuzzer_runs)
|
||||||
|
{
|
||||||
|
if (!processWithFuzzing(full_query))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
this_query_begin = this_query_end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we know for sure where the query ends.
|
||||||
|
// Look for the hint in the text of query + insert data + trailing
|
||||||
|
// comments, e.g. insert into t format CSV 'a' -- { serverError 123 }.
|
||||||
|
// Use the updated query boundaries we just calculated.
|
||||||
|
TestHint test_hint(test_mode, full_query);
|
||||||
|
|
||||||
|
// Echo all queries if asked; makes for a more readable reference file.
|
||||||
|
echo_query = test_hint.echoQueries().value_or(echo_query);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
processParsedSingleQuery(full_query, query_to_execute, parsed_query, echo_query, false);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
// Surprisingly, this is a client error. A server error would
|
||||||
|
// have been reported w/o throwing (see onReceiveSeverException()).
|
||||||
|
client_exception = std::make_unique<Exception>(getCurrentExceptionMessage(print_stack_trace), getCurrentExceptionCode());
|
||||||
|
have_error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether the error (or its absence) matches the test hints
|
||||||
|
// (or their absence).
|
||||||
|
bool error_matches_hint = true;
|
||||||
|
if (have_error)
|
||||||
|
{
|
||||||
|
if (test_hint.serverError())
|
||||||
|
{
|
||||||
|
if (!server_exception)
|
||||||
|
{
|
||||||
|
error_matches_hint = false;
|
||||||
|
fmt::print(stderr, "Expected server error code '{}' but got no server error (query: {}).\n",
|
||||||
|
test_hint.serverError(), full_query);
|
||||||
|
}
|
||||||
|
else if (server_exception->code() != test_hint.serverError())
|
||||||
|
{
|
||||||
|
error_matches_hint = false;
|
||||||
|
fmt::print(stderr, "Expected server error code: {} but got: {} (query: {}).\n",
|
||||||
|
test_hint.serverError(), server_exception->code(), full_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (test_hint.clientError())
|
||||||
|
{
|
||||||
|
if (!client_exception)
|
||||||
|
{
|
||||||
|
error_matches_hint = false;
|
||||||
|
fmt::print(stderr, "Expected client error code '{}' but got no client error (query: {}).\n",
|
||||||
|
test_hint.clientError(), full_query);
|
||||||
|
}
|
||||||
|
else if (client_exception->code() != test_hint.clientError())
|
||||||
|
{
|
||||||
|
error_matches_hint = false;
|
||||||
|
fmt::print(stderr, "Expected client error code '{}' but got '{}' (query: {}).\n",
|
||||||
|
test_hint.clientError(), client_exception->code(), full_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!test_hint.clientError() && !test_hint.serverError())
|
||||||
|
{
|
||||||
|
// No error was expected but it still occurred. This is the
|
||||||
|
// default case w/o test hint, doesn't need additional
|
||||||
|
// diagnostics.
|
||||||
|
error_matches_hint = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (test_hint.clientError())
|
||||||
|
{
|
||||||
|
error_matches_hint = false;
|
||||||
|
fmt::print(stderr,
|
||||||
|
"The query succeeded but the client error '{}' was expected (query: {}).\n",
|
||||||
|
test_hint.clientError(), full_query);
|
||||||
|
}
|
||||||
|
if (test_hint.serverError())
|
||||||
|
{
|
||||||
|
error_matches_hint = false;
|
||||||
|
fmt::print(stderr,
|
||||||
|
"The query succeeded but the server error '{}' was expected (query: {}).\n",
|
||||||
|
test_hint.serverError(), full_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the error is expected, force reconnect and ignore it.
|
||||||
|
if (have_error && error_matches_hint)
|
||||||
|
{
|
||||||
|
client_exception.reset();
|
||||||
|
server_exception.reset();
|
||||||
|
|
||||||
|
have_error = false;
|
||||||
|
|
||||||
|
if (!connection->checkConnected())
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For INSERTs with inline data: use the end of inline data as
|
||||||
|
// reported by the format parser (it is saved in sendData()).
|
||||||
|
// This allows us to handle queries like:
|
||||||
|
// insert into t values (1); select 1
|
||||||
|
// , where the inline data is delimited by semicolon and not by a
|
||||||
|
// newline.
|
||||||
|
auto * insert_ast = parsed_query->as<ASTInsertQuery>();
|
||||||
|
if (insert_ast && isSyncInsertWithData(*insert_ast, global_context))
|
||||||
|
{
|
||||||
|
this_query_end = insert_ast->end;
|
||||||
|
adjustQueryEnd(this_query_end, all_queries_end, global_context->getSettingsRef().max_parser_depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report error.
|
||||||
|
if (have_error)
|
||||||
|
processError(full_query);
|
||||||
|
|
||||||
|
// Stop processing queries if needed.
|
||||||
|
if (have_error && !ignore_error)
|
||||||
|
return is_interactive;
|
||||||
|
|
||||||
|
this_query_begin = this_query_end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool ClientBase::processQueryText(const String & text)
|
bool ClientBase::processQueryText(const String & text)
|
||||||
{
|
{
|
||||||
if (exit_strings.end() != exit_strings.find(trim(text, [](char c) { return isWhitespaceASCII(c) || c == ';'; })))
|
if (exit_strings.end() != exit_strings.find(trim(text, [](char c) { return isWhitespaceASCII(c) || c == ';'; })))
|
||||||
@ -1967,6 +2182,8 @@ void ClientBase::init(int argc, char ** argv)
|
|||||||
("suggestion_limit", po::value<int>()->default_value(10000),
|
("suggestion_limit", po::value<int>()->default_value(10000),
|
||||||
"Suggestion limit for how many databases, tables and columns to fetch.")
|
"Suggestion limit for how many databases, tables and columns to fetch.")
|
||||||
|
|
||||||
|
("testmode,T", "enable test hints in comments")
|
||||||
|
|
||||||
("format,f", po::value<std::string>(), "default output format")
|
("format,f", po::value<std::string>(), "default output format")
|
||||||
("vertical,E", "vertical output format, same as --format=Vertical or FORMAT Vertical or \\G at end of command")
|
("vertical,E", "vertical output format, same as --format=Vertical or FORMAT Vertical or \\G at end of command")
|
||||||
("highlight", po::value<bool>()->default_value(true), "enable or disable basic syntax highlight in interactive command line")
|
("highlight", po::value<bool>()->default_value(true), "enable or disable basic syntax highlight in interactive command line")
|
||||||
@ -2072,6 +2289,8 @@ void ClientBase::init(int argc, char ** argv)
|
|||||||
config().setBool("interactive", true);
|
config().setBool("interactive", true);
|
||||||
if (options.count("pager"))
|
if (options.count("pager"))
|
||||||
config().setString("pager", options["pager"].as<std::string>());
|
config().setString("pager", options["pager"].as<std::string>());
|
||||||
|
if (options.count("testmode"))
|
||||||
|
config().setBool("testmode", true);
|
||||||
|
|
||||||
if (options.count("log-level"))
|
if (options.count("log-level"))
|
||||||
Poco::Logger::root().setLevel(options["log-level"].as<std::string>());
|
Poco::Logger::root().setLevel(options["log-level"].as<std::string>());
|
||||||
|
@ -61,7 +61,6 @@ protected:
|
|||||||
throw Exception("Query processing with fuzzing is not implemented", ErrorCodes::NOT_IMPLEMENTED);
|
throw Exception("Query processing with fuzzing is not implemented", ErrorCodes::NOT_IMPLEMENTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool executeMultiQuery(const String & all_queries_text) = 0;
|
|
||||||
virtual void connect() = 0;
|
virtual void connect() = 0;
|
||||||
virtual void processError(const String & query) const = 0;
|
virtual void processError(const String & query) const = 0;
|
||||||
virtual String getName() const = 0;
|
virtual String getName() const = 0;
|
||||||
@ -77,6 +76,7 @@ protected:
|
|||||||
ASTPtr parseQuery(const char *& pos, const char * end, bool allow_multi_statements) const;
|
ASTPtr parseQuery(const char *& pos, const char * end, bool allow_multi_statements) const;
|
||||||
static void setupSignalHandler();
|
static void setupSignalHandler();
|
||||||
|
|
||||||
|
bool executeMultiQuery(const String & all_queries_text);
|
||||||
MultiQueryProcessingStage analyzeMultiQueryText(
|
MultiQueryProcessingStage analyzeMultiQueryText(
|
||||||
const char *& this_query_begin, const char *& this_query_end, const char * all_queries_end,
|
const char *& this_query_begin, const char *& this_query_end, const char * all_queries_end,
|
||||||
String & query_to_execute, ASTPtr & parsed_query, const String & all_queries_text,
|
String & query_to_execute, ASTPtr & parsed_query, const String & all_queries_text,
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
OK
|
10
tests/queries/0_stateless/02234_clickhouse_local_test_mode.sh
Executable file
10
tests/queries/0_stateless/02234_clickhouse_local_test_mode.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
CURDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
# shellcheck source=../shell_config.sh
|
||||||
|
. "$CURDIR"/../shell_config.sh
|
||||||
|
|
||||||
|
|
||||||
|
$CLICKHOUSE_LOCAL --query="SELECT n" 2>&1 | grep -q "Code: 47. DB::Exception: Missing columns:" && echo 'OK' || echo 'FAIL' ||:
|
||||||
|
$CLICKHOUSE_LOCAL --testmode --query="SELECT n -- { serverError 47 }"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user