diff --git a/example/2_simple/pipeline.cpp b/example/2_simple/pipeline.cpp index e22361b07..aaadf05fb 100644 --- a/example/2_simple/pipeline.cpp +++ b/example/2_simple/pipeline.cpp @@ -57,8 +57,7 @@ asio::awaitable> batch_prepare( for (auto stmt_sql : statements) req.add_prepare_statement(stmt_sql); - // Run the pipeline. Using as_tuple prevents async_run_pipeline from throwing. - // This allows us to include the diagnostics object diag in the thrown exception. + // Run the pipeline. // stage_response is a variant-like type that can hold the response of any stage type. std::vector pipe_res; co_await conn.async_run_pipeline(req, pipe_res); diff --git a/example/Jamfile b/example/Jamfile index c03c99a20..47c618fab 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -7,8 +7,6 @@ import os ; -project : requirements /boost/mysql//boost_mysql ; - path-constant this_dir : . ; # The hostname to use for examples diff --git a/include/boost/mysql/impl/with_diagnostics.hpp b/include/boost/mysql/impl/with_diagnostics.hpp index 9c70edb2f..a1fb50756 100644 --- a/include/boost/mysql/impl/with_diagnostics.hpp +++ b/include/boost/mysql/impl/with_diagnostics.hpp @@ -50,8 +50,14 @@ struct with_diag_handler_fn std::shared_ptr owning_diag; }; +// By default, don't modify the signature. +// This makes asio::as_tuple(with_diagnostics(X)) equivalent +// to asio::as_tuple(X). template -struct with_diag_signature; +struct with_diag_signature +{ + using type = Signature; +}; template struct with_diag_signature @@ -146,14 +152,23 @@ struct with_diag_init : public Initiation } }; -} // namespace detail -} // namespace mysql +// Did with_diagnostics modify any of the signatures? +// We really support only modifying all or none, and that's enough. +template +using with_diag_has_original_signature = std:: + is_same::type>; -namespace asio { +template +using with_diag_has_original_signatures = mp11:: + mp_all_of, with_diag_has_original_signature>; +template +struct with_diagnostics_async_result; + +// async_result when the signature was modified template -struct async_result, Signatures...> - : async_result::type...> +struct with_diagnostics_async_result + : asio::async_result::type...> { template using maybe_const_token_t = typename std::conditional< @@ -163,26 +178,65 @@ struct async_result, Signatures...> template static auto initiate(Initiation&& initiation, RawCompletionToken&& token, Args&&... args) - -> decltype(async_initiate< + -> decltype(asio::async_initiate< maybe_const_token_t, - typename mysql::detail::with_diag_signature::type...>( - std::declval::type>>(), - mysql::detail::access::get_impl(token), + typename with_diag_signature::type...>( + with_diag_init::type>{std::forward(initiation)}, + access::get_impl(token), std::forward(args)... )) { - return async_initiate< + return asio::async_initiate< maybe_const_token_t, - typename mysql::detail::with_diag_signature::type...>( - mysql::detail::with_diag_init::type>{ - std::forward(initiation) - }, - mysql::detail::access::get_impl(token), + typename with_diag_signature::type...>( + with_diag_init::type>{std::forward(initiation)}, + access::get_impl(token), std::forward(args)... ); } }; +// async_result when the signature wasn't modified (pass-through) +template +struct with_diagnostics_async_result + : asio::async_result +{ + template + using maybe_const_token_t = typename std::conditional< + std::is_const::type>::value, + const CompletionToken, + CompletionToken>::type; + + template + static auto initiate(Initiation&& initiation, RawCompletionToken&& token, Args&&... args) + -> decltype(asio::async_initiate, Signatures...>( + std::forward(initiation), + access::get_impl(token), + std::forward(args)... + )) + { + return asio::async_initiate, Signatures...>( + std::forward(initiation), + access::get_impl(token), + std::forward(args)... + ); + } +}; + +} // namespace detail +} // namespace mysql + +namespace asio { + +template +struct async_result, Signatures...> + : mysql::detail::with_diagnostics_async_result< + CompletionToken, + mysql::detail::with_diag_has_original_signatures::value, + Signatures...> +{ +}; + } // namespace asio } // namespace boost diff --git a/include/boost/mysql/with_diagnostics.hpp b/include/boost/mysql/with_diagnostics.hpp index b61302554..e7ef778a4 100644 --- a/include/boost/mysql/with_diagnostics.hpp +++ b/include/boost/mysql/with_diagnostics.hpp @@ -24,20 +24,26 @@ namespace mysql { * Uses knowledge of Boost.MySQL internals to grab any \ref diagnostics * that the operation may produce to create a `std::exception_ptr` pointing to an \ref error_with_diagnostics * object. On success, the generated `std::exception_ptr` will be `nullptr`. - * \n + * * Using `with_diagnostics` to wrap tokens that throw exceptions (like `deferred` + `co_await` or * `yield_context`) enhances the thrown exceptions with diagnostics information, matching the ones thrown by * sync functions. If you don't use this token, Asio will use `system_error` exceptions, containing less info. - * \n + * * This token can only be used with operations involving Boost.MySQL, as it relies on its internals. - * \n + * * Like `asio::as_tuple`, this class wraps another completion token. For instance, * `with_diagnostics(asio::deferred)` will generate a deferred operation with an adapted * signature, which will throw `error_with_diagnostics` when `co_await`'ed. - * \n - * This token can only be used for operations with `void(error_code, T...)` signature. - * If this precondition is broken, a compile-time error will be issued. In particular, - * `with_diagnostics(asio::as_tuple(X))` is an error. + * + * If this token is applied to a function with a handler signature that + * does not match `void(error_code, T...)`, the token acts as a pass-through: + * it does not modify the signature, and calls the underlying token's initiation directly. + * This has the following implications: + * + * - `asio::as_tuple(with_diagnostics(X))` is equivalent to `asio::as_tuple(X)`. + * - `asio::redirect_error(with_diagnostics(X))` is equivalent to `asio::redirect_error(X)`. + * - Tokens like `asio::as_tuple` and `asio::redirect_error` can be used as partial tokens + * when `with_diagnostics` is the default completion token, as is the case for \ref any_connection. */ template class with_diagnostics_t diff --git a/test/Jamfile b/test/Jamfile index e58fd072c..5541bb429 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -15,8 +15,6 @@ import ac ; import indirect ; import config : requires ; -project : requirements /boost/mysql//boost_mysql ; - # Support header-only builds feature.feature boost.mysql.separate-compilation : on off : propagated composite ; @@ -67,6 +65,7 @@ local requirements = msvc:_SILENCE_CXX17_ALLOCATOR_VOID_DEPRECATION_WARNING msvc:_SILENCE_CXX17_ADAPTOR_TYPEDEFS_DEPRECATION_WARNING msvc:_SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING + msvc:_CRT_SECURE_NO_WARNINGS # Required by Asio # gcc-13+ doesn't understand view types and issues array bound warnings that don't make sense. # -Wno-implicit-fallthrough is required by Asio SSL components gcc:"-Wno-dangling-reference -Wno-array-bounds -Wno-implicit-fallthrough" @@ -90,7 +89,7 @@ local requirements = alias boost_mysql : - /boost/charconv//boost_charconv + $(boost_dependencies)/off /openssl//ssl/shared /openssl//crypto/shared : requirements diff --git a/test/integration/test/any_connection.cpp b/test/integration/test/any_connection.cpp index 180d66013..868b67248 100644 --- a/test/integration/test/any_connection.cpp +++ b/test/integration/test/any_connection.cpp @@ -22,11 +22,13 @@ #include +#include #include #include #include #include #include +#include #include #include @@ -317,6 +319,47 @@ BOOST_FIXTURE_TEST_CASE(default_token_cancel_after, any_connection_fixture) }); } +// Using as_tuple as partial token works +BOOST_FIXTURE_TEST_CASE(default_token_as_tuple, any_connection_fixture) +{ + run_coro(ctx, [&]() -> asio::awaitable { + // connect + auto [ec] = co_await conn.async_connect(connect_params_builder().build(), asio::as_tuple); + BOOST_TEST_REQUIRE(ec == error_code()); + + // Returning a value works + auto [ec2, stmt] = co_await conn.async_prepare_statement("SELECT ?", asio::as_tuple); + BOOST_TEST_REQUIRE(ec2 == error_code()); + BOOST_TEST(stmt.valid()); + + // Error case + results result; + auto [ec3] = co_await conn.async_execute("SELECT * FROM bad_table", result, asio::as_tuple); + BOOST_TEST(ec3 == common_server_errc::er_no_such_table); + }); +} + +// Using redirect_error as partial token works +BOOST_FIXTURE_TEST_CASE(default_token_redirect_error, any_connection_fixture) +{ + run_coro(ctx, [&]() -> asio::awaitable { + // connect + error_code ec; + co_await conn.async_connect(connect_params_builder().build(), asio::redirect_error(ec)); + BOOST_TEST_REQUIRE(ec == error_code()); + + // Returning a value works + auto stmt = co_await conn.async_prepare_statement("SELECT ?", asio::redirect_error(ec)); + BOOST_TEST_REQUIRE(ec == error_code()); + BOOST_TEST(stmt.valid()); + + // Error case + results result; + co_await conn.async_execute("SELECT * FROM bad_table", result, asio::redirect_error(ec)); + BOOST_TEST(ec == common_server_errc::er_no_such_table); + }); +} + // Spotcheck: immediate completions dispatched to the immediate executor BOOST_FIXTURE_TEST_CASE(immediate_completions, any_connection_fixture) { diff --git a/test/unit/test/with_diagnostics.cpp b/test/unit/test/with_diagnostics.cpp index cf9b4864f..cbd7c63f1 100644 --- a/test/unit/test/with_diagnostics.cpp +++ b/test/unit/test/with_diagnostics.cpp @@ -305,4 +305,61 @@ static_assert( "" ); +// Applying with_diagnostics to an unkown signature is a pass-through +struct no_ec_initiation +{ + template + void operator()(Handler&& handler, T1&& arg1, T2&& arg2, T3&& arg3) + { + // T1 should be a non-const lvalue + static_assert(std::is_same&>::value, ""); + BOOST_TEST(arg1 != nullptr); + + // T2 should be a const lvalue + static_assert(std::is_same&>::value, ""); + BOOST_TEST(arg2 != nullptr); + + // T3 should be a rvalue + static_assert(std::is_same>::value, ""); + BOOST_TEST(arg3 != nullptr); + auto arg3_move = std::move(arg3); + boost::ignore_unused(arg3_move); + + // Just call the handler + std::move(handler)(42); + } +}; + +template +void async_no_ec( + std::shared_ptr& arg1, + const std::shared_ptr& arg2, + std::shared_ptr&& arg3, + CompletionToken&& token +) +{ + asio::async_initiate(no_ec_initiation{}, token, arg1, arg2, std::move(arg3)); +} + +BOOST_AUTO_TEST_CASE(signature_no_ec) +{ + // Setup + auto arg1 = std::make_shared(42); + auto arg2 = arg1; + auto arg3 = arg1; + bool called = false; + auto handler = [&](int val) { + BOOST_TEST(val == 42); + called = true; + }; + + // Call the operation + async_no_ec(arg1, arg2, std::move(arg3), with_diagnostics(handler)); + + // lvalues not moved, rvalue moved + BOOST_TEST(arg1 != nullptr); + BOOST_TEST(arg2 != nullptr); + BOOST_TEST(arg3 == nullptr); +} + BOOST_AUTO_TEST_SUITE_END()