From bf2eb5969242c35d0c82bcb01491994a40f93517 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 26 Jan 2024 22:23:45 +0100 Subject: [PATCH] Character set tracking Added current_character_set and set_character_set close #206 --- include/boost/mysql/any_connection.hpp | 148 +++++++++++++++++- include/boost/mysql/detail/algo_params.hpp | 10 ++ .../boost/mysql/detail/connection_impl.hpp | 15 ++ include/boost/mysql/impl/connection_impl.ipp | 6 + .../impl/internal/sansio/connection_state.hpp | 4 +- .../internal/sansio/connection_state_data.hpp | 10 ++ .../mysql/impl/internal/sansio/handshake.hpp | 24 +++ .../impl/internal/sansio/reset_connection.hpp | 15 +- .../internal/sansio/set_character_set.hpp | 92 +++++++++++ include/boost/mysql/impl/run_algo.ipp | 1 + test/integration/CMakeLists.txt | 1 + test/integration/Jamfile | 1 + .../include/test_integration/common.hpp | 16 +- test/integration/test/any_connection.cpp | 31 ++-- .../test/character_set_tracking.cpp | 86 ++++++++++ test/integration/test/handshake.cpp | 1 + test/integration/test/reconnect.cpp | 21 +-- test/integration/test/spotchecks.cpp | 71 ++++++++- test/unit/CMakeLists.txt | 1 + test/unit/Jamfile | 1 + test/unit/test/sansio/reset_connection.cpp | 13 +- test/unit/test/sansio/set_character_set.cpp | 114 ++++++++++++++ 22 files changed, 627 insertions(+), 55 deletions(-) create mode 100644 include/boost/mysql/impl/internal/sansio/set_character_set.hpp create mode 100644 test/integration/test/character_set_tracking.cpp create mode 100644 test/unit/test/sansio/set_character_set.cpp diff --git a/include/boost/mysql/any_connection.hpp b/include/boost/mysql/any_connection.hpp index 566877d09..e81d6c46f 100644 --- a/include/boost/mysql/any_connection.hpp +++ b/include/boost/mysql/any_connection.hpp @@ -8,6 +8,7 @@ #ifndef BOOST_MYSQL_ANY_CONNECTION_HPP #define BOOST_MYSQL_ANY_CONNECTION_HPP +#include #include #include #include @@ -241,6 +242,44 @@ class any_connection */ bool backslash_escapes() const noexcept { return impl_.backslash_escapes(); } + /** + * \brief Returns the character set used by this connection. + * \details + * The MySQL protocol doesn't expose a clean way to track the character set + * used by this connection. This can change inadvertly by SQL queries or by calling \ref reset_connection. + * \n + * Connections attempt to keep track of the current character set + * used by the connection. When the character set is known, this function returns + * a non-null pointer to the character set currently in use. If the character set + * is unknown, returns `nullptr`. + * \n + * The following functions can modify the return value of this function: \n + * \li Prior to connection, the character set is always unknown. + * \li \ref connect and \ref async_connect may set the current character set + * to a known value, depending on the requested collation. + * \li \ref set_character_set always and \ref async_set_character_set always + * set the current character set to the passed value. + * \li \ref reset_connection and \ref async_reset_connection always resets the current character + * set to an unknown value. + * + * \par Avoid changing the character set directly + * If you change the connection's character set directly using SQL statements, + * like in `conn.execute("SET NAMES utf8mb4")`, the client has no way to track this change, + * and this function will return incorrect results. If you're using this function, avoid: \n + * \li The `SET NAMES` statement + * \li The `SET CHARACTER SET` statement + * \li Modifying the `character_set_client`, `character_set_connection` and `character_set_results` + * session variables. + * + * \par Exception safety + * No-throw guarantee. + * + * \par Object lifetimes + * This function returns a pointer to the connection's internal storage. It will be valid + * as long as `*this` is alive and valid. + */ + const character_set* current_character_set() const noexcept { return impl_.current_character_set(); } + /// \copydoc connection::meta_mode metadata_mode meta_mode() const noexcept { return impl_.meta_mode(); } @@ -278,6 +317,10 @@ class any_connection * If the server doesn't support it, this function will fail with \ref * client_errc::server_doesnt_support_ssl. * \n + * If `params.connection_collation` is within a set of well-known collations, this function + * sets the current character set, such that \ref current_character_set returns a non-null value. + * The default collation (`utf8mb4_general_ci`) is the only one guaranteed to be in the set of well-known + * collations. */ void connect(const connect_params& params, error_code& ec, diagnostics& diag) { @@ -798,6 +841,66 @@ class any_connection ); } + /** + * \brief Sets the connection's character set, as per SET NAMES. + * \details + * Sets the connection's character set by running a + * `SET NAMES` + * SQL statement, using the passed \ref character_set::name as the charset name to set. + * \n + * This function will also update the value returned by \ref current_character_set, so + * prefer using this function over raw SQL statements. + * \n + * If the server was unable to set the character set to the requested value (e.g. because + * the server does not support the requested charset), this function will fail, + * as opposed to how \ref connect behaves when an unsupported collation is passed. + * This is a limitation of MySQL servers. + * \n + * You need to perform connection establishment for this function to succeed, since it + * involves communicating with the server. + * + * \par Object lifetimes + * `charset` will be copied as required, and does not need to be kept alive. + */ + void set_character_set(const character_set& charset, error_code& err, diagnostics& diag) + { + impl_.run(impl_.make_params_set_character_set(charset, diag), err); + } + + /// \copydoc set_character_set + void set_character_set(const character_set& charset) + { + error_code err; + diagnostics diag; + set_character_set(charset, err, diag); + detail::throw_on_error_loc(err, diag, BOOST_CURRENT_LOCATION); + } + + /** + * \copydoc set_character_set + * \details + * \n + * \par Handler signature + * The handler signature for this operation is `void(boost::mysql::error_code)`. + */ + template + auto async_set_character_set(const character_set& charset, CompletionToken&& token) + BOOST_MYSQL_RETURN_TYPE(detail::async_set_character_set_t) + { + return async_set_character_set(charset, impl_.shared_diag(), std::forward(token)); + } + + /// \copydoc async_set_character_set + template + auto async_set_character_set(const character_set& charset, diagnostics& diag, CompletionToken&& token) + BOOST_MYSQL_RETURN_TYPE(detail::async_set_character_set_t) + { + return impl_.async_run( + impl_.make_params_set_character_set(charset, diag), + std::forward(token) + ); + } + /// \copydoc connection::ping void ping(error_code& err, diagnostics& diag) { impl_.run(impl_.make_params_ping(diag), err); } @@ -825,7 +928,42 @@ class any_connection return impl_.async_run(impl_.make_params_ping(diag), std::forward(token)); } - /// \copydoc connection::reset_connection + /** + * \brief Resets server-side session state, like variables and prepared statements. + * \details + * Resets all server-side state for the current session: + * \n + * \li Rolls back any active transactions and resets autocommit mode. + * \li Releases all table locks. + * \li Drops all temporary tables. + * \li Resets all session system variables to their default values (including the ones set by `SET + * NAMES`) and clears all user-defined variables. + * \li Closes all prepared statements. + * \n + * A full reference on the affected session state can be found + * here. + * \n + * \n + * This function will not reset the current physical connection and won't cause re-authentication. + * It is faster than closing and re-opening a connection. + * \n + * The connection must be connected and authenticated before calling this function. + * This function involves communication with the server, and thus may fail. + * + * \par Warning on character sets + * This function will restore the connection's character set and collation **to the server's default**, + * and not to the one specified during connection establishment. Some servers have `latin1` as their + * default character set, which is not usually what you want. Since there is no way to know this + * character set, \ref current_character_set will return `nullptr` after the operation succeeds. + * We recommend always using \ref set_character_set or \ref async_set_character_set after calling this + * function. + * \n + * You can find the character set that your server will use after the reset by running: + * \code + * SELECT @@global.character_set_client, @@global.character_set_connection, + * @@global.character_set_results; + * \endcode + */ void reset_connection(error_code& err, diagnostics& diag) { impl_.run(impl_.make_params_reset_connection(diag), err); @@ -840,7 +978,13 @@ class any_connection detail::throw_on_error_loc(err, diag, BOOST_CURRENT_LOCATION); } - /// \copydoc connection::async_reset_connection + /** + * \copydoc reset_connection + * \details + * \n + * \par Handler signature + * The handler signature for this operation is `void(boost::mysql::error_code)`. + */ template auto async_reset_connection(CompletionToken&& token) BOOST_MYSQL_RETURN_TYPE(detail::async_reset_connection_t) diff --git a/include/boost/mysql/detail/algo_params.hpp b/include/boost/mysql/detail/algo_params.hpp index 1ae2d5e2e..ae1052c1e 100644 --- a/include/boost/mysql/detail/algo_params.hpp +++ b/include/boost/mysql/detail/algo_params.hpp @@ -8,6 +8,7 @@ #ifndef BOOST_MYSQL_DETAIL_ALGO_PARAMS_HPP #define BOOST_MYSQL_DETAIL_ALGO_PARAMS_HPP +#include #include #include #include @@ -23,6 +24,7 @@ namespace boost { namespace mysql { + namespace detail { struct connect_algo_params @@ -114,6 +116,14 @@ struct reset_connection_algo_params using result_type = void; }; +struct set_character_set_algo_params +{ + diagnostics* diag; + character_set charset; + + using result_type = void; +}; + struct quit_connection_algo_params { diagnostics* diag; diff --git a/include/boost/mysql/detail/connection_impl.hpp b/include/boost/mysql/detail/connection_impl.hpp index 441e99394..22e2952be 100644 --- a/include/boost/mysql/detail/connection_impl.hpp +++ b/include/boost/mysql/detail/connection_impl.hpp @@ -47,6 +47,8 @@ namespace mysql { template class static_execution_state; +struct character_set; + namespace detail { // Forward decl @@ -233,6 +235,7 @@ class connection_impl BOOST_MYSQL_DECL std::vector& get_shared_fields() noexcept; BOOST_MYSQL_DECL bool ssl_active() const noexcept; BOOST_MYSQL_DECL bool backslash_escapes() const noexcept; + BOOST_MYSQL_DECL const character_set* current_character_set() const noexcept; // Generic algorithm template @@ -429,6 +432,15 @@ class connection_impl return {&diag, stmt.id()}; } + // Set character set + set_character_set_algo_params make_params_set_character_set( + const character_set& charset, + diagnostics& diag + ) const noexcept + { + return {&diag, charset}; + } + // Ping ping_algo_params make_params_ping(diagnostics& diag) const noexcept { return {&diag}; } @@ -495,6 +507,9 @@ using async_prepare_statement_t = async_run_t using async_close_statement_t = async_run_t; +template +using async_set_character_set_t = async_run_t; + template using async_ping_t = async_run_t; diff --git a/include/boost/mysql/impl/connection_impl.ipp b/include/boost/mysql/impl/connection_impl.ipp index 0bf736878..321fdcce7 100644 --- a/include/boost/mysql/impl/connection_impl.ipp +++ b/include/boost/mysql/impl/connection_impl.ipp @@ -69,4 +69,10 @@ boost::mysql::diagnostics& boost::mysql::detail::connection_impl::shared_diag() return st_->data().shared_diag; } +const boost::mysql::character_set* boost::mysql::detail::connection_impl::current_character_set( +) const noexcept +{ + return st_->data().charset_ptr(); +} + #endif diff --git a/include/boost/mysql/impl/internal/sansio/connection_state.hpp b/include/boost/mysql/impl/internal/sansio/connection_state.hpp index 6444532b3..f57a91feb 100644 --- a/include/boost/mysql/impl/internal/sansio/connection_state.hpp +++ b/include/boost/mysql/impl/internal/sansio/connection_state.hpp @@ -29,13 +29,13 @@ #include #include #include +#include #include #include #include #include -#include namespace boost { namespace mysql { @@ -52,6 +52,7 @@ template <> struct get_algo { using type = read_some template <> struct get_algo { using type = read_some_rows_dynamic_algo; }; template <> struct get_algo { using type = prepare_statement_algo; }; template <> struct get_algo { using type = close_statement_algo; }; +template <> struct get_algo { using type = set_character_set_algo; }; template <> struct get_algo { using type = ping_algo; }; template <> struct get_algo { using type = reset_connection_algo; }; template <> struct get_algo { using type = quit_connection_algo; }; @@ -71,6 +72,7 @@ class connection_state read_some_rows_dynamic_algo, prepare_statement_algo, close_statement_algo, + set_character_set_algo, ping_algo, reset_connection_algo, quit_connection_algo, diff --git a/include/boost/mysql/impl/internal/sansio/connection_state_data.hpp b/include/boost/mysql/impl/internal/sansio/connection_state_data.hpp index e04e905f4..84609f287 100644 --- a/include/boost/mysql/impl/internal/sansio/connection_state_data.hpp +++ b/include/boost/mysql/impl/internal/sansio/connection_state_data.hpp @@ -8,6 +8,7 @@ #ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CONNECTION_STATE_DATA_HPP #define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CONNECTION_STATE_DATA_HPP +#include #include #include #include @@ -59,6 +60,9 @@ struct connection_state_data // be disabled using a variable. OK packets include a flag with this info. bool backslash_escapes{true}; + // The current character set, or a default-constructed character set (will all nullptrs) if unknown + character_set current_charset{}; + // Reader and writer message_reader reader; message_writer writer; @@ -66,6 +70,11 @@ struct connection_state_data bool ssl_active() const noexcept { return ssl == ssl_state::active; } bool supports_ssl() const noexcept { return ssl != ssl_state::unsupported; } + const character_set* charset_ptr() const noexcept + { + return current_charset.name == nullptr ? nullptr : ¤t_charset; + } + connection_state_data(std::size_t read_buffer_size, bool transport_supports_ssl = false) : ssl(transport_supports_ssl ? ssl_state::inactive : ssl_state::unsupported), reader(read_buffer_size) { @@ -82,6 +91,7 @@ struct connection_state_data if (supports_ssl()) ssl = ssl_state::inactive; backslash_escapes = true; + current_charset = character_set{}; } }; diff --git a/include/boost/mysql/impl/internal/sansio/handshake.hpp b/include/boost/mysql/impl/internal/sansio/handshake.hpp index 0d16d6447..e364ba316 100644 --- a/include/boost/mysql/impl/internal/sansio/handshake.hpp +++ b/include/boost/mysql/impl/internal/sansio/handshake.hpp @@ -8,9 +8,11 @@ #ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_HANDSHAKE_HPP #define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_HANDSHAKE_HPP +#include #include #include #include +#include #include #include @@ -70,6 +72,27 @@ class handshake_algo : public sansio_algorithm, asio::coroutine auth_response auth_resp_; std::uint8_t sequence_number_{0}; + // Attempts to map the collection_id to a character set. We try to be conservative + // here, since servers will happily accept unknown collation IDs, silently defaulting + // to the server's default character set (often latin1, which is not Unicode). + static character_set collation_id_to_charset(std::uint16_t collation_id) noexcept + { + switch (collation_id) + { + case mysql_collations::utf8mb4_bin: + case mysql_collations::utf8mb4_general_ci: return utf8mb4_charset; + case mysql_collations::latin1_german1_ci: + case mysql_collations::latin1_swedish_ci: + case mysql_collations::latin1_danish_ci: + case mysql_collations::latin1_german2_ci: + case mysql_collations::latin1_bin: + case mysql_collations::latin1_general_ci: + case mysql_collations::latin1_general_cs: + case mysql_collations::latin1_spanish_ci: return latin1_charset; + default: return character_set{}; + } + } + // Once the handshake is processed, the capabilities are stored in the connection state bool use_ssl() const noexcept { return st_->current_capabilities.has(CLIENT_SSL); } @@ -157,6 +180,7 @@ class handshake_algo : public sansio_algorithm, asio::coroutine { st_->is_connected = true; st_->backslash_escapes = ok.backslash_escapes(); + st_->current_charset = collation_id_to_charset(hparams_.connection_collation()); } error_code process_ok() diff --git a/include/boost/mysql/impl/internal/sansio/reset_connection.hpp b/include/boost/mysql/impl/internal/sansio/reset_connection.hpp index 6b97f6ca3..1c233f3bc 100644 --- a/include/boost/mysql/impl/internal/sansio/reset_connection.hpp +++ b/include/boost/mysql/impl/internal/sansio/reset_connection.hpp @@ -8,6 +8,7 @@ #ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_RESET_CONNECTION_HPP #define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_RESET_CONNECTION_HPP +#include #include #include @@ -50,12 +51,14 @@ class reset_connection_algo : public sansio_algorithm, asio::coroutine BOOST_ASIO_CORO_YIELD return read(seqnum_); // Verify it's what we expected - return deserialize_ok_response( - st_->reader.message(), - st_->flavor, - *diag_, - st_->backslash_escapes - ); + ec = deserialize_ok_response(st_->reader.message(), st_->flavor, *diag_, st_->backslash_escapes); + if (ec) + return ec; + + // Reset was successful. Resetting changes the connection's character set + // to the server's default, which is an unknown value that doesn't have to match + // what was specified in handshake. As a safety measure, clear the current charset + st_->current_charset = character_set{}; } return next_action(); diff --git a/include/boost/mysql/impl/internal/sansio/set_character_set.hpp b/include/boost/mysql/impl/internal/sansio/set_character_set.hpp new file mode 100644 index 000000000..61e47d22c --- /dev/null +++ b/include/boost/mysql/impl/internal/sansio/set_character_set.hpp @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_SET_CHARACTER_SET_HPP +#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_SET_CHARACTER_SET_HPP + +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +class set_character_set_algo : public sansio_algorithm, asio::coroutine +{ + diagnostics* diag_; + character_set charset_; + std::uint8_t seqnum_{0}; + + static std::string compose_query(const character_set& charset) + { + BOOST_ASSERT(charset.name != nullptr); + + // Charset names must not have special characters. + // TODO: this can be improved to use query formatting when we implement it. + // See https://github.com/boostorg/mysql/issues/69 + string_view charset_name = charset.name; + BOOST_ASSERT(charset_name.find('\'') == string_view::npos); + + std::string res("SET NAMES '"); + res.append(charset_name.data(), charset_name.size()); + res.push_back('\''); + return res; + } + +public: + set_character_set_algo(connection_state_data& st, set_character_set_algo_params params) noexcept + : sansio_algorithm(st), diag_(params.diag), charset_(params.charset) + { + } + + next_action resume(error_code ec) + { + if (ec) + return ec; + + // SET NAMES never returns rows. Using execute requires us to allocate + // a results object, which we can avoid by simply sending the query and reading the OK response. + BOOST_ASIO_CORO_REENTER(*this) + { + // Setup + diag_->clear(); + + // Send the execution request + BOOST_ASIO_CORO_YIELD return write(query_command{compose_query(charset_)}, seqnum_); + + // Read the response + BOOST_ASIO_CORO_YIELD return read(seqnum_); + + // Verify it's what we expected + ec = deserialize_ok_response(st_->reader.message(), st_->flavor, *diag_, st_->backslash_escapes); + if (ec) + return ec; + + // If we were successful, update the character set + st_->current_charset = charset_; + } + + return next_action(); + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/impl/run_algo.ipp b/include/boost/mysql/impl/run_algo.ipp index 280361500..262d398ae 100644 --- a/include/boost/mysql/impl/run_algo.ipp +++ b/include/boost/mysql/impl/run_algo.ipp @@ -162,6 +162,7 @@ BOOST_MYSQL_INSTANTIATE_ALGO(read_some_rows_algo_params) BOOST_MYSQL_INSTANTIATE_ALGO(read_some_rows_dynamic_algo_params) BOOST_MYSQL_INSTANTIATE_ALGO(prepare_statement_algo_params) BOOST_MYSQL_INSTANTIATE_ALGO(close_statement_algo_params) +BOOST_MYSQL_INSTANTIATE_ALGO(set_character_set_algo_params) BOOST_MYSQL_INSTANTIATE_ALGO(ping_algo_params) BOOST_MYSQL_INSTANTIATE_ALGO(reset_connection_algo_params) BOOST_MYSQL_INSTANTIATE_ALGO(quit_connection_algo_params) diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index aa60f12ee..6eaf8c8f6 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable( test/static_interface.cpp test/reconnect.cpp test/any_connection.cpp + test/character_set_tracking.cpp test/connection_pool.cpp test/db_specific.cpp test/database_types.cpp diff --git a/test/integration/Jamfile b/test/integration/Jamfile index 9dc81f1e1..9e6e31298 100644 --- a/test/integration/Jamfile +++ b/test/integration/Jamfile @@ -57,6 +57,7 @@ run test/static_interface.cpp test/reconnect.cpp test/any_connection.cpp + test/character_set_tracking.cpp test/connection_pool.cpp test/db_specific.cpp test/database_types.cpp diff --git a/test/integration/include/test_integration/common.hpp b/test/integration/include/test_integration/common.hpp index 4be1dca06..942eee8ab 100644 --- a/test/integration/include/test_integration/common.hpp +++ b/test/integration/include/test_integration/common.hpp @@ -8,10 +8,12 @@ #ifndef BOOST_MYSQL_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_COMMON_HPP #define BOOST_MYSQL_TEST_INTEGRATION_INCLUDE_TEST_INTEGRATION_COMMON_HPP +#include #include #include #include #include +#include #include #include @@ -19,12 +21,11 @@ #include #include -#include #include "test_integration/er_connection.hpp" #include "test_integration/er_network_variant.hpp" +#include "test_integration/get_endpoint.hpp" #include "test_integration/metadata_validator.hpp" -#include "test_integration/network_test.hpp" namespace boost { namespace mysql { @@ -34,6 +35,17 @@ constexpr const char* default_user = "integ_user"; constexpr const char* default_passwd = "integ_password"; constexpr const char* default_db = "boost_mysql_integtests"; +inline connect_params default_connect_params(ssl_mode ssl = ssl_mode::enable) +{ + connect_params res; + res.server_address.emplace_host_and_port(get_hostname()); + res.username = default_user; + res.password = default_passwd; + res.database = default_db; + res.ssl = ssl; + return res; +} + struct network_fixture_base { handshake_params params{default_user, default_passwd, default_db}; diff --git a/test/integration/test/any_connection.cpp b/test/integration/test/any_connection.cpp index 1c9421be7..d24732fd9 100644 --- a/test/integration/test/any_connection.cpp +++ b/test/integration/test/any_connection.cpp @@ -43,17 +43,6 @@ using netmaker_execute = netfun_maker_mem params(new connect_params(create_params(ssl_mode::disable))); + std::unique_ptr params(new connect_params(default_connect_params(ssl_mode::disable))); // Launch the function auto res = create_net_result(); @@ -219,7 +208,7 @@ BOOST_AUTO_TEST_CASE(async_connect_deferred_lifetimes) any_connection conn(ctx); // Create params with SSL disabled to save runtime - std::unique_ptr params(new connect_params(create_params(ssl_mode::disable))); + std::unique_ptr params(new connect_params(default_connect_params(ssl_mode::disable))); // Create a deferred object auto res = create_net_result(); @@ -247,7 +236,7 @@ BOOST_AUTO_TEST_CASE(backslash_escapes) BOOST_TEST(conn.backslash_escapes()); // Connect doesn't change the value - connect_fn(conn, create_params(ssl_mode::disable)).validate_no_error(); + connect_fn(conn, default_connect_params(ssl_mode::disable)).validate_no_error(); BOOST_TEST(conn.backslash_escapes()); // Setting the SQL mode to NO_BACKSLASH_ESCAPES updates the value @@ -266,7 +255,7 @@ BOOST_AUTO_TEST_CASE(backslash_escapes) // Reconnecting clears the value execute_fn(conn, "SET sql_mode = 'NO_BACKSLASH_ESCAPES'", r).validate_no_error(); BOOST_TEST(!conn.backslash_escapes()); - connect_fn(conn, create_params(ssl_mode::disable)).validate_no_error(); + connect_fn(conn, default_connect_params(ssl_mode::disable)).validate_no_error(); BOOST_TEST(conn.backslash_escapes()); } diff --git a/test/integration/test/character_set_tracking.cpp b/test/integration/test/character_set_tracking.cpp new file mode 100644 index 000000000..cd895ac74 --- /dev/null +++ b/test/integration/test/character_set_tracking.cpp @@ -0,0 +1,86 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include + +#include +#include + +#include "test_integration/common.hpp" + +using namespace boost::mysql; +using namespace boost::mysql::test; +namespace asio = boost::asio; + +BOOST_AUTO_TEST_SUITE(test_character_set_tracking) + +static void validate_db_charset(any_connection& conn, string_view expected_charset) +{ + results r; + conn.execute("SELECT @@character_set_client, @@character_set_connection, @@character_set_results", r); + const auto rw = r.rows().at(0); + BOOST_TEST(rw.at(0).as_string() == expected_charset); + BOOST_TEST(rw.at(1).as_string() == expected_charset); + BOOST_TEST(rw.at(2).as_string() == expected_charset); +} + +BOOST_AUTO_TEST_CASE(charset_lifecycle) +{ + // Setup + asio::io_context ctx; + any_connection conn(ctx); + + // Non-connected connections have an unknown charset + BOOST_TEST(conn.current_character_set() == nullptr); + + // Connect with the default character set uses utf8mb4, both in the client + // and in the server. This double-checks that all supported servers support the + // collation we use by default. + conn.connect(default_connect_params(ssl_mode::disable)); + BOOST_TEST(conn.current_character_set()->name == string_view("utf8mb4")); + validate_db_charset(conn, "utf8mb4"); + + // Using set_character_set updates the character set everywhere + character_set greek_charset{"greek", latin1_charset.next_char}; + conn.set_character_set(greek_charset); + BOOST_TEST(conn.current_character_set()->name == string_view("greek")); + validate_db_charset(conn, "greek"); + + // Using reset_connection wipes out client-side character set information + conn.reset_connection(); + BOOST_TEST(conn.current_character_set() == nullptr); + + // We can use set_character_set to recover from this + conn.set_character_set(greek_charset); + BOOST_TEST(conn.current_character_set()->name == string_view("greek")); + validate_db_charset(conn, "greek"); +} + +BOOST_AUTO_TEST_CASE(connect_with_unknown_collation) +{ + // Setup + asio::io_context ctx; + any_connection conn(ctx); + + // Connect with a collation that some servers may not support, or that we don't know of + auto params = default_connect_params(ssl_mode::disable); + params.connection_collation = mysql_collations::utf8mb4_0900_ai_ci; // not supported by MariaDB, triggers + // fallback + conn.connect(params); + BOOST_TEST(conn.current_character_set() == nullptr); + + // Explicitly setting the character set solves the issue + conn.set_character_set(latin1_charset); + BOOST_TEST(conn.current_character_set()->name == string_view("latin1")); + validate_db_charset(conn, "latin1"); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/integration/test/handshake.cpp b/test/integration/test/handshake.cpp index 0fff09fd1..b1680459c 100644 --- a/test/integration/test/handshake.cpp +++ b/test/integration/test/handshake.cpp @@ -21,6 +21,7 @@ #include "test_integration/common.hpp" #include "test_integration/er_network_variant.hpp" #include "test_integration/get_endpoint.hpp" +#include "test_integration/network_test.hpp" #include "test_integration/server_ca.hpp" #include "test_integration/streams.hpp" #include "test_integration/tcp_network_fixture.hpp" diff --git a/test/integration/test/reconnect.cpp b/test/integration/test/reconnect.cpp index 7c8a8d20c..385ac4468 100644 --- a/test/integration/test/reconnect.cpp +++ b/test/integration/test/reconnect.cpp @@ -33,6 +33,7 @@ #include "test_common/netfun_maker.hpp" #include "test_integration/common.hpp" #include "test_integration/get_endpoint.hpp" +#include "test_integration/network_test.hpp" #include "test_integration/run_stackful_coro.hpp" using namespace boost::mysql::test; @@ -65,16 +66,6 @@ auto samples_any = create_network_samples({ BOOST_AUTO_TEST_SUITE(test_reconnect) -connect_params base_connect_params() -{ - connect_params res; - res.server_address.emplace_host_and_port(get_hostname()); - res.username = default_user; - res.password = default_passwd; - res.database = default_db; - return res; -} - struct reconnect_fixture : network_fixture { void do_query_ok() @@ -141,7 +132,7 @@ BOOST_AUTO_TEST_CASE(reconnect_after_cancel) { run_stackful_coro([](boost::asio::yield_context yield) { // Setup - auto connect_prms = base_connect_params(); + auto connect_prms = default_connect_params(); any_connection conn(yield.get_executor()); results r; boost::mysql::error_code ec; @@ -224,10 +215,10 @@ struct change_stream_type_fixture : network_fixture_base connect_params tcp_params; connect_params tcp_ssl_params; - change_stream_type_fixture() : tcp_params(base_connect_params()), tcp_ssl_params(base_connect_params()) + change_stream_type_fixture() + : tcp_params(default_connect_params(ssl_mode::disable)), + tcp_ssl_params(default_connect_params(ssl_mode::require)) { - tcp_params.ssl = ssl_mode::disable; - tcp_ssl_params.ssl = ssl_mode::require; } }; @@ -247,7 +238,7 @@ BOOST_TEST_DECORATOR(*boost::unit_test::label("unix")) BOOST_FIXTURE_TEST_CASE(change_stream_type_unix, change_stream_type_fixture) { // UNIX connect params - auto unix_params = base_connect_params(); + auto unix_params = default_connect_params(); unix_params.server_address.emplace_unix_path(default_unix_path); test_case_t test_cases[] = { diff --git a/test/integration/test/spotchecks.cpp b/test/integration/test/spotchecks.cpp index df3e77159..e2f72face 100644 --- a/test/integration/test/spotchecks.cpp +++ b/test/integration/test/spotchecks.cpp @@ -5,6 +5,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include @@ -13,21 +14,19 @@ #include #include #include +#include #include #include "test_common/create_basic.hpp" +#include "test_common/netfun_maker.hpp" #include "test_integration/common.hpp" #include "test_integration/er_connection.hpp" -#include "test_integration/metadata_validator.hpp" +#include "test_integration/network_test.hpp" #include "test_integration/static_rows.hpp" +using namespace boost::mysql; using namespace boost::mysql::test; -using boost::mysql::common_server_errc; -using boost::mysql::execution_state; -using boost::mysql::field_view; -using boost::mysql::results; -using boost::mysql::row_view; BOOST_AUTO_TEST_SUITE(test_spotchecks) @@ -570,4 +569,64 @@ BOOST_MYSQL_NETWORK_TEST(read_some_rows_error, network_fixture, err_net_samples) } #endif +// set_character_set. Since this is only available in any_connection, we spotcheck this +// with netmakers and don't cover all streams +using set_charset_netmaker = netfun_maker_mem; + +struct +{ + string_view name; + set_charset_netmaker::signature set_character_set; +} set_charset_all_fns[] = { + {"sync_errc", set_charset_netmaker::sync_errc(&any_connection::set_character_set)}, + {"sync_exc", set_charset_netmaker::sync_exc(&any_connection::set_character_set)}, + {"async_errinfo", set_charset_netmaker::async_errinfo(&any_connection::async_set_character_set, false)}, + {"async_noerrinfo", set_charset_netmaker::async_noerrinfo(&any_connection::async_set_character_set, false) + }, +}; + +BOOST_AUTO_TEST_CASE(spotcheck_success) +{ + for (const auto& fns : set_charset_all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + // Setup + boost::asio::io_context ctx; + any_connection conn(ctx); + conn.connect(default_connect_params()); + + // Issue the command + fns.set_character_set(conn, latin1_charset).validate_no_error(); + + // Success + BOOST_TEST(conn.current_character_set()->name == string_view("latin1")); + } + } +} + +BOOST_AUTO_TEST_CASE(spotcheck_error) +{ + for (const auto& fns : set_charset_all_fns) + { + BOOST_TEST_CONTEXT(fns.name) + { + // Setup + boost::asio::io_context ctx; + any_connection conn(ctx); + conn.connect(default_connect_params(ssl_mode::disable)); + + // Issue the command + fns.set_character_set(conn, character_set{"bad_charset", nullptr}) + .validate_error_exact( + common_server_errc::er_unknown_character_set, + "Unknown character set: 'bad_charset'" + ); + + // The character set was not modified + BOOST_TEST(conn.current_character_set()->name == string_view("utf8mb4")); + } + } +} + BOOST_AUTO_TEST_SUITE_END() // test_spotchecks diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index bfc282924..575c20ed7 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -36,6 +36,7 @@ add_executable( test/sansio/read_some_rows_dynamic.cpp test/sansio/execute.cpp test/sansio/close_statement.cpp + test/sansio/set_character_set.cpp test/sansio/ping.cpp test/sansio/reset_connection.cpp test/network_algorithms/run_algo_impl.cpp diff --git a/test/unit/Jamfile b/test/unit/Jamfile index b76b8ba25..584903c52 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -44,6 +44,7 @@ run test/sansio/read_some_rows_dynamic.cpp test/sansio/execute.cpp test/sansio/close_statement.cpp + test/sansio/set_character_set.cpp test/sansio/ping.cpp test/sansio/reset_connection.cpp test/network_algorithms/run_algo_impl.cpp diff --git a/test/unit/test/sansio/reset_connection.cpp b/test/unit/test/sansio/reset_connection.cpp index 2b0d94989..1782d70a2 100644 --- a/test/unit/test/sansio/reset_connection.cpp +++ b/test/unit/test/sansio/reset_connection.cpp @@ -5,6 +5,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include @@ -36,6 +37,7 @@ BOOST_AUTO_TEST_CASE(success) { // Setup fixture fix; + fix.st.current_charset = utf8mb4_charset; // Run the algo algo_test() @@ -43,8 +45,9 @@ BOOST_AUTO_TEST_CASE(success) .expect_read(create_ok_frame(1, ok_builder().build())) .check(fix); - // The OK packet was processed correctly + // The OK packet was processed correctly. The charset was reset BOOST_TEST(fix.st.backslash_escapes); + BOOST_TEST(fix.st.charset_ptr() == nullptr); } BOOST_AUTO_TEST_CASE(success_no_backslash_escapes) @@ -58,8 +61,10 @@ BOOST_AUTO_TEST_CASE(success_no_backslash_escapes) .expect_read(create_ok_frame(1, ok_builder().no_backslash_escapes(true).build())) .check(fix); - // The OK packet was processed correctly + // The OK packet was processed correctly. + // It's OK to run this algo without a known charset BOOST_TEST(!fix.st.backslash_escapes); + BOOST_TEST(fix.st.charset_ptr() == nullptr); } BOOST_AUTO_TEST_CASE(error_network) @@ -75,6 +80,7 @@ BOOST_AUTO_TEST_CASE(error_response) { // Setup fixture fix; + fix.st.current_charset = utf8mb4_charset; // Run the algo algo_test() @@ -85,6 +91,9 @@ BOOST_AUTO_TEST_CASE(error_response) .message("my_message") .build_frame()) .check(fix, common_server_errc::er_bad_db_error, create_server_diag("my_message")); + + // The charset was not updated + BOOST_TEST(fix.st.charset_ptr()->name == string_view("utf8mb4")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/set_character_set.cpp b/test/unit/test/sansio/set_character_set.cpp new file mode 100644 index 000000000..7bc99dbd7 --- /dev/null +++ b/test/unit/test/sansio/set_character_set.cpp @@ -0,0 +1,114 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "test_common/create_diagnostics.hpp" +#include "test_unit/algo_test.hpp" +#include "test_unit/create_err.hpp" +#include "test_unit/create_frame.hpp" +#include "test_unit/create_ok.hpp" +#include "test_unit/create_ok_frame.hpp" + +using namespace boost::mysql::test; +using namespace boost::mysql; + +BOOST_AUTO_TEST_SUITE(test_set_character_set) + +struct fixture : algo_fixture_base +{ + detail::set_character_set_algo algo{ + st, + {&diag, utf8mb4_charset} + }; +}; + +// GCC raises a spurious warning here +#if BOOST_GCC >= 110000 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-overread" +#endif +static std::vector create_query_body(string_view sql) +{ + std::vector res{0x03}; // COM_QUERY command id + const auto* data = reinterpret_cast(sql.data()); + res.insert(res.end(), data, data + sql.size()); + return res; +} +#if BOOST_GCC >= 110000 +#pragma GCC diagnostic pop +#endif + +BOOST_AUTO_TEST_CASE(success) +{ + // Setup + fixture fix; + + // Run the algo + algo_test() + .expect_write(create_frame(0, create_query_body("SET NAMES 'utf8mb4'"))) + .expect_read(create_ok_frame(1, ok_builder().build())) + .check(fix); + + // The charset was updated + BOOST_TEST(fix.st.charset_ptr()->name == string_view("utf8mb4")); +} + +BOOST_AUTO_TEST_CASE(success_previous_charset) +{ + // Setup + fixture fix; + fix.st.current_charset = latin1_charset; + + // Run the algo + algo_test() + .expect_write(create_frame(0, create_query_body("SET NAMES 'utf8mb4'"))) + .expect_read(create_ok_frame(1, ok_builder().build())) + .check(fix); + + // The charset was updated + BOOST_TEST(fix.st.charset_ptr()->name == string_view("utf8mb4")); +} + +BOOST_AUTO_TEST_CASE(error_network) +{ + // This covers errors in read and write + algo_test() + .expect_write(create_frame(0, create_query_body("SET NAMES 'utf8mb4'"))) + .expect_read(create_ok_frame(1, ok_builder().build())) + .check_network_errors(); +} + +BOOST_AUTO_TEST_CASE(error_response) +{ + // Setup + fixture fix; + + // Run the algo + algo_test() + .expect_write(create_frame(0, create_query_body("SET NAMES 'utf8mb4'"))) + .expect_read(err_builder() + .seqnum(1) + .code(common_server_errc::er_unknown_character_set) + .message("Unknown charset") + .build_frame()) + .check(fix, common_server_errc::er_unknown_character_set, create_server_diag("Unknown charset")); + + // The current character set was not updated + BOOST_TEST(fix.st.charset_ptr() == nullptr); +} + +BOOST_AUTO_TEST_SUITE_END()